feat: store subscription posts with a read mark and split the library controller

This commit is contained in:
randogoth 2026-10-11 13:12:48 +03:00
parent b311b9f7dd
commit 953ae798eb
15 changed files with 1787 additions and 386 deletions

View file

@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart' show SignalBuilder;
import '../application/actors/fetch_actor.dart';
import '../application/controllers/bookmarks_controller.dart';
import '../application/controllers/browser_controller.dart';
import '../application/controllers/reader_settings_controller.dart';
import '../application/controllers/subscriptions_controller.dart';
import '../application/internal_actions.dart';
import '../application/internal_pages.dart';
import '../database/database.dart';
@ -24,9 +26,10 @@ class AppDependencies {
fetch = fetch ?? Actor.create(FetchHandler.new),
strings = strings ?? Strings() {
settings = ReaderSettingsController(db);
library = LibraryController(db);
internalPages = InternalPages(
db: db, library: library, settings: settings, strings: this.strings);
bookmarks = BookmarksController(db);
subscriptions = SubscriptionsController(db);
internalPages =
InternalPages(db: db, settings: settings, strings: this.strings);
internalActions = InternalActions(settings: settings);
browser = BrowserController(
db: db,
@ -43,13 +46,15 @@ class AppDependencies {
final Actor<FetchCommand, FetchResult> fetch;
late final BrowserController browser;
late final ReaderSettingsController settings;
late final LibraryController library;
late final BookmarksController bookmarks;
late final SubscriptionsController subscriptions;
late final InternalPages internalPages;
late final InternalActions internalActions;
Future<void> start() async {
await settings.load();
await library.load();
await bookmarks.load();
await subscriptions.load();
await browser.restoreLastSession();
}
}

View file

@ -132,28 +132,26 @@ class _AddressBarState extends State<AddressBar> {
if (canSubscribe)
IconButton(
icon: Icon(
deps.library.isSubscribed(page.url)
deps.subscriptions.isSubscribed(page.url)
? Icons.notifications_active
: Icons.notifications_outlined,
),
tooltip: deps.library.isSubscribed(page.url)
tooltip: deps.subscriptions.isSubscribed(page.url)
? strings.t('reader-unsubscribe')
: strings.t('reader-subscribe'),
onPressed: () => deps.library
.toggleSubscription(page.url, page.title, page.entries),
onPressed: () => deps.subscriptions.toggle(page),
),
if (canBookmark)
IconButton(
icon: Icon(
deps.library.isBookmarked(page.url)
deps.bookmarks.isBookmarked(page.url)
? Icons.star
: Icons.star_border,
),
tooltip: deps.library.isBookmarked(page.url)
tooltip: deps.bookmarks.isBookmarked(page.url)
? strings.t('reader-bookmark-remove')
: strings.t('reader-bookmark-add'),
onPressed: () =>
deps.library.toggleBookmark(page.url, page.title),
onPressed: () => deps.bookmarks.toggle(page.url, page.title),
),
],
),

View file

@ -28,7 +28,6 @@ class _ReaderScreenState extends State<ReaderScreen> {
/// scrolls down, back on any upward scroll.
bool _chromeVisible = true;
bool _showBrokenText = false;
final _reportedNewPosts = <String>{};
final _checkedSubscriptions = <String>{};
@override
@ -151,7 +150,6 @@ class _ReaderScreenState extends State<ReaderScreen> {
final fontScale = settings.fontScale.value;
final variant = settings.variant.value;
final visited = deps.browser.visitedUrls.value;
_maybeReportNewPosts(context, page);
final palette = paletteFor(
pageVariant: page.variant,
@ -251,32 +249,15 @@ class _ReaderScreenState extends State<ReaderScreen> {
);
}
/// Revalidates a subscribed page once per page load so new entries surface
/// (spec 6.1). The library controller no-ops for unsubscribed pages.
/// Records the posts of a subscribed page once per page load, so what is new
/// shows up on the subscriptions and posts pages (spec 6.1). The controller
/// no-ops for a page the reader is not subscribed to.
void _checkSubscription(BuildContext context, MewsPage page) {
if (_checkedSubscriptions.contains(page.url)) return;
_checkedSubscriptions.add(page.url);
final deps = AppScope.of(context).deps;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) deps.library.refreshSubscription(page);
});
}
/// Shows the new-posts snackbar once per site after the refresh reports
/// entries the reader has not seen.
void _maybeReportNewPosts(BuildContext context, MewsPage page) {
final scope = AppScope.of(context);
final fresh = scope.deps.library.newEntries.value[page.url];
if (fresh == null || fresh.isEmpty || _reportedNewPosts.contains(page.url)) return;
_reportedNewPosts.add(page.url);
final strings = scope.strings;
final message = fresh.length == 1
? strings.t('new-posts-one')
: strings.t('new-posts-many', {'count': fresh.length});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
if (mounted) deps.subscriptions.refreshSite(page);
});
}
}