// 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(); // 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 onInit() async { super.onInit(); await _initAudioDir(); _initPlayerListeners(); } Future _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 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 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 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 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 stopPlayback() async { await _player.stop(); playState.value = AudioPlayState.idle; playingAudioId.value = null; playPositionMs.value = 0; } // ── Törlés ──────────────────────────────────────────────────────────────── Future 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 updateCaption( NoteItemAudio audio, String caption) async { final updated = audio.copyWith(caption: caption); await AppDatabase.instance.updateNoteAudio(updated); return updated; } Future> loadAudios(int noteItemId) => AppDatabase.instance.listNoteAudios(noteItemId); // ── Segéd ───────────────────────────────────────────────────────────────── Future _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(); } }