458 lines
14 KiB
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(() {});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|