Terepbejárás geometria hang és képi dokumentáció létrehozása. Gradle verzió frissítése
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user