Implement offline mode
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:sheetless/core/models/change.dart';
|
||||
import 'package:sheetless/core/models/config.dart';
|
||||
import 'package:sheetless/core/models/sheet.dart';
|
||||
|
||||
/// Keys for secure storage (credentials and tokens).
|
||||
enum SecureStorageKey { url, jwt, email }
|
||||
@@ -29,6 +32,45 @@ class StoredAnnotation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for managing local storage operations.
|
||||
///
|
||||
/// Uses [FlutterSecureStorage] for sensitive data (credentials, tokens)
|
||||
/// and [Hive] for general app data (config, sheet access times, change queue,
|
||||
/// and PDF annotations).
|
||||
/// Data class for a pending annotation upload.
|
||||
class PendingAnnotationUpload {
|
||||
final String sheetUuid;
|
||||
final int page;
|
||||
final String annotationsJson;
|
||||
final DateTime lastModified;
|
||||
|
||||
PendingAnnotationUpload({
|
||||
required this.sheetUuid,
|
||||
required this.page,
|
||||
required this.annotationsJson,
|
||||
required this.lastModified,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'sheetUuid': sheetUuid,
|
||||
'page': page,
|
||||
'annotationsJson': annotationsJson,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
};
|
||||
|
||||
factory PendingAnnotationUpload.fromMap(Map<dynamic, dynamic> map) {
|
||||
return PendingAnnotationUpload(
|
||||
sheetUuid: map['sheetUuid'] as String,
|
||||
page: map['page'] as int,
|
||||
annotationsJson: map['annotationsJson'] as String,
|
||||
lastModified: DateTime.parse(map['lastModified'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique key for deduplication (newer uploads replace older ones).
|
||||
String get key => '${sheetUuid}_page_$page';
|
||||
}
|
||||
|
||||
/// Service for managing local storage operations.
|
||||
///
|
||||
/// Uses [FlutterSecureStorage] for sensitive data (credentials, tokens)
|
||||
@@ -40,6 +82,8 @@ class StorageService {
|
||||
static const String _configBox = 'config';
|
||||
static const String _changeQueueBox = 'changeQueue';
|
||||
static const String _annotationsBox = 'annotations';
|
||||
static const String _sheetsBox = 'sheets';
|
||||
static const String _pendingAnnotationsBox = 'pendingAnnotations';
|
||||
|
||||
late final FlutterSecureStorage _secureStorage;
|
||||
|
||||
@@ -317,4 +361,95 @@ class StorageService {
|
||||
await box.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sheets Cache (Offline Support)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Reads cached sheets from local storage.
|
||||
///
|
||||
/// Returns an empty list if no cached sheets exist.
|
||||
Future<List<Sheet>> readCachedSheets() async {
|
||||
final box = await Hive.openBox(_sheetsBox);
|
||||
final sheetsJson = box.get('sheets');
|
||||
|
||||
if (sheetsJson == null) return [];
|
||||
|
||||
final List<dynamic> sheetsList = jsonDecode(sheetsJson as String);
|
||||
return sheetsList
|
||||
.map((json) => Sheet.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Caches the sheets list to local storage.
|
||||
Future<void> writeCachedSheets(List<Sheet> sheets) async {
|
||||
final box = await Hive.openBox(_sheetsBox);
|
||||
final sheetsJson = jsonEncode(sheets.map((s) => s.toJson()).toList());
|
||||
await box.put('sheets', sheetsJson);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pending Annotation Uploads (Offline Support)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Adds or updates a pending annotation upload.
|
||||
///
|
||||
/// If an upload for the same sheet/page already exists, it will be replaced
|
||||
/// with the newer version.
|
||||
Future<void> writePendingAnnotationUpload(
|
||||
PendingAnnotationUpload upload,
|
||||
) async {
|
||||
final box = await Hive.openBox(_pendingAnnotationsBox);
|
||||
await box.put(upload.key, upload.toMap());
|
||||
}
|
||||
|
||||
/// Reads all pending annotation uploads.
|
||||
Future<List<PendingAnnotationUpload>> readPendingAnnotationUploads() async {
|
||||
final box = await Hive.openBox(_pendingAnnotationsBox);
|
||||
final uploads = <PendingAnnotationUpload>[];
|
||||
|
||||
for (final value in box.values) {
|
||||
uploads.add(PendingAnnotationUpload.fromMap(value as Map));
|
||||
}
|
||||
|
||||
return uploads;
|
||||
}
|
||||
|
||||
/// Removes a pending annotation upload after successful sync.
|
||||
Future<void> deletePendingAnnotationUpload(String key) async {
|
||||
final box = await Hive.openBox(_pendingAnnotationsBox);
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
/// Checks if there are any pending annotation uploads.
|
||||
Future<bool> hasPendingAnnotationUploads() async {
|
||||
final box = await Hive.openBox(_pendingAnnotationsBox);
|
||||
return box.isNotEmpty;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Change Queue Enhancements
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Returns the number of pending changes.
|
||||
Future<int> getChangeQueueLength() async {
|
||||
final box = await Hive.openBox(_changeQueueBox);
|
||||
return box.length;
|
||||
}
|
||||
|
||||
/// Clears all pending changes.
|
||||
///
|
||||
/// Use with caution - only call after all changes are synced.
|
||||
Future<void> clearChangeQueue() async {
|
||||
final box = await Hive.openBox(_changeQueueBox);
|
||||
await box.clear();
|
||||
}
|
||||
|
||||
/// Gets all changes as a list (for batch upload).
|
||||
Future<List<Change>> readChangeList() async {
|
||||
final box = await Hive.openBox(_changeQueueBox);
|
||||
return box.values
|
||||
.map((map) => Change.fromMap(map as Map<dynamic, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user