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

358 lines
13 KiB
Dart
Raw Normal View History

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
import '../main.dart' show isBusinessApp;
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: 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: [
appColors.brandPrimary.withValues(alpha: 0.12),
Colors.transparent,
],
),
),
),
),
),
ListView(
padding: const EdgeInsets.all(16),
children: [
// 成長記録セクションヘッダー
_buildSectionHeader(context, '成長記録', LucideIcons.activity, appColors),
// Gamification Section
const LevelTitleCard(),
const SizedBox(height: 12),
const ActivityStats(),
const SizedBox(height: 12),
const BadgeCase(),
const SizedBox(height: 24),
// プロフィールセクションヘッダー
_buildSectionHeader(context, t['profile'], LucideIcons.fingerprint, appColors),
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),
),
],
),
),
const SizedBox(height: 24),
// あなたの日本酒キャラセクションヘッダー
_buildSectionHeader(context, 'あなたの日本酒キャラ', LucideIcons.sparkles, appColors),
Card(
color: appColors.surfaceSubtle,
child: Column(
children: [
// Real MBTI (User Input)
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),
// Sake Persona (AI Diagnosis)
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),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Icon(LucideIcons.chevronRight, color: appColors.iconSubtle),
onTap: () {
ref.read(currentTabIndexProvider.notifier).setIndex(2);
},
),
],
),
),
const SizedBox(height: 24),
// Display Settings (新設 - カラーテーマ + グリッド + フォント + 明るさ)
const DisplaySettingsSection(),
const SizedBox(height: 24),
// other Settings
OtherSettingsSection(
title: t['otherSettings'],
showBusinessMode: isBusinessApp,
),
const SizedBox(height: 24),
BackupSettingsSection(),
],
), // ListView
], // Stack children
), // Stack
);
}
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,
),
),
],
),
);
}
// 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'];
}
}
}