2026-01-29 15:54:22 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
|
|
|
import '../../models/sake_item.dart';
|
2026-02-15 15:13:12 +00:00
|
|
|
import '../../theme/app_colors.dart';
|
2026-01-29 15:54:22 +00:00
|
|
|
import '../sake_radar_chart.dart';
|
|
|
|
|
|
|
|
|
|
class SakeDetailChart extends StatelessWidget {
|
|
|
|
|
final SakeItem sake;
|
2026-02-15 15:13:12 +00:00
|
|
|
final ValueChanged<Map<String, int>>? onTasteStatsEdited;
|
2026-01-29 15:54:22 +00:00
|
|
|
|
2026-02-15 15:13:12 +00:00
|
|
|
const SakeDetailChart({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.sake,
|
|
|
|
|
this.onTasteStatsEdited,
|
|
|
|
|
});
|
2026-01-29 15:54:22 +00:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
if (sake.hiddenSpecs.tasteStats.isEmpty || sake.itemType == ItemType.set) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 15:13:12 +00:00
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
|
|
|
|
|
2026-01-29 15:54:22 +00:00
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(LucideIcons.barChart2,
|
|
|
|
|
size: 16, color: Theme.of(context).colorScheme.onSurface),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'Visual Tasting',
|
|
|
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
|
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-02-15 15:13:12 +00:00
|
|
|
const Spacer(),
|
|
|
|
|
if (onTasteStatsEdited != null)
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: Icon(LucideIcons.pencil, size: 18, color: appColors.iconSubtle),
|
|
|
|
|
tooltip: 'チャートを手動編集',
|
|
|
|
|
onPressed: () => _showEditModal(context),
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 200,
|
|
|
|
|
child: SakeRadarChart(
|
|
|
|
|
tasteStats: {
|
|
|
|
|
'aroma': sake.hiddenSpecs.sakeTasteStats.aroma.round(),
|
|
|
|
|
'sweetness': sake.hiddenSpecs.sakeTasteStats.sweetness.round(),
|
|
|
|
|
'acidity': sake.hiddenSpecs.sakeTasteStats.acidity.round(),
|
|
|
|
|
'bitterness': sake.hiddenSpecs.sakeTasteStats.bitterness.round(),
|
|
|
|
|
'body': sake.hiddenSpecs.sakeTasteStats.body.round(),
|
|
|
|
|
},
|
|
|
|
|
primaryColor: Theme.of(context).primaryColor,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-15 15:13:12 +00:00
|
|
|
|
|
|
|
|
void _showEditModal(BuildContext context) {
|
|
|
|
|
final currentStats = {
|
|
|
|
|
'aroma': sake.hiddenSpecs.sakeTasteStats.aroma.round(),
|
|
|
|
|
'sweetness': sake.hiddenSpecs.sakeTasteStats.sweetness.round(),
|
|
|
|
|
'acidity': sake.hiddenSpecs.sakeTasteStats.acidity.round(),
|
|
|
|
|
'bitterness': sake.hiddenSpecs.sakeTasteStats.bitterness.round(),
|
|
|
|
|
'body': sake.hiddenSpecs.sakeTasteStats.body.round(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
builder: (context) => _TasteEditModal(
|
|
|
|
|
initialStats: currentStats,
|
|
|
|
|
onSave: (newStats) {
|
|
|
|
|
onTasteStatsEdited?.call(newStats);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 五味チャート編集モーダル
|
|
|
|
|
class _TasteEditModal extends StatefulWidget {
|
|
|
|
|
final Map<String, int> initialStats;
|
|
|
|
|
final ValueChanged<Map<String, int>> onSave;
|
|
|
|
|
|
|
|
|
|
const _TasteEditModal({
|
|
|
|
|
required this.initialStats,
|
|
|
|
|
required this.onSave,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<_TasteEditModal> createState() => _TasteEditModalState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _TasteEditModalState extends State<_TasteEditModal> {
|
|
|
|
|
late Map<String, int> _editingStats;
|
|
|
|
|
|
|
|
|
|
// 日本語ラベル
|
|
|
|
|
static const Map<String, String> _labels = {
|
|
|
|
|
'aroma': '香り',
|
|
|
|
|
'sweetness': '甘み',
|
|
|
|
|
'acidity': '酸味',
|
|
|
|
|
'bitterness': 'キレ',
|
|
|
|
|
'body': 'コク',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// アイコン
|
|
|
|
|
static const Map<String, IconData> _icons = {
|
|
|
|
|
'aroma': LucideIcons.wind,
|
|
|
|
|
'sweetness': LucideIcons.candy,
|
|
|
|
|
'acidity': LucideIcons.citrus,
|
|
|
|
|
'bitterness': LucideIcons.zap,
|
|
|
|
|
'body': LucideIcons.droplets,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_editingStats = Map.from(widget.initialStats);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
),
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(24),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
// Handle bar
|
|
|
|
|
Container(
|
|
|
|
|
width: 40,
|
|
|
|
|
height: 4,
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: appColors.divider,
|
|
|
|
|
borderRadius: BorderRadius.circular(2),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// Header
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'チャートを編集',
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
// リセット: 元の値に戻す
|
|
|
|
|
setState(() {
|
|
|
|
|
_editingStats = Map.from(widget.initialStats);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
child: Text(
|
|
|
|
|
'リセット',
|
|
|
|
|
style: TextStyle(color: appColors.textSecondary),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'AI解析の値を手動で微調整できます',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: appColors.textSecondary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
// Sliders
|
|
|
|
|
..._labels.entries.map((entry) {
|
|
|
|
|
final key = entry.key;
|
|
|
|
|
final label = entry.value;
|
|
|
|
|
final icon = _icons[key] ?? LucideIcons.circle;
|
|
|
|
|
final value = _editingStats[key] ?? 3;
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(icon, size: 18, color: appColors.brandPrimary),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 40,
|
|
|
|
|
child: Text(
|
|
|
|
|
label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
color: appColors.textPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: SliderTheme(
|
|
|
|
|
data: SliderTheme.of(context).copyWith(
|
|
|
|
|
activeTrackColor: appColors.brandPrimary,
|
|
|
|
|
inactiveTrackColor: appColors.divider,
|
|
|
|
|
thumbColor: appColors.brandPrimary,
|
|
|
|
|
overlayColor: appColors.brandPrimary.withValues(alpha: 0.1),
|
|
|
|
|
trackHeight: 4,
|
|
|
|
|
),
|
|
|
|
|
child: Slider(
|
|
|
|
|
value: value.toDouble(),
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 5,
|
|
|
|
|
divisions: 5,
|
|
|
|
|
onChanged: (newValue) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_editingStats[key] = newValue.round();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 24,
|
|
|
|
|
child: Text(
|
|
|
|
|
'$value',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: appColors.brandPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
// Buttons
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: OutlinedButton(
|
|
|
|
|
onPressed: () => Navigator.pop(context),
|
|
|
|
|
style: OutlinedButton.styleFrom(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
|
|
|
),
|
|
|
|
|
child: const Text('キャンセル'),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
widget.onSave(_editingStats);
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: appColors.brandPrimary,
|
|
|
|
|
foregroundColor: appColors.surfaceSubtle,
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
|
|
|
),
|
|
|
|
|
child: const Text('保存'),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-29 15:54:22 +00:00
|
|
|
}
|