ponshu-room-lite/lib/widgets/home/sake_list_item.dart

226 lines
9.6 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 '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<AppColors>()!;
// 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 = <String>[];
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,
);
}
}