Fix local dev config and ALOTTTT OFF FLUTTER, im tired boss..

This commit is contained in:
Wowieee4
2026-05-26 21:09:13 +07:00
parent b8ad8df993
commit a629357e8c
72 changed files with 2566 additions and 530 deletions
@@ -0,0 +1,57 @@
import '../services/haptic_service.dart';
import '../services/tts_service.dart';
import 'obstacle_analyzer.dart';
abstract class ObstacleAlertStrategy {
Future<void> alert(DetectionResult detection);
}
class TtsOnlyObstacleAlertStrategy implements ObstacleAlertStrategy {
final TtsService _ttsService;
const TtsOnlyObstacleAlertStrategy(this._ttsService);
@override
Future<void> alert(DetectionResult detection) {
return _ttsService.speakImmediate(detection.spokenId);
}
}
class HapticOnlyObstacleAlertStrategy implements ObstacleAlertStrategy {
final HapticService _hapticService;
const HapticOnlyObstacleAlertStrategy(this._hapticService);
@override
Future<void> alert(DetectionResult detection) {
return _vibrateByDistance(detection);
}
Future<void> _vibrateByDistance(DetectionResult detection) {
final distance = detection.estimatedDistance.toLowerCase();
if (distance.startsWith('very close')) {
return _hapticService.obstacleVeryClose();
}
if (distance.startsWith('close')) {
return _hapticService.obstacleClose();
}
return _hapticService.obstacleMedium();
}
}
class TtsWithHapticObstacleAlertStrategy implements ObstacleAlertStrategy {
final TtsService _ttsService;
final HapticService _hapticService;
const TtsWithHapticObstacleAlertStrategy(
this._ttsService,
this._hapticService,
);
@override
Future<void> alert(DetectionResult detection) async {
final haptic = HapticOnlyObstacleAlertStrategy(_hapticService);
await haptic.alert(detection);
await _ttsService.speakImmediate(detection.spokenId);
}
}
@@ -1,19 +1,28 @@
import 'package:dio/dio.dart';
class ApiService {
static const baseUrl = String.fromEnvironment(
'WALKGUIDE_API_BASE_URL',
defaultValue: 'http://202.46.28.160:8080/api/v1',
);
import 'constants/app_constants.dart';
final Dio _dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 5),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
@Deprecated('Use ApiClient for authenticated requests. Kept for legacy callers.')
class ApiService {
ApiService._(String baseUrl)
: _dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 5),
headers: const {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
final Dio _dio;
static Future<ApiService> create() async {
final serverUrl = await AppConstants.getServerUrl();
if (serverUrl == null || serverUrl.isEmpty) {
throw StateError('WalkGuide server URL has not been configured.');
}
return ApiService._(AppConstants.buildApiUrl(serverUrl));
}
Future<Response> post(String path, Map<String, dynamic> data) async {
return await _dio.post(path, data: data);
@@ -0,0 +1,31 @@
class AppStrings {
final String localeCode;
const AppStrings(this.localeCode);
static const supportedLocales = ['id-ID', 'en-US'];
String get walkGuideStarted => _pick(
id: 'WalkGuide dimulai',
en: 'WalkGuide started',
);
String get walkGuideStopped => _pick(
id: 'WalkGuide dihentikan',
en: 'WalkGuide stopped',
);
String get sosSent => _pick(
id: 'SOS terkirim. Guardian sudah diberi tahu.',
en: 'SOS sent. Your guardian has been alerted.',
);
String get notificationsOpened => _pick(
id: 'Notifikasi dibuka',
en: 'Notifications opened',
);
String _pick({required String id, required String en}) {
return localeCode == 'en-US' ? en : id;
}
}
@@ -0,0 +1,131 @@
import 'package:flutter/services.dart';
import '../network/api_client.dart';
enum HardwareShortcutAction {
callGuardian,
startWalkguide,
stopWalkguide,
sendSos,
openNotification,
}
class HardwareShortcutBinding {
final HardwareShortcutAction action;
final int buttonCode;
final String buttonName;
final bool enabled;
const HardwareShortcutBinding({
required this.action,
required this.buttonCode,
required this.buttonName,
required this.enabled,
});
}
class HardwareShortcutListener {
final ApiClient _apiClient;
final Map<int, HardwareShortcutBinding> _bindings = {};
bool _listening = false;
void Function(HardwareShortcutAction action)? _onAction;
void Function(int buttonCode, String buttonName)? _captureCallback;
HardwareShortcutListener(this._apiClient);
Future<void> startListening({
required void Function(HardwareShortcutAction action) onAction,
}) async {
_onAction = onAction;
if (!_listening) {
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
_listening = true;
}
await loadFromBackend();
}
void stopListening() {
if (!_listening) return;
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
_listening = false;
}
Future<void> loadFromBackend() async {
final response = await _apiClient.dio.get('/user/shortcuts');
final body = response.data;
final data = body is Map ? body['data'] : body;
if (data is! List) return;
_bindings
..clear()
..addEntries(
data
.whereType<Map>()
.map((item) => _bindingFromJson(Map<String, dynamic>.from(item)))
.whereType<HardwareShortcutBinding>()
.map((binding) => MapEntry(binding.buttonCode, binding)),
);
}
void captureNextButton(void Function(int buttonCode, String buttonName) onCapture) {
_captureCallback = onCapture;
}
bool _handleKeyEvent(KeyEvent event) {
if (event is! KeyDownEvent) return false;
final code = _buttonCode(event.logicalKey);
final name = event.logicalKey.keyLabel.isNotEmpty
? event.logicalKey.keyLabel
: event.logicalKey.debugName ?? 'Button $code';
final capture = _captureCallback;
if (capture != null) {
_captureCallback = null;
capture(code, name);
return true;
}
final binding = _bindings[code];
if (binding == null || !binding.enabled) return false;
_onAction?.call(binding.action);
return true;
}
int _buttonCode(LogicalKeyboardKey key) {
if (key == LogicalKeyboardKey.audioVolumeUp) return 24;
if (key == LogicalKeyboardKey.audioVolumeDown) return 25;
return key.keyId & 0xFFFFFFFF;
}
}
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() ?? '');
if (action == null || code == null || code <= 0) return null;
return HardwareShortcutBinding(
action: action,
buttonCode: code,
buttonName: item['buttonName']?.toString() ?? 'Button $code',
enabled: enabled,
);
}
HardwareShortcutAction? _actionFromBackend(String? key) {
switch (key) {
case 'CALL_GUARDIAN':
return HardwareShortcutAction.callGuardian;
case 'START_WALKGUIDE':
return HardwareShortcutAction.startWalkguide;
case 'STOP_WALKGUIDE':
return HardwareShortcutAction.stopWalkguide;
case 'SEND_SOS':
return HardwareShortcutAction.sendSos;
case 'OPEN_NOTIFICATION':
return HardwareShortcutAction.openNotification;
default:
return null;
}
}
@@ -1,8 +1,5 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../network/api_client.dart';
import '../storage/local_database.dart';
class OfflineRequest {
final String method;
@@ -33,26 +30,35 @@ class OfflineRequest {
}
class OfflineQueueService {
static const _key = 'offline_request_queue';
final LocalDatabase _database;
OfflineQueueService(this._database);
Future<void> enqueue(OfflineRequest request) async {
final prefs = await SharedPreferences.getInstance();
final queue = await readAll();
queue.add(request);
await prefs.setString(_key, jsonEncode(queue.map((e) => e.toJson()).toList()));
await _database.offlineRequests.insert(OfflineRequestRecord(
method: request.method,
path: request.path,
body: request.body,
createdAt: request.createdAt,
));
}
Future<List<OfflineRequest>> readAll() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
final decoded = jsonDecode(raw) as List<dynamic>;
return decoded.map((e) => OfflineRequest.fromJson(Map<String, dynamic>.from(e as Map))).toList();
final records = await _database.offlineRequests.getAll();
return records
.map(
(record) => OfflineRequest(
method: record.method,
path: record.path,
body: record.body,
createdAt: record.createdAt,
),
)
.toList();
}
Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
await _database.offlineRequests.clear();
}
Future<int> syncPending(ApiClient apiClient) async {
@@ -79,11 +85,21 @@ class OfflineQueueService {
}
}
final prefs = await SharedPreferences.getInstance();
if (remaining.isEmpty) {
await prefs.remove(_key);
await clear();
} else {
await prefs.setString(_key, jsonEncode(remaining.map((e) => e.toJson()).toList()));
await _database.offlineRequests.replaceAll(
remaining
.map(
(request) => OfflineRequestRecord(
method: request.method,
path: request.path,
body: request.body,
createdAt: request.createdAt,
),
)
.toList(),
);
}
return synced;
}
@@ -0,0 +1,51 @@
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart';
class LocalCacheStore {
LocalCacheStore._();
static final LocalCacheStore instance = LocalCacheStore._();
static const _table = 'walkguide_cache';
Database? _database;
Future<String?> get(String key) async {
final db = await _open();
final result = db.select(
'SELECT value FROM $_table WHERE cache_key = ? LIMIT 1',
[key],
);
if (result.isEmpty) return null;
return result.first['value'] as String?;
}
Future<void> set(String key, String value) async {
final db = await _open();
db.execute(
'INSERT INTO $_table(cache_key, value, updated_at) VALUES (?, ?, ?) '
'ON CONFLICT(cache_key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at',
[key, value, DateTime.now().toIso8601String()],
);
}
Future<void> remove(String key) async {
final db = await _open();
db.execute('DELETE FROM $_table WHERE cache_key = ?', [key]);
}
Future<Database> _open() async {
final existing = _database;
if (existing != null) return existing;
final directory = await getApplicationDocumentsDirectory();
final db = sqlite3.open('${directory.path}/walkguide_cache.sqlite');
db.execute('''
CREATE TABLE IF NOT EXISTS $_table (
cache_key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''');
_database = db;
return db;
}
}
@@ -0,0 +1,22 @@
import 'package:shared_preferences/shared_preferences.dart';
class LocalCacheStore {
LocalCacheStore._();
static final LocalCacheStore instance = LocalCacheStore._();
Future<String?> get(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
Future<void> set(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
Future<void> remove(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
}
}
@@ -0,0 +1,239 @@
import 'dart:convert';
import 'local_cache_store_web.dart'
if (dart.library.io) 'local_cache_store_native.dart';
class CachedActivityLog {
final int? id;
final int? userId;
final String logType;
final String description;
final String? metadata;
final DateTime createdAt;
final bool synced;
const CachedActivityLog({
this.id,
this.userId,
required this.logType,
required this.description,
this.metadata,
required this.createdAt,
this.synced = false,
});
Map<String, dynamic> toJson() => {
'id': id,
'userId': userId,
'logType': logType,
'description': description,
'metadata': metadata,
'createdAt': createdAt.toIso8601String(),
'synced': synced,
};
factory CachedActivityLog.fromJson(Map<String, dynamic> json) {
return CachedActivityLog(
id: json['id'] as int?,
userId: json['userId'] as int?,
logType: json['logType'] as String,
description: json['description'] as String,
metadata: json['metadata'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
synced: json['synced'] == true,
);
}
}
class CachedObstacleLog {
final String label;
final String direction;
final String estimatedDistance;
final DateTime createdAt;
final bool synced;
const CachedObstacleLog({
required this.label,
required this.direction,
required this.estimatedDistance,
required this.createdAt,
this.synced = false,
});
Map<String, dynamic> toJson() => {
'label': label,
'direction': direction,
'estimatedDistance': estimatedDistance,
'createdAt': createdAt.toIso8601String(),
'synced': synced,
};
factory CachedObstacleLog.fromJson(Map<String, dynamic> json) {
return CachedObstacleLog(
label: json['label'] as String,
direction: json['direction'] as String,
estimatedDistance: json['estimatedDistance'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
synced: json['synced'] == true,
);
}
}
class CachedNotification {
final int? id;
final String notificationType;
final String? content;
final String? voiceNoteUrl;
final bool isRead;
final DateTime createdAt;
const CachedNotification({
this.id,
required this.notificationType,
this.content,
this.voiceNoteUrl,
required this.isRead,
required this.createdAt,
});
Map<String, dynamic> toJson() => {
'id': id,
'notificationType': notificationType,
'content': content,
'voiceNoteUrl': voiceNoteUrl,
'isRead': isRead,
'createdAt': createdAt.toIso8601String(),
};
factory CachedNotification.fromJson(Map<String, dynamic> json) {
return CachedNotification(
id: json['id'] as int?,
notificationType: json['notificationType'] as String,
content: json['content'] as String?,
voiceNoteUrl: json['voiceNoteUrl'] as String?,
isRead: json['isRead'] == true,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
class LocalDatabase {
final activityLogs = ActivityLogDao();
final obstacleLogs = ObstacleLogDao();
final notifications = NotificationDao();
final offlineRequests = OfflineRequestDao();
}
abstract class _JsonListDao<T> {
String get storageKey;
T fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson(T item);
Future<List<T>> getAll() async {
final raw = await LocalCacheStore.instance.get(storageKey);
if (raw == null || raw.isEmpty) return [];
final decoded = jsonDecode(raw) as List<dynamic>;
return decoded
.map((item) => fromJson(Map<String, dynamic>.from(item as Map)))
.toList();
}
Future<void> replaceAll(List<T> items) async {
await LocalCacheStore.instance.set(
storageKey,
jsonEncode(items.map(toJson).toList()),
);
}
Future<void> insert(T item) async {
final items = await getAll();
items.add(item);
await replaceAll(items);
}
Future<void> clear() async {
await LocalCacheStore.instance.remove(storageKey);
}
}
class ActivityLogDao extends _JsonListDao<CachedActivityLog> {
@override
String get storageKey => 'cached_activity_logs';
@override
CachedActivityLog fromJson(Map<String, dynamic> json) {
return CachedActivityLog.fromJson(json);
}
@override
Map<String, dynamic> toJson(CachedActivityLog item) => item.toJson();
}
class ObstacleLogDao extends _JsonListDao<CachedObstacleLog> {
@override
String get storageKey => 'cached_obstacle_logs';
@override
CachedObstacleLog fromJson(Map<String, dynamic> json) {
return CachedObstacleLog.fromJson(json);
}
@override
Map<String, dynamic> toJson(CachedObstacleLog item) => item.toJson();
}
class NotificationDao extends _JsonListDao<CachedNotification> {
@override
String get storageKey => 'cached_notifications';
@override
CachedNotification fromJson(Map<String, dynamic> json) {
return CachedNotification.fromJson(json);
}
@override
Map<String, dynamic> toJson(CachedNotification item) => item.toJson();
}
class OfflineRequestRecord {
final String method;
final String path;
final Map<String, dynamic> body;
final DateTime createdAt;
const OfflineRequestRecord({
required this.method,
required this.path,
required this.body,
required this.createdAt,
});
Map<String, dynamic> toJson() => {
'method': method,
'path': path,
'body': body,
'createdAt': createdAt.toIso8601String(),
};
factory OfflineRequestRecord.fromJson(Map<String, dynamic> json) {
return OfflineRequestRecord(
method: json['method'] as String,
path: json['path'] as String,
body: Map<String, dynamic>.from(json['body'] as Map),
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
class OfflineRequestDao extends _JsonListDao<OfflineRequestRecord> {
@override
String get storageKey => 'offline_request_queue';
@override
OfflineRequestRecord fromJson(Map<String, dynamic> json) {
return OfflineRequestRecord.fromJson(json);
}
@override
Map<String, dynamic> toJson(OfflineRequestRecord item) => item.toJson();
}
@@ -0,0 +1,13 @@
import 'package:flutter/foundation.dart';
Future<T?> ignoreInitFailure<T>(
Future<T> Function() action, {
required String label,
}) async {
try {
return await action();
} catch (error) {
debugPrint('$label skipped: $error');
return null;
}
}
@@ -0,0 +1,18 @@
import 'dart:async';
Future<T?> guarded<T>(
Future<T> Function() action, {
void Function(Object error)? onError,
void Function()? onTimeout,
}) async {
try {
return await action();
} on TimeoutException catch (error) {
onTimeout?.call();
onError?.call(error);
return null;
} catch (error) {
onError?.call(error);
return null;
}
}