Implement annotation syncing to and from server
This commit is contained in:
@@ -6,6 +6,29 @@ import 'package:sheetless/core/models/config.dart';
|
||||
/// Keys for secure storage (credentials and tokens).
|
||||
enum SecureStorageKey { url, jwt, email }
|
||||
|
||||
/// Data class for storing annotations with metadata.
|
||||
class StoredAnnotation {
|
||||
final String annotationsJson;
|
||||
final DateTime lastModified;
|
||||
|
||||
StoredAnnotation({
|
||||
required this.annotationsJson,
|
||||
required this.lastModified,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'annotationsJson': annotationsJson,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
};
|
||||
|
||||
factory StoredAnnotation.fromMap(Map<dynamic, dynamic> map) {
|
||||
return StoredAnnotation(
|
||||
annotationsJson: map['annotationsJson'] as String,
|
||||
lastModified: DateTime.parse(map['lastModified'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for managing local storage operations.
|
||||
///
|
||||
/// Uses [FlutterSecureStorage] for sensitive data (credentials, tokens)
|
||||
@@ -134,16 +157,70 @@ class StorageService {
|
||||
/// Returns the JSON string of annotations, or null if none exist.
|
||||
Future<String?> readAnnotations(String sheetUuid, int pageNumber) async {
|
||||
final box = await Hive.openBox(_annotationsBox);
|
||||
return box.get(_annotationKey(sheetUuid, pageNumber));
|
||||
final value = box.get(_annotationKey(sheetUuid, pageNumber));
|
||||
|
||||
// Handle legacy format (plain string) and new format (map with metadata)
|
||||
if (value == null) return null;
|
||||
if (value is String) return value;
|
||||
if (value is Map) {
|
||||
final stored = StoredAnnotation.fromMap(value);
|
||||
return stored.annotationsJson;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Reads annotations with metadata for a specific sheet page.
|
||||
///
|
||||
/// Returns [StoredAnnotation] with annotations and lastModified, or null if none exist.
|
||||
Future<StoredAnnotation?> readAnnotationsWithMetadata(
|
||||
String sheetUuid,
|
||||
int pageNumber,
|
||||
) async {
|
||||
final box = await Hive.openBox(_annotationsBox);
|
||||
final value = box.get(_annotationKey(sheetUuid, pageNumber));
|
||||
|
||||
if (value == null) return null;
|
||||
|
||||
// Handle legacy format (plain string) - treat as very old
|
||||
if (value is String) {
|
||||
return StoredAnnotation(
|
||||
annotationsJson: value,
|
||||
lastModified: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
}
|
||||
|
||||
if (value is Map) {
|
||||
return StoredAnnotation.fromMap(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Writes annotations for a specific sheet page.
|
||||
///
|
||||
/// Pass null or empty string to delete annotations for that page.
|
||||
/// Automatically sets lastModified to current time.
|
||||
Future<void> writeAnnotations(
|
||||
String sheetUuid,
|
||||
int pageNumber,
|
||||
String? annotationsJson,
|
||||
) async {
|
||||
await writeAnnotationsWithMetadata(
|
||||
sheetUuid,
|
||||
pageNumber,
|
||||
annotationsJson,
|
||||
DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes annotations with a specific lastModified timestamp.
|
||||
///
|
||||
/// Used when syncing from server to preserve server's timestamp.
|
||||
Future<void> writeAnnotationsWithMetadata(
|
||||
String sheetUuid,
|
||||
int pageNumber,
|
||||
String? annotationsJson,
|
||||
DateTime lastModified,
|
||||
) async {
|
||||
final box = await Hive.openBox(_annotationsBox);
|
||||
final key = _annotationKey(sheetUuid, pageNumber);
|
||||
@@ -151,7 +228,11 @@ class StorageService {
|
||||
if (annotationsJson == null || annotationsJson.isEmpty) {
|
||||
await box.delete(key);
|
||||
} else {
|
||||
await box.put(key, annotationsJson);
|
||||
final stored = StoredAnnotation(
|
||||
annotationsJson: annotationsJson,
|
||||
lastModified: lastModified,
|
||||
);
|
||||
await box.put(key, stored.toMap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +250,54 @@ class StorageService {
|
||||
final pageNumber = int.tryParse(pageStr);
|
||||
if (pageNumber != null) {
|
||||
final value = box.get(key);
|
||||
if (value != null && value is String && value.isNotEmpty) {
|
||||
result[pageNumber] = value;
|
||||
if (value != null) {
|
||||
// Handle legacy format (plain string) and new format (map)
|
||||
if (value is String && value.isNotEmpty) {
|
||||
result[pageNumber] = value;
|
||||
} else if (value is Map) {
|
||||
final stored = StoredAnnotation.fromMap(value);
|
||||
if (stored.annotationsJson.isNotEmpty) {
|
||||
result[pageNumber] = stored.annotationsJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Reads all annotations with metadata for a sheet (all pages).
|
||||
///
|
||||
/// Returns a map of page number to [StoredAnnotation].
|
||||
Future<Map<int, StoredAnnotation>> readAllAnnotationsWithMetadata(
|
||||
String sheetUuid,
|
||||
) async {
|
||||
final box = await Hive.openBox(_annotationsBox);
|
||||
final prefix = '${sheetUuid}_page_';
|
||||
final result = <int, StoredAnnotation>{};
|
||||
|
||||
for (final key in box.keys) {
|
||||
if (key is String && key.startsWith(prefix)) {
|
||||
final pageStr = key.substring(prefix.length);
|
||||
final pageNumber = int.tryParse(pageStr);
|
||||
if (pageNumber != null) {
|
||||
final value = box.get(key);
|
||||
if (value != null) {
|
||||
StoredAnnotation? stored;
|
||||
// Handle legacy format (plain string) and new format (map)
|
||||
if (value is String && value.isNotEmpty) {
|
||||
stored = StoredAnnotation(
|
||||
annotationsJson: value,
|
||||
lastModified: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
} else if (value is Map) {
|
||||
stored = StoredAnnotation.fromMap(value);
|
||||
}
|
||||
if (stored != null && stored.annotationsJson.isNotEmpty) {
|
||||
result[pageNumber] = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user