feat: build v1 Mews client: reader UI, gestures, Drift store, fetch actor

This commit is contained in:
randogoth 2026-10-11 09:33:25 +03:00
parent d5a0fcbf48
commit 01a36e8aee
56 changed files with 7287 additions and 2 deletions

View file

@ -0,0 +1,157 @@
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<Column> 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<Column> 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<void> recordVisit(String url, String? title) =>
(into(historyItems).insert(HistoryItemsCompanion.insert(url: url, title: Value(title))));
Future<String?> 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;
}
// Bookmarks -----------------------------------------------------------
Future<List<Bookmark>> allBookmarks() => select(bookmarks).get();
Future<void> addBookmark(String url, String? title) => into(bookmarks).insert(
BookmarksCompanion.insert(url: url, title: Value(title)),
mode: InsertMode.insertOrIgnore);
Future<void> removeBookmark(String url) =>
(delete(bookmarks)..where((r) => r.url.equals(url))).go();
// Subscriptions ---------------------------------------------------------
Future<void> addSubscription(String url, String? title, List<String> entryUrls) => into(
subscriptions)
.insert(
SubscriptionsCompanion.insert(
url: url, title: Value(title), entryUrls: Value(jsonEncode(entryUrls))),
mode: InsertMode.insertOrIgnore);
Future<void> removeSubscription(String url) =>
(delete(subscriptions)..where((r) => r.url.equals(url))).go();
Future<List<Subscription>> allSubscriptions() => select(subscriptions).get();
Future<void> updateSubscription(String url,
{String? title, List<String>? 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<ReaderSetting?> readSettings() async {
final query = select(readerSettings)..where((r) => r.id.equals(0));
return query.getSingleOrNull();
}
Future<void> 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?> cachedPage(String url) async =>
(select(cachedPages)..where((r) => r.url.equals(url))).getSingleOrNull();
Future<void> saveCachedPage(CachedPagesCompanion entry) =>
into(cachedPages).insert(entry, mode: InsertMode.insertOrReplace);
}