2026-01-11 08:17:29 +00:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2026-01-11 08:17:29 +00:00
|
|
|
|
import '../providers/theme_provider.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
|
import '../providers/navigation_provider.dart'; // Navigation
|
|
|
|
|
|
import '../utils/translations.dart'; // Translation helper
|
|
|
|
|
|
import '../widgets/settings/display_settings_section.dart';
|
2026-01-11 08:17:29 +00:00
|
|
|
|
import '../widgets/settings/other_settings_section.dart';
|
|
|
|
|
|
import '../widgets/settings/backup_settings_section.dart';
|
2026-01-13 09:13:23 +00:00
|
|
|
|
import '../widgets/gamification/level_title_card.dart';
|
|
|
|
|
|
import '../widgets/gamification/badge_case.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
|
import '../widgets/gamification/activity_stats.dart';
|
|
|
|
|
|
import '../theme/app_colors.dart';
|
|
|
|
|
|
import '../services/mbti_types.dart'; // Needed for type title display
|
2026-04-05 01:11:54 +00:00
|
|
|
|
import '../main.dart' show isBusinessApp;
|
2026-01-29 15:54:22 +00:00
|
|
|
|
|
2026-01-11 08:17:29 +00:00
|
|
|
|
class SoulScreen extends ConsumerStatefulWidget {
|
|
|
|
|
|
const SoulScreen({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
ConsumerState<SoulScreen> createState() => _SoulScreenState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _SoulScreenState extends ConsumerState<SoulScreen> {
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final userProfile = ref.watch(userProfileProvider);
|
2026-01-29 15:54:22 +00:00
|
|
|
|
final t = Translations(userProfile.locale); // Translation helper
|
|
|
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
2026-01-11 08:17:29 +00:00
|
|
|
|
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
appBar: AppBar(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
title: Text(t['myPage']),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
centerTitle: true,
|
|
|
|
|
|
),
|
2026-04-05 04:35:03 +00:00
|
|
|
|
body: Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Ambient Glow — 左上の薄い光
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: -80,
|
|
|
|
|
|
left: -60,
|
|
|
|
|
|
child: IgnorePointer(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 320,
|
|
|
|
|
|
height: 320,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
gradient: RadialGradient(
|
|
|
|
|
|
colors: [
|
2026-04-05 05:33:40 +00:00
|
|
|
|
appColors.brandPrimary.withValues(alpha: 0.12),
|
2026-04-05 04:35:03 +00:00
|
|
|
|
Colors.transparent,
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
ListView(
|
2026-01-11 08:17:29 +00:00
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
children: [
|
2026-01-13 09:13:23 +00:00
|
|
|
|
|
2026-04-05 04:32:52 +00:00
|
|
|
|
// 成長記録セクションヘッダー
|
|
|
|
|
|
_buildSectionHeader(context, '成長記録', LucideIcons.activity, appColors),
|
|
|
|
|
|
|
|
|
|
|
|
// Gamification Section
|
2026-01-13 09:13:23 +00:00
|
|
|
|
const LevelTitleCard(),
|
2026-04-05 04:32:52 +00:00
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
const ActivityStats(),
|
|
|
|
|
|
const SizedBox(height: 12),
|
2026-01-13 09:13:23 +00:00
|
|
|
|
const BadgeCase(),
|
2026-04-05 04:32:52 +00:00
|
|
|
|
const SizedBox(height: 24),
|
2026-01-13 09:13:23 +00:00
|
|
|
|
|
2026-04-05 04:32:52 +00:00
|
|
|
|
// プロフィールセクションヘッダー
|
|
|
|
|
|
_buildSectionHeader(context, t['profile'], LucideIcons.fingerprint, appColors),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
Card(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
color: appColors.surfaceSubtle,
|
2026-01-11 08:17:29 +00:00
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
ListTile(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
leading: Icon(LucideIcons.user, color: appColors.iconDefault),
|
|
|
|
|
|
title: Text(t['nickname'], style: TextStyle(color: appColors.textPrimary)),
|
|
|
|
|
|
subtitle: Text(userProfile.nickname ?? t['notSet'], style: TextStyle(color: appColors.textSecondary)),
|
|
|
|
|
|
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
|
|
|
|
|
|
onTap: () => _showNicknameDialog(context, userProfile.nickname, t),
|
2026-01-13 09:13:23 +00:00
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
Divider(height: 1, color: appColors.divider),
|
2026-01-13 09:13:23 +00:00
|
|
|
|
ListTile(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
leading: Icon(LucideIcons.personStanding, color: appColors.iconDefault),
|
|
|
|
|
|
title: Text(t['gender'], style: TextStyle(color: appColors.textPrimary)),
|
|
|
|
|
|
subtitle: Text(_getGenderLabel(userProfile.gender, t), style: TextStyle(color: appColors.textSecondary)),
|
|
|
|
|
|
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
|
|
|
|
|
|
onTap: () => _showGenderDialog(context, userProfile.gender, t),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
2026-04-05 04:32:52 +00:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
|
|
// あなたの日本酒キャラセクションヘッダー
|
|
|
|
|
|
_buildSectionHeader(context, 'あなたの日本酒キャラ', LucideIcons.sparkles, appColors),
|
|
|
|
|
|
Card(
|
|
|
|
|
|
color: appColors.surfaceSubtle,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Real MBTI (User Input)
|
2026-01-11 08:17:29 +00:00
|
|
|
|
ListTile(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
leading: Icon(LucideIcons.brainCircuit, color: appColors.iconDefault),
|
|
|
|
|
|
title: Text("あなたのMBTI", style: TextStyle(color: appColors.textPrimary)),
|
|
|
|
|
|
subtitle: Text(userProfile.mbti ?? t['notSet'], style: TextStyle(color: appColors.textSecondary)),
|
|
|
|
|
|
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
|
|
|
|
|
|
onTap: () => _showRealMbtiDialog(context, userProfile.mbti, t),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
Divider(height: 1, color: appColors.divider),
|
2026-04-05 04:32:52 +00:00
|
|
|
|
// Sake Persona (AI Diagnosis)
|
2026-01-13 00:57:18 +00:00
|
|
|
|
ListTile(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
leading: Icon(LucideIcons.sparkles, color: appColors.brandAccent),
|
|
|
|
|
|
title: Text(t['mbtiDiagnosis'], style: TextStyle(color: appColors.textPrimary)),
|
|
|
|
|
|
subtitle: Text(
|
|
|
|
|
|
userProfile.sakePersonaMbti != null
|
|
|
|
|
|
? MBTIType.types[userProfile.sakePersonaMbti]?.title ?? userProfile.sakePersonaMbti!
|
|
|
|
|
|
: '未診断(AI分析)',
|
2026-04-04 17:51:42 +00:00
|
|
|
|
style: TextStyle(color: appColors.textSecondary),
|
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-01-29 15:54:22 +00:00
|
|
|
|
),
|
|
|
|
|
|
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
ref.read(currentTabIndexProvider.notifier).setIndex(2);
|
|
|
|
|
|
},
|
2026-01-13 00:57:18 +00:00
|
|
|
|
),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
|
// Display Settings (新設 - カラーテーマ + グリッド + フォント + 明るさ)
|
|
|
|
|
|
const DisplaySettingsSection(),
|
|
|
|
|
|
|
2026-01-11 08:17:29 +00:00
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
|
|
// other Settings
|
2026-01-29 15:54:22 +00:00
|
|
|
|
OtherSettingsSection(
|
|
|
|
|
|
title: t['otherSettings'],
|
2026-04-05 01:11:54 +00:00
|
|
|
|
showBusinessMode: isBusinessApp,
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
BackupSettingsSection(),
|
|
|
|
|
|
],
|
2026-04-05 04:35:03 +00:00
|
|
|
|
), // ListView
|
|
|
|
|
|
], // Stack children
|
|
|
|
|
|
), // Stack
|
2026-01-11 08:17:29 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 04:32:52 +00:00
|
|
|
|
Widget _buildSectionHeader(BuildContext context, String title, IconData icon, AppColors appColors) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8, left: 4, right: 4),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(icon, size: 20, color: appColors.iconDefault),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
title,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
color: appColors.textPrimary,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
|
// Simplified Dialog for Real MBTI Selection
|
|
|
|
|
|
void _showRealMbtiDialog(BuildContext context, String? current, Translations t) {
|
|
|
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
|
|
|
|
|
// Standard MBTI Types
|
2026-01-11 08:17:29 +00:00
|
|
|
|
const typesWithLabels = {
|
|
|
|
|
|
'INTJ': '建築家',
|
|
|
|
|
|
'INTP': '論理学者',
|
|
|
|
|
|
'ENTJ': '指揮官',
|
|
|
|
|
|
'ENTP': '討論者',
|
|
|
|
|
|
'INFJ': '提唱者',
|
|
|
|
|
|
'INFP': '仲介者',
|
|
|
|
|
|
'ENFJ': '主人公',
|
|
|
|
|
|
'ENFP': '広報運動家',
|
|
|
|
|
|
'ISTJ': '管理者',
|
|
|
|
|
|
'ISFJ': '擁護者',
|
|
|
|
|
|
'ESTJ': '幹部',
|
|
|
|
|
|
'ESFJ': '領事官',
|
|
|
|
|
|
'ISTP': '巨匠',
|
|
|
|
|
|
'ISFP': '冒険家',
|
|
|
|
|
|
'ESTP': '起業家',
|
|
|
|
|
|
'ESFP': 'エンターテイナー',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
showDialog(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
builder: (context) => SimpleDialog(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
title: Text(t['selectMbti']),
|
|
|
|
|
|
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 16),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
children: [
|
2026-01-29 15:54:22 +00:00
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
"診断済みのMBTIタイプを選択してください。\n性格に合った日本酒をおすすめします。",
|
|
|
|
|
|
style: TextStyle(fontSize: 12, color: appColors.textSecondary),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: double.maxFinite,
|
2026-01-29 15:54:22 +00:00
|
|
|
|
height: 300,
|
2026-01-11 08:17:29 +00:00
|
|
|
|
child: ListView(
|
|
|
|
|
|
children: typesWithLabels.entries.map((entry) => SimpleDialogOption(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
ref.read(userProfileProvider.notifier).setIdentity(mbti: entry.key);
|
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
|
},
|
2026-01-29 15:54:22 +00:00
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(
|
|
|
|
|
|
entry.key == current ? Icons.check_circle : Icons.circle_outlined,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
color: entry.key == current
|
|
|
|
|
|
? appColors.brandPrimary
|
|
|
|
|
|
: appColors.iconSubtle,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: RichText(
|
|
|
|
|
|
text: TextSpan(
|
|
|
|
|
|
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 16),
|
|
|
|
|
|
children: [
|
|
|
|
|
|
TextSpan(
|
|
|
|
|
|
text: entry.key,
|
|
|
|
|
|
style: TextStyle(fontWeight: FontWeight.bold, color: appColors.textPrimary),
|
|
|
|
|
|
),
|
|
|
|
|
|
TextSpan(
|
|
|
|
|
|
text: ' (${entry.value})',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
color: appColors.textSecondary,
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
|
|
|
|
|
)).toList(),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
// Link to 16Personalities
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
|
child: InkWell(
|
|
|
|
|
|
onTap: () async {
|
|
|
|
|
|
final uri = Uri.parse('https://www.16personalities.com/ja/無料性格診断テスト');
|
|
|
|
|
|
if (await canLaunchUrl(uri)) {
|
|
|
|
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (context.mounted) {
|
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
|
const SnackBar(content: Text('ブラウザを開けませんでした')),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'自分のタイプがわからない場合\n(16Personalitiesで診断)',
|
|
|
|
|
|
style: TextStyle(fontSize: 10, color: appColors.brandAccent, decoration: TextDecoration.underline),
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
),
|
2026-01-11 08:17:29 +00:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
|
void _showNicknameDialog(BuildContext context, String? current, Translations t) {
|
2026-01-13 00:57:18 +00:00
|
|
|
|
final controller = TextEditingController(text: current);
|
|
|
|
|
|
showDialog(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
builder: (context) => AlertDialog(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
title: Text(t['changeNickname']),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
content: TextField(
|
|
|
|
|
|
controller: controller,
|
2026-01-29 15:54:22 +00:00
|
|
|
|
decoration: InputDecoration(hintText: t['enterName']),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
autofocus: true,
|
|
|
|
|
|
),
|
|
|
|
|
|
actions: [
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () => Navigator.pop(context),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
child: Text(t['cancel']),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
),
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
ref.read(userProfileProvider.notifier).setIdentity(nickname: controller.text);
|
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
|
},
|
2026-01-29 15:54:22 +00:00
|
|
|
|
child: Text(t['save']),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
|
void _showGenderDialog(BuildContext context, String? current, Translations t) {
|
2026-01-13 00:57:18 +00:00
|
|
|
|
showDialog(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
builder: (context) => SimpleDialog(
|
2026-01-29 15:54:22 +00:00
|
|
|
|
title: Text(t['selectGender']),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
children: [
|
2026-01-29 15:54:22 +00:00
|
|
|
|
_buildGenderOption(context, 'male', t['male'], current),
|
|
|
|
|
|
_buildGenderOption(context, 'female', t['female'], current),
|
|
|
|
|
|
_buildGenderOption(context, 'other', t['genderOther'], current),
|
|
|
|
|
|
_buildGenderOption(context, '', t['genderNotAnswer'], current),
|
2026-01-13 00:57:18 +00:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildGenderOption(BuildContext context, String? value, String label, String? current) {
|
2026-01-29 15:54:22 +00:00
|
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
2026-01-13 00:57:18 +00:00
|
|
|
|
return SimpleDialogOption(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
ref.read(userProfileProvider.notifier).setIdentity(gender: value);
|
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
|
},
|
2026-01-29 15:54:22 +00:00
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(
|
|
|
|
|
|
value == current ? Icons.check_circle : Icons.circle_outlined,
|
|
|
|
|
|
color: value == current
|
|
|
|
|
|
? appColors.brandPrimary
|
|
|
|
|
|
: appColors.iconSubtle,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
Text(label),
|
|
|
|
|
|
],
|
2026-01-13 00:57:18 +00:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
|
String _getGenderLabel(String? gender, Translations t) {
|
2026-01-13 00:57:18 +00:00
|
|
|
|
switch (gender) {
|
2026-01-29 15:54:22 +00:00
|
|
|
|
case 'male': return t['male'];
|
|
|
|
|
|
case 'female': return t['female'];
|
|
|
|
|
|
case 'other': return t['genderOther'];
|
|
|
|
|
|
default: return t['notSet'];
|
2026-01-13 00:57:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-11 08:17:29 +00:00
|
|
|
|
}
|