import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io'; import '../../models/sake_item.dart'; import '../../providers/menu_providers.dart'; import '../../screens/sake_detail_screen.dart'; import '../../theme/app_theme.dart'; import '../../theme/app_colors.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../common/pressable.dart'; class SakeListItem extends ConsumerWidget { final SakeItem sake; final bool isMenuMode; final int? index; // For ReorderableDragStartListener const SakeListItem({ super.key, required this.sake, required this.isMenuMode, this.index, }); @override Widget build(BuildContext context, WidgetRef ref) { final isSelected = ref.watch(selectedMenuSakeIdsProvider).contains(sake.id); final appColors = Theme.of(context).extension()!; // Adaptive selection color final selectedColor = appColors.brandAccent.withValues(alpha: 0.15); return Pressable( child: Card( clipBehavior: Clip.antiAlias, elevation: 1, color: isMenuMode && isSelected ? selectedColor : null, shape: isMenuMode && isSelected ? RoundedRectangleBorder(side: BorderSide(color: appColors.brandAccent, width: 2), borderRadius: BorderRadius.circular(6)) : RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), // Reverted to 6px as requested child: InkWell( onTap: () { if (isMenuMode) { ref.read(selectedMenuSakeIdsProvider.notifier).toggle(sake.id); return; } Navigator.of(context).push( MaterialPageRoute( builder: (context) => SakeDetailScreen(sake: sake), ), ); }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Selection Checkbox for ListView if (isMenuMode) Padding( padding: const EdgeInsets.symmetric(vertical: AppTheme.spacingXLarge, horizontal: AppTheme.spacingMedium), child: Icon( isSelected ? Icons.check_circle : Icons.check_circle_outline, color: isSelected ? appColors.brandPrimary : appColors.iconSubtle, size: 28, ), ), ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 80, // 100 → 80(縦長に見えるように横幅を縮小) height: 120, // 100 → 120(縦長のアスペクト比: 2:3) child: Hero( tag: sake.id, child: sake.displayData.imagePaths.isNotEmpty ? Image.file( File(sake.displayData.imagePaths.first), fit: BoxFit.cover, // 縦長の瓶がつぶれずに表示される cacheWidth: 160, // 80 x 2 (高解像度対応) cacheHeight: 240, // 120 x 2 // 段階的に画像を表示(体感速度向上) frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) return child; return AnimatedOpacity( opacity: frame == null ? 0 : 1, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, child: child, ); }, errorBuilder: (context, error, stackTrace) { return Container( color: appColors.surfaceSubtle, child: Center( child: Icon( LucideIcons.imageOff, color: appColors.iconSubtle, ), ), ); }, ) : (sake.itemType == ItemType.set ? Image.asset( 'assets/images/set_placeholder.png', fit: BoxFit.cover, ) : Container( color: appColors.surfaceSubtle, child: Center( child: Icon( LucideIcons.image, size: 40, color: appColors.iconSubtle, ), ), )), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(AppTheme.spacingMedium), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ // セット商品バッジ if (sake.itemType == ItemType.set) Container( margin: const EdgeInsets.only(right: 6), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: appColors.brandAccent, borderRadius: BorderRadius.circular(4), ), child: Text( 'セット', style: TextStyle( color: appColors.surfaceElevated, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), Expanded( child: Text( sake.displayData.displayName, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), ], ), ), if (sake.userData.isFavorite && !isMenuMode) const Icon(Icons.favorite, color: Colors.pink, size: 16), ], ), const SizedBox(height: 3), // 酒蔵 · 都道府県(セット商品では非表示) if (sake.itemType != ItemType.set && (sake.displayData.displayBrewery.isNotEmpty || sake.displayData.displayPrefecture.isNotEmpty)) Text( '${sake.displayData.displayBrewery} · ${sake.displayData.displayPrefecture}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: appColors.textSecondary, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), // セット商品の説明文(ユーザー入力のため維持) if (sake.itemType == ItemType.set && sake.displayData.catchCopy != null) Text( sake.displayData.catchCopy!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: appColors.textSecondary, fontStyle: FontStyle.italic, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), // 特定名称 + アルコール度数(ラベルから直接読んだ正確な情報) if (!isMenuMode && sake.itemType != ItemType.set) ...[ const SizedBox(height: 6), _buildSpecLine(context, appColors), ], ], ), ), ), ], ), // Row ), // InkWell ), // Card ); // Pressable } /// 特定名称 + アルコール度数(どちらもある場合は両方、片方のみも対応) Widget _buildSpecLine(BuildContext context, AppColors appColors) { final type = sake.hiddenSpecs.type; final alcohol = sake.hiddenSpecs.alcoholContent; final parts = []; if (type != null && type.isNotEmpty) parts.add(type); if (alcohol != null) parts.add('${alcohol.toStringAsFixed(alcohol.truncateToDouble() == alcohol ? 0 : 1)}%'); if (parts.isEmpty) return const SizedBox.shrink(); return Text( parts.join(' · '), style: TextStyle( fontSize: 11, color: appColors.textSecondary, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ); } }