Focus chrome was implemented twice, once in the focusable wrapper and once in the focus builders; both now go through FocusChrome. TvColorPicker's channel row was a copy of TvNumberSpinner and is now that widget in compact density. Also trims unused helpers and fields and simplifies the Jellyfin browse paths.
208 lines
8.2 KiB
Dart
208 lines
8.2 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import '../mixins/disposable_change_notifier_mixin.dart';
|
|
import 'multi_server_provider.dart';
|
|
import '../services/multi_server_manager.dart';
|
|
import '../services/offline_mode_source.dart';
|
|
|
|
enum OfflineModeReason {
|
|
online,
|
|
noNetworkConnection,
|
|
waitingForServerStatus,
|
|
noKnownVisibleServers,
|
|
onlyAuthErrorServers,
|
|
noServerConnection,
|
|
}
|
|
|
|
/// Tracks offline mode status based on network connectivity and server reachability.
|
|
class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMixin implements OfflineModeSource {
|
|
final MultiServerManager _serverManager;
|
|
MultiServerProvider? _multiServerProvider;
|
|
|
|
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
|
StreamSubscription<Map<String, bool>>? _serverStatusSubscription;
|
|
|
|
bool _hasNetworkConnection = true;
|
|
late bool _hasServerConnection;
|
|
bool _lastOfflineState = false;
|
|
bool _isInitialized = false;
|
|
|
|
/// Latest raw connectivity results. This provider owns the app's single
|
|
/// `Connectivity()` subscription; consumers needing the connection *type*
|
|
/// (e.g. the WiFi-reconnect sync trigger in main.dart) read it from here
|
|
/// instead of subscribing themselves.
|
|
List<ConnectivityResult> _lastConnectivityResults = const [];
|
|
bool _lastWifiOrEthernetState = false;
|
|
|
|
/// Whether the current connection is WiFi or Ethernet (unmetered-ish).
|
|
bool get hasWifiOrEthernet =>
|
|
_lastConnectivityResults.contains(ConnectivityResult.wifi) ||
|
|
_lastConnectivityResults.contains(ConnectivityResult.ethernet);
|
|
|
|
/// True once [MultiServerManager] has emitted its first server-status
|
|
/// snapshot. Until then we don't actually know whether any server is
|
|
/// online — the binder hasn't finished its first connect yet — so we
|
|
/// treat the app as online to avoid flashing the "offline" UI for the
|
|
/// few hundred ms it takes to come up. After the first emission we
|
|
/// trust the real flag.
|
|
bool _hasReceivedServerStatus = false;
|
|
|
|
OfflineModeProvider(this._serverManager, {MultiServerProvider? multiServerProvider})
|
|
: _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) or the active profile's
|
|
// visibility filter has already settled.
|
|
_markServerStatusKnownIfSettled();
|
|
_lastOfflineState = isOffline;
|
|
_multiServerProvider?.addListener(_handleMultiServerProviderChanged);
|
|
}
|
|
|
|
/// Whether the app is currently in offline mode
|
|
/// Offline = no network OR (we know servers are unreachable)
|
|
@override
|
|
bool get isOffline =>
|
|
offlineReason == OfflineModeReason.noNetworkConnection || offlineReason == OfflineModeReason.noServerConnection;
|
|
|
|
OfflineModeReason get offlineReason {
|
|
if (!_hasNetworkConnection) return OfflineModeReason.noNetworkConnection;
|
|
if (!_hasReceivedServerStatus) return OfflineModeReason.waitingForServerStatus;
|
|
if (!_hasKnownVisibleServers) return OfflineModeReason.noKnownVisibleServers;
|
|
if (_hasOnlyAuthErrorServers) return OfflineModeReason.onlyAuthErrorServers;
|
|
if (!_hasServerConnection) return OfflineModeReason.noServerConnection;
|
|
return OfflineModeReason.online;
|
|
}
|
|
|
|
/// Whether there is network connectivity (WiFi, mobile data, etc.)
|
|
@visibleForTesting
|
|
bool get hasNetworkConnection => _hasNetworkConnection;
|
|
|
|
/// Whether at least one media server (Plex or Jellyfin) is reachable
|
|
@visibleForTesting
|
|
bool get hasServerConnection => _hasServerConnection;
|
|
|
|
bool get _hasKnownVisibleServers =>
|
|
(_multiServerProvider?.expectedServerIds.length ?? _serverManager.serverIds.length) > 0;
|
|
|
|
bool get _hasOnlyAuthErrorServers {
|
|
final provider = _multiServerProvider;
|
|
if (provider == null) return false;
|
|
final serverCount = provider.expectedServerIds.length;
|
|
return serverCount > 0 && provider.authErrorServerIds.length == serverCount;
|
|
}
|
|
|
|
/// Attach the profile-visible server provider. Offline state is evaluated
|
|
/// against visible servers, not global manager state, so another profile's
|
|
/// online server does not keep the active profile out of offline mode.
|
|
void updateMultiServerProvider(MultiServerProvider provider) {
|
|
if (identical(_multiServerProvider, provider)) return;
|
|
_multiServerProvider?.removeListener(_handleMultiServerProviderChanged);
|
|
_multiServerProvider = provider;
|
|
_multiServerProvider?.addListener(_handleMultiServerProviderChanged);
|
|
_hasServerConnection = provider.hasConnectedServers;
|
|
_markServerStatusKnownIfSettled();
|
|
_notifyIfOfflineChanged();
|
|
}
|
|
|
|
/// Updates network and server connection flags
|
|
Future<void> _updateConnectionFlags() async {
|
|
try {
|
|
final connectivityResult = await Connectivity().checkConnectivity().timeout(
|
|
const Duration(seconds: 3),
|
|
onTimeout: () => [ConnectivityResult.other],
|
|
);
|
|
_lastConnectivityResults = connectivityResult;
|
|
_lastWifiOrEthernetState = hasWifiOrEthernet;
|
|
_hasNetworkConnection = !connectivityResult.contains(ConnectivityResult.none);
|
|
} catch (e) {
|
|
// connectivity_plus can throw PlatformException on Windows (NetworkManager::StartListen)
|
|
_hasNetworkConnection = true;
|
|
}
|
|
_hasServerConnection = _multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty;
|
|
}
|
|
|
|
void _handleMultiServerProviderChanged() {
|
|
_hasServerConnection = _multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty;
|
|
_markServerStatusKnownIfSettled();
|
|
_notifyIfOfflineChanged();
|
|
}
|
|
|
|
void _markServerStatusKnownIfSettled() {
|
|
if (_hasServerConnection || (_multiServerProvider?.hasExplicitVisibleServerFilter ?? false)) {
|
|
_hasReceivedServerStatus = true;
|
|
}
|
|
}
|
|
|
|
void _notifyIfOfflineChanged() {
|
|
final offline = isOffline;
|
|
if (_lastOfflineState == offline) return;
|
|
_lastOfflineState = offline;
|
|
safeNotifyListeners();
|
|
}
|
|
|
|
/// Initialize the provider and start monitoring
|
|
Future<void> initialize() async {
|
|
if (_isInitialized) return;
|
|
_isInitialized = true;
|
|
|
|
// Check initial connectivity
|
|
await _updateConnectionFlags();
|
|
|
|
// Monitor connectivity changes — runZonedGuarded catches async errors from
|
|
// connectivity_plus (e.g. DBusServiceUnknownException on Linux without NetworkManager)
|
|
runZonedGuarded(
|
|
() {
|
|
_connectivitySubscription = Connectivity().onConnectivityChanged.listen(
|
|
(results) {
|
|
_lastConnectivityResults = results;
|
|
_hasNetworkConnection = !results.contains(ConnectivityResult.none);
|
|
// Notify on connection-type changes too (WiFi <-> cellular), not
|
|
// just offline flips — type consumers listen through this provider.
|
|
final wifiNow = hasWifiOrEthernet;
|
|
if (wifiNow != _lastWifiOrEthernetState) {
|
|
_lastWifiOrEthernetState = wifiNow;
|
|
_lastOfflineState = isOffline;
|
|
safeNotifyListeners();
|
|
} else {
|
|
_notifyIfOfflineChanged();
|
|
}
|
|
},
|
|
onError: (e) {
|
|
_hasNetworkConnection = true;
|
|
},
|
|
);
|
|
},
|
|
(error, stack) {
|
|
// connectivity_plus throws DBusServiceUnknownException on Linux without NetworkManager
|
|
_hasNetworkConnection = true;
|
|
},
|
|
);
|
|
|
|
// Monitor server status from MultiServerManager
|
|
_serverStatusSubscription = _serverManager.statusStream.listen((statusMap) {
|
|
_hasServerConnection = _multiServerProvider?.hasConnectedServers ?? statusMap.values.any((isOnline) => isOnline);
|
|
_hasReceivedServerStatus = true;
|
|
_notifyIfOfflineChanged();
|
|
});
|
|
|
|
_lastOfflineState = isOffline;
|
|
safeNotifyListeners();
|
|
}
|
|
|
|
/// Force a refresh of connectivity status
|
|
Future<void> refresh() async {
|
|
await _updateConnectionFlags();
|
|
_lastOfflineState = isOffline;
|
|
safeNotifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_multiServerProvider?.removeListener(_handleMultiServerProviderChanged);
|
|
_connectivitySubscription?.cancel();
|
|
_serverStatusSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|