Terepbejárás geometria hang és képi dokumentáció létrehozása. Gradle verzió frissítése

This commit is contained in:
2026-06-20 22:29:15 +02:00
parent ab5ce48f9c
commit 0828630a5b
16 changed files with 1621 additions and 22 deletions
+100
View File
@@ -5,6 +5,8 @@ import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:terepi_seged/enums/note_type.dart';
import 'package:terepi_seged/models/note_item.dart';
import 'package:terepi_seged/models/note_item_audio.dart';
import 'package:terepi_seged/models/note_item_photo.dart';
import 'package:terepi_seged/models/track.dart';
import 'package:uuid/uuid.dart';
import '../models/project.dart';
@@ -132,6 +134,36 @@ class AppDatabase {
await db
.execute('CREATE INDEX idx_notes_project ON note_items(project_id)');
await db.execute('''
CREATE TABLE IF NOT EXISTS note_item_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_item_id INTEGER NOT NULL REFERENCES note_items(id) ON DELETE CASCADE,
local_path TEXT NOT NULL,
storage_path TEXT,
caption TEXT NOT NULL DEFAULT '',
latitude REAL,
longitude REAL,
created_at TEXT NOT NULL
)
''');
await db.execute(
'CREATE INDEX idx_photos_note ON note_item_photos(note_item_id)');
await db.execute('''
CREATE TABLE IF NOT EXISTS note_item_audios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_item_id INTEGER NOT NULL REFERENCES note_items(id) ON DELETE CASCADE,
local_path TEXT NOT NULL,
caption TEXT NOT NULL DEFAULT '',
duration_seconds INTEGER NOT NULL DEFAULT 0,
latitude REAL,
longitude REAL,
created_at TEXT NOT NULL
)
''');
await db.execute(
'CREATE INDEX idx_audios_note ON note_item_audios(note_item_id)');
await db.execute('''
CREATE TABLE IF NOT EXISTS pending_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -459,4 +491,72 @@ class AppDatabase {
whereArgs: [projectId],
);
}
// -------- NoteItemPhoto
Future<int> insertNotePhoto(NoteItemPhoto photo) async {
final db = await database;
return db.insert('note_item_photos', photo.toMap());
}
Future<void> updateNotePhoto(NoteItemPhoto photo) async {
final db = await database;
await db.update(
'note_item_photos',
photo.toMap(),
where: 'id = ?',
whereArgs: [photo.id],
);
}
Future<void> deleteNotePhoto(int id) async {
final db = await database;
await db.delete('note_item_photos', where: 'id = ?', whereArgs: [id]);
}
Future<List<NoteItemPhoto>> listNotePhotos(int noteItemId) async {
final db = await database;
final rows = await db.query(
'note_item_photos',
where: 'note_item_id = ?',
whereArgs: [noteItemId],
orderBy: 'created_at ASC',
);
return rows.map(NoteItemPhoto.fromMap).toList();
}
Future<void> deleteAllNotePhotos(int noteItemId) async {
final db = await database;
await db.delete('note_item_photos',
where: 'note_item_id = ?', whereArgs: [noteItemId]);
}
// -------------- NoteItemAudio
Future<int> insertNoteAudio(NoteItemAudio audio) async {
final db = await database;
return db.insert('note_item_audios', audio.toMap());
}
Future<void> updateNoteAudio(NoteItemAudio audio) async {
final db = await database;
await db.update('note_item_audios', audio.toMap(),
where: 'id = ?', whereArgs: [audio.id]);
}
Future<void> deleteNoteAudio(int id) async {
final db = await database;
await db.delete('note_item_audios', where: 'id = ?', whereArgs: [id]);
}
Future<List<NoteItemAudio>> listNoteAudios(int noteItemId) async {
final db = await database;
final rows = await db.query(
'note_item_audios',
where: 'note_item_id = ?',
whereArgs: [noteItemId],
orderBy: 'created_at ASC',
);
return rows.map(NoteItemAudio.fromMap).toList();
}
}
+265
View File
@@ -0,0 +1,265 @@
// Hangjegyzet service:
// - Felvétel (record csomag, AAC/m4a)
// - Lejátszás (audioplayers csomag)
// - Fájl kezelés + SQLite mentés
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import 'package:audioplayers/audioplayers.dart';
import '../models/note_item_audio.dart';
import '../services/app_database.dart';
// ─── Felvétel állapot ─────────────────────────────────────────────────────────
enum AudioRecordState { idle, recording, stopped }
enum AudioPlayState { idle, playing, paused }
// ─── NoteAudioService ─────────────────────────────────────────────────────────
class NoteAudioService extends GetxService {
static NoteAudioService get to => Get.find();
// ── Reaktív állapot ────────────────────────────────────────────────────────
final recordState = AudioRecordState.idle.obs;
final recordDurationMs = 0.obs; // eltelt ms felvétel közben
final playingAudioId = Rxn<int>(); // melyik clip játszik épp
final playState = AudioPlayState.idle.obs;
final playPositionMs = 0.obs;
// ── Belső ──────────────────────────────────────────────────────────────────
final _recorder = AudioRecorder();
final _player = AudioPlayer();
String? _audioDir;
String? _currentRecordPath;
Timer? _recordTimer;
int? _recordStartMs;
// ── Inicializálás ──────────────────────────────────────────────────────────
@override
Future<void> onInit() async {
super.onInit();
await _initAudioDir();
_initPlayerListeners();
}
Future<void> _initAudioDir() async {
final ext = await getExternalStorageDirectory();
final dir = Directory(p.join(ext!.path, 'audio'));
if (!await dir.exists()) await dir.create(recursive: true);
_audioDir = dir.path;
}
void _initPlayerListeners() {
// Lejátszás pozíció frissítése
_player.onPositionChanged.listen((pos) {
playPositionMs.value = pos.inMilliseconds;
});
// Lejátszás vége
_player.onPlayerComplete.listen((_) {
playState.value = AudioPlayState.idle;
playingAudioId.value = null;
playPositionMs.value = 0;
});
}
// ── Felvétel ──────────────────────────────────────────────────────────────
Future<bool> startRecording(int noteItemId) async {
// Engedély ellenőrzés
final hasPermission = await _recorder.hasPermission();
if (!hasPermission) {
Get.snackbar(
'Engedély szükséges', 'Mikrofon hozzáférés engedélyezése szükséges.',
snackPosition: SnackPosition.BOTTOM);
return false;
}
// Aktív lejátszás leállítása
if (playState.value != AudioPlayState.idle) {
await stopPlayback();
}
final fileName = 'audio_${noteItemId}_'
'${DateTime.now().millisecondsSinceEpoch}.m4a';
_currentRecordPath = p.join(_audioDir!, fileName);
await _recorder.start(
const RecordConfig(
encoder: AudioEncoder.aacLc, // jó minőség, kis méret
bitRate: 64000, // 64kbps elegendő hanghoz
sampleRate: 22050,
),
path: _currentRecordPath!,
);
_recordStartMs = DateTime.now().millisecondsSinceEpoch;
recordState.value = AudioRecordState.recording;
recordDurationMs.value = 0;
// Másodpercenkénti számláló
_recordTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
recordDurationMs.value =
DateTime.now().millisecondsSinceEpoch - _recordStartMs!;
});
return true;
}
/// Felvétel leállítása és SQLite mentés
Future<NoteItemAudio?> stopRecording(int noteItemId) async {
if (recordState.value != AudioRecordState.recording) return null;
_recordTimer?.cancel();
_recordTimer = null;
final path = await _recorder.stop();
recordState.value = AudioRecordState.idle;
if (path == null) return null;
final durationSec =
(DateTime.now().millisecondsSinceEpoch - _recordStartMs!) ~/ 1000;
recordDurationMs.value = 0;
_recordStartMs = null;
// Nagyon rövid felvétel dobja el (véletlen érintés)
if (durationSec < 1) {
await File(path).delete().catchError((_) => File(path));
return null;
}
// GPS pozíció
final pos = await _currentPosition();
final audio = NoteItemAudio(
noteItemId: noteItemId,
localPath: path,
durationSeconds: durationSec,
latitude: pos?.latitude,
longitude: pos?.longitude,
createdAt: DateTime.now(),
);
final id = await AppDatabase.instance.insertNoteAudio(audio);
return NoteItemAudio(
id: id,
noteItemId: audio.noteItemId,
localPath: audio.localPath,
durationSeconds: audio.durationSeconds,
latitude: audio.latitude,
longitude: audio.longitude,
createdAt: audio.createdAt,
);
}
/// Felvétel megszakítása (mentés nélkül)
Future<void> cancelRecording() async {
if (recordState.value != AudioRecordState.recording) return;
_recordTimer?.cancel();
_recordTimer = null;
recordDurationMs.value = 0;
final path = await _recorder.stop();
recordState.value = AudioRecordState.idle;
if (path != null) {
await File(path).delete().catchError((_) => File(path));
}
}
// ── Lejátszás ─────────────────────────────────────────────────────────────
Future<void> playAudio(NoteItemAudio audio) async {
if (!audio.fileExists) {
Get.snackbar('Fájl nem található', 'A hangjegyzet fájlja törlődött.',
snackPosition: SnackPosition.BOTTOM);
return;
}
// Ha ugyanaz játszik → pause/resume toggle
if (playingAudioId.value == audio.id) {
if (playState.value == AudioPlayState.playing) {
await _player.pause();
playState.value = AudioPlayState.paused;
} else {
await _player.resume();
playState.value = AudioPlayState.playing;
}
return;
}
// Más vagy új clip
await _player.stop();
playingAudioId.value = audio.id;
playState.value = AudioPlayState.playing;
playPositionMs.value = 0;
await _player.play(DeviceFileSource(audio.localPath));
}
Future<void> stopPlayback() async {
await _player.stop();
playState.value = AudioPlayState.idle;
playingAudioId.value = null;
playPositionMs.value = 0;
}
// ── Törlés ────────────────────────────────────────────────────────────────
Future<void> deleteAudio(NoteItemAudio audio) async {
// Leállítás ha épp játszik
if (playingAudioId.value == audio.id) await stopPlayback();
final file = File(audio.localPath);
if (await file.exists()) await file.delete();
await AppDatabase.instance.deleteNoteAudio(audio.id!);
}
Future<NoteItemAudio> updateCaption(
NoteItemAudio audio, String caption) async {
final updated = audio.copyWith(caption: caption);
await AppDatabase.instance.updateNoteAudio(updated);
return updated;
}
Future<List<NoteItemAudio>> loadAudios(int noteItemId) =>
AppDatabase.instance.listNoteAudios(noteItemId);
// ── Segéd ─────────────────────────────────────────────────────────────────
Future<Position?> _currentPosition() async {
try {
return await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 3)),
).timeout(const Duration(seconds: 3));
} catch (_) {
return Geolocator.getLastKnownPosition();
}
}
// Formázott időtartam: ms → "0:42"
String formatMs(int ms) {
final s = ms ~/ 1000;
final m = s ~/ 60;
return '$m:${(s % 60).toString().padLeft(2, '0')}';
}
@override
void onClose() {
_recorder.dispose();
_player.dispose();
_recordTimer?.cancel();
super.onClose();
}
}
+146
View File
@@ -0,0 +1,146 @@
// lib/services/note_photo_service.dart
//
// Fotó kezelő service:
// - Kamera / galéria hozzáférés (image_picker)
// - Fájl mentés külső tárhelyre
// - SQLite CRUD
// - Opcionális Supabase Storage szinkron
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../models/note_item_photo.dart';
import '../services/app_database.dart';
class NotePhotoService extends GetxService {
static NotePhotoService get to => Get.find();
final _picker = ImagePicker();
String? _photoDir;
@override
Future<void> onInit() async {
super.onInit();
await _initPhotoDir();
}
Future<void> _initPhotoDir() async {
final ext = await getExternalStorageDirectory();
final dir = Directory(p.join(ext!.path, 'photos'));
if (!await dir.exists()) await dir.create(recursive: true);
_photoDir = dir.path;
}
// ── Fotó készítése kamerával ─────────────────────────────────────
Future<NoteItemPhoto?> takePhoto(int noteItemId) async {
return _pickAndSave(
noteItemId: noteItemId,
source: ImageSource.camera,
);
}
// ── Fotó választása galériából ───────────────────────────────────
Future<NoteItemPhoto?> pickFromGallery(int noteItemId) async {
return _pickAndSave(
noteItemId: noteItemId,
source: ImageSource.gallery,
);
}
// ── Közös mentési logika ─────────────────────────────────────────
Future<NoteItemPhoto?> _pickAndSave({
required int noteItemId,
required ImageSource source,
}) async {
try {
final picked = await _picker.pickImage(
source: source,
imageQuality: 85, // tömörítés a tárhelyért
maxWidth: 2048,
maxHeight: 2048,
);
if (picked == null) return null; // felhasználó visszalépett
// GPS pozíció a fotókészítés pillanatában
final pos = await _currentPosition();
// Fájl másolása az app saját könyvtárába
final fileName =
'photo_${noteItemId}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final destPath = p.join(_photoDir!, fileName);
await File(picked.path).copy(destPath);
// SQLite mentés
final photo = NoteItemPhoto(
noteItemId: noteItemId,
localPath: destPath,
latitude: pos?.latitude,
longitude: pos?.longitude,
createdAt: DateTime.now(),
);
final id = await AppDatabase.instance.insertNotePhoto(photo);
return NoteItemPhoto(
id: id,
noteItemId: photo.noteItemId,
localPath: photo.localPath,
latitude: photo.latitude,
longitude: photo.longitude,
createdAt: photo.createdAt,
);
} catch (e) {
debugPrint('NotePhotoService hiba: $e');
Get.snackbar('Hiba', 'Fotó mentése sikertelen.',
snackPosition: SnackPosition.BOTTOM);
return null;
}
}
// ── Fotó törlése ─────────────────────────────────────────────────
Future<void> deletePhoto(NoteItemPhoto photo) async {
// Fájl törlése
final file = File(photo.localPath);
if (await file.exists()) await file.delete();
// SQLite törlése
await AppDatabase.instance.deleteNotePhoto(photo.id!);
}
// ── Felirat frissítése ────────────────────────────────────────────
Future<NoteItemPhoto> updateCaption(
NoteItemPhoto photo, String caption) async {
final updated = photo.copyWith(caption: caption);
await AppDatabase.instance.updateNotePhoto(updated);
return updated;
}
// ── GPS pozíció ──────────────────────────────────────────────────
Future<Position?> _currentPosition() async {
try {
return await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 3),
),
).timeout(const Duration(seconds: 3));
} catch (_) {
return Geolocator.getLastKnownPosition();
}
}
// ── Fotók betöltése ──────────────────────────────────────────────
Future<List<NoteItemPhoto>> loadPhotos(int noteItemId) =>
AppDatabase.instance.listNotePhotos(noteItemId);
}