Terepbejárás geometria hang és képi dokumentáció létrehozása. Gradle verzió frissítése
This commit is contained in:
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/enums/map_edit_tool.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
import 'package:terepi_seged/widgets/map_edit_tools/note_audio_widget.dart';
|
||||
import 'package:terepi_seged/widgets/map_edit_tools/note_photo_gallery.dart';
|
||||
|
||||
import 'color_row.dart';
|
||||
import 'label_field.dart';
|
||||
@@ -66,6 +68,10 @@ class MapFeatureSaveSheet extends StatelessWidget {
|
||||
])
|
||||
: const SizedBox.shrink()),
|
||||
LabelField(ctrl: ctrl),
|
||||
const SizedBox(height: 16),
|
||||
NotePhotoGallery(noteItemId: ctrl.editingNoteItemId),
|
||||
const SizedBox(height: 16),
|
||||
NoteAudioWidget(noteItemId: ctrl.editingNoteItemId),
|
||||
const SizedBox(height: 24),
|
||||
SaveSheetActions(ctrl: ctrl),
|
||||
SizedBox(
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
// Hangjegyzet widget a MapFeatureSaveSheet-ben:
|
||||
// - Felvétel gomb animált mikrofonnal
|
||||
// - Felvett klipek listája play/pause/delete gombokkal
|
||||
// - Haladásjelző csúszka lejátszás közben
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../models/note_item_audio.dart';
|
||||
import '../../services/note_audio_service.dart';
|
||||
|
||||
class NoteAudioWidget extends StatefulWidget {
|
||||
final int? noteItemId;
|
||||
const NoteAudioWidget({super.key, required this.noteItemId});
|
||||
|
||||
@override
|
||||
State<NoteAudioWidget> createState() => _NoteAudioWidgetState();
|
||||
}
|
||||
|
||||
class _NoteAudioWidgetState extends State<NoteAudioWidget> {
|
||||
List<NoteItemAudio> _audios = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
if (widget.noteItemId == null) return;
|
||||
final list = await NoteAudioService.to.loadAudios(widget.noteItemId!);
|
||||
if (mounted) setState(() => _audios = list);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.noteItemId == null) {
|
||||
return const _DisabledAudio();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Fejléc sor
|
||||
Row(children: [
|
||||
Text('Hangjegyzetek',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
|
||||
const SizedBox(width: 6),
|
||||
if (_audios.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('${_audios.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Felvétel gomb
|
||||
_RecordButton(
|
||||
noteItemId: widget.noteItemId!,
|
||||
onRecorded: (audio) {
|
||||
setState(() => _audios.add(audio));
|
||||
},
|
||||
),
|
||||
|
||||
// Felvett klipek listája
|
||||
if (_audios.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
..._audios.map((audio) => _AudioClipTile(
|
||||
audio: audio,
|
||||
onDelete: () async {
|
||||
await NoteAudioService.to.deleteAudio(audio);
|
||||
setState(() => _audios.removeWhere((a) => a.id == audio.id));
|
||||
},
|
||||
)),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Felvétel gomb ───────────────────────────────────────────────────────────
|
||||
|
||||
class _RecordButton extends StatelessWidget {
|
||||
final int noteItemId;
|
||||
final ValueChanged<NoteItemAudio> onRecorded;
|
||||
|
||||
const _RecordButton({
|
||||
required this.noteItemId,
|
||||
required this.onRecorded,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final svc = NoteAudioService.to;
|
||||
|
||||
return Obx(() {
|
||||
final isRecording = svc.recordState.value == AudioRecordState.recording;
|
||||
final durationMs = svc.recordDurationMs.value;
|
||||
|
||||
return Row(children: [
|
||||
// Mikrofon gomb
|
||||
GestureDetector(
|
||||
onTap: () => isRecording ? _stopRecording(svc) : _startRecording(svc),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: isRecording
|
||||
? Colors.red
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: isRecording
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.red.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
isRecording ? Icons.stop : Icons.mic,
|
||||
color: isRecording
|
||||
? Colors.white
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
Expanded(
|
||||
child: isRecording
|
||||
// Felvétel közben: időmérő + animált hullám
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: [
|
||||
_PulsingDot(),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Felvétel: ${svc.formatMs(durationMs)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.red,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Megállításhoz nyomd meg a gombot',
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: Colors.grey.shade500),
|
||||
),
|
||||
],
|
||||
)
|
||||
// Alap állapot
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Hangjegyzet rögzítése',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500, fontSize: 14)),
|
||||
Text('Nyomd meg a mikrofon gombot',
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: Colors.grey.shade500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Mégse — csak felvétel közben
|
||||
if (isRecording)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await svc.cancelRecording();
|
||||
},
|
||||
child: const Text('Mégse', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _startRecording(NoteAudioService svc) async {
|
||||
await svc.startRecording(noteItemId);
|
||||
}
|
||||
|
||||
Future<void> _stopRecording(NoteAudioService svc) async {
|
||||
final audio = await svc.stopRecording(noteItemId);
|
||||
if (audio != null) onRecorded(audio);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Egy felvett klip sor ────────────────────────────────────────────────────
|
||||
|
||||
class _AudioClipTile extends StatelessWidget {
|
||||
final NoteItemAudio audio;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _AudioClipTile({
|
||||
required this.audio,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final svc = NoteAudioService.to;
|
||||
|
||||
return Obx(() {
|
||||
final isThisPlaying = svc.playingAudioId.value == audio.id;
|
||||
final pState = svc.playState.value;
|
||||
final posMs = svc.playPositionMs.value;
|
||||
final totalMs = audio.durationSeconds * 1000;
|
||||
|
||||
// Haladás 0.0 – 1.0
|
||||
final progress = (isThisPlaying && totalMs > 0)
|
||||
? (posMs / totalMs).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isThisPlaying
|
||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: isThisPlaying
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.4))
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: [
|
||||
// Play / Pause gomb
|
||||
GestureDetector(
|
||||
onTap: () => svc.playAudio(audio),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isThisPlaying && pState == AudioPlayState.playing
|
||||
? Icons.pause
|
||||
: Icons.play_arrow,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Felirat vagy dátum
|
||||
if (audio.caption.isNotEmpty)
|
||||
Text(audio.caption,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis)
|
||||
else
|
||||
Text(
|
||||
_formatDate(audio.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Haladásjelző csúszka
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
value: isThisPlaying && totalMs > 0
|
||||
? (posMs / totalMs).clamp(0.0, 1.0)
|
||||
: 0.0,
|
||||
minHeight: 3,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// Időtartam
|
||||
Text(
|
||||
isThisPlaying
|
||||
? '${svc.formatMs(posMs)} / '
|
||||
'${audio.durationFormatted}'
|
||||
: audio.durationFormatted,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade500,
|
||||
fontFeatures: const [FontFeature.tabularFigures()]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 4),
|
||||
|
||||
// Menü: felirat / törlés
|
||||
PopupMenuButton<_AudioAction>(
|
||||
icon: Icon(Icons.more_vert,
|
||||
size: 18, color: Colors.grey.shade500),
|
||||
onSelected: (a) => _onAction(a, context),
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(
|
||||
value: _AudioAction.caption,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_outlined),
|
||||
title: Text('Felirat'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: _AudioAction.delete,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete_outline, color: Colors.red),
|
||||
title:
|
||||
Text('Törlés', style: TextStyle(color: Colors.red)),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onAction(_AudioAction action, BuildContext context) {
|
||||
switch (action) {
|
||||
case _AudioAction.caption:
|
||||
_editCaption();
|
||||
case _AudioAction.delete:
|
||||
_confirmDelete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editCaption() async {
|
||||
final ctrl = TextEditingController(text: audio.caption);
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Felirat'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
decoration: const InputDecoration(hintText: 'Hangjegyzet leírása...'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
await NoteAudioService.to.updateCaption(audio, ctrl.text.trim());
|
||||
},
|
||||
child: const Text('Mentés'),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete() async {
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Hangjegyzet törlése'),
|
||||
content: const Text('Ez a felvétel 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) onDelete();
|
||||
}
|
||||
|
||||
String _formatDate(DateTime dt) =>
|
||||
'${dt.year}.${dt.month.toString().padLeft(2, '0')}.'
|
||||
'${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:'
|
||||
'${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// ─── Segéd widgetek ──────────────────────────────────────────────────────────
|
||||
|
||||
class _DisabledAudio extends StatelessWidget {
|
||||
const _DisabledAudio();
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text('Mentés után adható hangjegyzet',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
||||
);
|
||||
}
|
||||
|
||||
class _PulsingDot extends StatefulWidget {
|
||||
@override
|
||||
State<_PulsingDot> createState() => _PulsingDotState();
|
||||
}
|
||||
|
||||
class _PulsingDotState extends State<_PulsingDot>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _ctrl;
|
||||
late Animation<double> _anim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 600))
|
||||
..repeat(reverse: true);
|
||||
_anim = Tween(begin: 0.3, end: 1.0).animate(_ctrl);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (_, __) => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(_anim.value),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
enum _AudioAction { caption, delete }
|
||||
@@ -0,0 +1,457 @@
|
||||
// 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(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user