diff --git a/lib/models/maps/prefecture_tile_layout.dart b/lib/models/maps/prefecture_tile_layout.dart index 0d51254..8953beb 100644 --- a/lib/models/maps/prefecture_tile_layout.dart +++ b/lib/models/maps/prefecture_tile_layout.dart @@ -21,24 +21,24 @@ class PrefectureTileLayout { static const Map finalLayout = { // Hokkaido - '北海道': TilePosition(col: 12, row: 0, width: 2, height: 2), + '北海道': TilePosition(col: 11, row: 0, width: 2, height: 2), // Tohoku - '青森': TilePosition(col: 12, row: 2), - '秋田': TilePosition(col: 11, row: 3), - '岩手': TilePosition(col: 12, row: 3), - '山形': TilePosition(col: 11, row: 4), - '宮城': TilePosition(col: 12, row: 4), - '福島': TilePosition(col: 12, row: 5), + '青森': TilePosition(col: 11, row: 2), + '秋田': TilePosition(col: 10, row: 3), // Was 11 - no wait, Akita was 11. + '岩手': TilePosition(col: 11, row: 3), + '山形': TilePosition(col: 10, row: 4), // Was 11 + '宮城': TilePosition(col: 11, row: 4), + '福島': TilePosition(col: 11, row: 5), // Kanto & Koshinetsu - '茨城': TilePosition(col: 13, row: 6), - '栃木': TilePosition(col: 12, row: 6), - '群馬': TilePosition(col: 11, row: 6), - '埼玉': TilePosition(col: 11, row: 7), - '東京': TilePosition(col: 11, row: 8), - '千葉': TilePosition(col: 12, row: 8), - '神奈川': TilePosition(col: 11, row: 9), + '茨城': TilePosition(col: 12, row: 6), + '栃木': TilePosition(col: 11, row: 6), + '群馬': TilePosition(col: 10, row: 6), // Was 11? No wait. + '埼玉': TilePosition(col: 10, row: 7), // Was 11 + '東京': TilePosition(col: 10, row: 8), // Was 11 + '千葉': TilePosition(col: 11, row: 8), + '神奈川': TilePosition(col: 10, row: 9), // Was 11 '山梨': TilePosition(col: 10, row: 7), '長野': TilePosition(col: 10, row: 6), '新潟': TilePosition(col: 11, row: 5), diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index b6dc2f6..038e59c 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; +import '../services/level_calculator.dart'; part 'user_profile.g.dart'; @@ -52,6 +53,18 @@ class UserProfile extends HiveObject { @HiveField(13) String? gender; // 'male', 'female', 'other', null + @HiveField(14, defaultValue: 0) + int totalExp; + + @HiveField(15, defaultValue: []) + List unlockedBadges; + + // Calculators + int get level => LevelCalculator.getLevel(totalExp); + String get title => LevelCalculator.getTitle(totalExp); + double get nextLevelProgress => LevelCalculator.getProgress(totalExp); + int get expToNextLevel => LevelCalculator.getExpToNextLevel(totalExp); + UserProfile({ this.fontPreference = 'sans', this.displayMode = 'list', @@ -65,6 +78,8 @@ class UserProfile extends HiveObject { this.hasCompletedOnboarding = false, this.nickname, this.gender, + this.totalExp = 0, + this.unlockedBadges = const [], }); UserProfile copyWith({ @@ -80,6 +95,8 @@ class UserProfile extends HiveObject { bool? hasCompletedOnboarding, String? nickname, String? gender, + int? totalExp, + List? unlockedBadges, }) { return UserProfile( fontPreference: fontPreference ?? this.fontPreference, @@ -94,6 +111,8 @@ class UserProfile extends HiveObject { hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding, nickname: nickname ?? this.nickname, gender: gender ?? this.gender, + totalExp: totalExp ?? this.totalExp, + unlockedBadges: unlockedBadges ?? this.unlockedBadges, ); } } diff --git a/lib/models/user_profile.g.dart b/lib/models/user_profile.g.dart index 696f7da..7a97696 100644 --- a/lib/models/user_profile.g.dart +++ b/lib/models/user_profile.g.dart @@ -29,13 +29,16 @@ class UserProfileAdapter extends TypeAdapter { hasCompletedOnboarding: fields[11] == null ? false : fields[11] as bool, nickname: fields[12] as String?, gender: fields[13] as String?, + totalExp: fields[14] == null ? 0 : fields[14] as int, + unlockedBadges: + fields[15] == null ? [] : (fields[15] as List).cast(), ); } @override void write(BinaryWriter writer, UserProfile obj) { writer - ..writeByte(12) + ..writeByte(14) ..writeByte(0) ..write(obj.fontPreference) ..writeByte(3) @@ -59,7 +62,11 @@ class UserProfileAdapter extends TypeAdapter { ..writeByte(12) ..write(obj.nickname) ..writeByte(13) - ..write(obj.gender); + ..write(obj.gender) + ..writeByte(14) + ..write(obj.totalExp) + ..writeByte(15) + ..write(obj.unlockedBadges); } @override diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 54318a8..a47c0fc 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -78,6 +78,14 @@ class UserProfileNotifier extends Notifier { ); await _save(newState); } + + Future updateTotalExp(int newExp) async { + final newState = state.copyWith( + totalExp: newExp, + updatedAt: DateTime.now(), + ); + await _save(newState); + } } // Helper Providers for easy access diff --git a/lib/screens/camera_screen.dart b/lib/screens/camera_screen.dart index 631fe00..34e0640 100644 --- a/lib/screens/camera_screen.dart +++ b/lib/screens/camera_screen.dart @@ -15,6 +15,8 @@ import '../models/sake_item.dart'; import '../theme/app_theme.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; // Gallery Import +import '../models/user_profile.dart'; +import '../providers/theme_provider.dart'; // userProfileProvider enum CameraMode { @@ -30,7 +32,7 @@ class CameraScreen extends ConsumerStatefulWidget { ConsumerState createState() => _CameraScreenState(); } -class _CameraScreenState extends ConsumerState with SingleTickerProviderStateMixin { +class _CameraScreenState extends ConsumerState with SingleTickerProviderStateMixin, WidgetsBindingObserver { CameraController? _controller; Future? _initializeControllerFuture; bool _isTakingPicture = false; @@ -362,10 +364,23 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr currentOrder.insert(0, sakeItem.id); // Insert at beginning await settingsBox.put('sake_sort_order', currentOrder); + // --- v1.3 Gamification Hook --- + // Award EXP + final userProfileState = ref.read(userProfileProvider); + final prevLevel = userProfileState.level; + + await ref.read(userProfileProvider.notifier).updateTotalExp(userProfileState.totalExp + 10); + + // Refetch updated state for level comparison + final updatedProfile = ref.read(userProfileProvider); + final newLevel = updatedProfile.level; + final isLevelUp = newLevel > prevLevel; + // Debug: Verify save debugPrint('✅ Saved to Hive: ${sakeItem.displayData.name} (ID: ${sakeItem.id})'); debugPrint('📦 Total items in box: ${box.length}'); debugPrint('📦 Prepended to sort order (now ${currentOrder.length} items)'); + debugPrint('🎮 Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel'); if (!mounted) return; @@ -375,11 +390,28 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr // Close Camera Screen (Return to Home) Navigator.of(context).pop(); - // Success Message + // Success Message (with EXP/Level Up info) ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${sakeItem.displayData.name} を登録しました!'), - duration: const Duration(seconds: 2), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${sakeItem.displayData.name} を登録しました!'), + const SizedBox(height: 4), + Row( + children: [ + const Icon(LucideIcons.sparkles, color: Colors.yellow, size: 16), + const SizedBox(width: 8), + Text( + '経験値 +10 GET! ${isLevelUp ? " (Level UP!)" : ""}', + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.yellowAccent), + ), + ], + ), + ], + ), + duration: const Duration(seconds: 4), // Longer display for level up ), ); diff --git a/lib/screens/sake_detail_screen.dart b/lib/screens/sake_detail_screen.dart index a792878..93234c8 100644 --- a/lib/screens/sake_detail_screen.dart +++ b/lib/screens/sake_detail_screen.dart @@ -903,7 +903,7 @@ class _SakeDetailScreenState extends ConsumerState { const SizedBox(height: 24), // 2. Variants (Inline Entry) - const Text('提供バリエーション', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('提供サイズ選択', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), // Presets Chips @@ -924,8 +924,18 @@ class _SakeDetailScreenState extends ConsumerState { } }); }, - backgroundColor: Colors.grey[200], - selectedColor: Colors.orange[100], + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[800] + : Colors.grey[200], + selectedColor: Theme.of(context).brightness == Brightness.dark + ? Colors.orange.withValues(alpha: 0.5) + : Colors.orange[100], + labelStyle: TextStyle( + color: (tempName == preset) + ? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) + : (Theme.of(context).brightness == Brightness.dark ? Colors.grey[300] : null), + fontWeight: (tempName == preset) ? FontWeight.bold : null, + ), ), ], ), diff --git a/lib/screens/soul_screen.dart b/lib/screens/soul_screen.dart index b99b51d..8d60e99 100644 --- a/lib/screens/soul_screen.dart +++ b/lib/screens/soul_screen.dart @@ -6,6 +6,9 @@ import '../providers/theme_provider.dart'; import '../widgets/settings/app_settings_section.dart'; import '../widgets/settings/other_settings_section.dart'; import '../widgets/settings/backup_settings_section.dart'; +import '../widgets/gamification/level_title_card.dart'; +import '../widgets/gamification/activity_stats.dart'; +import '../widgets/gamification/badge_case.dart'; class SoulScreen extends ConsumerStatefulWidget { const SoulScreen({super.key}); @@ -32,17 +35,34 @@ class _SoulScreenState extends ConsumerState { body: ListView( padding: const EdgeInsets.all(16), children: [ + + // Gamification Section (v1.3) + const LevelTitleCard(), + const SizedBox(height: 16), + const ActivityStats(), + const SizedBox(height: 16), + const BadgeCase(), + const SizedBox(height: 32), + // Identity Section _buildSectionHeader('プロフィール (ID)', LucideIcons.fingerprint), Card( child: Column( children: [ ListTile( - leading: Icon(LucideIcons.brainCircuit, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), - title: const Text('MBTI診断'), - subtitle: Text(userProfile.mbti ?? '未設定'), + leading: Icon(LucideIcons.user, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), + title: const Text('ニックネーム'), + subtitle: Text(userProfile.nickname ?? '未設定'), trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), - onTap: () => _showMbtiDialog(context, userProfile.mbti), + onTap: () => _showNicknameDialog(context, userProfile.nickname), + ), + const Divider(height: 1), + ListTile( + leading: Icon(LucideIcons.personStanding, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), + title: const Text('性別'), + subtitle: Text(_getGenderLabel(userProfile.gender)), + trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), + onTap: () => _showGenderDialog(context, userProfile.gender), ), const Divider(height: 1), ListTile( @@ -54,22 +74,16 @@ class _SoulScreenState extends ConsumerState { trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), onTap: () => _pickBirthDate(context, userProfile.birthdate), ), - ListTile( - leading: Icon(LucideIcons.user, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), - title: const Text('ニックネーム'), - subtitle: Text(userProfile.nickname ?? '未設定'), - trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), - onTap: () => _showNicknameDialog(context, userProfile.nickname), - ), const Divider(height: 1), ListTile( - leading: Icon(LucideIcons.users, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), - title: const Text('性別'), - subtitle: Text(_getGenderLabel(userProfile.gender)), + leading: Icon(LucideIcons.brainCircuit, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), + title: const Text('MBTI診断'), + subtitle: Text(userProfile.mbti ?? '未設定'), trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), - onTap: () => _showGenderDialog(context, userProfile.gender), + onTap: () => _showMbtiDialog(context, userProfile.mbti), ), const Divider(height: 1), + ], ), ), diff --git a/lib/services/level_calculator.dart b/lib/services/level_calculator.dart new file mode 100644 index 0000000..efb6210 --- /dev/null +++ b/lib/services/level_calculator.dart @@ -0,0 +1,60 @@ + +class LevelCalculator { + static const List> _levelTable = [ + {'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': 'ポンシュマスター'}, + ]; + + static int getLevel(int totalExp) { + for (var i = _levelTable.length - 1; i >= 0; i--) { + if (totalExp >= (_levelTable[i]['requiredExp'] as int)) { + return _levelTable[i]['level'] as int; + } + } + return 1; // Fallback + } + + static String getTitle(int totalExp) { + final level = getLevel(totalExp); + return _levelTable.firstWhere( + (entry) => entry['level'] == level, + orElse: () => _levelTable[0], + )['title'] as String; + } + + static double getProgress(int totalExp) { + final currentLevel = getLevel(totalExp); + final currentEntry = _levelTable.firstWhere((e) => e['level'] == currentLevel); + + // Find next level entry + final currentIndex = _levelTable.indexOf(currentEntry); + if (currentIndex == _levelTable.length - 1) return 1.0; // Max level + + final nextEntry = _levelTable[currentIndex + 1]; + + final currentLevelExp = currentEntry['requiredExp'] as int; + final nextLevelExp = nextEntry['requiredExp'] as int; + + if (nextLevelExp == currentLevelExp) return 1.0; + + return (totalExp - currentLevelExp) / (nextLevelExp - currentLevelExp); + } + + static int getExpToNextLevel(int totalExp) { + final currentLevel = getLevel(totalExp); + final currentEntry = _levelTable.firstWhere((e) => e['level'] == currentLevel); + + // Find next level entry + final currentIndex = _levelTable.indexOf(currentEntry); + if (currentIndex == _levelTable.length - 1) return 0; // Max level + + final nextEntry = _levelTable[currentIndex + 1]; + return (nextEntry['requiredExp'] as int) - totalExp; + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index eac0709..b44446c 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -15,8 +15,10 @@ class AppTheme { static ThemeData createTheme(String fontPreference, Brightness brightness) { final textTheme = (fontPreference == 'serif') - ? GoogleFonts.notoSerifJpTextTheme() - : GoogleFonts.notoSansJpTextTheme(); + ? GoogleFonts.notoSerifJpTextTheme() // Mincho + : (fontPreference == 'digital') + ? GoogleFonts.dotGothic16TextTheme() // Digital/Retro + : GoogleFonts.notoSansJpTextTheme(); // Gothic/Sans (Default) final baseColorScheme = ColorScheme.fromSeed( seedColor: posimaiBlue, diff --git a/lib/widgets/gamification/activity_stats.dart b/lib/widgets/gamification/activity_stats.dart new file mode 100644 index 0000000..7ad7f89 --- /dev/null +++ b/lib/widgets/gamification/activity_stats.dart @@ -0,0 +1,96 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../providers/sake_list_provider.dart'; + + +class ActivityStats extends ConsumerWidget { + const ActivityStats({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final allSakeAsync = ref.watch(sakeListProvider); + + return allSakeAsync.when( + data: (sakes) { + final totalSakes = sakes.length; + final favoriteCount = sakes.where((s) => s.userData.isFavorite).length; + + // Recording Days + final dates = sakes.map((s) { + final d = s.metadata.createdAt; + return DateTime(d.year, d.month, d.day); + }).toSet(); + final recordingDays = dates.length; + + // Avg Price + int totalPrice = 0; + int priceCount = 0; + for (var s in sakes) { + if (s.userData.price != null && s.userData.price! > 0) { + totalPrice += s.userData.price!; + priceCount++; + } + } + final avgPrice = priceCount > 0 ? (totalPrice / priceCount).round() : 0; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'あなたの活動深度', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem(context, '総登録数', '$totalSakes本', LucideIcons.wine), + _buildStatItem(context, 'お気に入り', '$favoriteCount本', LucideIcons.heart), + _buildStatItem(context, '撮影日数', '$recordingDays日', LucideIcons.calendar), + _buildStatItem(context, '平均価格', '¥$avgPrice', LucideIcons.banknote), + ], + ), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ); + } + + Widget _buildStatItem(BuildContext context, String label, String value, IconData icon) { + return Column( + children: [ + Icon(icon, size: 20, color: Colors.grey[400]), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 10, color: Colors.grey[600]), + ), + ], + ); + } +} diff --git a/lib/widgets/gamification/badge_case.dart b/lib/widgets/gamification/badge_case.dart new file mode 100644 index 0000000..0255ee7 --- /dev/null +++ b/lib/widgets/gamification/badge_case.dart @@ -0,0 +1,110 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../providers/theme_provider.dart'; + + +class BadgeCase extends ConsumerWidget { + const BadgeCase({super.key}); + + static const List> _badges = [ + {'id': 'regional_tohoku', 'name': '東北制覇', 'icon': '👹', 'desc': '東北6県の日本酒を登録'}, + {'id': 'flavor_dry', 'name': '辛口党', 'icon': '🌶️', 'desc': '辛口(+5以上)を10本登録'}, + // Add more future badges here + ]; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + final unlocked = userProfile.unlockedBadges.toSet(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'バッジケース', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + '${unlocked.length} / ${_badges.length}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: _badges.map((badge) { + final isUnlocked = unlocked.contains(badge['id']); + return Tooltip( + message: '${badge['name']}\n${badge['desc']}', + triggerMode: TooltipTriggerMode.tap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 64, + height: 80, + child: Column( + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isUnlocked + ? Colors.orange.withValues(alpha: 0.1) + : Colors.grey.withValues(alpha: 0.1), + shape: BoxShape.circle, + border: Border.all( + color: isUnlocked + ? Colors.orange + : Colors.grey.withValues(alpha: 0.3), + width: 2, + ), + boxShadow: isUnlocked ? [ + BoxShadow( + color: Colors.orange.withValues(alpha: 0.3), + blurRadius: 8, + ) + ] : [], + ), + child: Text( + isUnlocked ? badge['icon'] : '🔒', + style: const TextStyle(fontSize: 24), + ), + ), + const SizedBox(height: 4), + Text( + badge['name'], + style: TextStyle( + fontSize: 10, + fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, + color: isUnlocked ? Theme.of(context).colorScheme.onSurface : Colors.grey, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/gamification/level_title_card.dart b/lib/widgets/gamification/level_title_card.dart new file mode 100644 index 0000000..9b70f68 --- /dev/null +++ b/lib/widgets/gamification/level_title_card.dart @@ -0,0 +1,120 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../providers/theme_provider.dart'; +import 'package:google_fonts/google_fonts.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 level = userProfile.level; + final title = userProfile.title; + final progress = userProfile.nextLevelProgress; + final expToNext = userProfile.expToNextLevel; + + return 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: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '現在の称号', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + title, + style: GoogleFonts.zenOldMincho( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Theme.of(context).primaryColor, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Theme.of(context).primaryColor.withValues(alpha: 0.3)), + ), + child: Text( + 'Lv.$level', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + + // Progress Bar + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + minHeight: 8, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), + ), + ), + const SizedBox(height: 8), + + // EXP Text + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total EXP: $totalExp', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + Text( + expToNext > 0 ? '次のレベルまで: ${expToNext}exp' : 'Max Level', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/onboarding_dialog.dart b/lib/widgets/onboarding_dialog.dart index ad2aaa5..7e9f5d1 100644 --- a/lib/widgets/onboarding_dialog.dart +++ b/lib/widgets/onboarding_dialog.dart @@ -86,7 +86,7 @@ class _OnboardingDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ if (iconData is IconData) - Icon(iconData, size: 80, color: Theme.of(context).primaryColor) + Icon(iconData, size: 80, color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Theme.of(context).primaryColor) else Text( iconData.toString(), diff --git a/lib/widgets/settings/app_settings_section.dart b/lib/widgets/settings/app_settings_section.dart index 5226e5b..213a9ff 100644 --- a/lib/widgets/settings/app_settings_section.dart +++ b/lib/widgets/settings/app_settings_section.dart @@ -22,13 +22,17 @@ class AppearanceSettingsSection extends ConsumerWidget { ListTile( leading: Icon(LucideIcons.type, color: isDark ? Colors.grey[400] : null), title: const Text('フォント'), - subtitle: Text(fontPref == 'serif' ? '明朝 (Serif)' : 'ゴシック (Sans)'), - trailing: Switch( - value: fontPref == 'serif', - onChanged: (val) { - ref.read(userProfileProvider.notifier) - .setFontPreference(val ? 'serif' : 'sans'); + subtitle: Text(_getFontName(fontPref)), + trailing: PopupMenuButton( + icon: Icon(LucideIcons.chevronRight, color: isDark ? Colors.grey[600] : null), + onSelected: (val) { + ref.read(userProfileProvider.notifier).setFontPreference(val); }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'sans', child: Text('ゴシック (標準)')), + const PopupMenuItem(value: 'serif', child: Text('明朝 (上品)')), + const PopupMenuItem(value: 'digital', child: Text('ドット (レトロ)')), + ], ), ), const Divider(height: 1), @@ -106,4 +110,12 @@ class AppearanceSettingsSection extends ConsumerWidget { ), ); } + + String _getFontName(String pref) { + switch (pref) { + case 'serif': return '明朝 (上品)'; + case 'digital': return 'ドット (レトロ)'; + default: return 'ゴシック (標準)'; + } + } } diff --git a/lib/widgets/settings/backup_settings_section.dart b/lib/widgets/settings/backup_settings_section.dart index 9f338d3..4cc4d56 100644 --- a/lib/widgets/settings/backup_settings_section.dart +++ b/lib/widgets/settings/backup_settings_section.dart @@ -7,7 +7,7 @@ class BackupSettingsSection extends StatefulWidget { const BackupSettingsSection({ super.key, - this.title = 'データバックアップ', + this.title = 'バックアップ・復元', }); @override @@ -173,7 +173,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } const SizedBox(width: 12), Expanded( child: Text( - 'バックアップにはWi-Fi環境を推奨します\n画像が多い場合、数百MB〜1GB以上になる可能性があります', + 'バックアップやデータ復元はWi-Fi環境下を推奨します\nデータ量が数100MB~1GB以上になる可能性があります', style: TextStyle( fontSize: 12, color: isDark ? Colors.blue[300] : Colors.blue[900], diff --git a/lib/widgets/settings/other_settings_section.dart b/lib/widgets/settings/other_settings_section.dart index 600ac78..65ce22e 100644 --- a/lib/widgets/settings/other_settings_section.dart +++ b/lib/widgets/settings/other_settings_section.dart @@ -31,7 +31,7 @@ class _OtherSettingsSectionState extends ConsumerState { final packageInfo = await PackageInfo.fromPlatform(); if (mounted) { setState(() { - _appVersion = 'v${packageInfo.version}+${packageInfo.buildNumber} (Lite)'; + _appVersion = 'v${packageInfo.version}+${packageInfo.buildNumber}'; }); } } @@ -50,7 +50,7 @@ class _OtherSettingsSectionState extends ConsumerState { if (widget.showBusinessMode) ...[ ListTile( leading: Icon(LucideIcons.store, color: isDark ? Colors.orange[300] : Colors.orange), - title: const Text('飲食店モード (Beta)'), + title: const Text('ビジネスモード (Beta)'), subtitle: const Text('お品書き作成機能など'), trailing: Switch( value: userProfile.isBusinessMode,