a bunch of flutter updates

This commit is contained in:
Wowieee4
2026-05-08 20:27:57 +07:00
parent 53e612e221
commit 22ebbb7db0
44 changed files with 3351 additions and 1240 deletions
@@ -0,0 +1,54 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/foundation.dart';
import '../constants/app_constants.dart';
import '../network/api_client.dart';
class CallService {
final ApiClient _apiClient;
RtcEngine? _engine;
CallService(this._apiClient);
Future<Map<String, dynamic>?> requestToken(String channelName) async {
final res = await _apiClient.dio.post('/shared/call/token', data: {'channelName': channelName});
final data = res.data['data'];
return data is Map<String, dynamic> ? data : null;
}
Future<void> notifyIncomingCall({required int receiverId, required String channelName}) async {
await _apiClient.dio.post('/shared/call/notify', data: {
'receiverId': receiverId,
'channelName': channelName,
});
}
Future<bool> joinChannel({required String channelName, int uid = 0}) async {
try {
final tokenData = await requestToken(channelName);
final token = tokenData?['token']?.toString();
_engine ??= createAgoraRtcEngine();
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
await _engine!.enableAudio();
await _engine!.joinChannel(
token: token ?? '',
channelId: channelName,
uid: uid,
options: const ChannelMediaOptions(),
);
return true;
} catch (e) {
debugPrint('Agora join skipped: $e');
return false;
}
}
Future<void> leave() async {
await _engine?.leaveChannel();
}
Future<void> dispose() async {
await _engine?.release();
_engine = null;
}
}
@@ -0,0 +1,63 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../network/api_client.dart';
class FcmService {
final ApiClient _apiClient;
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
FcmService(this._apiClient);
Future<void> init() async {
if (kIsWeb) return;
try {
await _localNotifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
),
);
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);
FirebaseMessaging.onMessage.listen((message) {
debugPrint('FCM foreground: ${message.data}');
_showLocalNotification(message);
});
} catch (e) {
debugPrint('FCM init skipped: $e');
}
}
Future<void> syncToken(String token) async {
try {
await _apiClient.dio.put('/auth/fcm-token', data: {'fcmToken': token});
} catch (e) {
debugPrint('FCM token sync skipped: $e');
}
}
Future<void> _showLocalNotification(RemoteMessage message) async {
final notification = message.notification;
final title = notification?.title ?? message.data['title']?.toString() ?? 'WalkGuide';
final body = notification?.body ?? message.data['body']?.toString() ?? 'Ada update baru';
await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
const NotificationDetails(
android: AndroidNotificationDetails(
'walkguide_alerts',
'WalkGuide Alerts',
channelDescription: 'SOS, pairing, call, and guardian notifications',
importance: Importance.max,
priority: Priority.high,
),
),
payload: message.data['type']?.toString(),
);
}
}
@@ -1,7 +1,7 @@
import 'package:vibration/vibration.dart';
class HapticService {
Future<bool> get _hasVibrator async => (await Vibration.hasVibrator()) ?? false;
Future<bool> get _hasVibrator async => Vibration.hasVibrator();
Future<void> obstacleVeryClose() async {
if (!await _hasVibrator) return;
@@ -34,4 +34,4 @@ class HapticService {
}
Future<void> stop() async => Vibration.cancel();
}
}
@@ -0,0 +1,53 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:geolocator/geolocator.dart';
import '../constants/app_constants.dart';
import '../network/api_client.dart';
import 'offline_queue_service.dart';
class LocationReporterService {
final ApiClient _apiClient;
final OfflineQueueService _offlineQueue;
Timer? _timer;
LocationReporterService(this._apiClient, this._offlineQueue);
Future<void> start({bool walkGuideActive = false}) async {
await stop();
final interval = Duration(
milliseconds: walkGuideActive ? AppConstants.locationIntervalWalkMs : AppConstants.locationIntervalIdleMs,
);
await _sendOnce();
_timer = Timer.periodic(interval, (_) => _sendOnce());
}
Future<void> stop() async {
_timer?.cancel();
_timer = null;
}
Future<void> _sendOnce() async {
try {
await Geolocator.requestPermission();
final position = await Geolocator.getCurrentPosition();
await _apiClient.dio.post('/user/location', data: {
'lat': position.latitude,
'lng': position.longitude,
'accuracy': position.accuracy,
'speed': position.speed,
'heading': position.heading,
});
} on DioException catch (_) {
await _offlineQueue.enqueue(OfflineRequest(
method: 'POST',
path: '/user/location',
body: const {'lat': null, 'lng': null},
createdAt: DateTime.now(),
));
} catch (_) {
// GPS permission can be unavailable during desktop/web testing.
}
}
}
@@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../network/api_client.dart';
class OfflineRequest {
final String method;
final String path;
final Map<String, dynamic> body;
final DateTime createdAt;
OfflineRequest({
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 OfflineRequest.fromJson(Map<String, dynamic> json) => OfflineRequest(
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 OfflineQueueService {
static const _key = 'offline_request_queue';
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()));
}
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();
}
Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
}
Future<int> syncPending(ApiClient apiClient) async {
final queue = await readAll();
if (queue.isEmpty) return 0;
final remaining = <OfflineRequest>[];
var synced = 0;
for (final request in queue) {
try {
switch (request.method.toUpperCase()) {
case 'POST':
await apiClient.dio.post(request.path, data: request.body);
break;
case 'PUT':
await apiClient.dio.put(request.path, data: request.body);
break;
default:
await apiClient.dio.get(request.path);
}
synced++;
} catch (_) {
remaining.add(request);
}
}
final prefs = await SharedPreferences.getInstance();
if (remaining.isEmpty) {
await prefs.remove(_key);
} else {
await prefs.setString(_key, jsonEncode(remaining.map((e) => e.toJson()).toList()));
}
return synced;
}
}
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use
import 'package:speech_to_text/speech_to_text.dart';
class SttService {
@@ -51,4 +53,4 @@ class SttService {
Future.delayed(const Duration(milliseconds: 500), startListening);
}
}
}
}
@@ -1,4 +1,3 @@
import 'package:get_it/get_it.dart';
import 'tts_service.dart';
import 'stt_service.dart';
@@ -72,4 +71,4 @@ class VoiceCommandHandler {
if (key == VoiceCommandKey.repeatLast) _tts.repeatLast();
if (key == VoiceCommandKey.stopTts) _tts.stop();
}
}
}
@@ -0,0 +1,41 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../constants/app_constants.dart';
import '../storage/secure_storage.dart';
class WebSocketService {
final SecureStorage _storage;
WebSocketChannel? _channel;
StreamSubscription? _subscription;
WebSocketService(this._storage);
Future<void> connect(String serverUrl, {void Function(dynamic event)? onMessage}) async {
await disconnect();
final token = await _storage.getAccessToken();
final wsUrl = Uri.parse('${AppConstants.buildWsUrl(serverUrl)}${token == null ? '' : '?token=$token'}');
try {
_channel = WebSocketChannel.connect(wsUrl);
_subscription = _channel!.stream.listen(
onMessage ?? (event) => debugPrint('$event'),
onError: (Object error, StackTrace stackTrace) => debugPrint('$error'),
);
} catch (e) {
debugPrint('WebSocket connect skipped: $e');
}
}
void send(Object message) {
_channel?.sink.add(message);
}
Future<void> disconnect() async {
await _subscription?.cancel();
_subscription = null;
await _channel?.sink.close();
_channel = null;
}
}