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