im so tired man

This commit is contained in:
Wowieee4
2026-05-28 11:27:06 +07:00
parent 66da2473e1
commit 6272ece15d
86 changed files with 1759 additions and 4090 deletions
@@ -22,6 +22,9 @@ class AppConstants {
if (!cleaned.startsWith('http://') && !cleaned.startsWith('https://')) {
cleaned = 'http://$cleaned';
}
cleaned = cleaned
.replaceFirst('://localhost', '://127.0.0.1')
.replaceFirst('://0.0.0.0', '://127.0.0.1');
while (cleaned.endsWith('/')) {
cleaned = cleaned.substring(0, cleaned.length - 1);
}
@@ -61,7 +64,6 @@ class AppConstants {
await prefs.setString(_selectedYoloModelKey, path);
}
// Agora App ID tetap bisa dioverride saat build: --dart-define=AGORA_APP_ID=...
static const String agoraAppId = String.fromEnvironment('AGORA_APP_ID',
defaultValue: 'e36c2b6592e34cfda1f6ea6432a5e68d');
// Set at build time with: --dart-define=AGORA_APP_ID=your_app_id
static const String agoraAppId = String.fromEnvironment('AGORA_APP_ID');
}
@@ -75,6 +75,8 @@ bool _looksTechnical(String message) {
'duplicate key',
'constraint',
'sql [',
'illegal base64',
'base64 character',
];
return blocked.any(lower.contains);
}
@@ -1,9 +1,19 @@
import 'package:flutter/widgets.dart';
class AppStrings {
final String localeCode;
const AppStrings(this.localeCode);
static const supportedLocales = ['id-ID', 'en-US'];
static const supportedLocales = [
Locale('id', 'ID'),
Locale('en', 'US'),
];
static AppStrings of(BuildContext context) {
return Localizations.of<AppStrings>(context, AppStrings) ??
const AppStrings('id-ID');
}
String get walkGuideStarted => _pick(
id: 'WalkGuide dimulai',
@@ -29,3 +39,21 @@ class AppStrings {
return localeCode == 'en-US' ? en : id;
}
}
class AppStringsDelegate extends LocalizationsDelegate<AppStrings> {
const AppStringsDelegate();
@override
bool isSupported(Locale locale) {
return locale.languageCode == 'id' || locale.languageCode == 'en';
}
@override
Future<AppStrings> load(Locale locale) async {
final code = locale.languageCode == 'en' ? 'en-US' : 'id-ID';
return AppStrings(code);
}
@override
bool shouldReload(covariant LocalizationsDelegate<AppStrings> old) => false;
}
@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../constants/app_constants.dart';
import '../storage/secure_storage.dart';
@@ -24,8 +25,15 @@ class ApiClient {
_dio.interceptors.addAll([
_AuthInterceptor(_secureStorage, _dio),
_ErrorInterceptor(),
LogInterceptor(requestBody: true, responseBody: true),
]);
if (kDebugMode) {
_dio.interceptors.add(LogInterceptor(
requestBody: false,
responseBody: false,
requestHeader: false,
responseHeader: false,
));
}
}
Dio get dio => _dio;
@@ -42,7 +50,8 @@ class _AuthInterceptor extends Interceptor {
_AuthInterceptor(this._storage, this._dio);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
@@ -166,6 +166,11 @@ class CallService {
debugPrint('Agora remote user offline: $remoteUid $reason');
_onRemoteUserOffline?.call();
},
onRemoteAudioStateChanged: (_, remoteUid, state, reason, __) {
debugPrint(
'Agora remote audio state: uid=$remoteUid state=$state reason=$reason',
);
},
onError: (type, msg) {
debugPrint('Agora error: $type $msg');
if (!joinCompleter.isCompleted) joinCompleter.complete(false);
@@ -175,9 +180,18 @@ class CallService {
await _engine!.setChannelProfile(
ChannelProfileType.channelProfileCommunication,
);
await _engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine!.setAudioProfile(
profile: AudioProfileType.audioProfileDefault,
scenario: AudioScenarioType.audioScenarioMeeting,
);
await _engine!.enableAudio();
await _engine!.enableLocalAudio(true);
await _engine!.muteAllRemoteAudioStreams(false);
await _engine!.muteLocalAudioStream(false);
await _engine!.adjustRecordingSignalVolume(100);
await _engine!.adjustPlaybackSignalVolume(100);
await _engine!.setDefaultAudioRouteToSpeakerphone(true);
await _engine!.setEnableSpeakerphone(true);
await _engine!.joinChannel(
token: token ?? '',
@@ -9,7 +9,6 @@ import '../network/api_client.dart';
class FcmService {
final ApiClient _apiClient;
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
@@ -18,6 +17,7 @@ class FcmService {
Future<void> init() async {
if (kIsWeb) return;
try {
final messaging = FirebaseMessaging.instance;
await _localNotifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
@@ -31,10 +31,10 @@ class FcmService {
} catch (_) {}
},
);
await _messaging.requestPermission(alert: true, badge: true, sound: true);
final token = await _messaging.getToken();
await messaging.requestPermission(alert: true, badge: true, sound: true);
final token = await messaging.getToken();
if (token != null) await syncToken(token);
FirebaseMessaging.instance.onTokenRefresh.listen(syncToken);
messaging.onTokenRefresh.listen(syncToken);
FirebaseMessaging.onMessage.listen((message) {
debugPrint('FCM foreground: ${message.data}');
_showLocalNotification(message);
@@ -55,6 +55,10 @@ class FcmService {
Future<void> syncToken(String token) async {
try {
if (_apiClient.baseUrl == null) {
debugPrint('FCM token sync skipped: server URL is not ready.');
return;
}
await _apiClient.dio.put('/auth/fcm-token', data: {'fcmToken': token});
} catch (e) {
debugPrint('FCM token sync skipped: $e');
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:battery_plus/battery_plus.dart';
import 'package:dio/dio.dart';
import 'package:geolocator/geolocator.dart';
@@ -10,6 +11,7 @@ import 'offline_queue_service.dart';
class LocationReporterService {
final ApiClient _apiClient;
final OfflineQueueService _offlineQueue;
final Battery _battery = Battery();
Timer? _timer;
LocationReporterService(this._apiClient, this._offlineQueue);
@@ -32,12 +34,14 @@ class LocationReporterService {
try {
await Geolocator.requestPermission();
final position = await Geolocator.getCurrentPosition();
final batteryLevel = await _readBatteryLevel();
await _apiClient.dio.post('/user/location', data: {
'lat': position.latitude,
'lng': position.longitude,
'accuracy': position.accuracy,
'speed': position.speed,
'heading': position.heading,
if (batteryLevel != null) 'batteryLevel': batteryLevel,
});
} on DioException catch (_) {
await _offlineQueue.enqueue(OfflineRequest(
@@ -50,4 +54,12 @@ class LocationReporterService {
// GPS permission can be unavailable during desktop/web testing.
}
}
Future<int?> _readBatteryLevel() async {
try {
return await _battery.batteryLevel;
} catch (_) {
return null;
}
}
}
@@ -19,6 +19,8 @@ class VoiceCommand {
/// Callback yang dipanggil saat command terdeteksi
/// Registered oleh router/screen yang relevan
typedef CommandCallback = void Function(VoiceCommandKey key);
typedef CommandRouter = void Function(String route);
typedef CommandAction = void Function();
class VoiceCommandHandler {
final SttService _stt;
@@ -26,9 +28,19 @@ class VoiceCommandHandler {
List<VoiceCommand> _commands = [];
CommandCallback? onCommand;
CommandRouter? _router;
final Map<VoiceCommandKey, CommandAction> _actions = {};
VoiceCommandHandler(this._stt, this._tts);
void registerRouter(CommandRouter router) {
_router = router;
}
void registerAction(VoiceCommandKey key, CommandAction action) {
_actions[key] = action;
}
void loadCommands(List<VoiceCommand> commands) {
_commands = commands;
_stt.onResult = _processText;
@@ -66,9 +78,28 @@ class VoiceCommandHandler {
}
void _handleCommand(VoiceCommandKey key) {
_routeFor(key);
_actions[key]?.call();
onCommand?.call(key);
// Built-in actions for TTS-only commands
if (key == VoiceCommandKey.repeatLast) _tts.repeatLast();
if (key == VoiceCommandKey.stopTts) _tts.stop();
}
void _routeFor(VoiceCommandKey key) {
final route = switch (key) {
VoiceCommandKey.openWalkguide || VoiceCommandKey.startWalkguide =>
'/user/walkguide',
VoiceCommandKey.openNotification => '/user/notifications',
VoiceCommandKey.openSos || VoiceCommandKey.sendSos => '/user/sos',
VoiceCommandKey.openActivity => '/user/activity',
VoiceCommandKey.openNavigation => '/user/navigation',
VoiceCommandKey.openSettings => '/user/settings',
VoiceCommandKey.callGuardian => '/call',
_ => null,
};
if (route != null) {
_router?.call(route);
}
}
}