ponshu-room-lite/lib/widgets/gamification/level_title_card.dart

245 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/theme_provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../contextual_help_icon.dart';
import '../common/pressable.dart';
import '../../theme/app_colors.dart';
class LevelTitleCard extends ConsumerWidget {
const LevelTitleCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userProfile = ref.watch(userProfileProvider);
final totalExp = userProfile.totalExp;
final appColors = Theme.of(context).extension<AppColors>()!;
final level = userProfile.level;
final title = userProfile.title;
final progress = userProfile.nextLevelProgress;
final expToNext = userProfile.expToNextLevel;
return Pressable(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: Theme.of(context).dividerColor.withValues(alpha: 0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (userProfile.nickname != null && userProfile.nickname!.isNotEmpty) ...[
Text(
userProfile.nickname!,
style: TextStyle(fontSize: 13, color: appColors.textSecondary, letterSpacing: 0.3),
),
const SizedBox(height: 6),
],
// 「現在の称号」ラベル行 + Lv.バッジを同行右寄せ
Row(
children: [
Text(
'現在の称号',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: appColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(width: 4),
ContextualHelpIcon(
title: 'レベルと称号について',
customContent: _buildLevelHelpContent(context),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: appColors.brandPrimary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: appColors.brandPrimary.withValues(alpha: 0.3)),
),
child: Text(
'Lv.$level',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 14,
color: appColors.brandPrimary,
),
),
),
],
),
const SizedBox(height: 6),
// 称号名1行・フォントサイズ縮小・overflow防止
Text(
title,
style: GoogleFonts.zenOldMincho(
fontSize: 26,
fontWeight: FontWeight.bold,
color: appColors.brandPrimary,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 20),
// Progress Bar (animated 0 → actual value on mount)
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: progress),
duration: const Duration(milliseconds: 900),
curve: Curves.easeOutCubic,
builder: (context, value, _) {
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: value,
minHeight: 8,
backgroundColor: appColors.divider,
valueColor: AlwaysStoppedAnimation<Color>(appColors.brandPrimary),
),
);
},
),
const SizedBox(height: 8),
// EXP Text
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total EXP: $totalExp',
style: TextStyle(fontSize: 12, color: appColors.textSecondary),
),
Text(
expToNext > 0 ? '次のレベルまで: ${expToNext}exp' : 'Max Level',
style: TextStyle(
fontSize: 12,
color: appColors.brandPrimary,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
), // Container
); // Pressable
}
static Widget _buildLevelHelpContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'レベルの上げ方',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'日本酒を1本登録するごとに 10 EXP 獲得できます。\nメニューを作成するとボーナスが入ることも!',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.6,
),
),
const SizedBox(height: 16),
Text(
'称号一覧',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildLevelTable(context),
],
);
}
static Widget _buildLevelTable(BuildContext context) {
// Level data from LevelCalculator
final levels = [
{'level': 1, 'requiredExp': 0, 'title': '見習い'},
{'level': 2, 'requiredExp': 10, 'title': '歩き飲み'},
{'level': 5, 'requiredExp': 50, 'title': '嗜み人'},
{'level': 10, 'requiredExp': 100, 'title': '呑兵衛'},
{'level': 20, 'requiredExp': 200, 'title': '酒豪'},
{'level': 30, 'requiredExp': 300, 'title': '利き酒師'},
{'level': 50, 'requiredExp': 500, 'title': '日本酒伝道師'},
{'level': 100, 'requiredExp': 1000, 'title': 'ポンシュマスター'},
];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
headingRowHeight: 40,
dataRowMinHeight: 32,
dataRowMaxHeight: 32,
columns: [
DataColumn(
label: Text(
'Lv',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
),
DataColumn(
label: Text(
'称号',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
),
DataColumn(
label: Text(
'必要EXP',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
),
],
rows: levels.map((levelData) {
return DataRow(
cells: [
DataCell(Text((levelData['level'] as int).toString(), style: const TextStyle(fontSize: 12))),
DataCell(Text(levelData['title'] as String, style: const TextStyle(fontSize: 12))),
DataCell(Text((levelData['requiredExp'] as int).toString(), style: const TextStyle(fontSize: 12))),
],
);
}).toList(),
),
),
);
}
}