fix(offline): honor settled profile visibility

This commit is contained in:
edde746
2026-05-24 03:12:33 +02:00
parent e74df0d245
commit a189ee5774
5 changed files with 51 additions and 7 deletions
+4
View File
@@ -45,6 +45,10 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
/// ids in the set surface through [serverIds] / [onlineServerIds].
Set<String>? _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
+11 -5
View File
@@ -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;
@@ -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).
@@ -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(<String>{});
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();
});
@@ -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<void>.delayed(Duration.zero);
expect(p.isOffline, isFalse);
expect(notifications, 0);
multi.setVisibleServerIds(<String>{});
await Future<void>.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));