From a189ee5774fe89d266912f15904a4b6eb463e166 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 24 May 2026 03:12:33 +0200 Subject: [PATCH] fix(offline): honor settled profile visibility --- lib/providers/multi_server_provider.dart | 4 +++ lib/providers/offline_mode_provider.dart | 16 +++++++---- .../jellyfin_client/parts/playback.dart | 6 +++-- .../providers/multi_server_provider_test.dart | 5 ++++ .../providers/offline_mode_provider_test.dart | 27 +++++++++++++++++++ 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/providers/multi_server_provider.dart b/lib/providers/multi_server_provider.dart index 6f014cb8..caaeb3e9 100644 --- a/lib/providers/multi_server_provider.dart +++ b/lib/providers/multi_server_provider.dart @@ -45,6 +45,10 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi /// ids in the set surface through [serverIds] / [onlineServerIds]. Set? _visibleServerIds; + /// True once the active profile has explicitly resolved visibility. An empty + /// set is meaningful: the profile has servers, but none are currently visible. + bool get hasExplicitVisibleServerFilter => _visibleServerIds != null; + /// Server ids the active profile is expected to have access to, including /// unreachable servers that do not have a live client in [MultiServerManager]. /// This is intentionally separate from [_visibleServerIds]: visible ids drive diff --git a/lib/providers/offline_mode_provider.dart b/lib/providers/offline_mode_provider.dart index 1905b8dd..5fc1e59c 100644 --- a/lib/providers/offline_mode_provider.dart +++ b/lib/providers/offline_mode_provider.dart @@ -31,9 +31,9 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi : _multiServerProvider = multiServerProvider, _hasServerConnection = (multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty) { // Pre-seed the "received status" flag if there are already online - // servers (e.g. provider rebuilt mid-session) — otherwise we'd - // incorrectly say "online" after the manager already emitted. - if (_hasServerConnection) _hasReceivedServerStatus = true; + // servers (e.g. provider rebuilt mid-session) or the active profile's + // visibility filter has already settled. + _markServerStatusKnownIfSettled(); _lastOfflineState = isOffline; _multiServerProvider?.addListener(_handleMultiServerProviderChanged); } @@ -74,7 +74,7 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi _multiServerProvider = provider; _multiServerProvider?.addListener(_handleMultiServerProviderChanged); _hasServerConnection = provider.hasConnectedServers; - if (_hasServerConnection) _hasReceivedServerStatus = true; + _markServerStatusKnownIfSettled(); _notifyIfOfflineChanged(); } @@ -95,10 +95,16 @@ class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMi void _handleMultiServerProviderChanged() { _hasServerConnection = _multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty; - if (_hasServerConnection) _hasReceivedServerStatus = true; + _markServerStatusKnownIfSettled(); _notifyIfOfflineChanged(); } + void _markServerStatusKnownIfSettled() { + if (_hasServerConnection || (_multiServerProvider?.hasExplicitVisibleServerFilter ?? false)) { + _hasReceivedServerStatus = true; + } + } + void _notifyIfOfflineChanged() { final offline = isOffline; if (_lastOfflineState == offline) return; diff --git a/lib/services/jellyfin_client/parts/playback.dart b/lib/services/jellyfin_client/parts/playback.dart index 1ba4b4aa..e409232f 100644 --- a/lib/services/jellyfin_client/parts/playback.dart +++ b/lib/services/jellyfin_client/parts/playback.dart @@ -429,8 +429,10 @@ mixin _JellyfinPlaybackMethods on MediaServerCacheMixin { /// transcode bitrate against the same ceiling. Original playback passes null /// to avoid capping high-bitrate files. [mediaSourceId] pins the negotiation /// to a specific version when the item has multiple sources. - /// [startTimeTicks] asks Jellyfin to start transcoding at the requested - /// source offset so server-generated streams line up with the app timeline. + /// [startTimeTicks] is forwarded to Jellyfin's playback negotiation for + /// resume-aware stream metadata. Our video transcode profile is HLS, and + /// Jellyfin omits `StartTimeTicks` from the returned HLS URL, so the player + /// still performs the initial seek. /// [audioStreamIndex] / [subtitleStreamIndex] tell the server which streams /// to pick for the transcode profile (Jellyfin's negotiation factors them in /// when picking codec compatibility). diff --git a/test/providers/multi_server_provider_test.dart b/test/providers/multi_server_provider_test.dart index b1d9a361..fb450f4f 100644 --- a/test/providers/multi_server_provider_test.dart +++ b/test/providers/multi_server_provider_test.dart @@ -95,14 +95,18 @@ void main() { var notified = 0; p.addListener(() => notified++); + expect(p.hasExplicitVisibleServerFilter, isFalse); + // Empty set is a real value (different from null) — switching from // null → {} should notify so consumers know the active profile has // no servers, not "all servers". p.setVisibleServerIds({}); expect(notified, 1); + expect(p.hasExplicitVisibleServerFilter, isTrue); p.setVisibleServerIds({'a', 'b'}); expect(notified, 2); + expect(p.hasExplicitVisibleServerFilter, isTrue); // Idempotent: same membership is a no-op. p.setVisibleServerIds({'b', 'a'}); @@ -111,6 +115,7 @@ void main() { // Clearing back to null after a real filter is a state change. p.setVisibleServerIds(null); expect(notified, 3); + expect(p.hasExplicitVisibleServerFilter, isFalse); p.dispose(); }); diff --git a/test/providers/offline_mode_provider_test.dart b/test/providers/offline_mode_provider_test.dart index 47902df2..afb00ccf 100644 --- a/test/providers/offline_mode_provider_test.dart +++ b/test/providers/offline_mode_provider_test.dart @@ -166,6 +166,33 @@ void main() { manager.dispose(); }); + test('expected but unreachable profile servers enter offline once visibility settles', () async { + final manager = MultiServerManager(); + final multi = MultiServerProvider(manager, DataAggregationService(manager)); + final p = OfflineModeProvider(manager, multiServerProvider: multi); + + expect(p.isOffline, isFalse); + + var notifications = 0; + p.addListener(() => notifications++); + + multi.setExpectedVisibleServerIds({'jf-machine'}); + await Future.delayed(Duration.zero); + + expect(p.isOffline, isFalse); + expect(notifications, 0); + + multi.setVisibleServerIds({}); + await Future.delayed(Duration.zero); + + expect(p.isOffline, isTrue); + expect(notifications, 1); + + p.dispose(); + multi.dispose(); + manager.dispose(); + }); + test('Plex auth errors without live clients stay out of generic offline', () async { final manager = MultiServerManager(); final multi = MultiServerProvider(manager, DataAggregationService(manager));