import 'dart:convert'; import 'package:signals/signals.dart'; import '../../database/database.dart'; import '../../mews/document.dart'; /// The reader's presentation choices (spec 8.2) plus the per-app limits they /// move within. Values persist through ReaderSettings in the database. class ReaderSettingsController { ReaderSettingsController(this._db); final MewsDatabase _db; static const minScale = 0.75; static const maxScale = 2.0; static const scaleStep = 0.125; final font = signal('Atkinson Hyperlegible'); final fontScale = signal(1.0); final themeMode = signal('system'); // system | light | dark final variant = signal('site'); // site | warm | cool | green | mono Future load() async { final settings = await _db.readSettings(); if (settings == null) return; font.value = settings.font; fontScale.value = settings.fontScale; themeMode.value = settings.themeMode; variant.value = settings.variant; } Future setFont(String family) async { font.value = family; await _persist(); } Future decreaseSize() => _setScale(fontScale.value - scaleStep); Future increaseSize() => _setScale(fontScale.value + scaleStep); Future _setScale(double value) async { if (value < minScale || value > maxScale) return; fontScale.value = value; await _persist(); } Future setThemeMode(String mode) async { themeMode.value = mode; await _persist(); } Future setVariant(String variant) async { this.variant.value = variant; await _persist(); } Future _persist() => _db.saveSettings( font: font.value, fontScale: fontScale.value, themeMode: themeMode.value, variant: variant.value, ); } /// Bookmarks and subscriptions. Signals expose derived state (which URLs are /// bookmarked or subscribed); subscription refreshes piggyback on page loads, /// comparing the page's dated links against the known entry URLs. class LibraryController { LibraryController(this._db); final MewsDatabase _db; final bookmarkedUrls = signal({}); final subscribedUrls = signal({}); /// New posts found on the most recent refresh of a subscribed site, keyed by /// site URL. Empty when there is nothing to report. final newEntries = signal(>{}); Future load() async { final bookmarks = await _db.allBookmarks(); bookmarkedUrls.value = bookmarks.map((b) => b.url).toSet(); final subs = await _db.allSubscriptions(); subscribedUrls.value = subs.map((s) => s.url).toSet(); } bool isBookmarked(String url) => bookmarkedUrls.value.contains(url); bool isSubscribed(String url) => subscribedUrls.value.contains(url); Future toggleBookmark(String url, String? title) async { if (isBookmarked(url)) { await _db.removeBookmark(url); bookmarkedUrls.value = {...bookmarkedUrls.value}..remove(url); } else { await _db.addBookmark(url, title); bookmarkedUrls.value = {...bookmarkedUrls.value, url}; } } Future toggleSubscription(String url, String? title, List entries) async { if (isSubscribed(url)) { await _db.removeSubscription(url); subscribedUrls.value = {...subscribedUrls.value}..remove(url); newEntries.value = {...newEntries.value}..remove(url); } else { await _db.addSubscription(url, title, entries.map((e) => e.url).toList()); subscribedUrls.value = {...subscribedUrls.value, url}; newEntries.value = {...newEntries.value}..remove(url); } } /// Revalidates every subscribed page and reports entries the reader has not /// seen yet (spec 6.1). Called when a subscribed page is opened. Future refreshSubscription(MewsPage page) async { final subs = await _db.allSubscriptions(); final sub = subs.where((s) => s.url == page.url).firstOrNull; if (sub == null) return; final known = (jsonDecode(sub.entryUrls) as List).cast().toSet(); final fresh = page.entries.where((e) => !known.contains(e.url)).toList(); if (fresh.isEmpty) { // Nothing new this time, so the site stops being flagged. newEntries.value = {...newEntries.value}..remove(sub.url); await _db.updateSubscription(sub.url, lastCheckedAt: DateTime.now()); return; } newEntries.value = {...newEntries.value, page.url: fresh}; await _db.updateSubscription( sub.url, entryUrls: [...known, ...fresh.map((e) => e.url)], lastCheckedAt: DateTime.now(), ); } }