feat: refactor input source handling and add audio input service

This commit is contained in:
Nedifinita
2025-12-05 17:01:42 +08:00
parent 7772112658
commit 99bc081583
13 changed files with 838 additions and 522 deletions

View File

@@ -0,0 +1,49 @@
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:developer' as developer;
class AudioInputService {
static final AudioInputService _instance = AudioInputService._internal();
factory AudioInputService() => _instance;
AudioInputService._internal();
static const _methodChannel = MethodChannel('org.noxylva.lbjconsole/audio_input');
bool _isListening = false;
bool get isListening => _isListening;
Future<bool> startListening() async {
if (_isListening) return true;
var status = await Permission.microphone.status;
if (!status.isGranted) {
status = await Permission.microphone.request();
if (!status.isGranted) {
developer.log('Microphone permission denied', name: 'AudioInput');
return false;
}
}
try {
await _methodChannel.invokeMethod('start');
_isListening = true;
developer.log('Audio input started', name: 'AudioInput');
return true;
} on PlatformException catch (e) {
developer.log('Failed to start audio input: ${e.message}', name: 'AudioInput');
return false;
}
}
Future<void> stopListening() async {
if (!_isListening) return;
try {
await _methodChannel.invokeMethod('stop');
_isListening = false;
developer.log('Audio input stopped', name: 'AudioInput');
} catch (e) {
developer.log('Error stopping audio input: $e', name: 'AudioInput');
}
}
}

View File

@@ -4,16 +4,23 @@ import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:convert';
import 'dart:developer' as developer;
import 'package:lbjconsole/models/train_record.dart';
enum InputSource {
bluetooth,
rtlTcp,
audioInput
}
class DatabaseService {
static final DatabaseService instance = DatabaseService._internal();
factory DatabaseService() => instance;
DatabaseService._internal();
static const String _databaseName = 'train_database';
static const _databaseVersion = 8;
static const _databaseVersion = 9;
static const String trainRecordsTable = 'train_records';
static const String appSettingsTable = 'app_settings';
@@ -63,6 +70,8 @@ class DatabaseService {
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
developer.log('Database upgrading from $oldVersion to $newVersion', name: 'Database');
if (oldVersion < 2) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN hideTimeOnlyRecords INTEGER NOT NULL DEFAULT 0');
@@ -97,6 +106,27 @@ class DatabaseService {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN rtlTcpPort TEXT NOT NULL DEFAULT "14423"');
}
if (oldVersion < 9) {
await db.execute(
'ALTER TABLE $appSettingsTable ADD COLUMN inputSource TEXT NOT NULL DEFAULT "bluetooth"');
try {
final List<Map<String, dynamic>> results = await db.query(appSettingsTable, columns: ['rtlTcpEnabled'], where: 'id = 1');
if (results.isNotEmpty) {
final int rtlTcpEnabled = results.first['rtlTcpEnabled'] as int? ?? 0;
if (rtlTcpEnabled == 1) {
await db.update(
appSettingsTable,
{'inputSource': 'rtlTcp'},
where: 'id = 1'
);
developer.log('Migrated V8 settings: inputSource set to rtlTcp', name: 'Database');
}
}
} catch (e) {
developer.log('Migration V8->V9 data update failed: $e', name: 'Database');
}
}
}
Future<void> _onCreate(Database db, int version) async {
@@ -150,7 +180,8 @@ class DatabaseService {
mapSettingsTimestamp INTEGER,
rtlTcpEnabled INTEGER NOT NULL DEFAULT 0,
rtlTcpHost TEXT NOT NULL DEFAULT '127.0.0.1',
rtlTcpPort TEXT NOT NULL DEFAULT '14423'
rtlTcpPort TEXT NOT NULL DEFAULT '14423',
inputSource TEXT NOT NULL DEFAULT 'bluetooth'
)
''');
@@ -177,11 +208,12 @@ class DatabaseService {
'groupBy': 'trainAndLoco',
'timeWindow': 'unlimited',
'mapTimeFilter': 'unlimited',
'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
'rtlTcpEnabled': 0,
'rtlTcpHost': '127.0.0.1',
'rtlTcpPort': '14423',
'hideUngroupableRecords': 0,
'mapSettingsTimestamp': null,
'rtlTcpEnabled': 0,
'rtlTcpHost': '127.0.0.1',
'rtlTcpPort': '14423',
'inputSource': 'bluetooth',
});
}
@@ -409,14 +441,13 @@ class DatabaseService {
StreamSubscription<void> onSettingsChanged(
Function(Map<String, dynamic>) listener) {
_settingsListeners.add(listener);
return Stream.value(null).listen((_) {})
..onData((_) {})
..onDone(() {
_settingsListeners.remove(listener);
});
return _SettingsListenerSubscription(() {
_settingsListeners.remove(listener);
});
}
void _notifySettingsChanged(Map<String, dynamic> settings) {
print('[Database] Notifying ${_settingsListeners.length} settings listeners');
for (final listener in _settingsListeners) {
listener(settings);
}
@@ -499,3 +530,39 @@ class DatabaseService {
}
}
}
class _SettingsListenerSubscription implements StreamSubscription<void> {
final void Function() _onCancel;
bool _isCanceled = false;
_SettingsListenerSubscription(this._onCancel);
@override
Future<void> cancel() async {
if (!_isCanceled) {
_isCanceled = true;
_onCancel();
}
}
@override
void onData(void Function(void data)? handleData) {}
@override
void onDone(void Function()? handleDone) {}
@override
void onError(Function? handleError) {}
@override
void pause([Future<void>? resumeSignal]) {}
@override
void resume() {}
@override
bool get isPaused => false;
@override
Future<E> asFuture<E>([E? futureValue]) => Future.value(futureValue);
}