Files
sheetless/lib/core/services/storage_service.dart

321 lines
10 KiB
Dart

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';
/// 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)
/// and [Hive] for general app data (config, sheet access times, change queue,
/// and PDF annotations).
class StorageService {
// Hive box names
static const String _sheetAccessTimesBox = 'sheetAccessTimes';
static const String _configBox = 'config';
static const String _changeQueueBox = 'changeQueue';
static const String _annotationsBox = 'annotations';
late final FlutterSecureStorage _secureStorage;
StorageService() {
_secureStorage = FlutterSecureStorage(
aOptions: const AndroidOptions(encryptedSharedPreferences: true),
);
}
// ---------------------------------------------------------------------------
// Secure Storage (Credentials & Tokens)
// ---------------------------------------------------------------------------
/// Reads a value from secure storage.
Future<String?> readSecure(SecureStorageKey key) {
return _secureStorage.read(key: key.name);
}
/// Writes a value to secure storage.
///
/// Pass `null` to delete the key.
Future<void> writeSecure(SecureStorageKey key, String? value) {
return _secureStorage.write(key: key.name, value: value);
}
/// Clears the JWT token from secure storage.
Future<void> clearToken() {
return writeSecure(SecureStorageKey.jwt, null);
}
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
/// Reads the app configuration from storage.
Future<Config> readConfig() async {
final box = await Hive.openBox(_configBox);
return Config(
twoPageMode: box.get(Config.keyTwoPageMode) ?? false,
fullscreen: box.get(Config.keyFullscreen) ?? false,
);
}
/// Writes the app configuration to storage.
Future<void> writeConfig(Config config) async {
final box = await Hive.openBox(_configBox);
await box.put(Config.keyTwoPageMode, config.twoPageMode);
await box.put(Config.keyFullscreen, config.fullscreen);
}
// ---------------------------------------------------------------------------
// Sheet Access Times (for sorting by recency)
// ---------------------------------------------------------------------------
/// Reads all sheet access times.
///
/// Returns a map of sheet UUID to last access time.
Future<Map<String, DateTime>> readSheetAccessTimes() async {
final box = await Hive.openBox(_sheetAccessTimesBox);
return box.toMap().map(
(key, value) =>
MapEntry(key as String, DateTime.parse(value as String)),
);
}
/// Records when a sheet was last accessed.
Future<void> writeSheetAccessTime(String uuid, DateTime datetime) async {
final box = await Hive.openBox(_sheetAccessTimesBox);
await box.put(uuid, datetime.toIso8601String());
}
// ---------------------------------------------------------------------------
// Change Queue (Offline Sync)
// ---------------------------------------------------------------------------
/// Adds a change to the offline queue.
Future<void> writeChange(Change change) async {
final box = await Hive.openBox(_changeQueueBox);
await box.add(change.toMap());
}
/// Reads all pending changes from the queue.
Future<ChangeQueue> readChangeQueue() async {
final box = await Hive.openBox(_changeQueueBox);
final queue = ChangeQueue();
for (final map in box.values) {
queue.addChange(Change.fromMap(map as Map<dynamic, dynamic>));
}
return queue;
}
/// Removes the oldest change from the queue.
///
/// Call this after successfully syncing a change to the server.
Future<void> deleteOldestChange() async {
final box = await Hive.openBox(_changeQueueBox);
if (box.isNotEmpty) {
await box.deleteAt(0);
}
}
// ---------------------------------------------------------------------------
// Annotations (PDF Drawing Persistence)
// ---------------------------------------------------------------------------
/// Generates a storage key for a specific page's annotations.
String _annotationKey(String sheetUuid, int pageNumber) {
return '${sheetUuid}_page_$pageNumber';
}
/// Reads annotations for a specific sheet page.
///
/// 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);
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);
if (annotationsJson == null || annotationsJson.isEmpty) {
await box.delete(key);
} else {
final stored = StoredAnnotation(
annotationsJson: annotationsJson,
lastModified: lastModified,
);
await box.put(key, stored.toMap());
}
}
/// Reads all annotations for a sheet (all pages).
///
/// Returns a map of page number to JSON string.
Future<Map<int, String>> readAllAnnotations(String sheetUuid) async {
final box = await Hive.openBox(_annotationsBox);
final prefix = '${sheetUuid}_page_';
final result = <int, String>{};
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) {
// 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;
}
}
}
}
}
return result;
}
/// Deletes all annotations for a sheet.
Future<void> deleteAllAnnotations(String sheetUuid) async {
final box = await Hive.openBox(_annotationsBox);
final prefix = '${sheetUuid}_page_';
final keysToDelete =
box.keys.where((key) => key is String && key.startsWith(prefix));
for (final key in keysToDelete.toList()) {
await box.delete(key);
}
}
}