136 lines
4.5 KiB
Dart
136 lines
4.5 KiB
Dart
|
|
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<void> 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<void> setFont(String family) async {
|
||
|
|
font.value = family;
|
||
|
|
await _persist();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> decreaseSize() => _setScale(fontScale.value - scaleStep);
|
||
|
|
Future<void> increaseSize() => _setScale(fontScale.value + scaleStep);
|
||
|
|
|
||
|
|
Future<void> _setScale(double value) async {
|
||
|
|
if (value < minScale || value > maxScale) return;
|
||
|
|
fontScale.value = value;
|
||
|
|
await _persist();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> setThemeMode(String mode) async {
|
||
|
|
themeMode.value = mode;
|
||
|
|
await _persist();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> setVariant(String variant) async {
|
||
|
|
this.variant.value = variant;
|
||
|
|
await _persist();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _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(<String>{});
|
||
|
|
final subscribedUrls = signal(<String>{});
|
||
|
|
|
||
|
|
/// 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(<String, List<MewsEntry>>{});
|
||
|
|
|
||
|
|
Future<void> 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<void> 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<void> toggleSubscription(String url, String? title, List<MewsEntry> 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<void> 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<String>().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(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|