ponshu-room-lite/lib/screens/sake_detail/widgets/sake_photo_edit_modal.dart

280 lines
9.6 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:hive_flutter/hive_flutter.dart';
import '../../../models/sake_item.dart';
import '../../../theme/app_colors.dart';
import '../../camera_screen.dart';
/// 写真編集モーダルウィジェット
class SakePhotoEditModal extends StatefulWidget {
final SakeItem sake;
final Function(SakeItem) onUpdated;
const SakePhotoEditModal({
super.key,
required this.sake,
required this.onUpdated,
});
@override
State<SakePhotoEditModal> createState() => _SakePhotoEditModalState();
}
class _SakePhotoEditModalState extends State<SakePhotoEditModal> {
late List<String> _imagePaths;
@override
void initState() {
super.initState();
_imagePaths = List.from(widget.sake.displayData.imagePaths);
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Handle bar
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).extension<AppColors>()!.divider,
borderRadius: BorderRadius.circular(2),
),
),
// Header
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'写真を編集',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(height: 1),
// Photo grid
Expanded(
child: _imagePaths.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.image, size: 64, color: Theme.of(context).extension<AppColors>()!.iconSubtle),
const SizedBox(height: 16),
Text(
'写真を追加してください',
style: TextStyle(color: Theme.of(context).extension<AppColors>()!.textSecondary),
),
],
),
)
: ReorderableListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _imagePaths.length,
onReorder: (oldIndex, newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = _imagePaths.removeAt(oldIndex);
_imagePaths.insert(newIndex, item);
});
},
itemBuilder: (context, index) {
final photoPath = _imagePaths[index];
return Card(
key: ValueKey(photoPath),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(photoPath),
width: 60,
height: 60,
fit: BoxFit.cover,
),
),
title: Text(
index == 0 ? 'メイン写真' : '写真 ${index + 1}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(LucideIcons.gripVertical, color: Theme.of(context).extension<AppColors>()!.iconSubtle, size: 32),
const SizedBox(width: 8),
IconButton(
icon: Icon(LucideIcons.trash2, color: Theme.of(context).extension<AppColors>()!.error),
onPressed: () => _deletePhoto(index),
),
],
),
),
);
},
),
),
// Bottom buttons
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: SafeArea(
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(LucideIcons.camera),
label: const Text('写真を追加'),
onPressed: _addPhoto,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _saveChanges,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).extension<AppColors>()!.brandPrimary,
foregroundColor: Theme.of(context).extension<AppColors>()!.surfaceSubtle,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text('保存'),
),
),
],
),
),
),
],
),
);
}
Future<void> _addPhoto() async {
final picker = ImagePicker();
// Show bottom sheet with camera/gallery options
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (context) => SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(LucideIcons.camera),
title: const Text('カメラで撮影'),
onTap: () async {
Navigator.pop(context); // Close sheet
// Navigate to CameraScreen in returnPath mode
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => CameraScreen(mode: CameraMode.returnPath)),
);
if (result is String && context.mounted) {
// Add the path
await _saveNewPhoto(result);
}
},
),
ListTile(
leading: const Icon(LucideIcons.image),
title: const Text('ギャラリーから選択'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
),
);
if (source == null) return;
// Handle Gallery (Camera is handled in ListTile callback)
if (source == ImageSource.gallery) {
try {
final XFile? pickedFile = await picker.pickImage(source: source);
if (pickedFile == null) return;
// Save to app directory
final appDir = await getApplicationDocumentsDirectory();
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, fileName);
await File(pickedFile.path).copy(savedPath);
if (!mounted) return;
await _saveNewPhoto(savedPath);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラー: $e')),
);
}
}
}
}
Future<void> _saveNewPhoto(String imagePath) async {
setState(() {
_imagePaths.add(imagePath);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('写真を追加しました')),
);
}
}
void _deletePhoto(int index) {
setState(() {
_imagePaths.removeAt(index);
});
}
Future<void> _saveChanges() async {
final box = Hive.box<SakeItem>('sake_items');
final updatedSake = widget.sake.copyWith(
imagePaths: _imagePaths,
isUserEdited: true,
);
await box.put(widget.sake.key, updatedSake);
widget.onUpdated(updatedSake);
if (mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('写真を更新しました')),
);
}
}
}