import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; part 'database.g.dart'; /// Visited pages, newest first; the most recent row is the app's start page. class HistoryItems extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get url => text()(); TextColumn get title => text().nullable()(); DateTimeColumn get visitedAt => dateTime().withDefault(currentDateAndTime)(); } class Bookmarks extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get url => text()(); TextColumn get title => text().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } /// A subscription to one page (spec 6). [entryUrls] is a JSON list of the /// entry URLs already seen, so a refresh can report only new posts. class Subscriptions extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get url => text()(); TextColumn get title => text().nullable()(); TextColumn get entryUrls => text().withDefault(const Constant('[]'))(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get lastCheckedAt => dateTime().nullable()(); } /// Single-row table (id 0) holding the reader's presentation choices. class ReaderSettings extends Table { IntColumn get id => integer().withDefault(const Constant(0))(); TextColumn get font => text().withDefault(const Constant('Atkinson Hyperlegible'))(); RealColumn get fontScale => real().withDefault(const Constant(1.0))(); TextColumn get themeMode => text().withDefault(const Constant('system'))(); TextColumn get variant => text().withDefault(const Constant('site'))(); @override Set get primaryKey => {id}; } /// The fetched source of a page, kept so the reader can reopen it offline. class CachedPages extends Table { TextColumn get url => text()(); TextColumn get title => text().nullable()(); TextColumn get source => text()(); TextColumn get etag => text().nullable()(); TextColumn get lastModified => text().nullable()(); DateTimeColumn get cachedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {url}; } @DriftDatabase(tables: [HistoryItems, Bookmarks, Subscriptions, ReaderSettings, CachedPages]) class MewsDatabase extends _$MewsDatabase { MewsDatabase() : super(_open()); /// For tests. MewsDatabase.test(super.executor); @override int get schemaVersion => 1; static QueryExecutor _open() => LazyDatabase(() async { final dir = await getApplicationDocumentsDirectory(); return NativeDatabase.createInBackground(File(p.join(dir.path, 'mews.sqlite'))); }); // History ------------------------------------------------------------- Future recordVisit(String url, String? title) => (into(historyItems).insert(HistoryItemsCompanion.insert(url: url, title: Value(title)))); Future lastVisitedUrl() async { final query = (select(historyItems) ..orderBy([ (r) => OrderingTerm.desc(r.visitedAt), // Tiebreak pages recorded within the same clock tick. (r) => OrderingTerm.desc(r.id), ]) ..limit(1)); final row = await query.getSingleOrNull(); return row?.url; } /// Row counts for the hub, so it can say what each page holds without /// reading every row. Future countHistory() => _countOf(historyItems, historyItems.id); Future countBookmarks() => _countOf(bookmarks, bookmarks.id); Future countSubscriptions() => _countOf(subscriptions, subscriptions.id); Future _countOf(ResultSetImplementation table, GeneratedColumn column) async { final total = column.count(); final query = selectOnly(table)..addColumns([total]); return (await query.getSingle()).read(total) ?? 0; } /// The most recent external visits, newest first. Future> recentVisits({int limit = 50}) async { final query = (select(historyItems) ..orderBy([ (r) => OrderingTerm.desc(r.visitedAt), (r) => OrderingTerm.desc(r.id), ]) ..limit(limit)); return query.get(); } /// Bookmarks in the order they were added, newest first. Future> bookmarksByDate() async { final query = (select(bookmarks) ..orderBy([ (r) => OrderingTerm.desc(r.createdAt), (r) => OrderingTerm.desc(r.id), ])); return query.get(); } // Bookmarks ----------------------------------------------------------- Future> allBookmarks() => select(bookmarks).get(); Future addBookmark(String url, String? title) => into(bookmarks).insert( BookmarksCompanion.insert(url: url, title: Value(title)), mode: InsertMode.insertOrIgnore); Future removeBookmark(String url) => (delete(bookmarks)..where((r) => r.url.equals(url))).go(); // Subscriptions --------------------------------------------------------- Future addSubscription(String url, String? title, List entryUrls) => into( subscriptions) .insert( SubscriptionsCompanion.insert( url: url, title: Value(title), entryUrls: Value(jsonEncode(entryUrls))), mode: InsertMode.insertOrIgnore); Future removeSubscription(String url) => (delete(subscriptions)..where((r) => r.url.equals(url))).go(); Future> allSubscriptions() => select(subscriptions).get(); Future updateSubscription(String url, {String? title, List? entryUrls, DateTime? lastCheckedAt}) => (update(subscriptions)..where((r) => r.url.equals(url))).write(SubscriptionsCompanion( title: Value(title), entryUrls: entryUrls == null ? const Value.absent() : Value(jsonEncode(entryUrls)), lastCheckedAt: Value(lastCheckedAt), )); // Settings --------------------------------------------------------------- Future readSettings() async { final query = select(readerSettings)..where((r) => r.id.equals(0)); return query.getSingleOrNull(); } Future saveSettings({ required String font, required double fontScale, required String themeMode, required String variant, }) => into(readerSettings).insert( ReaderSettingsCompanion.insert( id: const Value(0), font: Value(font), fontScale: Value(fontScale), themeMode: Value(themeMode), variant: Value(variant), ), mode: InsertMode.insertOrReplace); // Cache -------------------------------------------------------------------- Future cachedPage(String url) async => (select(cachedPages)..where((r) => r.url.equals(url))).getSingleOrNull(); Future saveCachedPage(CachedPagesCompanion entry) => into(cachedPages).insert(entry, mode: InsertMode.insertOrReplace); }