mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 18:53:54 +01:00
Flutter's Linux embedder manages ~20fps at 3840x2160 -- measured on a stock `flutter create` counter app, so it is the toolkit rather than this code -- where the same Dart rendered by Chrome sustains ~144fps (evidence in ~/todo/docs/desktop-performance-findings.md). The desktop target therefore becomes a Flutter *web* build; Android is untouched. `dart:io` does not fail a web *compile* -- it resolves to a stub that throws at runtime -- so every platform edge moves behind a conditional export rather than being discovered later as a blank white window: - storage: `document_store*.dart`, files on Android and IndexedDB in the browser (mirrored to the desktop wrapper, which is what makes a wiped Chrome profile recoverable). Document names stay the exact pre-web filenames so an installed phone keeps reading its existing log. - photos: `blob_store*.dart` plus `attached_image*.dart`, since `Image.file` cannot resolve an IndexedDB key. The picker itself has web support; what changed is the storage and display model. Camera *capture* is genuinely lost on desktop -- and photos never crossed devices anyway, `imagePath` being stripped before push. - scheduling: `background_tasks*.dart`. A browser has no WorkManager equivalent -- a closed tab runs nothing -- so the desktop runs an in-page 5-minute check while its window is open. That loses nothing on the PC, where `diet-guard-gate.timer` already locks the screen on an unlogged slot, which is strictly stronger than a notification. - notifications: `notification_backend*.dart`, the browser Notifications API in place of flutter_local_notifications. - the GitHub token: `token_vault*.dart`. On web it is *not* held by the browser at all -- flutter_secure_storage's web backend is WebCrypto-wrapped localStorage with the wrap key beside the ciphertext, which is obfuscation, not encryption at rest -- so the desktop wrapper owns it and proxies the API (`github_client_factory*.dart`). `Platform.isAndroid` itself throws on web, so every platform branch now tests `kIsWeb` first; the battery-optimization row (permission_handler, no web implementation) is hidden off Android. The desktop pushes under device id `desktop`, not `phone`: sharing an id would have made the two devices overwrite each other's pushed log on every tick. The Python side enumerates device directories generically, so it needs no change. Tests move onto the same seams -- the file-backed stores keep the atomicity and unreadable-file guarantees where they belong. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.3 KiB
Dart
97 lines
3.3 KiB
Dart
/// IndexedDB-backed [DocumentStore] used by the browser-hosted desktop app.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/services/desktop_wrapper.dart';
|
|
import 'package:diet_guard_app/services/document_store.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:idb_shim/idb_browser.dart';
|
|
|
|
/// [DocumentStore] backed by IndexedDB, mirrored to the desktop wrapper's
|
|
/// on-disk copy.
|
|
///
|
|
/// IndexedDB rather than `localStorage`: the latter caps at roughly 5-10MB per
|
|
/// origin and is evicted more eagerly, and these documents are the desktop
|
|
/// device's primary copy of the food log.
|
|
///
|
|
/// The wrapper mirror is the recovery path for a wiped Chrome profile. GitHub
|
|
/// sync would eventually restore the log too, but only what other devices have
|
|
/// already seen -- a meal logged on this desktop and never pushed exists
|
|
/// nowhere else. Reads fall back to the mirror when IndexedDB is empty; writes
|
|
/// to it are best-effort so the app still works opened in a plain browser tab
|
|
/// with no wrapper running.
|
|
class IndexedDbDocumentStore implements DocumentStore {
|
|
/// Creates a store over an already-open [database], mirroring to [baseUrl].
|
|
IndexedDbDocumentStore(
|
|
this._database, {
|
|
this.baseUrl = desktopWrapperOrigin,
|
|
http.Client? httpClient,
|
|
}) : _client = httpClient ?? http.Client();
|
|
|
|
final Database _database;
|
|
final http.Client _client;
|
|
|
|
/// Origin of the desktop wrapper.
|
|
final String baseUrl;
|
|
|
|
/// Object store holding one entry per document name.
|
|
static const storeName = 'documents';
|
|
|
|
/// IndexedDB database name.
|
|
static const databaseName = 'diet_guard';
|
|
|
|
@override
|
|
Future<String?> read(String name) async {
|
|
final txn = _database.transaction(storeName, idbModeReadOnly);
|
|
final value = await txn.objectStore(storeName).getObject(name);
|
|
await txn.completed;
|
|
if (value is String) return value;
|
|
// Empty IndexedDB: either a first run or a cleared profile. Recover from
|
|
// the wrapper's disk copy rather than silently starting from nothing.
|
|
return _readMirror(name);
|
|
}
|
|
|
|
@override
|
|
Future<void> write(String name, String contents) async {
|
|
final txn = _database.transaction(storeName, idbModeReadWrite);
|
|
await txn.objectStore(storeName).put(contents, name);
|
|
await txn.completed;
|
|
await _writeMirror(name, contents);
|
|
}
|
|
|
|
Future<String?> _readMirror(String name) async {
|
|
try {
|
|
final response = await _client.get(_uriFor(name));
|
|
if (response.statusCode != 200 || response.body.isEmpty) return null;
|
|
return response.body;
|
|
} on Exception {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> _writeMirror(String name, String contents) async {
|
|
try {
|
|
await _client.post(_uriFor(name), body: contents);
|
|
} on Exception {
|
|
// Best-effort by design; see the class docs.
|
|
}
|
|
}
|
|
|
|
Uri _uriFor(String name) =>
|
|
Uri.parse('$baseUrl${WrapperPaths.documents}$name');
|
|
}
|
|
|
|
/// Opens (creating if needed) the IndexedDB-backed document store.
|
|
Future<DocumentStore> openDocumentStore() async {
|
|
final database = await idbFactoryBrowser.open(
|
|
IndexedDbDocumentStore.databaseName,
|
|
version: 1,
|
|
onUpgradeNeeded: (event) {
|
|
final db = event.database;
|
|
if (!db.objectStoreNames.contains(IndexedDbDocumentStore.storeName)) {
|
|
db.createObjectStore(IndexedDbDocumentStore.storeName);
|
|
}
|
|
},
|
|
);
|
|
return IndexedDbDocumentStore(database);
|
|
}
|