Implement annotation syncing to and from server

This commit is contained in:
2026-02-06 16:05:55 +01:00
parent e5c71c9261
commit 9a11e42571
4 changed files with 348 additions and 9 deletions

View File

@@ -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;
}
}
}
}