ponshu-room-lite/lib/screens/menu_settings_screen.dart

560 lines
23 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 'package:intl/intl.dart';
import 'package:flutter/cupertino.dart'; // For Rolling Picker
import 'pdf_preview_screen.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../providers/menu_providers.dart';
import '../models/sake_item.dart';
import '../models/menu_settings.dart';
import '../providers/sake_list_provider.dart';
import '../widgets/step_indicator.dart';
import '../theme/app_theme.dart';
class MenuSettingsScreen extends ConsumerStatefulWidget {
const MenuSettingsScreen({super.key});
@override
ConsumerState<MenuSettingsScreen> createState() => _MenuSettingsScreenState();
}
class _MenuSettingsScreenState extends ConsumerState<MenuSettingsScreen> {
// Display options
bool includePhoto = true;
bool includePoem = true;
bool includeChart = true;
bool includePrice = true;
bool includeDate = true;
bool includeQr = false; // Default false for QR
// PDF settings
String pdfSize = 'a4'; // 'a4', 'a5', 'b5'
bool isMonochrome = false;
// TextEditingControllers
late final TextEditingController _titleController;
late final TextEditingController _dateController; // Stores date string
@override
void initState() {
super.initState();
_loadSettings();
}
// Load saved settings from Hive
void _loadSettings() {
final box = Hive.box<MenuSettings>('menu_settings');
final savedSettings = box.get('current', defaultValue: MenuSettings());
// Initialize controllers first to avoid null reference errors
_titleController = TextEditingController();
_dateController = TextEditingController(
text: DateFormat('yyyy年M月d日').format(DateTime.now()),
);
if (savedSettings != null) {
setState(() {
includePhoto = savedSettings.includePhoto;
includePoem = savedSettings.includePoem;
includeChart = savedSettings.includeChart;
includePrice = savedSettings.includePrice;
includeDate = savedSettings.includeDate;
includeQr = savedSettings.includeQr ?? false; // Handle migration safely if null
pdfSize = savedSettings.pdfSize;
isMonochrome = savedSettings.isMonochrome;
});
// Update controller text if saved settings exist
_titleController.text = savedSettings.title;
}
}
// Save settings to Hive
Future<void> _saveSettings() async {
final box = Hive.box<MenuSettings>('menu_settings');
final settings = MenuSettings(
title: _titleController.text,
includePhoto: includePhoto,
includePoem: includePoem,
includeChart: includeChart,
includePrice: includePrice,
includeDate: includeDate,
includeQr: includeQr,
pdfSize: pdfSize,
isMonochrome: isMonochrome,
);
await box.put('current', settings);
}
@override
void dispose() {
_titleController.dispose();
_dateController.dispose();
super.dispose();
}
Future<void> _showRollingDatePicker() async {
DateTime initialDate = DateTime.now();
try {
// Try parsing current text, fallback to now
final format = DateFormat('yyyy年M月d日');
initialDate = format.parse(_dateController.text);
} catch (_) {}
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 216,
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: CupertinoDatePicker(
initialDateTime: initialDate,
mode: CupertinoDatePickerMode.date,
use24hFormat: true,
// Japanese locale logic if needed, but standard picker usually sufficient
onDateTimeChanged: (DateTime newDate) {
setState(() {
_dateController.text = DateFormat('yyyy年M月d日').format(newDate);
});
_saveSettings();
},
),
),
),
);
}
@override
Widget build(BuildContext context) {
// Determine selected items and order
final selectedIds = ref.watch(selectedMenuSakeIdsProvider);
final orderedIds = ref.watch(menuOrderedIdsProvider);
final sakeListAsync = ref.watch(sakeListProvider);
final selectedItems = sakeListAsync.when(
data: (list) {
if (orderedIds.isNotEmpty) {
final sakeMap = {for (var s in list) s.id: s};
return orderedIds
.map((id) => sakeMap[id])
.whereType<SakeItem>()
.where((s) => selectedIds.contains(s.id))
.toList();
} else {
return list.where((item) => selectedIds.contains(item.id)).toList();
}
},
loading: () => <SakeItem>[],
error: (_, __) => <SakeItem>[],
);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const StepIndicator(currentStep: 3, totalSteps: 3),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.close),
tooltip: '終了',
onPressed: () => _showExitDialog(context, ref),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2),
child: LinearProgressIndicator(
value: 1.0, // Step 3 of 3 = 100%
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(AppTheme.posimaiBlue),
minHeight: 2,
),
),
),
body: selectedItems.isEmpty
? const Center(child: Text('お酒が選択されていません'))
: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Preview List (Simple)
Text(
'選択中: ${selectedItems.length}銘柄',
style: Theme.of(context).textTheme.titleMedium,
),
const Divider(),
...selectedItems.map((sake) => ListTile(
leading: const Icon(Icons.check, size: 16),
title: Text(sake.displayData.name),
subtitle: Text('${sake.displayData.brewery} / ${sake.displayData.prefecture}'),
dense: true,
)),
const Divider(height: 32),
// Menu Attributes
Text(
'お品書き情報',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'タイトル',
hintText: '日本酒リスト',
border: OutlineInputBorder(),
),
onChanged: (value) => _saveSettings(),
),
const SizedBox(height: 24),
// Display Toggles with SwitchListTile
Text(
'表示項目',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.posimaiBlue,
),
),
const SizedBox(height: 8),
Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
SwitchListTile(
title: const Text('写真'),
value: includePhoto,
onChanged: (val) => setState(() {
includePhoto = val;
_saveSettings();
}),
),
const Divider(height: 1),
SwitchListTile(
title: const Text('キャッチコピー / 説明文'),
value: includePoem,
onChanged: (val) => setState(() {
includePoem = val;
_saveSettings();
}),
),
const Divider(height: 1),
SwitchListTile(
title: const Text('価格'),
value: includePrice,
onChanged: (val) => setState(() {
includePrice = val;
_saveSettings();
}),
),
const Divider(height: 1),
// QR Toggle
SwitchListTile(
title: const Text('QRコード(銘柄情報表示)'),
subtitle: const Text('ぽんるーむアプリでスキャンすると銘柄情報を表示', style: TextStyle(fontSize: 10, color: Colors.grey)),
value: includeQr,
onChanged: (val) => setState(() {
includeQr = val;
_saveSettings();
}),
),
const Divider(height: 1),
// Date Toggle in List
SwitchListTile(
title: const Text('日付'),
value: includeDate,
onChanged: (val) => setState(() {
includeDate = val;
_saveSettings();
}),
),
if (includeDate) ...[
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.calendar_today, size: 20),
title: Text(_dateController.text),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: _showRollingDatePicker,
dense: true,
),
],
],
),
),
const SizedBox(height: 24),
// PDF Settings (Restored)
Text(
'PDF設定',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.posimaiBlue,
),
),
const SizedBox(height: 8),
// Paper Size Selection
Text(
'用紙サイズ',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
_buildPaperSizeCard('a5', 'A5', '148 × 210 mm'),
const SizedBox(width: 12),
_buildPaperSizeCard('b5', 'B5', '182 × 257 mm'),
const SizedBox(width: 12),
_buildPaperSizeCard('a4', 'A4', '210 × 297 mm'),
],
),
const SizedBox(height: 16),
// Orientation Toggle (Custom Layout)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('縦向き', style: TextStyle(fontWeight: FontWeight.bold)),
Switch(
value: ref.watch(pdfIsPortraitProvider),
activeThumbColor: AppTheme.posimaiBlue,
onChanged: (val) => ref.read(pdfIsPortraitProvider.notifier).set(val),
),
],
),
),
const Divider(height: 1),
// Density Slider (Pro Feature)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('銘柄の間隔', style: TextStyle(fontWeight: FontWeight.bold)),
Text(
'${(ref.watch(pdfDensityProvider) * 100).round()}%',
style: TextStyle(fontWeight: FontWeight.bold, color: AppTheme.posimaiBlue),
),
],
),
const SizedBox(height: 4),
Text(
'数値を上げると1枚に多く入ります',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey),
),
Slider(
value: ref.watch(pdfDensityProvider).clamp(1.0, 2.0),
min: 1.0,
max: 2.0,
divisions: 10,
label: '${(ref.watch(pdfDensityProvider) * 100).round()}%',
activeColor: AppTheme.posimaiBlue,
onChanged: (val) {
ref.read(pdfDensityProvider.notifier).set(val);
},
),
],
),
),
const Divider(height: 1),
// Color/Monochrome Toggle (Custom Layout)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('カラー', style: TextStyle(fontWeight: FontWeight.bold)),
Switch(
value: !isMonochrome, // ON = Color (!Monochrome)
activeThumbColor: AppTheme.posimaiBlue,
onChanged: (val) => setState(() {
isMonochrome = !val; // Toggle logic
ref.read(pdfIsMonochromeProvider.notifier).set(isMonochrome);
_saveSettings();
}),
),
],
),
),
],
),
),
SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// 戻るボタン (左端)
SizedBox(
width: 56,
height: 56,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Colors.grey[300]!),
padding: EdgeInsets.zero,
),
child: Icon(Icons.arrow_back, color: Colors.grey[700]),
),
),
const SizedBox(width: 12),
// プレビューボタン (右側いっぱいに広がる)
Expanded(
child: SizedBox(
height: 56,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PdfPreviewScreen(
items: selectedItems,
title: _titleController.text,
date: _dateController.text,
includePhoto: includePhoto,
includePoem: includePoem,
includeChart: includeChart,
includePrice: includePrice,
includeDate: includeDate,
includeQr: includeQr, // New Argument
pdfSize: pdfSize,
isMonochrome: isMonochrome,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.posimaiBlue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.picture_as_pdf),
label: const Text('プレビュー', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
),
],
),
),
),
],
),
);
}
Future<void> _showExitDialog(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('お品書き作成を終了しますか?'),
content: const Text('入力内容は保存されません。'),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () => Navigator.pop(context, false),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.posimaiBlue,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(context, true),
child: const Text('終了'),
),
],
),
);
if (confirmed == true && context.mounted) {
ref.read(menuModeProvider.notifier).set(false);
ref.read(selectedMenuSakeIdsProvider.notifier).clear();
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
Widget _buildPaperSizeCard(String value, String label, String sublabel) {
final isSelected = pdfSize == value;
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: () {
setState(() {
pdfSize = value;
_saveSettings();
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primaryContainer.withValues(alpha: 0.2)
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? colorScheme.primary : Colors.transparent,
width: 2,
),
),
child: Column(
children: [
Icon(
isSelected ? Icons.check_box : Icons.crop_portrait, // Use crop_portrait as generic paper icon
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isSelected ? colorScheme.primary : colorScheme.onSurface,
),
),
Text(
sublabel,
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}