ponshu-room-lite/lib/widgets/sake_3d_carousel_with_reaso...

240 lines
8.0 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 'dart:io';
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import '../services/sake_recommendation_service.dart';
import '../screens/sake_detail_screen.dart';
import 'package:lucide_icons/lucide_icons.dart';
/// 3D風カルーセルウィジェット推薦理由付き
/// 円形配置で左右対称、奥行き感のあるローリングスライダー
class Sake3DCarouselWithReason extends StatefulWidget {
final List<RecommendedSake> recommendations;
final double height;
const Sake3DCarouselWithReason({
super.key,
required this.recommendations,
this.height = 240,
});
@override
State<Sake3DCarouselWithReason> createState() => _Sake3DCarouselWithReasonState();
}
class _Sake3DCarouselWithReasonState extends State<Sake3DCarouselWithReason> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.recommendations.isEmpty) {
return SizedBox(
height: widget.height,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.info, color: Colors.grey[400], size: 32),
const SizedBox(height: 8),
Text(
'関連する日本酒を追加すると\nおすすめが表示されます',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
);
}
return Column(
children: [
CarouselSlider.builder(
itemCount: widget.recommendations.length,
options: CarouselOptions(
height: widget.height - 40,
enlargeCenterPage: true,
enlargeFactor: 0.3, // 中央の拡大率
viewportFraction: 0.45, // 左右の見える範囲(広くすると両側が見える)
enableInfiniteScroll: widget.recommendations.length > 2, // 3枚以上で無限ループ
autoPlay: false,
onPageChanged: (index, reason) {
setState(() {
_currentIndex = index;
});
},
),
itemBuilder: (context, index, realIndex) {
final rec = widget.recommendations[index];
final isCurrent = index == _currentIndex;
return _buildCarouselCard(rec, isCurrent);
},
),
const SizedBox(height: 12),
// インジケーター
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
widget.recommendations.length,
(index) => Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentIndex == index
? Theme.of(context).colorScheme.secondary
: Colors.grey[400],
),
),
),
),
],
);
}
Widget _buildCarouselCard(RecommendedSake rec, bool isCurrent) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SakeDetailScreen(sake: rec.item),
),
);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
margin: EdgeInsets.symmetric(
vertical: isCurrent ? 0 : 12,
horizontal: 8,
),
child: Card(
elevation: isCurrent ? 12 : 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: Stack(
fit: StackFit.expand,
children: [
// 背景画像
rec.item.displayData.imagePaths.isNotEmpty
? Image.file(
File(rec.item.displayData.imagePaths.first),
fit: BoxFit.cover,
)
: Container(
color: Colors.grey[300],
child: Icon(
LucideIcons.image,
size: 50,
color: Colors.grey[600],
),
),
// グラデーションオーバーレイ
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
),
),
),
// テキスト情報
Positioned(
bottom: 16,
left: 12,
right: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
rec.item.displayData.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
color: Colors.black,
blurRadius: 4,
),
],
),
),
const SizedBox(height: 4),
Text(
rec.item.displayData.prefecture,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 11,
shadows: const [
Shadow(
color: Colors.black,
blurRadius: 4,
),
],
),
),
const SizedBox(height: 8),
// 推薦理由
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4),
),
child: Text(
rec.reason,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
),
),
// 中央のカードにインジケーター
if (isCurrent)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
child: const Icon(
Icons.star,
color: Colors.white,
size: 16,
),
),
),
],
),
),
),
);
}
}