Jellyfin rows reload after watching: the Stopped report clears the server's cached lists

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 16:24:38 +03:00
1 parent 86eb30f498
commit 1508d2f804
5 files changed
+30 -3

No files matched your search

@@ -543,6 +543,17 @@ class JellyfinAccounts extends ChangeNotifier {
/// Drops cached browse results (refresh button, after a pick). /// Drops cached browse results (refresh button, after a pick).
void invalidate(String serverId) => _clearMemo(serverId); void invalidate(String serverId) => _clearMemo(serverId);
final _watched = StreamController<String>.broadcast();
/// Server ids whose watched state this app just changed (a stop report):
/// their cached lists are gone, and browse views reload.
Stream<String> get watched => _watched.stream;
void watchedChanged(String serverId) {
_clearMemo(serverId);
_watched.add(serverId);
}
@override @override
void dispose() { void dispose() {
_disposed = true; _disposed = true;
@@ -157,7 +157,14 @@ class BrowseController extends ChangeNotifier {
final JellyfinAccounts accounts; final JellyfinAccounts accounts;
/// Clock for the Continue row's age cut-off (tests pin it). /// Clock for the Continue row's age cut-off (tests pin it).
final DateTime Function() now; 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<String> _watchedSub;
String? _serverId; String? _serverId;
List<BrowseRoute> _stack = const [HomeRoute()]; List<BrowseRoute> _stack = const [HomeRoute()];
@@ -259,6 +266,7 @@ class BrowseController extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_disposed = true; _disposed = true;
unawaited(_watchedSub.cancel());
super.dispose(); super.dispose();
} }
} }
+6 -1
View File
@@ -56,6 +56,7 @@ class JellyfinProgressReporter {
this.interval = const Duration(seconds: 10), this.interval = const Duration(seconds: 10),
this.onError, this.onError,
this.onUnauthorized, this.onUnauthorized,
this.onStopped,
DateTime Function()? clock, DateTime Function()? clock,
}) : _clock = clock ?? DateTime.now, }) : _clock = clock ?? DateTime.now,
_keepPlayed = playedBefore, _keepPlayed = playedBefore,
@@ -67,6 +68,10 @@ class JellyfinProgressReporter {
/// marks an item played past its "max resume" share (90 % by default). /// marks an item played past its "max resume" share (90 % by default).
static const _noPositionFrom = 0.85; 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<void> _chain = Future.value(); Future<void> _chain = Future.value();
bool _playing = false; bool _playing = false;
bool _dead = false; bool _dead = false;
@@ -178,7 +183,7 @@ class JellyfinProgressReporter {
itemId: itemId, mediaSourceId: source.id, playSessionId: playSessionId, positionTicks: ticks); itemId: itemId, mediaSourceId: source.id, playSessionId: playSessionId, positionTicks: ticks);
_send('Stopped', () => client.reportPlaybackStopped(report)); _send('Stopped', () => client.reportPlaybackStopped(report));
if (ticks == null) _send('played', () => client.markPlayed(itemId)); if (ticks == null) _send('played', () => client.markPlayed(itemId));
return _chain; return _chain = _chain.then((_) => onStopped?.call());
} }
void _mark(PlayerSnapshot s) { void _mark(PlayerSnapshot s) {
@@ -1750,6 +1750,7 @@ class SessionCoordinator {
playedBefore: before?.played ?? false, playedBefore: before?.played ?? false,
resumeTicksBefore: before?.playbackPositionTicks ?? 0, resumeTicksBefore: before?.playbackPositionTicks ?? 0,
onUnauthorized: () => jellyfin.markExpired(ref.serverId), onUnauthorized: () => jellyfin.markExpired(ref.serverId),
onStopped: () => jellyfin.watchedChanged(ref.serverId),
onError: (e) { onError: (e) {
if (_jfToastShown) return; if (_jfToastShown) return;
_jfToastShown = true; _jfToastShown = true;
+3 -1
View File
@@ -1268,7 +1268,9 @@ Jellyfin on the desktop (`lib/src/jellyfin/`, over the core client):
series) kept alive under `Visibility(maintainState)` so back keeps series) kept alive under `Visibility(maintainState)` so back keeps
scroll and loaded pages; search (300 ms debounce) overlays them. Where scroll and loaded pages; search (300 ms debounce) overlays them. Where
it is (server, stack, a data version that moves after watched-state 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. `BrowseController` shared with the header chips and the details panel.
Cards (`cards.dart`) fill the width with whole columns (`fitColumns`); Cards (`cards.dart`) fill the width with whole columns (`fitColumns`);
rows are `HScroll`s, rows and grids lazy builders; library grids page rows are `HScroll`s, rows and grids lazy builders; library grids page