import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../models/sake_item.dart'; import '../../theme/app_colors.dart'; import 'package:hive_flutter/hive_flutter.dart'; class SakeDetailSpecs extends StatefulWidget { final SakeItem sake; final ValueChanged onUpdate; const SakeDetailSpecs({ super.key, required this.sake, required this.onUpdate, }); @override State createState() => _SakeDetailSpecsState(); } class _SakeDetailSpecsState extends State { bool _isEditing = false; // v1.0.12: ExpansionTileController deprecated → ExpansibleController migration final ExpansibleController _expansionController = ExpansibleController(); // Controllers late final TextEditingController _typeController; late final TextEditingController _polishingController; late final TextEditingController _alcoholController; late final TextEditingController _sakeMeterController; late final TextEditingController _riceController; late final TextEditingController _yeastController; late final TextEditingController _manufacturingController; // Unused in UI currently but reserved // TODO: Phase X で甘味度・ボディスコアの編集UIを追加する予定 /* late final TextEditingController _sweetnessController; late final TextEditingController _bodyController; */ @override void initState() { super.initState(); _initControllers(); } void _initControllers() { final specs = widget.sake.hiddenSpecs; _typeController = TextEditingController(text: specs.type); _polishingController = TextEditingController(text: specs.polishingRatio?.toString() ?? ''); _alcoholController = TextEditingController(text: specs.alcoholContent?.toString() ?? ''); _sakeMeterController = TextEditingController(text: specs.sakeMeterValue?.toString() ?? ''); _riceController = TextEditingController(text: specs.riceVariety); _yeastController = TextEditingController(text: specs.yeast); _manufacturingController = TextEditingController(text: specs.manufacturingYearMonth); /* _sweetnessController = TextEditingController( text: specs.sweetnessScore?.toStringAsFixed(1) ?? ''); _bodyController = TextEditingController(text: specs.bodyScore?.toStringAsFixed(1) ?? ''); */ } @override void didUpdateWidget(SakeDetailSpecs oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.sake != widget.sake) { if (_isEditing) { // Warn user about external update while editing ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('データが外部から更新されました。編集をキャンセルして再度お試しください。'), backgroundColor: Colors.orange, ), ); _cancel(); // Force exit edit mode } else { // Update values without disposing controllers _updateControllers(); } } } void _updateControllers() { final specs = widget.sake.hiddenSpecs; _updateTextIfNotEditing(_typeController, specs.type ?? ''); _updateTextIfNotEditing(_polishingController, specs.polishingRatio?.toString() ?? ''); _updateTextIfNotEditing(_alcoholController, specs.alcoholContent?.toString() ?? ''); _updateTextIfNotEditing(_sakeMeterController, specs.sakeMeterValue?.toString() ?? ''); _updateTextIfNotEditing(_riceController, specs.riceVariety ?? ''); _updateTextIfNotEditing(_yeastController, specs.yeast ?? ''); _updateTextIfNotEditing(_manufacturingController, specs.manufacturingYearMonth ?? ''); } void _updateTextIfNotEditing(TextEditingController controller, String newValue) { if (controller.text != newValue) { controller.text = newValue; } } void _disposeControllers() { _typeController.dispose(); _polishingController.dispose(); _alcoholController.dispose(); _sakeMeterController.dispose(); _riceController.dispose(); _yeastController.dispose(); _manufacturingController.dispose(); // _sweetnessController.dispose(); // _bodyController.dispose(); } @override void dispose() { _disposeControllers(); super.dispose(); } Future _save() async { final box = Hive.box('sake_items'); final updated = widget.sake.copyWith( specificDesignation: _typeController.text, polishingRatio: int.tryParse(_polishingController.text), alcoholContent: double.tryParse(_alcoholController.text), sakeMeterValue: double.tryParse(_sakeMeterController.text), riceVariety: _riceController.text, yeast: _yeastController.text, manufacturingYearMonth: _manufacturingController.text, isUserEdited: true, ); await box.put(widget.sake.key, updated); widget.onUpdate(updated); setState(() { _isEditing = false; }); } // AI分析情報の編集をキャンセル void _cancel() { setState(() { _isEditing = false; // コントローラーを元の値にリセット _updateControllers(); }); } @override Widget build(BuildContext context) { if (widget.sake.itemType == ItemType.set) return const SizedBox.shrink(); final appColors = Theme.of(context).extension()!; final textColor = Theme.of(context).brightness == Brightness.dark ? Colors.white : null; return Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( controller: _expansionController, // v1.0.12+24: 編集中は折りたたみを無効化(Option A) // 編集中にタイトルタップで折りたたみを試みた場合、強制的に展開状態を維持 onExpansionChanged: (isExpanded) { if (_isEditing && !isExpanded) { // 編集中は折りたたみを許可しない Future.microtask(() => _expansionController.expand()); } }, leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault), title: Text( '詳細', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: textColor, ), ), trailing: _isEditing ? Row( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: _cancel, child: const Text('キャンセル'), ), const SizedBox(width: 4), FilledButton( onPressed: _save, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), ), child: const Text('保存'), ), ], ) : IconButton( icon: const Icon(LucideIcons.edit2, size: 18), tooltip: '編集', onPressed: () { setState(() => _isEditing = true); _expansionController.expand(); }, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), ), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( children: [ _buildEditableSpecRow( context, '特定名称', _typeController, _isEditing), _buildEditableSpecRow( context, '精米歩合', _polishingController, _isEditing, keyboardType: TextInputType.number, suffix: '%', ), _buildEditableSpecRow( context, 'アルコール分', _alcoholController, _isEditing, keyboardType: TextInputType.number, suffix: '度', ), _buildEditableSpecRow( context, '日本酒度', _sakeMeterController, _isEditing, keyboardType: const TextInputType.numberWithOptions(signed: true), ), _buildEditableSpecRow( context, '酒米', _riceController, _isEditing), _buildEditableSpecRow( context, '酵母', _yeastController, _isEditing), _buildEditableSpecRow( context, '製造年月', _manufacturingController, _isEditing, suffixIcon: LucideIcons.calendar, onSuffixTap: () => _showDatePicker(context), ), ], ), ), ], ), ); } void _showDatePicker(BuildContext context) { if (!_isEditing) return; // Parse current value or use now DateTime initialDate = DateTime.now(); try { final parts = _manufacturingController.text.split('-'); if (parts.length >= 2) { final year = int.parse(parts[0]); final month = int.parse(parts[1]); initialDate = DateTime(year, month); } } catch (_) {} showCupertinoModalPopup( context: context, builder: (BuildContext context) => Container( height: 280, padding: const EdgeInsets.only(top: 6.0), margin: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), color: CupertinoColors.systemBackground.resolveFrom(context), child: SafeArea( top: false, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CupertinoButton( child: const Text('キャンセル'), onPressed: () => Navigator.pop(context), ), CupertinoButton( child: const Text('完了'), onPressed: () { Navigator.pop(context); // Update Text Field (YYYY-MM) // Note: Simply using the variable won't work because callback is needed? // Actually we can update controller here directly // But wait, the picker value changes. We need state for temp value? // CupertinoDatePicker onDateTimeChanged is consistent. // We update controller in onDateTimeChanged, or store in temp? // Updating controller directly is fine as long as we don't save yet. }, ), ], ), Expanded( child: CupertinoDatePicker( initialDateTime: initialDate, mode: CupertinoDatePickerMode.monthYear, use24hFormat: true, // This is called every time the user scrolls the picker onDateTimeChanged: (DateTime newDate) { final text = "${newDate.year}-${newDate.month.toString().padLeft(2, '0')}"; _updateTextIfNotEditing(_manufacturingController, text); }, ), ), ], ), ), ), ); } Widget _buildEditableSpecRow( BuildContext context, String label, TextEditingController controller, bool isEditing, { TextInputType keyboardType = TextInputType.text, String? suffix, String? helperText, IconData? suffixIcon, VoidCallback? onSuffixTap, }) { final appColors = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 100, child: Text( label, style: TextStyle( color: appColors.textSecondary, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), Expanded( child: isEditing ? TextField( controller: controller, keyboardType: keyboardType, style: const TextStyle(fontSize: 14), decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8), border: const OutlineInputBorder(), suffixText: suffix, helperText: helperText, suffixIcon: suffixIcon != null ? IconButton( icon: Icon(suffixIcon, size: 18), onPressed: onSuffixTap, ) : null, ), ) : Text( (controller.text.isEmpty) ? '-' : '${controller.text}${suffix ?? ""}', style: const TextStyle(fontSize: 14), ), ), ], ), ); } }