a bunch of flutter updates
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export 'obstacle_analyzer.dart';
|
||||
export 'yolo_detector.dart';
|
||||
@@ -0,0 +1,37 @@
|
||||
enum ObstacleDirection { left, center, right }
|
||||
|
||||
class DetectionResult {
|
||||
final String label;
|
||||
final double confidence;
|
||||
final ObstacleDirection direction;
|
||||
final String estimatedDistance;
|
||||
|
||||
const DetectionResult({
|
||||
required this.label,
|
||||
required this.confidence,
|
||||
required this.direction,
|
||||
required this.estimatedDistance,
|
||||
});
|
||||
|
||||
String get directionName => direction.name.toUpperCase();
|
||||
|
||||
String get spokenId {
|
||||
final area = switch (direction) {
|
||||
ObstacleDirection.left => 'kiri',
|
||||
ObstacleDirection.center => 'tengah',
|
||||
ObstacleDirection.right => 'kanan',
|
||||
};
|
||||
return 'Hati-hati, $label di $area. Jarak $estimatedDistance.';
|
||||
}
|
||||
}
|
||||
|
||||
class ObstacleAnalyzer {
|
||||
DetectionResult analyzeFallback({String label = 'person', double confidence = 0.86}) {
|
||||
return DetectionResult(
|
||||
label: label,
|
||||
confidence: confidence,
|
||||
direction: ObstacleDirection.center,
|
||||
estimatedDistance: 'Close',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../constants/app_constants.dart';
|
||||
import 'obstacle_analyzer.dart';
|
||||
|
||||
class YoloDetector {
|
||||
final ObstacleAnalyzer _analyzer;
|
||||
List<String> _labels = const [];
|
||||
bool _ready = false;
|
||||
|
||||
YoloDetector(this._analyzer);
|
||||
|
||||
bool get isReady => _ready;
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
_labels = (await rootBundle.loadString(AppConstants.yoloLabelsPath))
|
||||
.split('\n')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
if (kIsWeb) {
|
||||
_ready = false;
|
||||
return;
|
||||
}
|
||||
await rootBundle.load(await AppConstants.getSelectedYoloModelPath());
|
||||
_ready = true;
|
||||
} catch (e) {
|
||||
debugPrint('YOLO fallback mode: $e');
|
||||
_ready = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DetectionResult?> detectFallback() async {
|
||||
if (_ready) {
|
||||
// Full tensor pre/post-processing belongs here once yolov8n.tflite is present.
|
||||
// The app keeps a deterministic fallback so demo flows remain testable.
|
||||
}
|
||||
final label = _labels.isNotEmpty ? _labels.first : 'person';
|
||||
return _analyzer.analyzeFallback(label: label);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_ready = false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppConstants {
|
||||
static const String _serverUrlKey = 'server_base_url';
|
||||
static const String _selectedYoloModelKey = 'selected_yolo_model';
|
||||
|
||||
// Ambil base URL dari SharedPreferences
|
||||
static Future<String?> getServerUrl() async {
|
||||
@@ -12,18 +13,29 @@ class AppConstants {
|
||||
// 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;
|
||||
final cleaned = normalizeServerUrl(url);
|
||||
await prefs.setString(_serverUrlKey, cleaned);
|
||||
}
|
||||
|
||||
static String normalizeServerUrl(String url) {
|
||||
var cleaned = url.trim();
|
||||
if (!cleaned.startsWith('http://') && !cleaned.startsWith('https://')) {
|
||||
cleaned = 'http://$cleaned';
|
||||
}
|
||||
while (cleaned.endsWith('/')) {
|
||||
cleaned = cleaned.substring(0, cleaned.length - 1);
|
||||
}
|
||||
return 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';
|
||||
static String buildWsUrl(String baseUrl) =>
|
||||
'${baseUrl.replaceFirst('http', 'ws')}/ws';
|
||||
|
||||
// Timeouts
|
||||
static const Duration connectTimeout = Duration(seconds: 10);
|
||||
@@ -39,6 +51,16 @@ class AppConstants {
|
||||
static const String yoloLabelsPath = 'assets/models/labels.txt';
|
||||
static const int yoloInputSize = 640;
|
||||
|
||||
static Future<String> getSelectedYoloModelPath() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_selectedYoloModelKey) ?? yoloModelPath;
|
||||
}
|
||||
|
||||
static Future<void> setSelectedYoloModelPath(String path) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_selectedYoloModelKey, path);
|
||||
}
|
||||
|
||||
// Agora - ganti dengan App ID dari agora.io
|
||||
static const String agoraAppId = 'YOUR_AGORA_APP_ID';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class ApiClient {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: _baseUrl!,
|
||||
connectTimeout: AppConstants.connectTimeout,
|
||||
sendTimeout: AppConstants.connectTimeout,
|
||||
receiveTimeout: AppConstants.receiveTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -97,4 +98,4 @@ class _ErrorInterceptor extends Interceptor {
|
||||
// Let the calling code handle it — just pass through
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SecureStorage {
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
static const _keyAccess = 'access_token';
|
||||
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';
|
||||
static const _keyRole = 'user_role';
|
||||
static const _keyUserId = 'user_id';
|
||||
static const _keyName = 'display_name';
|
||||
static const _keyUid = 'unique_user_id';
|
||||
static const _keys = [
|
||||
_keyAccess,
|
||||
_keyRefresh,
|
||||
_keyRole,
|
||||
_keyUserId,
|
||||
_keyName,
|
||||
_keyUid,
|
||||
];
|
||||
|
||||
Future<void> saveTokens({
|
||||
required String accessToken,
|
||||
@@ -20,22 +30,56 @@ class SecureStorage {
|
||||
String? displayName,
|
||||
String? uniqueUserId,
|
||||
}) async {
|
||||
if (kIsWeb) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await Future.wait([
|
||||
prefs.setString(_keyAccess, accessToken),
|
||||
prefs.setString(_keyRefresh, refreshToken),
|
||||
prefs.setString(_keyRole, role),
|
||||
prefs.setString(_keyUserId, userId),
|
||||
if (displayName != null) prefs.setString(_keyName, displayName),
|
||||
if (uniqueUserId != null) prefs.setString(_keyUid, uniqueUserId),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_storage.write(key: _keyAccess, value: accessToken),
|
||||
_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),
|
||||
_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<String?> getAccessToken() async => _read(_keyAccess);
|
||||
Future<String?> getRefreshToken() async => _read(_keyRefresh);
|
||||
Future<String?> getUserRole() async => _read(_keyRole);
|
||||
Future<String?> getUserId() async => _read(_keyUserId);
|
||||
Future<String?> getDisplayName() async => _read(_keyName);
|
||||
Future<String?> getUniqueUserId() async => _read(_keyUid);
|
||||
|
||||
Future<void> clearAll() async => _storage.deleteAll();
|
||||
}
|
||||
Future<void> clearAll() async {
|
||||
if (kIsWeb) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await Future.wait(_keys.map(prefs.remove));
|
||||
return;
|
||||
}
|
||||
await _storage.deleteAll();
|
||||
}
|
||||
|
||||
Future<String?> _read(String key) async {
|
||||
if (kIsWeb) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(key);
|
||||
}
|
||||
try {
|
||||
return await _storage.read(key: key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
static const primary = Color(0xFF1A56DB);
|
||||
static const danger = Color(0xFFDC2626);
|
||||
static const success = Color(0xFF16A34A);
|
||||
static const surface = Color(0xFFF8FAFC);
|
||||
static const text = Color(0xFF0F172A);
|
||||
static const muted = Color(0xFF64748B);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
String compactDateTime(DateTime value) {
|
||||
final y = value.year.toString().padLeft(4, '0');
|
||||
final m = value.month.toString().padLeft(2, '0');
|
||||
final d = value.day.toString().padLeft(2, '0');
|
||||
final hh = value.hour.toString().padLeft(2, '0');
|
||||
final mm = value.minute.toString().padLeft(2, '0');
|
||||
return '$y-$m-$d $hh:$mm';
|
||||
}
|
||||
Reference in New Issue
Block a user