ponshu-room-lite/lib/screens/soul_screen.dart

321 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'];
}
}
}