266 lines
8.9 KiB
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();
|
|
}
|
|
}
|