feat: store subscription posts with a read mark and split the library controller
This commit is contained in:
parent
b311b9f7dd
commit
953ae798eb
15 changed files with 1787 additions and 386 deletions
35
app/lib/application/controllers/bookmarks_controller.dart
Normal file
35
app/lib/application/controllers/bookmarks_controller.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:signals/signals.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
|
||||
/// Which pages the reader has bookmarked. Only the set of addresses is held
|
||||
/// here, because that is all the chrome watches; the list itself is read from
|
||||
/// the database whenever a page needs it.
|
||||
class BookmarksController {
|
||||
BookmarksController(this._db);
|
||||
|
||||
final MewsDatabase _db;
|
||||
|
||||
final bookmarkedUrls = signal(<String>{});
|
||||
|
||||
Future<void> load() async {
|
||||
final rows = await _db.allBookmarks();
|
||||
bookmarkedUrls.value = rows.map((row) => row.url).toSet();
|
||||
}
|
||||
|
||||
bool isBookmarked(String url) => bookmarkedUrls.value.contains(url);
|
||||
|
||||
Future<void> toggle(String url, String? title) async {
|
||||
if (isBookmarked(url)) {
|
||||
await remove(url);
|
||||
return;
|
||||
}
|
||||
await _db.addBookmark(url, title);
|
||||
bookmarkedUrls.value = {...bookmarkedUrls.value, url};
|
||||
}
|
||||
|
||||
Future<void> remove(String url) async {
|
||||
await _db.removeBookmark(url);
|
||||
bookmarkedUrls.value = {...bookmarkedUrls.value}..remove(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
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.
|
||||
|
|
@ -76,75 +73,3 @@ class ReaderSettingsController {
|
|||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:signals/signals.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../mews/document.dart';
|
||||
|
||||
/// Which sites the reader subscribes to (spec 6). The posts themselves live in
|
||||
/// the database, so an unread post survives a restart; only the set of
|
||||
/// addresses is held here, because that is what the chrome watches.
|
||||
class SubscriptionsController {
|
||||
SubscriptionsController(this._db);
|
||||
|
||||
final MewsDatabase _db;
|
||||
|
||||
final subscribedUrls = signal(<String>{});
|
||||
|
||||
Future<void> load() async {
|
||||
final rows = await _db.allSubscriptions();
|
||||
subscribedUrls.value = rows.map((row) => row.url).toSet();
|
||||
}
|
||||
|
||||
bool isSubscribed(String url) => subscribedUrls.value.contains(url);
|
||||
|
||||
Future<void> toggle(MewsPage page) async {
|
||||
if (isSubscribed(page.url)) {
|
||||
await remove(page.url);
|
||||
return;
|
||||
}
|
||||
await _db.addSubscription(page.url, page.title, page.atomFeedHref);
|
||||
subscribedUrls.value = {...subscribedUrls.value, page.url};
|
||||
// The posts already on the page are the site's back catalogue, not news,
|
||||
// so they are stored read and only later posts are flagged.
|
||||
await _db.addEntries(page.url, page.entries, read: true);
|
||||
}
|
||||
|
||||
Future<void> remove(String siteUrl) async {
|
||||
await _db.removeSubscription(siteUrl);
|
||||
subscribedUrls.value = {...subscribedUrls.value}..remove(siteUrl);
|
||||
}
|
||||
|
||||
/// Records the posts on a subscribed page the reader just opened, and
|
||||
/// returns the ones that are new. Does nothing for a site not subscribed to.
|
||||
Future<List<MewsEntry>> refreshSite(MewsPage page) async {
|
||||
if (!isSubscribed(page.url)) return const [];
|
||||
await _db.describeSubscription(page.url,
|
||||
title: page.title, atomHref: page.atomFeedHref);
|
||||
final fresh = await _db.addEntries(page.url, page.entries);
|
||||
await _db.touchSubscription(page.url, DateTime.now());
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import '../database/database.dart';
|
|||
import '../l10n/strings.dart';
|
||||
import '../mews/document.dart';
|
||||
import 'controllers/reader_settings_controller.dart'
|
||||
show LibraryController, ReaderSettingsController;
|
||||
show ReaderSettingsController;
|
||||
import 'pages/bookmarks_page.dart';
|
||||
import 'pages/history_page.dart';
|
||||
import 'pages/hub_page.dart';
|
||||
|
|
@ -16,16 +16,13 @@ import 'pages/subscriptions_page.dart';
|
|||
class InternalPages {
|
||||
InternalPages({
|
||||
required MewsDatabase db,
|
||||
required LibraryController library,
|
||||
required ReaderSettingsController settings,
|
||||
required Strings strings,
|
||||
}) : _db = db,
|
||||
_library = library,
|
||||
_settings = settings,
|
||||
_strings = strings;
|
||||
|
||||
final MewsDatabase _db;
|
||||
final LibraryController _library;
|
||||
final ReaderSettingsController _settings;
|
||||
final Strings _strings;
|
||||
|
||||
|
|
@ -39,8 +36,7 @@ class InternalPages {
|
|||
return switch (uri.path) {
|
||||
'hub' => buildHubPage(uri, db: _db, strings: _strings),
|
||||
'bookmarks' => buildBookmarksPage(uri, db: _db, strings: _strings),
|
||||
'subscriptions' => buildSubscriptionsPage(uri,
|
||||
db: _db, library: _library, strings: _strings),
|
||||
'subscriptions' => buildSubscriptionsPage(uri, db: _db, strings: _strings),
|
||||
'history' => buildHistoryPage(uri, db: _db, strings: _strings),
|
||||
'settings' =>
|
||||
buildSettingsPage(uri, settings: _settings, strings: _strings),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import '../../database/database.dart';
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import '../controllers/reader_settings_controller.dart' show LibraryController;
|
||||
import 'page_blocks.dart';
|
||||
|
||||
Future<MewsPage> buildSubscriptionsPage(
|
||||
Uri url, {
|
||||
required MewsDatabase db,
|
||||
required LibraryController library,
|
||||
required Strings strings,
|
||||
}) async {
|
||||
final sites = await db.allSubscriptions();
|
||||
final unread = await db.unreadCounts();
|
||||
final title = strings.t('subscriptions-title');
|
||||
|
||||
return internalPage(
|
||||
|
|
@ -27,17 +26,16 @@ Future<MewsPage> buildSubscriptionsPage(
|
|||
href: site.url,
|
||||
label: site.title ?? site.url,
|
||||
date: dayText(site.createdAt),
|
||||
note: _unreadNote(library, strings, site.url),
|
||||
note: _unreadNote(strings, unread[site.url] ?? 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The unread count for a site, or null when it has nothing new, so the row
|
||||
/// carries no empty line.
|
||||
String? _unreadNote(LibraryController library, Strings strings, String siteUrl) {
|
||||
final count = library.newEntries.value[siteUrl]?.length ?? 0;
|
||||
/// The unread count for a site, or null when it has nothing unread, so the
|
||||
/// row carries no second line.
|
||||
String? _unreadNote(Strings strings, int count) {
|
||||
if (count == 0) return null;
|
||||
return countText(strings, count,
|
||||
none: 'count-unread-none',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue