import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; import 'package:path_provider/path_provider.dart'; import '../../providers/sake_list_provider.dart'; import '../../services/shuko_diagnosis_service.dart'; import '../../providers/theme_provider.dart'; // v1.1 Fix import '../../theme/app_colors.dart'; import '../../widgets/sake_radar_chart.dart'; import '../../widgets/contextual_help_icon.dart'; import '../../services/mbti_diagnosis_service.dart'; import '../../widgets/mbti/mbti_result_card.dart'; import '../../widgets/mbti_analyzing_dialog.dart'; import '../../services/mbti_types.dart'; import '../../models/user_profile.dart'; // Ensure UserProfile is available import '../../models/sake_item.dart'; // Ensure SakeItem is available import '../../constants/app_constants.dart'; import '../../widgets/sakenowa/sakenowa_ranking_section.dart'; import '../../widgets/sakenowa/sakenowa_new_recommendation_section.dart'; import '../../widgets/common/error_retry_widget.dart'; class SommelierScreen extends ConsumerStatefulWidget { const SommelierScreen({super.key}); @override ConsumerState createState() => _SommelierScreenState(); } class _SommelierScreenState extends ConsumerState { final ScreenshotController _screenshotController = ScreenshotController(); bool _isSharing = false; Future _shareCard() async { setState(() { _isSharing = true; }); try { final image = await _screenshotController.capture( delay: const Duration(milliseconds: 10), pixelRatio: 3.0, // High res for sharing ); if (image == null) return; final directory = await getTemporaryDirectory(); final imagePath = await File('${directory.path}/sommelier_card.png').create(); await imagePath.writeAsBytes(image); // Share the file (using deprecated Share API - migration to SharePlus planned) // ignore: deprecated_member_use await Share.shareXFiles( [XFile(imagePath.path)], text: '私の酒向タイプはこれ! #ポンシュルーム', ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('シェアに失敗しました: $e')), ); } } finally { if (mounted) { setState(() { _isSharing = false; }); } } } @override Widget build(BuildContext context) { final sakeListAsync = ref.watch(allSakeItemsProvider); // v1.2: 全件対象(フィルタ無視) final userProfile = ref.watch(userProfileProvider); // v1.1 final diagnosisService = ref.watch(shukoDiagnosisServiceProvider); return Scaffold( appBar: AppBar( title: const Text('AIソムリエ診断'), centerTitle: true, ), body: sakeListAsync.when( data: (sakeList) { final baseProfile = diagnosisService.diagnose(sakeList); // Personalize Title final personalizedTitle = diagnosisService.personalizeTitle( ShukoTitle(title: baseProfile.title, description: baseProfile.description), userProfile.gender ); return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), child: Column( children: [ /* Greeting Removed */ // const SizedBox(height: 8), Screenshot( controller: _screenshotController, child: _buildShukoCard(context, baseProfile, personalizedTitle, userProfile.nickname), // Pass nickname ), const SizedBox(height: 16), // Card下 _buildActionButtons(context), const SizedBox(height: 8), // ボタン下(チラ見せ強化) const Divider(), const SizedBox(height: 16), // 区切り線下 // --- New: MBTI Diagnosis Section --- _buildMBTIDiagnosisSection(context, userProfile, sakeList), const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), // --- New: New Sake Recommendations Section --- const SakenowaNewRecommendationSection(displayCount: 5), const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), // --- New: Sakenowa Ranking Section --- const SakenowaRankingSection(displayCount: 10), const SizedBox(height: 32), ], ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => ErrorRetryWidget( message: 'AIソムリエ診断の読み込みに失敗しました', details: err.toString(), onRetry: () => ref.refresh(allSakeItemsProvider), ), ), ); } Widget _buildShukoCard(BuildContext context, ShukoProfile profile, ShukoTitle titleInfo, String? nickname) { final appColors = Theme.of(context).extension()!; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), border: isDark ? null : Border.all(color: appColors.divider.withValues(alpha: 0.5), width: 1), // Add border for light mode visibility // Premium Card Gradient (Adjusted for Dark Mode visibility) gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isDark ? [ // Dark Mode: Lighter grey for visibility against black background const Color(0xFF2C2C2E), const Color(0xFF1C1C1E), ] : [ // Light Mode: Original subtle gradient appColors.surfaceSubtle, appColors.surfaceElevated, ], ), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withValues(alpha: 0.5) // Deeper shadow for dark mode : Colors.black.withValues(alpha: 0.08), // Stronger shadow for light mode (was divider.withValues(alpha: 0.3)) blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Stack( children: [ // Background Pattern (Optional subtle decoration) Positioned( right: -20, top: -20, child: Icon( LucideIcons.sparkles, size: 150, color: appColors.brandAccent.withValues(alpha: 0.05), ), ), // Subtle Sake Emoji Positioned( left: 20, top: 20, child: Opacity( opacity: 0.3, // Subtle child: const Text( '🍶', style: TextStyle(fontSize: 40), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), // Optimized for チラ見せ effect child: Column( children: [ // 1. Header (Name & Rank) with unified help icon Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( (nickname != null && nickname.isNotEmpty) ? '$nicknameさんの酒向タイプ' : 'あなたの酒向タイプ', style: Theme.of(context).textTheme.bodySmall?.copyWith(letterSpacing: 1.5, color: appColors.textSecondary), ), const SizedBox(width: 4), ContextualHelpIcon( title: '酒向タイプ・チャートの見方', customContent: _buildUnifiedHelpContent(context), ), ], ), const SizedBox(height: 8), // 2. Title (no help icon - moved to header) Text( titleInfo.title, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: appColors.brandPrimary, shadows: [ Shadow( color: appColors.brandPrimary.withValues(alpha: 0.3), blurRadius: 10, offset: const Offset(0, 2), ), ], ), ), const SizedBox(height: 8), Text( titleInfo.description, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.5, color: appColors.textPrimary, ), ), const SizedBox(height: 32), SizedBox( height: 200, child: SakeRadarChart( tasteStats: { 'aroma': (profile.avgStats.aroma).round(), 'sweetness': (profile.avgStats.sweetness).round(), 'acidity': (profile.avgStats.acidity).round(), 'bitterness': (profile.avgStats.bitterness).round(), 'body': (profile.avgStats.body).round(), }, primaryColor: appColors.brandPrimary, ), ), const SizedBox(height: 24), // 4. Stats Footer Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: appColors.surfaceSubtle.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Text( '分析対象: ${profile.analyzedCount} 本', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: appColors.textSecondary), ), ), ], ), ), ], ), ); } Widget _buildActionButtons(BuildContext context) { final appColors = Theme.of(context).extension()!; return Column( children: [ // SizedBox(width: double.infinity) removed to allow button to size itself FilledButton.icon( onPressed: _isSharing ? null : _shareCard, icon: _isSharing ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Icon(LucideIcons.share2), label: const Text( 'シェア', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 16), // Wide padding for "Pill" look backgroundColor: appColors.brandPrimary, foregroundColor: appColors.surfaceSubtle, shape: const StadiumBorder(), ), ), ], ); } // --- MBTI Diagnosis Logic --- Widget _buildMBTIDiagnosisSection(BuildContext context, UserProfile userProfile, List sakeList) { final appColors = Theme.of(context).extension()!; return Card( elevation: 2, color: appColors.surfaceSubtle, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ Icon(LucideIcons.sparkles, size: 40, color: appColors.brandAccent), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( '酒向タイプ診断', // Removed (MBTI風) style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), textAlign: TextAlign.center, ), ), const SizedBox(width: 4), ContextualHelpIcon( title: 'MBTI風診断について', customContent: _buildMBTIHelpContent(context), ), ], ), const SizedBox(height: 8), Text( 'AIがあなたの飲酒スタイルを分析', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: appColors.textSecondary), ), const SizedBox(height: 24), // Result or Prompt if (userProfile.sakePersonaMbti != null) ...[ // Already Diagnosed Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: appColors.surfaceElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: appColors.brandAccent.withValues(alpha: 0.3)), ), child: Column( children: [ Text('あなたの診断結果', style: TextStyle(fontSize: 12, color: appColors.textSecondary)), const SizedBox(height: 8), Text( MBTIType.types[userProfile.sakePersonaMbti]?.title ?? userProfile.sakePersonaMbti!, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), ], ), ), const SizedBox(height: 24), ], // Action Button FilledButton.icon( onPressed: () => _runMBTIDiagnosis(context, sakeList), icon: const Icon(LucideIcons.brainCircuit), label: Text( userProfile.sakePersonaMbti == null ? '診断を開始する' : '再診断する', style: const TextStyle(fontWeight: FontWeight.bold), ), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), backgroundColor: appColors.brandPrimary, foregroundColor: Theme.of(context).colorScheme.onPrimary, shape: const StadiumBorder(), // Consistent shape ), ), if (sakeList.length < AppConstants.mbtiMinimumRecords) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '※診断には${AppConstants.mbtiMinimumRecords}本以上の記録が必要です (現在: ${sakeList.length}本)', style: TextStyle(color: appColors.error, fontSize: 11), ), ), ], ), ), ); } Future _runMBTIDiagnosis(BuildContext context, List sakeList) async { // 1. Check Data Count if (sakeList.length < AppConstants.mbtiMinimumRecords) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('データ不足です!あと${AppConstants.mbtiMinimumRecords - sakeList.length}本の記録が必要です。'), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 4), ), ); return; } // 2. BuildContextキャプチャ(async gapの前) if (!mounted) return; final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); // 3. Show MBTI Analyzing Dialog showDialog( context: context, barrierDismissible: false, builder: (_) => const MBTIAnalyzingDialog(), ); try { // 4. Simulate delay final minWait = Future.delayed(const Duration(seconds: 3)); // Logic final service = MBTIDiagnosisService(); final result = service.diagnose(sakeList); await minWait; // 5. async gap後はキャプチャしたnavigatorを使用 if (!mounted) return; navigator.pop(); // ✅ BuildContextを直接使わない // 6. Show Result Card if (!mounted) return; // contextは関数開始時(async gap前)にパラメータとして受け取り済み // showDialogは新しいWidgetツリーを構築するだけなので技術的に安全 // ignore: use_build_context_synchronously await showDialog( // ignore: use_build_context_synchronously context: context, builder: (dialogContext) => Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(16), child: MBTIResultCard( result: result, onShare: () { if (!mounted) return; messenger.showSnackBar(const SnackBar(content: Text('シェア機能は開発中です!'))); }, onShowRecommendations: () { navigator.pop(); // dialogContextではなくnavigatorを使用 // Save Result to "SakePersona" field (not Real MBTI) ref.read(userProfileProvider.notifier).setSakePersonaMbti(result.type.code); if (!mounted) return; messenger.showSnackBar( SnackBar( content: Text('「${result.type.title}」として診断結果を保存しました!'), duration: const Duration(seconds: 3), ), ); }, ), ), ); } catch (e) { debugPrint('Diagnosis Error: $e'); if (!mounted) return; navigator.pop(); messenger.showSnackBar(SnackBar(content: Text('エラー: $e'))); } } Widget _buildUnifiedHelpContent(BuildContext context) { final appColors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section 1: 酒向タイプとは Text( '酒向タイプとは?', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), ), const SizedBox(height: 8), Text( 'あなたのテイスティング記録から、AIが分析した「好みの傾向」を称号で表します。', style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.6, ), ), const SizedBox(height: 16), Text( '主な称号', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), ), const SizedBox(height: 8), _buildTitleExample(context, '辛口サムライ', 'キレのある辛口がお好み'), _buildTitleExample(context, 'フルーティーマスター', '華やかな香りと甘みを愛する'), _buildTitleExample(context, '旨口探求者', 'お米の旨みとコクを重視'), _buildTitleExample(context, '香りの貴族', '吟醸香など華やかな香りを楽しむ'), _buildTitleExample(context, 'バランスの賢者', '様々なタイプを楽しむオールラウンダー'), _buildTitleExample(context, '酒道の旅人', 'これから自分だけの味を見つける冒険者'), const SizedBox(height: 12), Text( '※ 記録が増えるほど、より正確な診断結果が得られます。\n※ 女性の方には一部の称号が変化します(例: サムライ→麗人)', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: appColors.textSecondary, height: 1.5, ), ), // Divider between sections const SizedBox(height: 24), Divider(color: appColors.divider, thickness: 1), const SizedBox(height: 16), // Section 2: チャートの見方 Text( 'チャートの見方', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), ), const SizedBox(height: 8), Text( 'AIがあなたの登録した日本酒の味覚データを分析し、好みの傾向をチャート化します。', style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.6, ), ), const SizedBox(height: 16), _buildChartAxisExplanation(context, '香り', '華やかな吟醸香、フルーティーさ'), _buildChartAxisExplanation(context, '甘み', '口当たりの甘さ、まろやかさ'), _buildChartAxisExplanation(context, '酸味', '爽やかな酸味、キレの良さ'), _buildChartAxisExplanation(context, 'キレ', '後味のキレ、ドライ感 (旧:苦味)'), _buildChartAxisExplanation(context, 'コク', 'お米の旨味、ボディ感'), ], ); } Widget _buildTitleExample(BuildContext context, String title, String description) { final appColors = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: const EdgeInsets.only(top: 6), width: 6, height: 6, decoration: BoxDecoration( color: appColors.brandAccent, shape: BoxShape.circle, ), ), const SizedBox(width: 12), Expanded( child: RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: appColors.textPrimary), children: [ TextSpan( text: '$title: ', style: const TextStyle(fontWeight: FontWeight.bold), ), TextSpan(text: description), ], ), ), ), ], ), ); } Widget _buildChartAxisExplanation(BuildContext context, String axis, String description) { final appColors = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: appColors.brandPrimary, shape: BoxShape.circle, ), ), const SizedBox(width: 12), Expanded( child: RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: appColors.textPrimary), children: [ TextSpan( text: '$axis: ', style: const TextStyle(fontWeight: FontWeight.bold), ), TextSpan(text: description), ], ), ), ), ], ), ); } Widget _buildMBTIHelpContent(BuildContext context) { final appColors = Theme.of(context).extension()!; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'MBTI風診断について', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), ), const SizedBox(height: 8), Text( 'あなたの日本酒の好みから、16種類の「酒向タイプ」を診断します。\nMBTI(性格診断)をモチーフにした、遊び心のある分析です。', style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.6, ), ), const SizedBox(height: 16), Text( '16種類のタイプ一覧', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: appColors.brandPrimary, ), ), const SizedBox(height: 12), // Generate list from MBTIType.types ...MBTIType.types.entries.map((entry) { final type = entry.value; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: appColors.surfaceElevated, borderRadius: BorderRadius.circular(8), border: Border.all(color: appColors.divider.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: appColors.brandAccent.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( type.code, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: appColors.brandAccent, ), ), ), const SizedBox(width: 8), Expanded( child: Text( type.title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 4), Text( type.catchphrase, style: TextStyle( fontSize: 11, fontStyle: FontStyle.italic, color: appColors.textSecondary, ), ), const SizedBox(height: 4), Text( '推奨: ${type.recommendedStyles}', style: TextStyle( fontSize: 10, color: appColors.textSecondary, ), ), ], ), ), ); }), const SizedBox(height: 8), Text( '※ 診断には${AppConstants.mbtiMinimumRecords}本以上の記録が必要です。\n※ より多くのデータを記録すると、診断精度が向上します。', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: appColors.textSecondary, height: 1.5, ), ), ], ), ); } }