import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/theme_provider.dart'; import '../providers/display_mode_provider.dart'; import 'camera_screen.dart'; import 'menu_creation_screen.dart'; import '../theme/app_theme.dart'; import '../providers/sake_list_provider.dart'; import '../providers/filter_providers.dart'; import '../providers/menu_providers.dart'; // Phase 2-1 import '../models/sake_item.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter/services.dart'; // Haptic import '../services/gemini_service.dart'; import '../services/image_compression_service.dart'; import '../widgets/analyzing_dialog.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:uuid/uuid.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' show join; import '../widgets/sake_search_delegate.dart'; import '../widgets/onboarding_dialog.dart'; import '../widgets/home/sake_filter_chips.dart'; import '../widgets/home/home_empty_state.dart'; import '../widgets/home/sake_no_match_state.dart'; import '../models/user_profile.dart'; // UserProfile import '../widgets/analyzing_dialog.dart'; import '../widgets/home/sake_list_view.dart'; import '../widgets/home/sake_grid_view.dart'; import '../widgets/add_set_item_dialog.dart'; import '../providers/ui_experiment_provider.dart'; // A/B Test import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../widgets/prefecture_filter_sheet.dart'; // Use a simple global variable for session check instead of StateProvider to avoid dependency issues bool _hasCheckedOnboarding = false; class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final displayMode = ref.watch(displayModeProvider); final sakeListAsync = ref.watch(sakeListProvider); // Onboarding Check (Run once per session) if (!_hasCheckedOnboarding) { Future.microtask(() { final profile = ref.read(userProfileProvider); if (!profile.hasCompletedOnboarding && context.mounted) { _showOnboardingDialog(context, ref); } _hasCheckedOnboarding = true; }); } // Filter States final isSearching = ref.watch(sakeSearchQueryProvider).isNotEmpty; final showFavorites = ref.watch(sakeFilterFavoriteProvider); final selectedPrefecture = ref.watch(sakeFilterPrefectureProvider); final isMenuMode = ref.watch(menuModeProvider); final userProfile = ref.watch(userProfileProvider); final isBusinessMode = userProfile.isBusinessMode; final hasItems = ref.watch(rawSakeListItemsProvider).asData?.value.isNotEmpty ?? false; return Scaffold( appBar: AppBar( title: isMenuMode ? const Text('お品書き作成', style: TextStyle(fontWeight: FontWeight.bold)) : (isSearching ? Row( children: [ Expanded( child: TextField( autofocus: true, decoration: const InputDecoration( hintText: '銘柄・酒蔵・都道府県...', border: InputBorder.none, hintStyle: TextStyle(color: Colors.white70), ), style: const TextStyle(color: Colors.white), onChanged: (value) => ref.read(sakeSearchQueryProvider.notifier).set(value), ), ), // Sort Button (Searching State) IconButton( icon: const Icon(LucideIcons.arrowUpDown), tooltip: '並び替え', onPressed: () => _showSortMenu(context, ref), ), ], ) : null), actions: [ // Normal Actions if (!isSearching) // Show Sort button here if not searching IconButton( icon: const Icon(LucideIcons.arrowUpDown), tooltip: '並び替え', onPressed: () => _showSortMenu(context, ref), ), IconButton( icon: const Icon(LucideIcons.search), onPressed: () => showSearch(context: context, delegate: SakeSearchDelegate(ref)), ), // ... rest of icons (Location, Favorite, DisplayMode, Guide) IconButton( icon: const Icon(LucideIcons.mapPin), onPressed: () => PrefectureFilterSheet.show(context), tooltip: '都道府県で絞り込み', color: selectedPrefecture != null ? AppTheme.posimaiBlue : null, ), IconButton( icon: Icon(showFavorites ? Icons.favorite : Icons.favorite_border), color: showFavorites ? Colors.pink : null, onPressed: () => ref.read(sakeFilterFavoriteProvider.notifier).toggle(), tooltip: 'Favorites Only', ), IconButton( icon: Icon(displayMode == 'list' ? LucideIcons.layoutGrid : LucideIcons.list), onPressed: () => ref.read(displayModeProvider.notifier).toggle(), ), IconButton( icon: const Icon(LucideIcons.helpCircle), onPressed: () => _showUsageGuide(context, ref), tooltip: 'ヘルプ・ガイド', ), ], ), body: SafeArea( child: Column( children: [ if (!isMenuMode && hasItems) SakeFilterChips( mode: isBusinessMode ? FilterChipMode.business : FilterChipMode.personal ), // Menu Info Banner Removed Expanded( child: sakeListAsync.when( data: (sakeList) { final showSelected = isMenuMode && ref.watch(menuShowSelectedOnlyProvider); List displayList; if (showSelected) { final orderedIds = ref.watch(menuOrderedIdsProvider); // Map Ordered Ids to Objects. // Note: O(N*M) if naive. Use Map for O(N). final sakeMap = {for (var s in sakeList) s.id: s}; displayList = orderedIds .map((id) => sakeMap[id]) .where((s) => s != null) .cast() .toList(); } else { displayList = sakeList; } // Check if Global List is empty vs Filtered List is empty final isListActuallyEmpty = ref.watch(rawSakeListItemsProvider).asData?.value.isEmpty ?? true; if (displayList.isEmpty) { if (showSelected) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.clipboardCheck, size: 60, color: Colors.grey[400]), const SizedBox(height: 16), const Text('お品書きに追加されたお酒はありません', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Text('リスト画面に戻って、掲載したいお酒の\nチェックボックスを選択してください', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey)), ], ), ); } else if (isListActuallyEmpty) { return HomeEmptyState(isMenuMode: isMenuMode); } else { return const SakeNoMatchState(); } } // Logic: Reorder only if Custom Sort is active (and not searching) final sortMode = ref.watch(sakeSortModeProvider); final isCustomSort = sortMode == SortMode.custom; final canReorder = isCustomSort && !isSearching; // Menu mode doesn't support reorder return displayMode == 'list' ? SakeListView(sakeList: displayList, isMenuMode: false, enableReorder: canReorder) : SakeGridView(sakeList: displayList, isMenuMode: false, enableReorder: canReorder); }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, st) => Center(child: Text('Error: $e')), ), ), ], ), ), floatingActionButton: isBusinessMode ? SpeedDial( icon: LucideIcons.plus, activeIcon: LucideIcons.x, backgroundColor: Colors.orange, foregroundColor: Colors.white, activeBackgroundColor: Colors.grey[800], overlayColor: Colors.black, overlayOpacity: 0.5, // A/B Test Animation animationCurve: ref.watch(uiExperimentProvider).fabAnimation == 'bounce' ? Curves.elasticOut : Curves.linear, animationDuration: ref.watch(uiExperimentProvider).fabAnimation == 'bounce' ? const Duration(milliseconds: 400) : const Duration(milliseconds: 250), spacing: 12, spaceBetweenChildren: 12, children: [ SpeedDialChild( child: const Text('🍶', style: TextStyle(fontSize: 24)), backgroundColor: Colors.white, label: 'お品書きを作成', labelStyle: const TextStyle(fontWeight: FontWeight.bold), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const MenuCreationScreen()), ); }, ), SpeedDialChild( child: const Icon(LucideIcons.packagePlus, color: Colors.orange), backgroundColor: Colors.white, label: 'セットを作成', labelStyle: const TextStyle(fontWeight: FontWeight.bold), onTap: () { showDialog( context: context, builder: (context) => const AddSetItemDialog(), ); }, ), SpeedDialChild( child: const Icon(LucideIcons.image, color: AppTheme.posimaiBlue), backgroundColor: Colors.white, label: 'ギャラリーから選択', labelStyle: const TextStyle(fontWeight: FontWeight.bold), onTap: () async { HapticFeedback.heavyImpact(); await _pickFromGallery(context); }, ), SpeedDialChild( child: const Icon(LucideIcons.camera, color: AppTheme.posimaiBlue), backgroundColor: Colors.white, label: 'カメラで撮影', labelStyle: const TextStyle(fontWeight: FontWeight.bold), onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const CameraScreen()), ); }, ), ], ) : GestureDetector( onLongPress: () async { HapticFeedback.heavyImpact(); await _pickFromGallery(context); }, child: FloatingActionButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const CameraScreen()), ); }, backgroundColor: AppTheme.posimaiBlue, foregroundColor: Colors.white, child: const Icon(LucideIcons.camera), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } Future _pickFromGallery(BuildContext context) async { final picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null && context.mounted) { _processImage(context, image.path); } } Future _processImage(BuildContext context, String sourcePath) async { try { // Show AnalyzingDialog immediately showDialog( context: context, barrierDismissible: false, builder: (context) => const AnalyzingDialog(), ); // Compress and copy image to app docs final directory = await getApplicationDocumentsDirectory(); final String targetPath = join(directory.path, '${const Uuid().v4()}.jpg'); final String imagePath = await ImageCompressionService.compressForGemini(sourcePath, targetPath: targetPath); // Gemini Analysis final geminiService = GeminiService(); final result = await geminiService.analyzeSakeLabel([imagePath]); // Create SakeItem // Create SakeItem (Schema v2.0) final sakeItem = SakeItem( id: const Uuid().v4(), displayData: DisplayData( name: result.name ?? '不明な日本酒', brewery: result.brand ?? '不明', prefecture: result.prefecture ?? '不明', catchCopy: result.catchCopy, imagePaths: [imagePath], rating: null, ), hiddenSpecs: HiddenSpecs( description: result.description, tasteStats: result.tasteStats, flavorTags: result.flavorTags, ), metadata: Metadata( createdAt: DateTime.now(), aiConfidence: result.confidenceScore, ), ); // Save to Hive final box = Hive.box('sake_items'); await box.add(sakeItem); // Prepend new item to sort order so it appears at the top final settingsBox = Hive.box('settings'); final List currentOrder = (settingsBox.get('sake_sort_order') as List?) ?.cast() ?? []; currentOrder.insert(0, sakeItem.id); // Insert at beginning await settingsBox.put('sake_sort_order', currentOrder); if (!context.mounted) return; // Close Dialog Navigator.of(context).pop(); // Success Message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${sakeItem.displayData.name} を登録しました!'), duration: const Duration(seconds: 2), ), ); } catch (e) { if (context.mounted) { // Attempt to pop dialog if it's open (this is heuristic, better state mgmt would be ideal) // But for now, we assume top is dialog. Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('解析エラー: $e')), ); } } } void _showOnboardingDialog(BuildContext context, WidgetRef ref) { showDialog( context: context, barrierDismissible: false, builder: (ctx) => OnboardingDialog( onFinish: () { Navigator.pop(ctx); ref.read(userProfileProvider.notifier).completeOnboarding(); }, ), ); } void _showUsageGuide(BuildContext context, WidgetRef ref) { final userProfile = ref.read(userProfileProvider); final isBusinessMode = userProfile.isBusinessMode; List>? pages; if (isBusinessMode) { pages = [ { 'title': 'ビジネスモードへようこそ', 'description': '飲食店様向けの機能を集約しました。\nメニュー作成から販促まで、\nプロの仕事を強力にサポートします。', 'icon': LucideIcons.store, }, { 'title': 'セット商品の作成', 'description': '飲み比べセットやコース料理など、\n複数のお酒をまとめた「セット商品」を\n簡単に作成・管理できます。', 'icon': LucideIcons.packagePlus, }, { 'title': 'インスタ販促', 'description': '本日のおすすめをSNSですぐに発信。\nInstaタブから、美しい画像を\nワンタップで生成できます。', 'icon': LucideIcons.instagram, }, { 'title': '売上分析', 'description': '売れ筋や味の傾向を分析。\nお客様に喜ばれるラインナップ作りを\nデータで支援します。', 'icon': LucideIcons.barChartBig, }, ]; } showDialog( context: context, barrierDismissible: true, builder: (ctx) => OnboardingDialog( pages: pages, onFinish: () => Navigator.pop(ctx), ), ); } void _showSortMenu(BuildContext context, WidgetRef ref) { final currentSort = ref.read(sakeSortModeProvider); showModalBottomSheet( context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Text('並び替え', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ), ListTile( leading: const Icon(LucideIcons.clock), title: const Text('新しい順(登録日)'), trailing: currentSort == SortMode.newest ? Icon(LucideIcons.check, color: AppTheme.posimaiBlue) : null, onTap: () { ref.read(sakeSortModeProvider.notifier).set(SortMode.newest); Navigator.pop(context); }, ), ListTile( leading: const Icon(LucideIcons.history), title: const Text('古い順(登録日)'), trailing: currentSort == SortMode.oldest ? Icon(LucideIcons.check, color: AppTheme.posimaiBlue) : null, onTap: () { ref.read(sakeSortModeProvider.notifier).set(SortMode.oldest); Navigator.pop(context); }, ), ListTile( leading: const Icon(LucideIcons.arrowDownAZ), title: const Text('名前順(あいうえお)'), trailing: currentSort == SortMode.name ? Icon(LucideIcons.check, color: AppTheme.posimaiBlue) : null, onTap: () { ref.read(sakeSortModeProvider.notifier).set(SortMode.name); Navigator.pop(context); }, ), ListTile( leading: const Icon(LucideIcons.gripHorizontal), title: const Text('カスタム(ドラッグ配置)'), trailing: currentSort == SortMode.custom ? Icon(LucideIcons.check, color: AppTheme.posimaiBlue) : null, onTap: () { ref.read(sakeSortModeProvider.notifier).set(SortMode.custom); Navigator.pop(context); }, ), const SizedBox(height: 16), ], ), ), ); } // Method _buildBusinessQuickFilters removed (Using SakeFilterChips instead) } // End of HomeScreen class