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

216 lines
9.4 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';
// Haptic via InkWell? No, explicit HapticFeedback used generally.
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 Card(
clipBehavior: Clip.antiAlias,
elevation: 1, // Slight elevation
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: AppTheme.spacingTiny),
// Brand / Prefecture (セット商品では非表示)
if (sake.itemType != ItemType.set &&
(sake.displayData.displayBrewery.isNotEmpty || sake.displayData.displayPrefecture.isNotEmpty))
Row(
children: [
Expanded(
child: Text(
'${sake.displayData.displayBrewery} / ${sake.displayData.displayPrefecture}',
style: Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
),
],
),
// セット商品の説明文表示
if (sake.itemType == ItemType.set && sake.displayData.catchCopy != null)
Text(
sake.displayData.catchCopy!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (!isMenuMode && sake.hiddenSpecs.flavorTags.isNotEmpty) ...[
const SizedBox(height: AppTheme.spacingSmall),
Wrap(
spacing: 4,
runSpacing: 4,
children: sake.hiddenSpecs.flavorTags.take(3).map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: appColors.surfaceSubtle,
borderRadius: BorderRadius.circular(4),
),
child: Text(
tag,
style: TextStyle(fontSize: 10, color: appColors.textSecondary),
),
)).toList(),
)
]
],
),
),
),
],
),
),
);
}
}