a bunch of flutter updates
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user