Merge the deduplication and dead-code removal pass

Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
This commit is contained in:
edde746
2026-07-26 19:41:23 +02:00
425 changed files with 12045 additions and 17543 deletions
+87 -142
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import '../media/ids.dart';
import '../media/media_server_client.dart';
import '../navigation/main_screen_scope.dart';
import 'dart:io' show Platform, exit;
@@ -34,6 +35,7 @@ import '../profiles/active_profile_binder.dart';
import '../connection/connection_registry.dart';
import '../profiles/active_profile_provider.dart';
import '../profiles/plex_home_service.dart';
import '../profiles/profile_selection_policy.dart';
import '../providers/catalog_sources_provider.dart';
import '../providers/download_provider.dart';
import '../providers/multi_server_provider.dart';
@@ -260,13 +262,12 @@ class _MainScreenState extends State<MainScreen>
bool _isShowingProfileSelection = false;
late List<Widget> _screens;
final GlobalKey<State<DiscoverScreen>> _discoverKey = GlobalKey();
final GlobalKey<State<ExploreScreen>> _exploreKey = GlobalKey();
final GlobalKey<State<LibrariesScreen>> _librariesKey = GlobalKey();
final GlobalKey<State<LiveTvScreen>> _liveTvKey = GlobalKey();
final GlobalKey<State<SearchScreen>> _searchKey = GlobalKey();
final GlobalKey<State<DownloadsScreen>> _downloadsKey = GlobalKey();
final GlobalKey<State<SettingsScreen>> _settingsKey = GlobalKey();
/// One [GlobalKey] per tab, so a tab's live [State] can be reached from
/// anywhere in this class via [_onScreen]. Deliberately untyped: every
/// consumer discards the concrete `State<X>` type and pattern-matches on a
/// capability mixin (Refreshable, FocusableTab, …) instead.
final Map<NavigationTabId, GlobalKey> _screenKeys = {for (final id in NavigationTabId.values) id: GlobalKey()};
final GlobalKey<SideNavigationRailState> _sideNavKey = GlobalKey();
/// Measures the mobile bottom navigation area for the music mini-player.
@@ -470,23 +471,11 @@ class _MainScreenState extends State<MainScreen>
}
void tryDownloadResume() {
if (_downloadResumeFired || !mounted) return;
// Wait for any online client before firing the resume — the download
// pipeline is backend-neutral (resumeQueuedDownloads accepts a
// MediaServerClient and per-item resolution picks up the right
// backend), so a Jellyfin-only setup can resume too.
final onlineClient = manager.onlineClients.values.firstOrNull;
if (onlineClient == null) return;
_downloadResumeFired = true;
_serverStatusSub?.cancel();
_serverStatusSub = null;
final downloadProvider = context.read<DownloadProvider>();
unawaited(
downloadProvider.ensureInitialized().then((_) {
if (!mounted) return;
downloadProvider.resumeQueuedDownloads(onlineClient);
}),
);
_resumeQueuedDownloadsOnce(manager.onlineClients.values.firstOrNull);
}
// Listen for binding-settle so the once-only priming runs after both
@@ -524,36 +513,35 @@ class _MainScreenState extends State<MainScreen>
if (!mounted) return;
context.read<OfflineWatchSyncService>().onServersConnected();
unawaited(context.read<DownloadProvider>().refreshMetadataFromCache());
_resumeQueuedDownloadsIfPossible(mp);
_resumeQueuedDownloadsOnce(
mp.onlineServerIds.map((id) => mp.getClientForServer(ServerId(id))).nonNulls.firstOrNull,
);
}
}
if (!mounted) return;
if (_discoverKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
if (_librariesKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
if (_searchKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
_fullRefreshContentTabs();
}
void _resumeQueuedDownloadsIfPossible(MultiServerProvider mp) {
/// Single-shot "resume queued downloads once any client is online" rule,
/// shared by the startup status-stream path and [_primeOnlineServices] —
/// each caller resolves its own candidate client (unfiltered manager view
/// vs the visibility-filtered provider) and hands it here. No-op once the
/// resume has fired, or while no client is online yet.
void _resumeQueuedDownloadsOnce(MediaServerClient? onlineClient) {
if (_downloadResumeFired || !mounted) return;
for (final serverId in mp.onlineServerIds) {
final onlineClient = mp.getClientForServer(ServerId(serverId));
if (onlineClient == null) continue;
_downloadResumeFired = true;
unawaited(
context.read<DownloadProvider>().ensureInitialized().then((_) {
if (!mounted) return;
context.read<DownloadProvider>().resumeQueuedDownloads(onlineClient);
}),
);
return;
}
if (onlineClient == null) return;
_downloadResumeFired = true;
// The status subscription exists only to drive this one-shot.
_serverStatusSub?.cancel();
_serverStatusSub = null;
final downloadProvider = context.read<DownloadProvider>();
unawaited(
downloadProvider.ensureInitialized().then((_) {
if (!mounted) return;
downloadProvider.resumeQueuedDownloads(onlineClient);
}),
);
}
void _onActiveProfileChanged() {
@@ -623,11 +611,15 @@ class _MainScreenState extends State<MainScreen>
// has no profile to bind, and the user lands on an empty screen with
// no way back to the picker.
final hasNoActive = activeProfile.active == null && activeProfile.profiles.isNotEmpty;
final requireOnOpen =
settingsService.read(SettingsService.requireProfileSelectionOnOpen) && activeProfile.hasMultipleProfiles;
if (!hasNoActive && !requireOnOpen) return;
if (!hasNoActive && !activeProfile.requiresSelectionOnOpen(settingsService)) return;
await _pushProfileSelection();
}
/// Push the picker in "must choose" mode, suppressing the tvOS menu-button
/// passthrough for as long as it is up.
Future<void> _pushProfileSelection() async {
_isShowingProfileSelection = true;
_setTvosMenuPassthrough(false);
await Navigator.of(
@@ -867,9 +859,7 @@ class _MainScreenState extends State<MainScreen>
_selectTab(NavigationTabId.search, focusSearchInput: !hasQuery);
if (hasQuery) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_searchKey.currentState case final SearchInputFocusable searchable) {
searchable.submitSearchQuery(trimmed);
}
_onScreen<SearchInputFocusable>(NavigationTabId.search, (screen) => screen.submitSearchQuery(trimmed));
});
}
};
@@ -954,21 +944,11 @@ class _MainScreenState extends State<MainScreen>
Future<void> _showProfileSelectionOnResume() async {
final settingsService = await SettingsService.getInstance();
if (!settingsService.read(SettingsService.requireProfileSelectionOnOpen)) return;
if (!mounted) return;
final activeProfile = context.read<ActiveProfileProvider>();
if (!activeProfile.hasMultipleProfiles) return;
if (!context.read<ActiveProfileProvider>().requiresSelectionOnOpen(settingsService)) return;
_isShowingProfileSelection = true;
_setTvosMenuPassthrough(false);
await Navigator.of(
context,
rootNavigator: true,
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
if (!mounted) return;
_isShowingProfileSelection = false;
_updateTvosMenuPassthrough();
await _pushProfileSelection();
}
/// IndexedStack that disables tickers for offscreen children to prevent
@@ -994,17 +974,17 @@ class _MainScreenState extends State<MainScreen>
return [
for (final tab in _getVisibleTabs(offline))
switch (tab.id) {
NavigationTabId.discover => DiscoverScreen(key: _discoverKey),
NavigationTabId.explore => ExploreScreen(key: _exploreKey),
NavigationTabId.discover => DiscoverScreen(key: _screenKeys[tab.id]),
NavigationTabId.explore => ExploreScreen(key: _screenKeys[tab.id]),
NavigationTabId.libraries => LibrariesScreen(
key: _librariesKey,
key: _screenKeys[tab.id],
onLibraryOrderChanged: _onLibraryOrderChanged,
onLibrarySelected: _handleLibrariesScreenSelected,
),
NavigationTabId.liveTv => LiveTvScreen(key: _liveTvKey),
NavigationTabId.search => SearchScreen(key: _searchKey),
NavigationTabId.downloads => DownloadsScreen(key: _downloadsKey),
NavigationTabId.settings => SettingsScreen(key: _settingsKey),
NavigationTabId.liveTv => LiveTvScreen(key: _screenKeys[tab.id]),
NavigationTabId.search => SearchScreen(key: _screenKeys[tab.id]),
NavigationTabId.downloads => DownloadsScreen(key: _screenKeys[tab.id]),
NavigationTabId.settings => SettingsScreen(key: _screenKeys[tab.id]),
},
];
}
@@ -1059,16 +1039,22 @@ class _MainScreenState extends State<MainScreen>
}());
}
void _handleLiveTvChanged() {
final hasLiveTv = _multiServerProvider?.hasLiveTv ?? false;
if (hasLiveTv == _lastHasLiveTv) return;
_lastHasLiveTv = hasLiveTv;
/// Rebuilds navigation after a tab's availability flipped: _currentTab may
/// need normalizing, and passthrough depends on it being the first tab.
void _handleTabAvailabilityChanged() {
setState(() {
_screens = _buildScreens(_isOffline);
_currentTab = _normalizeTabForMode(_currentTab, _isOffline);
});
_updateTvosMenuPassthrough();
}
void _handleLiveTvChanged() {
final hasLiveTv = _multiServerProvider?.hasLiveTv ?? false;
if (hasLiveTv == _lastHasLiveTv) return;
_lastHasLiveTv = hasLiveTv;
_handleTabAvailabilityChanged();
// A preferred startup section (only Live TV can be deferred) just became
// available — switch to it via _selectTab so it gets the usual visibility
@@ -1084,13 +1070,7 @@ class _MainScreenState extends State<MainScreen>
if (hasExplore == _lastHasExplore) return;
_lastHasExplore = hasExplore;
setState(() {
_screens = _buildScreens(_isOffline);
_currentTab = _normalizeTabForMode(_currentTab, _isOffline);
});
// Same as the live-TV handler: the passthrough flag depends on whether
// _currentTab is the first tab, which the normalize above can change.
_updateTvosMenuPassthrough();
_handleTabAvailabilityChanged();
}
void _handleOfflineStatusChanged() {
@@ -1183,17 +1163,8 @@ class _MainScreenState extends State<MainScreen>
// This preserves the user's focus position when returning from sidebar.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (restorePreviousFocus) {
if (_contentFocusScope.focusedChild == null) {
if (_screenKeyFor(_currentTab)?.currentState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
}
}
} else {
if (_screenKeyFor(_currentTab)?.currentState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
}
}
if (restorePreviousFocus && _contentFocusScope.focusedChild != null) return;
_onScreen<FocusableTab>(_currentTab, (screen) => screen.focusActiveTabIfReady());
});
}
@@ -1396,9 +1367,7 @@ class _MainScreenState extends State<MainScreen>
if (_isSidebarFocused) _focusContent();
// Schedule focus after the frame so the search screen is visible in the IndexedStack
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_searchKey.currentState case final SearchInputFocusable searchable) {
searchable.focusSearchInput();
}
_onScreen<SearchInputFocusable>(NavigationTabId.search, (screen) => screen.focusSearchInput());
});
return KeyEventResult.handled;
}
@@ -1419,9 +1388,7 @@ class _MainScreenState extends State<MainScreen>
_miniPlayerInsets?.setNavBarSuspended(true);
// Called when a child route is pushed on top (e.g., video player)
if (_currentTab == NavigationTabId.discover) {
if (_discoverKey.currentState case final TabVisibilityAware aware) {
aware.onTabHidden();
}
_onScreen<TabVisibilityAware>(NavigationTabId.discover, (screen) => screen.onTabHidden());
}
}
@@ -1440,9 +1407,7 @@ class _MainScreenState extends State<MainScreen>
_updateTvosMenuPassthrough();
_miniPlayerInsets?.setNavBarSuspended(false);
if (_currentTab == NavigationTabId.discover) {
if (_discoverKey.currentState case final TabVisibilityAware aware) {
aware.onTabShown();
}
_onScreen<TabVisibilityAware>(NavigationTabId.discover, (screen) => screen.onTabShown());
_onDiscoverBecameVisible();
}
}
@@ -1450,9 +1415,7 @@ class _MainScreenState extends State<MainScreen>
void _onDiscoverBecameVisible() {
appLogger.d('Navigated to home');
// Refresh content when returning to discover page
if (_discoverKey.currentState case final Refreshable refreshable) {
refreshable.refresh();
}
_onScreen<Refreshable>(NavigationTabId.discover, (screen) => screen.refresh());
}
void _onLibraryOrderChanged() {
@@ -1497,15 +1460,7 @@ class _MainScreenState extends State<MainScreen>
playbackStateProvider.clearShuffle();
if (_discoverKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
if (_librariesKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
if (_searchKey.currentState case final FullRefreshable refreshable) {
refreshable.fullRefresh();
}
_fullRefreshContentTabs();
// Refresh user-level settings (audio/sub defaults) for the new identity.
if (mounted) {
@@ -1533,14 +1488,9 @@ class _MainScreenState extends State<MainScreen>
if (previousTab != tab) {
// Notify previous screen it's being hidden
if (_screenKeyFor(previousTab)?.currentState case final TabVisibilityAware aware) {
aware.onTabHidden();
}
_onScreen<TabVisibilityAware>(previousTab, (screen) => screen.onTabHidden());
// Notify and focus new screen
final newState = _screenKeyFor(tab)?.currentState;
if (newState case final TabVisibilityAware aware) {
aware.onTabShown();
}
_onScreen<TabVisibilityAware>(tab, (screen) => screen.onTabShown());
// Back-to-home keeps the sidebar focused (chain: content → sidebar →
// home → exit); stealing focus here left _isSidebarFocused stuck true
// while real focus sat on a content card (#1411).
@@ -1548,9 +1498,7 @@ class _MainScreenState extends State<MainScreen>
// search input, since focusing it auto-opens the on-screen keyboard; the
// query submit focuses results instead.
if (!_isSidebarFocused && (tab != NavigationTabId.search || focusSearchInput)) {
if (newState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
}
_onScreen<FocusableTab>(tab, (screen) => screen.focusActiveTabIfReady());
}
}
@@ -1564,9 +1512,7 @@ class _MainScreenState extends State<MainScreen>
// submit runs the search and focuses results without opening the keyboard.
if (tab == NavigationTabId.search && focusSearchInput) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_searchKey.currentState case final SearchInputFocusable searchable) {
searchable.focusSearchInput();
}
_onScreen<SearchInputFocusable>(NavigationTabId.search, (screen) => screen.focusSearchInput());
});
}
}
@@ -1576,12 +1522,8 @@ class _MainScreenState extends State<MainScreen>
_selectedLibraryGlobalKey = libraryGlobalKey;
_selectTab(NavigationTabId.libraries);
// Tell LibrariesScreen to load this library after tab switch
if (_librariesKey.currentState case final LibraryLoadable loadable) {
loadable.loadLibraryByKey(libraryGlobalKey);
}
if (_librariesKey.currentState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
}
_onScreen<LibraryLoadable>(NavigationTabId.libraries, (screen) => screen.loadLibraryByKey(libraryGlobalKey));
_onScreen<FocusableTab>(NavigationTabId.libraries, (screen) => screen.focusActiveTabIfReady());
}
void _openSettings() {
@@ -1672,17 +1614,20 @@ class _MainScreenState extends State<MainScreen>
);
}
/// Get the GlobalKey for a given tab.
GlobalKey? _screenKeyFor(NavigationTabId tab) {
return switch (tab) {
NavigationTabId.discover => _discoverKey,
NavigationTabId.explore => _exploreKey,
NavigationTabId.libraries => _librariesKey,
NavigationTabId.liveTv => _liveTvKey,
NavigationTabId.search => _searchKey,
NavigationTabId.downloads => _downloadsKey,
NavigationTabId.settings => _settingsKey,
};
/// Invoke [fn] on the tab's current [State] when it exists and implements
/// the capability [T]. Screens are only built for visible tabs and mount a
/// frame later, so a missing key or a non-matching state is a no-op.
void _onScreen<T>(NavigationTabId tab, void Function(T state) fn) {
if (_screenKeys[tab]?.currentState case final T state) fn(state);
}
/// Full-refresh the primary content tabs. Shared by the online-entry hook
/// ([_primeOnlineServices]) and the profile-switch invalidation
/// ([_invalidateAllScreens]), which refresh the same set.
void _fullRefreshContentTabs() {
for (final tab in const [NavigationTabId.discover, NavigationTabId.libraries, NavigationTabId.search]) {
_onScreen<FullRefreshable>(tab, (screen) => screen.fullRefresh());
}
}
Widget _buildBottomNavigationBar(BuildContext context, {required bool hideLabels}) {