feat: check every subscription in the background and add the posts page

This commit is contained in:
randogoth 2026-10-11 13:20:00 +03:00
parent 953ae798eb
commit 1b426ccd8b
15 changed files with 688 additions and 58 deletions

View file

@ -286,6 +286,51 @@ class MewsDatabase extends _$MewsDatabase {
return statement.write(const FeedEntriesCompanion(readAt: Value(null)));
}
/// Every stored post with the site it came from, for the posts page.
Future<List<RiverEntry>> riverEntries({
EntrySort sort = EntrySort.date,
bool unreadOnly = false,
int limit = 200,
}) async {
final site = alias(subscriptions, 'site');
final query = select(feedEntries).join([
leftOuterJoin(site, site.url.equalsExp(feedEntries.siteUrl)),
]);
if (unreadOnly) query.where(feedEntries.readAt.isNull());
// A site without a title sorts under its address, so the order never
// depends on a null.
final sourceName = coalesce([site.title, feedEntries.siteUrl]);
query.orderBy(switch (sort) {
EntrySort.date => [
OrderingTerm.desc(feedEntries.date),
OrderingTerm.asc(feedEntries.url),
],
EntrySort.source => [
OrderingTerm.asc(sourceName),
OrderingTerm.desc(feedEntries.date),
],
EntrySort.unread => [
// Unread first, whichever way the database orders nulls.
OrderingTerm.desc(feedEntries.readAt.isNull()),
OrderingTerm.desc(feedEntries.date),
],
});
query.limit(limit);
return (await query.get()).map((row) {
final entry = row.readTable(feedEntries);
return RiverEntry(
siteUrl: entry.siteUrl,
siteTitle: row.readTableOrNull(site)?.title,
url: entry.url,
title: entry.title,
date: entry.date,
readAt: entry.readAt,
);
}).toList();
}
/// How many posts are unread, for one site or across all of them.
Future<int> unreadCount({String? siteUrl}) async {
final total = feedEntries.url.count();
@ -371,3 +416,43 @@ class MewsDatabase extends _$MewsDatabase {
Future<void> saveCachedPage(CachedPagesCompanion entry) =>
into(cachedPages).insert(entry, mode: InsertMode.insertOrReplace);
}
/// How the posts page is ordered.
enum EntrySort {
date,
source,
unread;
/// Reads the order from an address, falling back to the default rather than
/// refusing a value the reader may have typed.
static EntrySort parse(String? name) => switch (name) {
'source' => EntrySort.source,
'unread' => EntrySort.unread,
_ => EntrySort.date,
};
}
/// One post together with the site that published it.
class RiverEntry {
const RiverEntry({
required this.siteUrl,
required this.siteTitle,
required this.url,
required this.title,
required this.date,
required this.readAt,
});
final String siteUrl;
final String? siteTitle;
final String url;
final String title;
final DateTime date;
final DateTime? readAt;
bool get unread => readAt == null;
/// The site as the reader knows it: its title, or its address until one is
/// recorded.
String get siteName => siteTitle ?? siteUrl;
}