MobilApp/lib/services/note_audio_service.dart

266 lines
8.9 KiB
Dart

// 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();
}
}