280 lines
9.6 KiB
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('写真を更新しました')),
|
|
);
|
|
}
|
|
}
|
|
}
|