ponshu-room-lite/lib/widgets/sake_detail/sake_detail_specs.dart

383 lines
13 KiB
Dart

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<SakeItem> onUpdate;
const SakeDetailSpecs({
super.key,
required this.sake,
required this.onUpdate,
});
@override
State<SakeDetailSpecs> createState() => _SakeDetailSpecsState();
}
class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
bool _isEditing = false;
final ExpansionTileController _expansionController = ExpansionTileController();
// 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<void> _save() async {
final box = Hive.box<SakeItem>('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<AppColors>()!;
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,
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<void>(
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<AppColors>()!;
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),
),
),
],
),
);
}
}