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 = {
// 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),

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

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) {
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,

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,
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(),

View File

@ -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 'ゴシック (標準)';
}
}
}

View File

@ -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データ量が数100MB1GB以上になる可能性があります',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.blue[300] : Colors.blue[900],

View File

@ -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,