321 lines
12 KiB
Dart
321 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:lucide_icons/lucide_icons.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
import '../providers/theme_provider.dart';
|
||
import '../providers/navigation_provider.dart'; // Navigation
|
||
import '../utils/translations.dart'; // Translation helper
|
||
import '../widgets/settings/display_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/badge_case.dart';
|
||
import '../widgets/gamification/activity_stats.dart';
|
||
import '../theme/app_colors.dart';
|
||
import '../services/mbti_types.dart'; // Needed for type title display
|
||
|
||
// v1.5
|
||
|
||
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);
|
||
final t = Translations(userProfile.locale); // Translation helper
|
||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text(t['myPage']),
|
||
centerTitle: true,
|
||
),
|
||
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: 16),
|
||
|
||
// Identity Section
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
LucideIcons.fingerprint,
|
||
size: 20,
|
||
color: appColors.iconDefault,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
t['profile'],
|
||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
color: appColors.textPrimary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Card(
|
||
color: appColors.surfaceSubtle,
|
||
child: Column(
|
||
children: [
|
||
ListTile(
|
||
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),
|
||
),
|
||
Divider(height: 1, color: appColors.divider),
|
||
ListTile(
|
||
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),
|
||
),
|
||
|
||
Divider(height: 1, color: appColors.divider),
|
||
// 1. Real MBTI (User Input) - Core Value for Recommendation
|
||
ListTile(
|
||
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),
|
||
),
|
||
Divider(height: 1, color: appColors.divider),
|
||
// 2. Sake Persona (AI Diagnosis) - Entertainment Value
|
||
ListTile(
|
||
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分析)',
|
||
style: TextStyle(color: appColors.textSecondary)
|
||
),
|
||
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
|
||
onTap: () {
|
||
// Navigate to Sommelier Tab (Index 2 in BottomNavBar)
|
||
ref.read(currentTabIndexProvider.notifier).setIndex(2);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
// Display Settings (新設 - カラーテーマ + グリッド + フォント + 明るさ)
|
||
const DisplaySettingsSection(),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
// other Settings
|
||
OtherSettingsSection(
|
||
title: t['otherSettings'],
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
BackupSettingsSection(),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// Simplified Dialog for Real MBTI Selection
|
||
void _showRealMbtiDialog(BuildContext context, String? current, Translations t) {
|
||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||
// Standard MBTI Types
|
||
const typesWithLabels = {
|
||
'INTJ': '建築家',
|
||
'INTP': '論理学者',
|
||
'ENTJ': '指揮官',
|
||
'ENTP': '討論者',
|
||
'INFJ': '提唱者',
|
||
'INFP': '仲介者',
|
||
'ENFJ': '主人公',
|
||
'ENFP': '広報運動家',
|
||
'ISTJ': '管理者',
|
||
'ISFJ': '擁護者',
|
||
'ESTJ': '幹部',
|
||
'ESFJ': '領事官',
|
||
'ISTP': '巨匠',
|
||
'ISFP': '冒険家',
|
||
'ESTP': '起業家',
|
||
'ESFP': 'エンターテイナー',
|
||
};
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => SimpleDialog(
|
||
title: Text(t['selectMbti']),
|
||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 16),
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0),
|
||
child: Text(
|
||
"診断済みのMBTIタイプを選択してください。\n性格に合った日本酒をおすすめします。",
|
||
style: TextStyle(fontSize: 12, color: appColors.textSecondary),
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: double.maxFinite,
|
||
height: 300,
|
||
child: ListView(
|
||
children: typesWithLabels.entries.map((entry) => SimpleDialogOption(
|
||
onPressed: () {
|
||
ref.read(userProfileProvider.notifier).setIdentity(mbti: entry.key);
|
||
Navigator.pop(context);
|
||
},
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)).toList(),
|
||
),
|
||
),
|
||
// 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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showNicknameDialog(BuildContext context, String? current, Translations t) {
|
||
final controller = TextEditingController(text: current);
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: Text(t['changeNickname']),
|
||
content: TextField(
|
||
controller: controller,
|
||
decoration: InputDecoration(hintText: t['enterName']),
|
||
autofocus: true,
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text(t['cancel']),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
ref.read(userProfileProvider.notifier).setIdentity(nickname: controller.text);
|
||
Navigator.pop(context);
|
||
},
|
||
child: Text(t['save']),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showGenderDialog(BuildContext context, String? current, Translations t) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => SimpleDialog(
|
||
title: Text(t['selectGender']),
|
||
children: [
|
||
_buildGenderOption(context, 'male', t['male'], current),
|
||
_buildGenderOption(context, 'female', t['female'], current),
|
||
_buildGenderOption(context, 'other', t['genderOther'], current),
|
||
_buildGenderOption(context, '', t['genderNotAnswer'], current),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildGenderOption(BuildContext context, String? value, String label, String? current) {
|
||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||
return SimpleDialogOption(
|
||
onPressed: () {
|
||
ref.read(userProfileProvider.notifier).setIdentity(gender: value);
|
||
Navigator.pop(context);
|
||
},
|
||
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),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
String _getGenderLabel(String? gender, Translations t) {
|
||
switch (gender) {
|
||
case 'male': return t['male'];
|
||
case 'female': return t['female'];
|
||
case 'other': return t['genderOther'];
|
||
default: return t['notSet'];
|
||
}
|
||
}
|
||
}
|