import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; // Added for Hive.box import 'package:image_picker/image_picker.dart'; import 'package:uuid/uuid.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import '../models/sake_item.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../services/gamification_service.dart'; import '../theme/app_colors.dart'; import '../constants/app_constants.dart'; class AddSetItemDialog extends ConsumerStatefulWidget { const AddSetItemDialog({super.key}); @override ConsumerState createState() => _AddSetItemDialogState(); } class _AddSetItemDialogState extends ConsumerState { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _descriptionController = TextEditingController(); final _priceController = TextEditingController(); bool _useDefaultImage = true; String? _pickedImagePath; final ImagePicker _picker = ImagePicker(); @override void dispose() { _nameController.dispose(); _descriptionController.dispose(); _priceController.dispose(); super.dispose(); } Future _pickImage(ImageSource source) async { try { final XFile? image = await _picker.pickImage( source: source, maxWidth: AppConstants.imageMaxDimensionGemini.toDouble(), maxHeight: AppConstants.imageMaxDimensionGemini.toDouble(), imageQuality: AppConstants.imageCompressionQuality, ); if (image != null) { setState(() { _pickedImagePath = image.path; _useDefaultImage = false; }); } } catch (e) { // Handle error } } Future _save() async { final appColors = Theme.of(context).extension()!; if (!_formKey.currentState!.validate()) return; // Save Logic final name = _nameController.text; final description = _descriptionController.text; final price = int.tryParse(_priceController.text) ?? 0; // Image handling List imagePaths = []; if (!_useDefaultImage && _pickedImagePath != null) { // Copy to app doc dir final appDir = await getApplicationDocumentsDirectory(); final fileName = '${const Uuid().v4()}.jpg'; final savedImage = await File(_pickedImagePath!).copy(path.join(appDir.path, fileName)); imagePaths.add(savedImage.path); } // Note: If using default image, we leave imagePaths empty. // The UI will show asset if imagePaths is empty AND itemType is set. final newItem = SakeItem( id: const Uuid().v4(), itemType: ItemType.set, displayData: DisplayData( name: name, brewery: 'Set Product', // Hidden in UI prefecture: 'Set', // Hidden in UI catchCopy: description, // Use catchCopy for description imagePaths: imagePaths, ), userData: UserData( price: price, isUserEdited: true, // It is manually created markup: 1.0, // Default for set ), hiddenSpecs: HiddenSpecs(description: description), metadata: Metadata(createdAt: DateTime.now()), ); // Save to Hive Directly (since sakeListProvider is read-only) final box = Hive.box('sake_items'); await box.add(newItem); // Check and unlock badges (no EXP for set items) final newlyUnlockedBadges = await GamificationService.checkAndUnlockBadges(ref); if (newlyUnlockedBadges.isNotEmpty) { debugPrint('🏅 Badges Unlocked: ${newlyUnlockedBadges.map((b) => b.name).join(", ")}'); } if (mounted) { Navigator.of(context).pop(); // Show badge unlock notification if any if (newlyUnlockedBadges.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$name を登録しました!'), const SizedBox(height: 4), for (final badge in newlyUnlockedBadges) Row( children: [ Text(badge.icon, style: const TextStyle(fontSize: 16)), const SizedBox(width: 8), Text( 'バッジ獲得: ${badge.name}', style: TextStyle(fontWeight: FontWeight.bold, color: appColors.brandAccent), ), ], ), ], ), duration: const Duration(seconds: 4), ), ); } } } @override Widget build(BuildContext context) { final appColors = Theme.of(context).extension()!; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( padding: const EdgeInsets.all(24), constraints: const BoxConstraints(maxWidth: 500), child: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(LucideIcons.package, color: Theme.of(context).colorScheme.primary), // Box icon const SizedBox(width: 8), Text( 'セット商品の登録', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 24), // Image Section Center( child: GestureDetector( onTap: () { // Toggle or show picker _showImageSourceDialog(); }, child: Container( width: 120, height: 120, decoration: BoxDecoration( color: appColors.surfaceSubtle, borderRadius: BorderRadius.circular(12), border: Border.all(color: appColors.divider), image: _useDefaultImage ? const DecorationImage(image: AssetImage('assets/images/set_placeholder.png'), fit: BoxFit.cover) : (_pickedImagePath != null ? DecorationImage(image: FileImage(File(_pickedImagePath!)), fit: BoxFit.cover) : null), ), child: !_useDefaultImage && _pickedImagePath == null ? Icon(LucideIcons.camera, size: 40, color: appColors.iconSubtle) : null, ), ), ), Center( child: TextButton( onPressed: _showImageSourceDialog, child: const Text('画像を変更'), ), ), const SizedBox(height: 16), // Name TextFormField( controller: _nameController, decoration: const InputDecoration( labelText: '商品名', hintText: '例: 3種飲み比べセット', border: OutlineInputBorder(), filled: true, ), validator: (val) => val == null || val.isEmpty ? '必須項目です' : null, ), const SizedBox(height: 16), // Price TextFormField( controller: _priceController, decoration: const InputDecoration( labelText: '価格 (税込)', suffixText: '円', border: OutlineInputBorder(), filled: true, ), keyboardType: TextInputType.number, validator: (val) => val == null || val.isEmpty ? '必須項目です' : null, ), const SizedBox(height: 16), // Description TextFormField( controller: _descriptionController, decoration: const InputDecoration( labelText: '説明文', hintText: '例: 当店おすすめの辛口3種です。', border: OutlineInputBorder(), filled: true, ), maxLines: 3, ), const SizedBox(height: 24), // Buttons Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: appColors.brandPrimary, foregroundColor: appColors.surfaceSubtle, elevation: 0, ), onPressed: _save, child: const Text('登録'), ), ], ), ], ), ), ), ), ); } void _showImageSourceDialog() { showModalBottomSheet( context: context, builder: (_) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(LucideIcons.image), title: const Text('プリセット画像を使用'), onTap: () { setState(() { _useDefaultImage = true; }); Navigator.pop(context); }, ), ListTile( leading: const Icon(LucideIcons.camera), title: const Text('カメラで撮影'), onTap: () { Navigator.pop(context); _pickImage(ImageSource.camera); }, ), ListTile( leading: const Icon(LucideIcons.image), title: const Text('アルバムから選択'), onTap: () { Navigator.pop(context); _pickImage(ImageSource.gallery); }, ), ], ), ), ); } }