Fix compilation error in SakePriceDialog

This commit is contained in:
Ponshu Developer 2026-01-13 18:13:23 +09:00
parent 3018d9a9d1
commit 318fa19bfb
16 changed files with 541 additions and 51 deletions

View File

@ -21,24 +21,24 @@ class PrefectureTileLayout {
static const Map<String, TilePosition> finalLayout = { static const Map<String, TilePosition> finalLayout = {
// Hokkaido // Hokkaido
'北海道': TilePosition(col: 12, row: 0, width: 2, height: 2), '北海道': TilePosition(col: 11, row: 0, width: 2, height: 2),
// Tohoku // Tohoku
'青森': TilePosition(col: 12, row: 2), '青森': TilePosition(col: 11, row: 2),
'秋田': TilePosition(col: 11, row: 3), '秋田': TilePosition(col: 10, row: 3), // Was 11 - no wait, Akita was 11.
'岩手': TilePosition(col: 12, row: 3), '岩手': TilePosition(col: 11, row: 3),
'山形': TilePosition(col: 11, row: 4), '山形': TilePosition(col: 10, row: 4), // Was 11
'宮城': TilePosition(col: 12, row: 4), '宮城': TilePosition(col: 11, row: 4),
'福島': TilePosition(col: 12, row: 5), '福島': TilePosition(col: 11, row: 5),
// Kanto & Koshinetsu // Kanto & Koshinetsu
'茨城': TilePosition(col: 13, row: 6), '茨城': TilePosition(col: 12, row: 6),
'栃木': TilePosition(col: 12, row: 6), '栃木': TilePosition(col: 11, row: 6),
'群馬': TilePosition(col: 11, row: 6), '群馬': TilePosition(col: 10, row: 6), // Was 11? No wait.
'埼玉': TilePosition(col: 11, row: 7), '埼玉': TilePosition(col: 10, row: 7), // Was 11
'東京': TilePosition(col: 11, row: 8), '東京': TilePosition(col: 10, row: 8), // Was 11
'千葉': TilePosition(col: 12, row: 8), '千葉': TilePosition(col: 11, row: 8),
'神奈川': TilePosition(col: 11, row: 9), '神奈川': TilePosition(col: 10, row: 9), // Was 11
'山梨': TilePosition(col: 10, row: 7), '山梨': TilePosition(col: 10, row: 7),
'長野': TilePosition(col: 10, row: 6), '長野': TilePosition(col: 10, row: 6),
'新潟': TilePosition(col: 11, row: 5), '新潟': TilePosition(col: 11, row: 5),

View File

@ -1,4 +1,5 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import '../services/level_calculator.dart';
part 'user_profile.g.dart'; part 'user_profile.g.dart';
@ -52,6 +53,18 @@ class UserProfile extends HiveObject {
@HiveField(13) @HiveField(13)
String? gender; // 'male', 'female', 'other', null String? gender; // 'male', 'female', 'other', null
@HiveField(14, defaultValue: 0)
int totalExp;
@HiveField(15, defaultValue: [])
List<String> 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({ UserProfile({
this.fontPreference = 'sans', this.fontPreference = 'sans',
this.displayMode = 'list', this.displayMode = 'list',
@ -65,6 +78,8 @@ class UserProfile extends HiveObject {
this.hasCompletedOnboarding = false, this.hasCompletedOnboarding = false,
this.nickname, this.nickname,
this.gender, this.gender,
this.totalExp = 0,
this.unlockedBadges = const [],
}); });
UserProfile copyWith({ UserProfile copyWith({
@ -80,6 +95,8 @@ class UserProfile extends HiveObject {
bool? hasCompletedOnboarding, bool? hasCompletedOnboarding,
String? nickname, String? nickname,
String? gender, String? gender,
int? totalExp,
List<String>? unlockedBadges,
}) { }) {
return UserProfile( return UserProfile(
fontPreference: fontPreference ?? this.fontPreference, fontPreference: fontPreference ?? this.fontPreference,
@ -94,6 +111,8 @@ class UserProfile extends HiveObject {
hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding, hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding,
nickname: nickname ?? this.nickname, nickname: nickname ?? this.nickname,
gender: gender ?? this.gender, gender: gender ?? this.gender,
totalExp: totalExp ?? this.totalExp,
unlockedBadges: unlockedBadges ?? this.unlockedBadges,
); );
} }
} }

View File

@ -29,13 +29,16 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
hasCompletedOnboarding: fields[11] == null ? false : fields[11] as bool, hasCompletedOnboarding: fields[11] == null ? false : fields[11] as bool,
nickname: fields[12] as String?, nickname: fields[12] as String?,
gender: fields[13] 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<String>(),
); );
} }
@override @override
void write(BinaryWriter writer, UserProfile obj) { void write(BinaryWriter writer, UserProfile obj) {
writer writer
..writeByte(12) ..writeByte(14)
..writeByte(0) ..writeByte(0)
..write(obj.fontPreference) ..write(obj.fontPreference)
..writeByte(3) ..writeByte(3)
@ -59,7 +62,11 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
..writeByte(12) ..writeByte(12)
..write(obj.nickname) ..write(obj.nickname)
..writeByte(13) ..writeByte(13)
..write(obj.gender); ..write(obj.gender)
..writeByte(14)
..write(obj.totalExp)
..writeByte(15)
..write(obj.unlockedBadges);
} }
@override @override

View File

@ -78,6 +78,14 @@ class UserProfileNotifier extends Notifier<UserProfile> {
); );
await _save(newState); await _save(newState);
} }
Future<void> updateTotalExp(int newExp) async {
final newState = state.copyWith(
totalExp: newExp,
updatedAt: DateTime.now(),
);
await _save(newState);
}
} }
// Helper Providers for easy access // Helper Providers for easy access

View File

@ -15,6 +15,8 @@ import '../models/sake_item.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import 'package:image_picker/image_picker.dart'; // Gallery Import import 'package:image_picker/image_picker.dart'; // Gallery Import
import '../models/user_profile.dart';
import '../providers/theme_provider.dart'; // userProfileProvider
enum CameraMode { enum CameraMode {
@ -30,7 +32,7 @@ class CameraScreen extends ConsumerStatefulWidget {
ConsumerState<CameraScreen> createState() => _CameraScreenState(); ConsumerState<CameraScreen> createState() => _CameraScreenState();
} }
class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerProviderStateMixin { class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
CameraController? _controller; CameraController? _controller;
Future<void>? _initializeControllerFuture; Future<void>? _initializeControllerFuture;
bool _isTakingPicture = false; bool _isTakingPicture = false;
@ -362,10 +364,23 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
currentOrder.insert(0, sakeItem.id); // Insert at beginning currentOrder.insert(0, sakeItem.id); // Insert at beginning
await settingsBox.put('sake_sort_order', currentOrder); 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 // Debug: Verify save
debugPrint('✅ Saved to Hive: ${sakeItem.displayData.name} (ID: ${sakeItem.id})'); debugPrint('✅ Saved to Hive: ${sakeItem.displayData.name} (ID: ${sakeItem.id})');
debugPrint('📦 Total items in box: ${box.length}'); debugPrint('📦 Total items in box: ${box.length}');
debugPrint('📦 Prepended to sort order (now ${currentOrder.length} items)'); debugPrint('📦 Prepended to sort order (now ${currentOrder.length} items)');
debugPrint('🎮 Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel');
if (!mounted) return; if (!mounted) return;
@ -375,11 +390,28 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
// Close Camera Screen (Return to Home) // Close Camera Screen (Return to Home)
Navigator.of(context).pop(); Navigator.of(context).pop();
// Success Message // Success Message (with EXP/Level Up info)
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('${sakeItem.displayData.name} を登録しました!'), content: Column(
duration: const Duration(seconds: 2), 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
), ),
); );

View File

@ -903,7 +903,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
// 2. Variants (Inline Entry) // 2. Variants (Inline Entry)
const Text('提供バリエーション', style: TextStyle(fontWeight: FontWeight.bold)), const Text('提供サイズ選択', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 8),
// Presets Chips // Presets Chips
@ -924,8 +924,18 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
} }
}); });
}, },
backgroundColor: Colors.grey[200], backgroundColor: Theme.of(context).brightness == Brightness.dark
selectedColor: Colors.orange[100], ? 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,
),
), ),
], ],
), ),

View File

@ -6,6 +6,9 @@ import '../providers/theme_provider.dart';
import '../widgets/settings/app_settings_section.dart'; import '../widgets/settings/app_settings_section.dart';
import '../widgets/settings/other_settings_section.dart'; import '../widgets/settings/other_settings_section.dart';
import '../widgets/settings/backup_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 { class SoulScreen extends ConsumerStatefulWidget {
const SoulScreen({super.key}); const SoulScreen({super.key});
@ -32,17 +35,34 @@ class _SoulScreenState extends ConsumerState<SoulScreen> {
body: ListView( body: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ 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 // Identity Section
_buildSectionHeader('プロフィール (ID)', LucideIcons.fingerprint), _buildSectionHeader('プロフィール (ID)', LucideIcons.fingerprint),
Card( Card(
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
leading: Icon(LucideIcons.brainCircuit, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), leading: Icon(LucideIcons.user, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null),
title: const Text('MBTI診断'), title: const Text('ニックネーム'),
subtitle: Text(userProfile.mbti ?? '未設定'), subtitle: Text(userProfile.nickname ?? '未設定'),
trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), 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), const Divider(height: 1),
ListTile( ListTile(
@ -54,22 +74,16 @@ class _SoulScreenState extends ConsumerState<SoulScreen> {
trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null),
onTap: () => _pickBirthDate(context, userProfile.birthdate), 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), const Divider(height: 1),
ListTile( ListTile(
leading: Icon(LucideIcons.users, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null), leading: Icon(LucideIcons.brainCircuit, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : null),
title: const Text('性別'), title: const Text('MBTI診断'),
subtitle: Text(_getGenderLabel(userProfile.gender)), subtitle: Text(userProfile.mbti ?? '未設定'),
trailing: Icon(LucideIcons.chevronRight, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600] : null), 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), const Divider(height: 1),
], ],
), ),
), ),

View File

@ -0,0 +1,60 @@
class LevelCalculator {
static const List<Map<String, dynamic>> _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;
}
}

View File

@ -15,8 +15,10 @@ class AppTheme {
static ThemeData createTheme(String fontPreference, Brightness brightness) { static ThemeData createTheme(String fontPreference, Brightness brightness) {
final textTheme = (fontPreference == 'serif') final textTheme = (fontPreference == 'serif')
? GoogleFonts.notoSerifJpTextTheme() ? GoogleFonts.notoSerifJpTextTheme() // Mincho
: GoogleFonts.notoSansJpTextTheme(); : (fontPreference == 'digital')
? GoogleFonts.dotGothic16TextTheme() // Digital/Retro
: GoogleFonts.notoSansJpTextTheme(); // Gothic/Sans (Default)
final baseColorScheme = ColorScheme.fromSeed( final baseColorScheme = ColorScheme.fromSeed(
seedColor: posimaiBlue, seedColor: posimaiBlue,

View File

@ -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]),
),
],
);
}
}

View File

@ -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<Map<String, dynamic>> _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(),
),
],
),
);
}
}

View File

@ -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<Color>(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,
),
),
],
),
],
),
);
}
}

View File

@ -86,7 +86,7 @@ class _OnboardingDialogState extends State<OnboardingDialog> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (iconData is IconData) 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 else
Text( Text(
iconData.toString(), iconData.toString(),

View File

@ -22,13 +22,17 @@ class AppearanceSettingsSection extends ConsumerWidget {
ListTile( ListTile(
leading: Icon(LucideIcons.type, color: isDark ? Colors.grey[400] : null), leading: Icon(LucideIcons.type, color: isDark ? Colors.grey[400] : null),
title: const Text('フォント'), title: const Text('フォント'),
subtitle: Text(fontPref == 'serif' ? '明朝 (Serif)' : 'ゴシック (Sans)'), subtitle: Text(_getFontName(fontPref)),
trailing: Switch( trailing: PopupMenuButton<String>(
value: fontPref == 'serif', icon: Icon(LucideIcons.chevronRight, color: isDark ? Colors.grey[600] : null),
onChanged: (val) { onSelected: (val) {
ref.read(userProfileProvider.notifier) ref.read(userProfileProvider.notifier).setFontPreference(val);
.setFontPreference(val ? 'serif' : 'sans');
}, },
itemBuilder: (context) => [
const PopupMenuItem(value: 'sans', child: Text('ゴシック (標準)')),
const PopupMenuItem(value: 'serif', child: Text('明朝 (上品)')),
const PopupMenuItem(value: 'digital', child: Text('ドット (レトロ)')),
],
), ),
), ),
const Divider(height: 1), 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 'ゴシック (標準)';
}
}
} }

View File

@ -7,7 +7,7 @@ class BackupSettingsSection extends StatefulWidget {
const BackupSettingsSection({ const BackupSettingsSection({
super.key, super.key,
this.title = 'データバックアップ', this.title = 'バックアップ・復元',
}); });
@override @override
@ -173,7 +173,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'バックアップにはWi-Fi環境を推奨します\n画像が多い場合、数百MB〜1GB以上になる可能性があります', 'バックアップやデータ復元はWi-Fi環境下を推奨します\nデータ量が数100MB1GB以上になる可能性があります',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: isDark ? Colors.blue[300] : Colors.blue[900], color: isDark ? Colors.blue[300] : Colors.blue[900],

View File

@ -31,7 +31,7 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
if (mounted) { if (mounted) {
setState(() { setState(() {
_appVersion = 'v${packageInfo.version}+${packageInfo.buildNumber} (Lite)'; _appVersion = 'v${packageInfo.version}+${packageInfo.buildNumber}';
}); });
} }
} }
@ -50,7 +50,7 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
if (widget.showBusinessMode) ...[ if (widget.showBusinessMode) ...[
ListTile( ListTile(
leading: Icon(LucideIcons.store, color: isDark ? Colors.orange[300] : Colors.orange), leading: Icon(LucideIcons.store, color: isDark ? Colors.orange[300] : Colors.orange),
title: const Text('飲食店モード (Beta)'), title: const Text('ビジネスモード (Beta)'),
subtitle: const Text('お品書き作成機能など'), subtitle: const Text('お品書き作成機能など'),
trailing: Switch( trailing: Switch(
value: userProfile.isBusinessMode, value: userProfile.isBusinessMode,