From 1508d2f80436ebbbd6419187aa59ad7e5a7cb50f Mon Sep 17 00:00:00 2001 From: foxyz Date: Sun, 11 Oct 2026 16:24:38 +0300 Subject: [PATCH] Jellyfin rows reload after watching: the Stopped report clears the server's cached lists Co-Authored-By: Claude Opus 5.5 --- apps/desktop/lib/src/jellyfin/accounts.dart | 11 +++++++++++ apps/desktop/lib/src/jellyfin/browse_scope.dart | 10 +++++++++- apps/desktop/lib/src/jellyfin/progress.dart | 7 ++++++- apps/desktop/lib/src/session/session_coordinator.dart | 1 + docs/ARCHITECTURE.md | 4 +++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/desktop/lib/src/jellyfin/accounts.dart b/apps/desktop/lib/src/jellyfin/accounts.dart index 4f4615a..5128d76 100644 --- a/apps/desktop/lib/src/jellyfin/accounts.dart +++ b/apps/desktop/lib/src/jellyfin/accounts.dart @@ -543,6 +543,17 @@ class JellyfinAccounts extends ChangeNotifier { /// Drops cached browse results (refresh button, after a pick). void invalidate(String serverId) => _clearMemo(serverId); + final _watched = StreamController.broadcast(); + + /// Server ids whose watched state this app just changed (a stop report): + /// their cached lists are gone, and browse views reload. + Stream get watched => _watched.stream; + + void watchedChanged(String serverId) { + _clearMemo(serverId); + _watched.add(serverId); + } + @override void dispose() { _disposed = true; diff --git a/apps/desktop/lib/src/jellyfin/browse_scope.dart b/apps/desktop/lib/src/jellyfin/browse_scope.dart index ea05ef1..cae23cf 100644 --- a/apps/desktop/lib/src/jellyfin/browse_scope.dart +++ b/apps/desktop/lib/src/jellyfin/browse_scope.dart @@ -157,7 +157,14 @@ class BrowseController extends ChangeNotifier { final JellyfinAccounts accounts; /// Clock for the Continue row's age cut-off (tests pin it). final DateTime Function() now; - BrowseController(this.accounts, {this.now = DateTime.now}); + BrowseController(this.accounts, {this.now = DateTime.now}) { + // Back from watching: the rows show what was just watched. + _watchedSub = accounts.watched.listen((id) { + if (!_disposed && id == serverId) dataChanged(); + }); + } + + late final StreamSubscription _watchedSub; String? _serverId; List _stack = const [HomeRoute()]; @@ -259,6 +266,7 @@ class BrowseController extends ChangeNotifier { @override void dispose() { _disposed = true; + unawaited(_watchedSub.cancel()); super.dispose(); } } diff --git a/apps/desktop/lib/src/jellyfin/progress.dart b/apps/desktop/lib/src/jellyfin/progress.dart index 0695e5a..8579081 100644 --- a/apps/desktop/lib/src/jellyfin/progress.dart +++ b/apps/desktop/lib/src/jellyfin/progress.dart @@ -56,6 +56,7 @@ class JellyfinProgressReporter { this.interval = const Duration(seconds: 10), this.onError, this.onUnauthorized, + this.onStopped, DateTime Function()? clock, }) : _clock = clock ?? DateTime.now, _keepPlayed = playedBefore, @@ -67,6 +68,10 @@ class JellyfinProgressReporter { /// marks an item played past its "max resume" share (90 % by default). static const _noPositionFrom = 0.85; + /// After Stopped (and a mark played) went out: the item's watched + /// state on the server may differ from what lists show. + final void Function()? onStopped; + Future _chain = Future.value(); bool _playing = false; bool _dead = false; @@ -178,7 +183,7 @@ class JellyfinProgressReporter { itemId: itemId, mediaSourceId: source.id, playSessionId: playSessionId, positionTicks: ticks); _send('Stopped', () => client.reportPlaybackStopped(report)); if (ticks == null) _send('played', () => client.markPlayed(itemId)); - return _chain; + return _chain = _chain.then((_) => onStopped?.call()); } void _mark(PlayerSnapshot s) { diff --git a/apps/desktop/lib/src/session/session_coordinator.dart b/apps/desktop/lib/src/session/session_coordinator.dart index 51db682..a853582 100644 --- a/apps/desktop/lib/src/session/session_coordinator.dart +++ b/apps/desktop/lib/src/session/session_coordinator.dart @@ -1750,6 +1750,7 @@ class SessionCoordinator { playedBefore: before?.played ?? false, resumeTicksBefore: before?.playbackPositionTicks ?? 0, onUnauthorized: () => jellyfin.markExpired(ref.serverId), + onStopped: () => jellyfin.watchedChanged(ref.serverId), onError: (e) { if (_jfToastShown) return; _jfToastShown = true; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d57cb6a..ca24503 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1268,7 +1268,9 @@ Jellyfin on the desktop (`lib/src/jellyfin/`, over the core client): series) kept alive under `Visibility(maintainState)` so back keeps scroll and loaded pages; search (300 ms debounce) overlays them. Where it is (server, stack, a data version that moves after watched-state - changes so rows reload in place, the Continue keys) is a + changes so rows reload in place — a change from the details panel, or + `JellyfinAccounts.watched` once a playback's Stopped report went out —, + the Continue keys) is a `BrowseController` shared with the header chips and the details panel. Cards (`cards.dart`) fill the width with whole columns (`fitColumns`); rows are `HScroll`s, rows and grids lazy builders; library grids page