MobilApp/lib/widgets/map_edit_tools/note_photo_gallery.dart

458 lines
14 KiB
Dart

// lib/widgets/map_edit_tools/note_photo_gallery.dart
//
// Fotó galéria a MapFeatureSaveSheet-ben:
// - Vízszintes görgetős sor
// - + gomb: kamera / galéria választó
// - Fotóra koppintva: teljes képernyős nézet + felirat szerkesztés
// - Fotóra hosszan nyomva: törlés
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../models/note_item_photo.dart';
import '../../services/note_photo_service.dart';
class NotePhotoGallery extends StatefulWidget {
/// A szerkesztett NoteItem id-ja — null ha az elem még nincs elmentve
final int? noteItemId;
const NotePhotoGallery({super.key, required this.noteItemId});
@override
State<NotePhotoGallery> createState() => _NotePhotoGalleryState();
}
class _NotePhotoGalleryState extends State<NotePhotoGallery> {
List<NoteItemPhoto> _photos = [];
bool _loading = false;
@override
void initState() {
super.initState();
_loadPhotos();
}
Future<void> _loadPhotos() async {
if (widget.noteItemId == null) return;
setState(() => _loading = true);
_photos = await NotePhotoService.to.loadPhotos(widget.noteItemId!);
if (mounted) setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
// NoteItem nem mentett még — nem lehet fotót hozzáadni
if (widget.noteItemId == null) {
return const _DisabledGallery();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text('Fotók',
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
const SizedBox(width: 6),
if (_photos.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${_photos.length}',
style:
const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
),
),
]),
const SizedBox(height: 8),
if (_loading)
const SizedBox(
height: 80,
child: Center(child: CircularProgressIndicator()),
)
else
SizedBox(
height: 88,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
// Meglévő fotók
..._photos.map((photo) => _PhotoThumb(
photo: photo,
onTap: () => _openViewer(photo),
onDelete: () => _delete(photo),
)),
// + Fotó hozzáadása gomb
_AddPhotoButton(
onCamera: () => _addPhoto(fromCamera: true),
onGallery: () => _addPhoto(fromCamera: false),
),
],
),
),
],
);
}
Future<void> _addPhoto({required bool fromCamera}) async {
final svc = NotePhotoService.to;
final photo = fromCamera
? await svc.takePhoto(widget.noteItemId!)
: await svc.pickFromGallery(widget.noteItemId!);
if (photo != null && mounted) {
setState(() => _photos.add(photo));
}
}
Future<void> _delete(NoteItemPhoto photo) async {
final ok = await Get.dialog<bool>(AlertDialog(
title: const Text('Fotó törlése'),
content: const Text('Ez a fotó véglegesen törlődik.'),
actions: [
TextButton(onPressed: Get.back, child: const Text('Mégse')),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Get.back(result: true),
child: const Text('Törlés'),
),
],
));
if (ok == true && mounted) {
await NotePhotoService.to.deletePhoto(photo);
setState(() => _photos.removeWhere((p) => p.id == photo.id));
}
}
void _openViewer(NoteItemPhoto photo) {
Get.to(() => _PhotoViewerPage(
photos: _photos,
initialIndex: _photos.indexWhere((p) => p.id == photo.id),
onCaptionSaved: (updated) {
setState(() {
final idx = _photos.indexWhere((p) => p.id == updated.id);
if (idx >= 0) _photos[idx] = updated;
});
},
));
}
}
// ─── Fotó bélyegkép ──────────────────────────────────────────────────────────
class _PhotoThumb extends StatelessWidget {
final NoteItemPhoto photo;
final VoidCallback onTap;
final VoidCallback onDelete;
const _PhotoThumb({
required this.photo,
required this.onTap,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: onTap,
onLongPress: onDelete,
child: Stack(children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: photo.fileExists
? Image.file(
photo.file,
width: 80, height: 80,
fit: BoxFit.cover,
cacheWidth: 160, // memória optimalizálás
)
: Container(
width: 80,
height: 80,
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
// Felirat jelzése ha van
if (photo.caption.isNotEmpty)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
borderRadius:
const BorderRadius.vertical(bottom: Radius.circular(8)),
),
child: Text(
photo.caption,
style: const TextStyle(color: Colors.white, fontSize: 9),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// GPS jelzése ha van
if (photo.location != null)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(Icons.location_on,
color: Colors.white, size: 10),
),
),
]),
),
);
}
}
// ─── Hozzáadás gomb ──────────────────────────────────────────────────────────
class _AddPhotoButton extends StatelessWidget {
final VoidCallback onCamera;
final VoidCallback onGallery;
const _AddPhotoButton({
required this.onCamera,
required this.onGallery,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showPicker(context),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade300,
width: 1.5,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_a_photo_outlined,
size: 24, color: Colors.grey.shade500),
const SizedBox(height: 4),
Text('Fotó',
style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
],
),
),
);
}
void _showPicker(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) => SafeArea(
child: Column(mainAxisSize: MainAxisSize.min, children: [
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Kamera'),
onTap: () {
Navigator.pop(context);
onCamera();
},
),
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: const Text('Galéria'),
onTap: () {
Navigator.pop(context);
onGallery();
},
),
]),
),
);
}
}
// ─── Letiltott galéria (elem még nincs mentve) ───────────────────────────────
class _DisabledGallery extends StatelessWidget {
const _DisabledGallery();
@override
Widget build(BuildContext context) {
return Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Mentés után adhatók hozzá fotók',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
);
}
}
// ─── Teljes képernyős fotónézegető ───────────────────────────────────────────
class _PhotoViewerPage extends StatefulWidget {
final List<NoteItemPhoto> photos;
final int initialIndex;
final ValueChanged<NoteItemPhoto> onCaptionSaved;
const _PhotoViewerPage({
required this.photos,
required this.initialIndex,
required this.onCaptionSaved,
});
@override
State<_PhotoViewerPage> createState() => _PhotoViewerPageState();
}
class _PhotoViewerPageState extends State<_PhotoViewerPage> {
late PageController _pageCtrl;
late int _currentIdx;
@override
void initState() {
super.initState();
_currentIdx = widget.initialIndex;
_pageCtrl = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageCtrl.dispose();
super.dispose();
}
NoteItemPhoto get _current => widget.photos[_currentIdx];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: widget.photos.length > 1
? Text('${_currentIdx + 1} / ${widget.photos.length}')
: null,
actions: [
// Felirat szerkesztés
IconButton(
icon: const Icon(Icons.edit_outlined, color: Colors.white),
tooltip: 'Felirat szerkesztése',
onPressed: _editCaption,
),
],
),
body: Column(children: [
// Fotó
Expanded(
child: PageView.builder(
controller: _pageCtrl,
itemCount: widget.photos.length,
onPageChanged: (i) => setState(() => _currentIdx = i),
itemBuilder: (_, i) {
final photo = widget.photos[i];
return InteractiveViewer(
child: Center(
child: photo.fileExists
? Image.file(photo.file, fit: BoxFit.contain)
: const Icon(Icons.broken_image,
color: Colors.white54, size: 64),
),
);
},
),
),
// Felirat + helyadatok
if (_current.caption.isNotEmpty || _current.location != null)
Container(
color: Colors.black.withOpacity(0.7),
padding: EdgeInsets.fromLTRB(
16,
10,
16,
10 + MediaQuery.of(context).padding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (_current.caption.isNotEmpty)
Text(_current.caption,
style:
const TextStyle(color: Colors.white, fontSize: 14)),
if (_current.location != null)
Text(
'📍 ${_current.latitude!.toStringAsFixed(6)}, '
'${_current.longitude!.toStringAsFixed(6)}',
style: const TextStyle(color: Colors.white54, fontSize: 11),
),
],
),
),
]),
);
}
Future<void> _editCaption() async {
final ctrl = TextEditingController(text: _current.caption);
final result = await Get.dialog<String>(AlertDialog(
title: const Text('Felirat'),
content: TextField(
controller: ctrl,
decoration: const InputDecoration(
hintText: 'Fotó leírása...',
),
autofocus: true,
maxLines: 3,
),
actions: [
TextButton(onPressed: Get.back, child: const Text('Mégse')),
FilledButton(
onPressed: () => Get.back(result: ctrl.text.trim()),
child: const Text('Mentés'),
),
],
));
if (result != null) {
final updated = await NotePhotoService.to.updateCaption(_current, result);
widget.onCaptionSaved(updated);
setState(() {});
}
}
}