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

358 lines
13 KiB
Dart
Raw 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
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'];
}
}
}