Fix compilation error in SakePriceDialog
This commit is contained in:
parent
3018d9a9d1
commit
318fa19bfb
|
|
@ -21,24 +21,24 @@ class PrefectureTileLayout {
|
|||
|
||||
static const Map<String, TilePosition> 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),
|
||||
|
|
|
|||
|
|
@ -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<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({
|
||||
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<String>? 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,16 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
|
|||
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<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@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<UserProfile> {
|
|||
..writeByte(12)
|
||||
..write(obj.nickname)
|
||||
..writeByte(13)
|
||||
..write(obj.gender);
|
||||
..write(obj.gender)
|
||||
..writeByte(14)
|
||||
..write(obj.totalExp)
|
||||
..writeByte(15)
|
||||
..write(obj.unlockedBadges);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -78,6 +78,14 @@ class UserProfileNotifier extends Notifier<UserProfile> {
|
|||
);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerProviderStateMixin {
|
||||
class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
CameraController? _controller;
|
||||
Future<void>? _initializeControllerFuture;
|
||||
bool _isTakingPicture = false;
|
||||
|
|
@ -362,10 +364,23 @@ class _CameraScreenState extends ConsumerState<CameraScreen> 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<CameraScreen> 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
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -903,7 +903,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
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<SakeDetailScreen> {
|
|||
}
|
||||
});
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<SoulScreen> {
|
|||
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<SoulScreen> {
|
|||
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),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ class _OnboardingDialogState extends State<OnboardingDialog> {
|
|||
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(),
|
||||
|
|
|
|||
|
|
@ -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<String>(
|
||||
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 'ゴシック (標準)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
|||
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<OtherSettingsSection> {
|
|||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue