2026-01-13 09:13:23 +00:00
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
import '../../providers/theme_provider.dart';
|
|
|
|
|
import 'package:google_fonts/google_fonts.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
import '../contextual_help_icon.dart';
|
2026-04-05 04:35:03 +00:00
|
|
|
import '../common/pressable.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
import '../../theme/app_colors.dart';
|
2026-01-13 09:13:23 +00:00
|
|
|
|
|
|
|
|
class LevelTitleCard extends ConsumerWidget {
|
|
|
|
|
const LevelTitleCard({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
final userProfile = ref.watch(userProfileProvider);
|
|
|
|
|
final totalExp = userProfile.totalExp;
|
2026-01-29 15:54:22 +00:00
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
|
|
|
|
|
2026-01-13 09:13:23 +00:00
|
|
|
final level = userProfile.level;
|
|
|
|
|
final title = userProfile.title;
|
|
|
|
|
final progress = userProfile.nextLevelProgress;
|
|
|
|
|
final expToNext = userProfile.expToNextLevel;
|
|
|
|
|
|
2026-04-05 04:35:03 +00:00
|
|
|
return Pressable(
|
|
|
|
|
child: Container(
|
2026-01-13 09:13:23 +00:00
|
|
|
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: [
|
2026-04-05 04:32:52 +00:00
|
|
|
if (userProfile.nickname != null && userProfile.nickname!.isNotEmpty) ...[
|
|
|
|
|
Text(
|
|
|
|
|
userProfile.nickname!,
|
|
|
|
|
style: TextStyle(fontSize: 13, color: appColors.textSecondary, letterSpacing: 0.3),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
],
|
2026-01-13 09:13:23 +00:00
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
2026-01-29 15:54:22 +00:00
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'現在の称号',
|
2026-04-05 04:35:03 +00:00
|
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
color: appColors.textSecondary,
|
|
|
|
|
letterSpacing: 0.5,
|
2026-01-29 15:54:22 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
ContextualHelpIcon(
|
|
|
|
|
title: 'レベルと称号について',
|
|
|
|
|
customContent: _buildLevelHelpContent(context),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
title,
|
|
|
|
|
style: GoogleFonts.zenOldMincho(
|
2026-04-05 04:35:03 +00:00
|
|
|
fontSize: 36,
|
2026-01-29 15:54:22 +00:00
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: appColors.brandPrimary,
|
2026-04-05 04:35:03 +00:00
|
|
|
height: 1.1,
|
2026-01-29 15:54:22 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 4, left: 8), // Align baseline-ish
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: appColors.brandPrimary.withValues(alpha: 0.1),
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: appColors.brandPrimary.withValues(alpha: 0.3)),
|
|
|
|
|
),
|
|
|
|
|
child: Text(
|
|
|
|
|
'Lv.$level',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w900,
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
color: appColors.brandPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-01-13 09:13:23 +00:00
|
|
|
),
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
2026-01-13 09:13:23 +00:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
2026-04-06 08:04:20 +00:00
|
|
|
// 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),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-01-13 09:13:23 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
2026-01-13 09:13:23 +00:00
|
|
|
// EXP Text
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Total EXP: $totalExp',
|
2026-01-29 15:54:22 +00:00
|
|
|
style: TextStyle(fontSize: 12, color: appColors.textSecondary),
|
2026-01-13 09:13:23 +00:00
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
expToNext > 0 ? '次のレベルまで: ${expToNext}exp' : 'Max Level',
|
|
|
|
|
style: TextStyle(
|
2026-01-29 15:54:22 +00:00
|
|
|
fontSize: 12,
|
|
|
|
|
color: appColors.brandPrimary,
|
2026-01-13 09:13:23 +00:00
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-04-05 04:35:03 +00:00
|
|
|
), // Container
|
|
|
|
|
); // Pressable
|
2026-01-13 09:13:23 +00:00
|
|
|
}
|
2026-04-05 04:35:03 +00:00
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
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(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-13 09:13:23 +00:00
|
|
|
}
|