api update and flutter update

This commit is contained in:
Wowieee4
2026-05-07 10:22:06 +07:00
parent c5d716248e
commit 53e612e221
25 changed files with 4423 additions and 142 deletions
@@ -0,0 +1,44 @@
import 'package:shared_preferences/shared_preferences.dart';
class AppConstants {
static const String _serverUrlKey = 'server_base_url';
// Ambil base URL dari SharedPreferences
static Future<String?> getServerUrl() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_serverUrlKey);
}
// Simpan URL setelah berhasil connect
static Future<void> setServerUrl(String url) async {
final prefs = await SharedPreferences.getInstance();
// Trim trailing slash
final cleaned = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
await prefs.setString(_serverUrlKey, cleaned);
}
static Future<void> clearServerUrl() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_serverUrlKey);
}
static String buildApiUrl(String baseUrl) => '$baseUrl/api/v1';
static String buildWsUrl(String baseUrl) => baseUrl.replaceFirst('http', 'ws') + '/ws';
// Timeouts
static const Duration connectTimeout = Duration(seconds: 10);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration pingTimeout = Duration(seconds: 5);
// Location intervals
static const int locationIntervalWalkMs = 5000;
static const int locationIntervalIdleMs = 30000;
// YOLO
static const String yoloModelPath = 'assets/models/yolov8n.tflite';
static const String yoloLabelsPath = 'assets/models/labels.txt';
static const int yoloInputSize = 640;
// Agora - ganti dengan App ID dari agora.io
static const String agoraAppId = 'YOUR_AGORA_APP_ID';
}
@@ -0,0 +1,36 @@
abstract class Failure {
final String message;
const Failure(this.message);
}
class ServerFailure extends Failure {
const ServerFailure(super.message);
}
class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
class AuthFailure extends Failure {
const AuthFailure(super.message);
}
class NotFoundFailure extends Failure {
const NotFoundFailure(super.message);
}
class PairingFailure extends Failure {
const PairingFailure(super.message);
}
class ValidationFailure extends Failure {
const ValidationFailure(super.message);
}
class CacheFailure extends Failure {
const CacheFailure(super.message);
}
class ConnectionFailure extends Failure {
const ConnectionFailure(super.message);
}
@@ -0,0 +1,100 @@
import 'package:dio/dio.dart';
import '../constants/app_constants.dart';
import '../storage/secure_storage.dart';
class ApiClient {
late Dio _dio;
final SecureStorage _secureStorage;
String? _baseUrl;
ApiClient(this._secureStorage);
Future<void> init(String serverUrl) async {
_baseUrl = AppConstants.buildApiUrl(serverUrl);
_dio = Dio(BaseOptions(
baseUrl: _baseUrl!,
connectTimeout: AppConstants.connectTimeout,
receiveTimeout: AppConstants.receiveTimeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_dio.interceptors.addAll([
_AuthInterceptor(_secureStorage, _dio),
_ErrorInterceptor(),
LogInterceptor(requestBody: true, responseBody: true),
]);
}
Dio get dio => _dio;
String? get baseUrl => _baseUrl;
}
// ── Auth Interceptor: inject token & auto-refresh on 401 ──────────────────────
class _AuthInterceptor extends Interceptor {
final SecureStorage _storage;
final Dio _dio;
bool _refreshing = false;
_AuthInterceptor(this._storage, this._dio);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 && !_refreshing) {
_refreshing = true;
try {
final refresh = await _storage.getRefreshToken();
if (refresh == null) {
_refreshing = false;
handler.next(err);
return;
}
final res = await _dio.post('/auth/refresh',
data: {'refreshToken': refresh},
options: Options(headers: {'Authorization': null}));
if (res.statusCode == 200 && res.data['success'] == true) {
final data = res.data['data'];
await _storage.saveTokens(
accessToken: data['accessToken'],
refreshToken: data['refreshToken'] ?? refresh,
role: data['role'],
userId: data['userId'].toString(),
displayName: data['displayName'],
uniqueUserId: data['uniqueUserId'],
);
// Retry original request
err.requestOptions.headers['Authorization'] =
'Bearer ${data['accessToken']}';
final retryRes = await _dio.fetch(err.requestOptions);
_refreshing = false;
handler.resolve(retryRes);
return;
}
} catch (_) {}
_refreshing = false;
await _storage.clearAll();
}
handler.next(err);
}
}
// ── Error Interceptor: normalize errors ───────────────────────────────────────
class _ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// Let the calling code handle it — just pass through
handler.next(err);
}
}
@@ -0,0 +1,37 @@
import 'package:vibration/vibration.dart';
class HapticService {
Future<bool> get _hasVibrator async => (await Vibration.hasVibrator()) ?? false;
Future<void> obstacleVeryClose() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 500, 100, 500, 100, 500]);
}
Future<void> obstacleClose() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 300, 100, 300]);
}
Future<void> obstacleMedium() async {
if (!await _hasVibrator) return;
Vibration.vibrate(duration: 150);
}
Future<void> sosTriggered() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 1000, 200, 1000, 200, 1000]);
}
Future<void> callIncoming() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 500, 500, 500, 500, 500, 500, 500]);
}
Future<void> success() async {
if (!await _hasVibrator) return;
Vibration.vibrate(duration: 80);
}
Future<void> stop() async => Vibration.cancel();
}
@@ -0,0 +1,54 @@
import 'package:speech_to_text/speech_to_text.dart';
class SttService {
final SpeechToText _stt = SpeechToText();
bool _available = false;
bool _listening = false;
Function(String)? onResult;
Future<bool> init() async {
_available = await _stt.initialize(
onError: (e) => _onError(e),
onStatus: (s) => _onStatus(s),
);
return _available;
}
Future<void> startListening() async {
if (!_available || _listening) return;
_listening = true;
await _stt.listen(
onResult: (result) {
if (result.finalResult && result.recognizedWords.isNotEmpty) {
onResult?.call(result.recognizedWords.toLowerCase().trim());
}
},
listenFor: const Duration(seconds: 10),
pauseFor: const Duration(seconds: 3),
localeId: 'id_ID',
cancelOnError: false,
);
}
Future<void> stopListening() async {
_listening = false;
await _stt.stop();
}
bool get isListening => _listening;
bool get isAvailable => _available;
void _onError(dynamic error) {
_listening = false;
// Auto-restart setelah error
Future.delayed(const Duration(seconds: 1), startListening);
}
void _onStatus(String status) {
if (status == 'done' || status == 'notListening') {
_listening = false;
// Auto-restart agar selalu mendengarkan
Future.delayed(const Duration(milliseconds: 500), startListening);
}
}
}
@@ -0,0 +1,61 @@
import 'package:flutter_tts/flutter_tts.dart';
class TtsService {
final FlutterTts _tts = FlutterTts();
final List<String> _queue = [];
bool _speaking = false;
String _lastSpoken = '';
Future<void> init({String language = 'id-ID', double pitch = 1.0, double rate = 0.5}) async {
await _tts.setLanguage(language);
await _tts.setPitch(pitch);
await _tts.setSpeechRate(rate);
await _tts.setVolume(1.0);
_tts.setCompletionHandler(() {
_speaking = false;
_processQueue();
});
}
/// Tambah ke antrian - tidak memotong yg sedang bicara
void speak(String text) {
if (text.isEmpty) return;
_queue.add(text);
if (!_speaking) _processQueue();
}
/// Potong TTS sekarang dan langsung ucapkan ini - untuk obstacle alert
Future<void> speakImmediate(String text) async {
if (text.isEmpty) return;
_queue.clear();
await _tts.stop();
_speaking = true;
_lastSpoken = text;
await _tts.speak(text);
}
Future<void> stop() async {
_queue.clear();
_speaking = false;
await _tts.stop();
}
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);
void repeatLast() {
if (_lastSpoken.isNotEmpty) speak(_lastSpoken);
}
void _processQueue() {
if (_queue.isEmpty || _speaking) return;
final text = _queue.removeAt(0);
_speaking = true;
_lastSpoken = text;
_tts.speak(text);
}
}
@@ -0,0 +1,75 @@
import 'package:get_it/get_it.dart';
import 'tts_service.dart';
import 'stt_service.dart';
enum VoiceCommandKey {
openWalkguide, startWalkguide, stopWalkguide,
callGuardian, openNotification, readAllNotif,
openSos, sendSos, whereAmI,
openActivity, openNavigation, openSettings,
repeatLast, stopTts,
}
class VoiceCommand {
final VoiceCommandKey key;
final String phrase;
final bool enabled;
const VoiceCommand({required this.key, required this.phrase, required this.enabled});
}
/// Callback yang dipanggil saat command terdeteksi
/// Registered oleh router/screen yang relevan
typedef CommandCallback = void Function(VoiceCommandKey key);
class VoiceCommandHandler {
final SttService _stt;
final TtsService _tts;
List<VoiceCommand> _commands = [];
CommandCallback? onCommand;
VoiceCommandHandler(this._stt, this._tts);
void loadCommands(List<VoiceCommand> commands) {
_commands = commands;
_stt.onResult = _processText;
}
void loadDefaultCommands() {
_commands = const [
VoiceCommand(key: VoiceCommandKey.openWalkguide, phrase: 'open walkguide', enabled: true),
VoiceCommand(key: VoiceCommandKey.startWalkguide, phrase: 'start walkguide', enabled: true),
VoiceCommand(key: VoiceCommandKey.stopWalkguide, phrase: 'stop walkguide', enabled: true),
VoiceCommand(key: VoiceCommandKey.callGuardian, phrase: 'call guardian', enabled: true),
VoiceCommand(key: VoiceCommandKey.openNotification, phrase: 'open notifications', enabled: true),
VoiceCommand(key: VoiceCommandKey.readAllNotif, phrase: 'read all my notifications', enabled: true),
VoiceCommand(key: VoiceCommandKey.openSos, phrase: 'open sos', enabled: true),
VoiceCommand(key: VoiceCommandKey.sendSos, phrase: 'send sos', enabled: true),
VoiceCommand(key: VoiceCommandKey.whereAmI, phrase: 'where am i', enabled: true),
VoiceCommand(key: VoiceCommandKey.openActivity, phrase: 'open activity log', enabled: true),
VoiceCommand(key: VoiceCommandKey.openNavigation, phrase: 'open navigation', enabled: true),
VoiceCommand(key: VoiceCommandKey.openSettings, phrase: 'open settings', enabled: true),
VoiceCommand(key: VoiceCommandKey.repeatLast, phrase: 'repeat', enabled: true),
VoiceCommand(key: VoiceCommandKey.stopTts, phrase: 'stop', enabled: true),
];
_stt.onResult = _processText;
}
void _processText(String text) {
final lower = text.toLowerCase().trim();
for (final cmd in _commands) {
if (!cmd.enabled) continue;
if (lower.contains(cmd.phrase.toLowerCase())) {
_handleCommand(cmd.key);
return;
}
}
}
void _handleCommand(VoiceCommandKey key) {
onCommand?.call(key);
// Built-in actions for TTS-only commands
if (key == VoiceCommandKey.repeatLast) _tts.repeatLast();
if (key == VoiceCommandKey.stopTts) _tts.stop();
}
}
@@ -0,0 +1,41 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static const _keyAccess = 'access_token';
static const _keyRefresh = 'refresh_token';
static const _keyRole = 'user_role';
static const _keyUserId = 'user_id';
static const _keyName = 'display_name';
static const _keyUid = 'unique_user_id';
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
required String role,
required String userId,
String? displayName,
String? uniqueUserId,
}) async {
await Future.wait([
_storage.write(key: _keyAccess, value: accessToken),
_storage.write(key: _keyRefresh, value: refreshToken),
_storage.write(key: _keyRole, value: role),
_storage.write(key: _keyUserId, value: userId),
if (displayName != null) _storage.write(key: _keyName, value: displayName),
if (uniqueUserId != null) _storage.write(key: _keyUid, value: uniqueUserId),
]);
}
Future<String?> getAccessToken() async => _storage.read(key: _keyAccess);
Future<String?> getRefreshToken() async => _storage.read(key: _keyRefresh);
Future<String?> getUserRole() async => _storage.read(key: _keyRole);
Future<String?> getUserId() async => _storage.read(key: _keyUserId);
Future<String?> getDisplayName() async => _storage.read(key: _keyName);
Future<String?> getUniqueUserId() async => _storage.read(key: _keyUid);
Future<void> clearAll() async => _storage.deleteAll();
}