Fix local dev config and ALOTTTT OFF FLUTTER, im tired boss..
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user