POLISHING STUFF and Smarter AI, and alot i mean.. ALOT OF BUG FIX AND OPTIMIZE
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
@@ -7,6 +8,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import '../../app/router.dart';
|
||||
import '../network/api_client.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
}
|
||||
|
||||
class FcmService {
|
||||
final ApiClient _apiClient;
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
@@ -17,6 +23,9 @@ class FcmService {
|
||||
Future<void> init() async {
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler);
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await _localNotifications.initialize(
|
||||
const InitializationSettings(
|
||||
|
||||
@@ -1,36 +1,107 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
class HapticService {
|
||||
Future<bool> get _hasVibrator async => Vibration.hasVibrator();
|
||||
bool _enabled = true;
|
||||
DateTime _lastObstacleVibrationAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
static const _obstacleCooldown = Duration(seconds: 3);
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
void setEnabled(bool enabled) {
|
||||
_enabled = enabled;
|
||||
if (!enabled) {
|
||||
Vibration.cancel().ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> get _hasVibrator async {
|
||||
try {
|
||||
final hasVibrator = await Vibration.hasVibrator();
|
||||
return hasVibrator == true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _canRunObstacleVibration() {
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastObstacleVibrationAt) < _obstacleCooldown) {
|
||||
return false;
|
||||
}
|
||||
_lastObstacleVibrationAt = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _vibrate({
|
||||
int? duration,
|
||||
List<int>? pattern,
|
||||
required Future<void> Function() fallback,
|
||||
bool obstacle = false,
|
||||
}) async {
|
||||
if (!_enabled) return;
|
||||
if (obstacle && !_canRunObstacleVibration()) return;
|
||||
|
||||
try {
|
||||
if (await _hasVibrator) {
|
||||
if (pattern != null) {
|
||||
await Vibration.vibrate(pattern: pattern);
|
||||
} else if (duration != null) {
|
||||
await Vibration.vibrate(duration: duration);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Use Flutter's platform haptics below when the vibration plugin fails.
|
||||
}
|
||||
|
||||
await fallback();
|
||||
}
|
||||
|
||||
Future<void> obstacleVeryClose() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(pattern: [0, 500, 100, 500, 100, 500]);
|
||||
await _vibrate(
|
||||
pattern: [0, 500, 100, 500, 100, 500],
|
||||
fallback: HapticFeedback.heavyImpact,
|
||||
obstacle: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> obstacleClose() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(pattern: [0, 300, 100, 300]);
|
||||
await _vibrate(
|
||||
pattern: [0, 300, 100, 300],
|
||||
fallback: HapticFeedback.mediumImpact,
|
||||
obstacle: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> obstacleMedium() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(duration: 150);
|
||||
await _vibrate(
|
||||
duration: 150,
|
||||
fallback: HapticFeedback.lightImpact,
|
||||
obstacle: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sosTriggered() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(pattern: [0, 1000, 200, 1000, 200, 1000]);
|
||||
await _vibrate(
|
||||
pattern: [0, 1000, 200, 1000, 200, 1000],
|
||||
fallback: HapticFeedback.heavyImpact,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> callIncoming() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(pattern: [0, 500, 500, 500, 500, 500, 500, 500]);
|
||||
await _vibrate(
|
||||
pattern: [0, 500, 500, 500, 500, 500, 500, 500],
|
||||
fallback: HapticFeedback.mediumImpact,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> success() async {
|
||||
if (!await _hasVibrator) return;
|
||||
Vibration.vibrate(duration: 80);
|
||||
await _vibrate(
|
||||
duration: 80,
|
||||
fallback: HapticFeedback.selectionClick,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stop() async => Vibration.cancel();
|
||||
|
||||
@@ -27,11 +27,14 @@ class HardwareShortcutBinding {
|
||||
class HardwareShortcutListener {
|
||||
final ApiClient _apiClient;
|
||||
final Map<int, HardwareShortcutBinding> _bindings = {};
|
||||
final Map<int, DateTime> _lastHandledAt = {};
|
||||
|
||||
bool _listening = false;
|
||||
void Function(HardwareShortcutAction action)? _onAction;
|
||||
void Function(int buttonCode, String buttonName)? _captureCallback;
|
||||
|
||||
static const Duration _repeatDebounce = Duration(milliseconds: 900);
|
||||
|
||||
HardwareShortcutListener(this._apiClient);
|
||||
|
||||
Future<void> startListening({
|
||||
@@ -68,7 +71,8 @@ class HardwareShortcutListener {
|
||||
);
|
||||
}
|
||||
|
||||
void captureNextButton(void Function(int buttonCode, String buttonName) onCapture) {
|
||||
void captureNextButton(
|
||||
void Function(int buttonCode, String buttonName) onCapture) {
|
||||
_captureCallback = onCapture;
|
||||
}
|
||||
|
||||
@@ -88,6 +92,12 @@ class HardwareShortcutListener {
|
||||
|
||||
final binding = _bindings[code];
|
||||
if (binding == null || !binding.enabled) return false;
|
||||
final now = DateTime.now();
|
||||
final lastHandled = _lastHandledAt[code];
|
||||
if (lastHandled != null && now.difference(lastHandled) < _repeatDebounce) {
|
||||
return true;
|
||||
}
|
||||
_lastHandledAt[code] = now;
|
||||
_onAction?.call(binding.action);
|
||||
return true;
|
||||
}
|
||||
@@ -103,7 +113,8 @@ HardwareShortcutBinding? _bindingFromJson(Map<String, dynamic> item) {
|
||||
final action = _actionFromBackend(item['shortcutKey']?.toString());
|
||||
final rawCode = item['buttonCode'];
|
||||
final enabled = item['enabled'] != false;
|
||||
final code = rawCode is int ? rawCode : int.tryParse(rawCode?.toString() ?? '');
|
||||
final code =
|
||||
rawCode is int ? rawCode : int.tryParse(rawCode?.toString() ?? '');
|
||||
if (action == null || code == null || code <= 0) return null;
|
||||
return HardwareShortcutBinding(
|
||||
action: action,
|
||||
|
||||
@@ -6,17 +6,24 @@ class SttService {
|
||||
final SpeechToText _stt = SpeechToText();
|
||||
bool _available = false;
|
||||
bool _listening = false;
|
||||
bool _shouldListen = false;
|
||||
bool _initializing = false;
|
||||
Function(String)? onResult;
|
||||
|
||||
Future<bool> init() async {
|
||||
if (_available) return true;
|
||||
if (_initializing) return _available;
|
||||
_initializing = true;
|
||||
_available = await _stt.initialize(
|
||||
onError: (e) => _onError(e),
|
||||
onStatus: (s) => _onStatus(s),
|
||||
);
|
||||
_initializing = false;
|
||||
return _available;
|
||||
}
|
||||
|
||||
Future<void> startListening() async {
|
||||
_shouldListen = true;
|
||||
if (!_available || _listening) return;
|
||||
_listening = true;
|
||||
await _stt.listen(
|
||||
@@ -25,14 +32,15 @@ class SttService {
|
||||
onResult?.call(result.recognizedWords.toLowerCase().trim());
|
||||
}
|
||||
},
|
||||
listenFor: const Duration(seconds: 10),
|
||||
pauseFor: const Duration(seconds: 3),
|
||||
listenFor: const Duration(seconds: 60),
|
||||
pauseFor: const Duration(seconds: 8),
|
||||
localeId: 'id_ID',
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stopListening() async {
|
||||
_shouldListen = false;
|
||||
_listening = false;
|
||||
await _stt.stop();
|
||||
}
|
||||
@@ -42,15 +50,17 @@ class SttService {
|
||||
|
||||
void _onError(dynamic error) {
|
||||
_listening = false;
|
||||
// Auto-restart setelah error
|
||||
Future.delayed(const Duration(seconds: 1), startListening);
|
||||
if (_shouldListen) {
|
||||
Future.delayed(const Duration(seconds: 2), startListening);
|
||||
}
|
||||
}
|
||||
|
||||
void _onStatus(String status) {
|
||||
if (status == 'done' || status == 'notListening') {
|
||||
_listening = false;
|
||||
// Auto-restart agar selalu mendengarkan
|
||||
Future.delayed(const Duration(milliseconds: 500), startListening);
|
||||
if (_shouldListen) {
|
||||
Future.delayed(const Duration(seconds: 2), startListening);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ class TtsService {
|
||||
final FlutterTts _tts = FlutterTts();
|
||||
final List<String> _queue = [];
|
||||
bool _speaking = false;
|
||||
bool _initialized = false;
|
||||
String _lastSpoken = '';
|
||||
DateTime _lastQueuedAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
Future<void> init({String language = 'id-ID', double pitch = 1.0, double rate = 0.5}) async {
|
||||
Future<void> init(
|
||||
{String language = 'id-ID',
|
||||
double pitch = 1.0,
|
||||
double rate = 0.5}) async {
|
||||
if (_initialized) return;
|
||||
await _tts.setLanguage(language);
|
||||
await _tts.setPitch(pitch);
|
||||
await _tts.setSpeechRate(rate);
|
||||
@@ -15,11 +21,25 @@ class TtsService {
|
||||
_speaking = false;
|
||||
_processQueue();
|
||||
});
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// Tambah ke antrian - tidak memotong yg sedang bicara
|
||||
void speak(String text) {
|
||||
if (text.isEmpty) return;
|
||||
if (!_initialized) {
|
||||
init().then((_) => speak(text));
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
if (text == _lastSpoken &&
|
||||
now.difference(_lastQueuedAt) < const Duration(milliseconds: 900)) {
|
||||
return;
|
||||
}
|
||||
_lastQueuedAt = now;
|
||||
if (_queue.length >= 3) {
|
||||
_queue.removeAt(0);
|
||||
}
|
||||
_queue.add(text);
|
||||
if (!_speaking) _processQueue();
|
||||
}
|
||||
@@ -27,6 +47,7 @@ class TtsService {
|
||||
/// Potong TTS sekarang dan langsung ucapkan ini - untuk obstacle alert
|
||||
Future<void> speakImmediate(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
await init();
|
||||
_queue.clear();
|
||||
await _tts.stop();
|
||||
_speaking = true;
|
||||
@@ -43,9 +64,20 @@ class TtsService {
|
||||
String get lastSpoken => _lastSpoken;
|
||||
bool get isSpeaking => _speaking;
|
||||
|
||||
Future<void> setLanguage(String lang) async => _tts.setLanguage(lang);
|
||||
Future<void> setPitch(double pitch) async => _tts.setPitch(pitch);
|
||||
Future<void> setRate(double rate) async => _tts.setSpeechRate(rate);
|
||||
Future<void> setLanguage(String lang) async {
|
||||
await init(language: lang);
|
||||
await _tts.setLanguage(lang);
|
||||
}
|
||||
|
||||
Future<void> setPitch(double pitch) async {
|
||||
await init();
|
||||
await _tts.setPitch(pitch);
|
||||
}
|
||||
|
||||
Future<void> setRate(double rate) async {
|
||||
await init();
|
||||
await _tts.setSpeechRate(rate);
|
||||
}
|
||||
|
||||
void repeatLast() {
|
||||
if (_lastSpoken.isNotEmpty) speak(_lastSpoken);
|
||||
@@ -58,4 +90,4 @@ class TtsService {
|
||||
_lastSpoken = text;
|
||||
_tts.speak(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user