iykyka/mobile/lib/db/app_database_web.dart

152 lines
4.7 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
/// Web/Chrome [AppDatabase] backed by [SharedPreferences] (IndexedDB under the hood).
///
/// SQLCipher / dart:ffi cannot compile for Flutter Web. Persistence is
/// **not encrypted** — acceptable for the demo/preview surface
/// (`docs/web-upgrade.md` N4-W1). Drift WASM is intentionally not used.
///
/// [AppDatabase.memory] stays ephemeral (unit tests / fallbacks).
class AppDatabase {
AppDatabase({
this.encrypted = false,
SharedPreferences? prefs,
bool persist = false,
}) : _prefs = prefs,
_persist = persist && prefs != null;
/// Always false on web — there is no SQLCipher path.
final bool encrypted;
final SharedPreferences? _prefs;
final bool _persist;
final Map<String, String> _kv = {};
List<String> _toneSamples = [];
final Map<int, DateTime> _snoozes = {};
static const _kTone = 'ykavu_web_db_tone_samples';
static const _kSnoozes = 'ykavu_web_db_snoozes';
static const _kvPrefix = 'ykavu_web_db_kv_';
factory AppDatabase.memory() => AppDatabase(encrypted: false, persist: false);
/// Opens a durable preview DB (survives hard refresh in the same browser profile).
static Future<AppDatabase> open() async {
final prefs = await SharedPreferences.getInstance();
final db = AppDatabase(encrypted: false, prefs: prefs, persist: true);
await db._hydrate();
return db;
}
Future<void> _hydrate() async {
final prefs = _prefs;
if (!_persist || prefs == null) return;
try {
final toneRaw = prefs.getString(_kTone);
if (toneRaw != null && toneRaw.isNotEmpty) {
final decoded = jsonDecode(toneRaw);
if (decoded is List) {
_toneSamples = decoded.map((e) => e.toString()).toList();
}
}
} catch (_) {
_toneSamples = [];
await prefs.remove(_kTone);
}
try {
final snoozeRaw = prefs.getString(_kSnoozes);
if (snoozeRaw != null && snoozeRaw.isNotEmpty) {
final decoded = jsonDecode(snoozeRaw);
if (decoded is Map) {
_snoozes
..clear()
..addEntries(
decoded.entries.map((e) {
final id = int.tryParse(e.key.toString());
final ms = e.value is int
? e.value as int
: int.tryParse(e.value.toString());
if (id == null || ms == null) return null;
return MapEntry(id, DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true));
}).whereType<MapEntry<int, DateTime>>(),
);
}
}
} catch (_) {
_snoozes.clear();
await prefs.remove(_kSnoozes);
}
for (final key in prefs.getKeys()) {
if (!key.startsWith(_kvPrefix)) continue;
final logical = key.substring(_kvPrefix.length);
final value = prefs.getString(key);
if (value != null) _kv[logical] = value;
}
}
Future<void> _persistTone() async {
if (!_persist || _prefs == null) return;
await _prefs.setString(_kTone, jsonEncode(_toneSamples));
}
Future<void> _persistSnoozes() async {
if (!_persist || _prefs == null) return;
final map = <String, int>{
for (final e in _snoozes.entries) e.key.toString(): e.value.toUtc().millisecondsSinceEpoch,
};
await _prefs.setString(_kSnoozes, jsonEncode(map));
}
Future<void> _persistKv(String key, String value) async {
if (!_persist || _prefs == null) return;
await _prefs.setString('$_kvPrefix$key', value);
}
Future<List<String>> loadToneSamples() async => List<String>.from(_toneSamples);
Future<void> replaceToneSamples(List<String> samples) async {
_toneSamples = List<String>.from(samples);
await _persistTone();
}
Future<String?> getKv(String key) async => _kv[key];
Future<void> setKv(String key, String value) async {
_kv[key] = value;
await _persistKv(key, value);
}
Future<bool> getBoolKv(String key, {bool defaultValue = false}) async {
final v = await getKv(key);
if (v == null) return defaultValue;
return v == '1' || v.toLowerCase() == 'true';
}
Future<void> setBoolKv(String key, bool value) async {
await setKv(key, value ? '1' : '0');
}
/// roadmap.md §2.7-F — on-device snooze; web preview persists via SharedPreferences.
Future<void> setSnoozedUntil(int conversationId, DateTime until) async {
_snoozes[conversationId] = until.toUtc();
await _persistSnoozes();
}
Future<DateTime?> getSnoozedUntil(int conversationId) async => _snoozes[conversationId];
Future<void> clearSnooze(int conversationId) async {
_snoozes.remove(conversationId);
await _persistSnoozes();
}
Future<Map<int, DateTime>> loadAllSnoozes() async => Map<int, DateTime>.from(_snoozes);
Future<void> close() async {}
}