From 582d419618555424bf42844fb7bf92b786e10ecc Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:11:09 +0200 Subject: [PATCH] fix(libraries): keep a sidebar library selection that mounts the Libraries tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Choosing a hidden library from the sidebar on a fresh launch opened the topmost visible library instead, and reordering the sidebar changed which wrong library appeared. A second attempt worked. The first visit to the Libraries tab mounts LibrariesScreen, and two post-frame callbacks then run in registration order: MainScreen's, which applies the requested library, and the one initState registers during that frame's build, which picks the saved or topmost-visible default. The second one overwrote the first — and a hidden library can be neither default, so the selection was always lost. Initialization now treats an existing selection as authoritative, both on entry and after each of its awaits, so a selection arriving while it is suspended (the refresh() stale-resume path) also survives. close #2207 --- lib/screens/libraries/libraries_screen.dart | 15 ++++++- .../libraries/libraries_screen_test.dart | 44 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/screens/libraries/libraries_screen.dart b/lib/screens/libraries/libraries_screen.dart index 9d38eccff..9892d7f12 100644 --- a/lib/screens/libraries/libraries_screen.dart +++ b/lib/screens/libraries/libraries_screen.dart @@ -136,10 +136,22 @@ class _LibrariesScreenState extends State /// Initialize the screen with libraries from the provider. /// This handles initial library selection and content loading. + /// + /// An existing selection is authoritative. A sidebar library selection that + /// mounts this screen (`MainScreen._selectLibrary`) applies its target from a + /// post-frame callback registered *before* the one in [initState], so this + /// runs second and must not replace the requested library — which may be + /// hidden, and so is never the saved or topmost-visible default — with a + /// default. The re-checks cover the reverse interleaving, where a selection + /// lands while this is suspended on an await ([refresh]'s stale-resume + /// entry point). Future _initializeWithLibraries() async { + if (_selectedLibraryGlobalKey != null) return; + final librariesProvider = context.read(); final hiddenLibrariesProvider = context.read(); await hiddenLibrariesProvider.ensureInitialized(); + if (!mounted || _selectedLibraryGlobalKey != null) return; final allLibraries = librariesProvider.libraries; if (allLibraries.isEmpty) { @@ -151,6 +163,7 @@ class _LibrariesScreenState extends State final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList(); final storage = await StorageService.getInstance(); + if (!mounted || _selectedLibraryGlobalKey != null) return; final savedLibraryKey = storage.getSelectedLibraryKey(); String? libraryGlobalKeyToLoad; @@ -165,7 +178,7 @@ class _LibrariesScreenState extends State libraryGlobalKeyToLoad = visibleLibraries.first.globalKey; } - if (libraryGlobalKeyToLoad != null && mounted) { + if (libraryGlobalKeyToLoad != null) { unawaited(_loadLibraryContent(libraryGlobalKeyToLoad)); } } diff --git a/test/screens/libraries/libraries_screen_test.dart b/test/screens/libraries/libraries_screen_test.dart index 1f1c4c07e..fd539171f 100644 --- a/test/screens/libraries/libraries_screen_test.dart +++ b/test/screens/libraries/libraries_screen_test.dart @@ -18,6 +18,7 @@ import 'package:plezy/providers/hidden_libraries_provider.dart'; import 'package:plezy/providers/libraries_provider.dart'; import 'package:plezy/providers/multi_server_provider.dart'; import 'package:plezy/screens/libraries/libraries_screen.dart'; +import 'package:plezy/screens/libraries/tabs/base_library_tab.dart'; import 'package:plezy/services/multi_server_manager.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/storage_service.dart'; @@ -68,6 +69,36 @@ void main() { tearDown(() => TvDetectionService.debugSetAppleTVOverride(null)); + testWidgets('a sidebar selection at first mount survives initialization', (tester) async { + final preferences = _GatedPreferences({'selected_library_key': _libraryA.globalKey}); + final harness = await _Harness.create(preferences); + addTearDown(harness.dispose); + // The requested library is hidden, so it can be neither the saved key nor + // the topmost visible default — exactly the reported repro. + await harness.hiddenLibraries.hideLibrary(_libraryB.globalKey); + final selected = []; + + // MainScreen._selectLibrary registers its loadLibraryByKey callback before + // the frame that mounts this screen, so it runs ahead of the post-frame + // callback initState registers during that very frame's build. + await harness.pump( + tester, + onLibrarySelected: selected.add, + onFirstPostFrame: () => + (tester.state(find.byType(LibrariesScreen)) as LibraryLoadable).loadLibraryByKey(_libraryB.globalKey), + ); + + // Initialization must not follow up with the default library. + expect(selected, [_libraryB.globalKey]); + // ...and the content on screen is the requested library's. + final mountedTabs = tester + .widgetList(find.byWidgetPredicate((widget) => widget is BaseLibraryTab)) + .cast() + .toList(); + expect(mountedTabs, isNotEmpty); + expect(mountedTabs.map((tab) => tab.library.globalKey).toSet(), {_libraryB.globalKey}); + }); + testWidgets('stale saved tab cannot replace the current library tab', (tester) async { final preferences = _GatedPreferences({ 'selected_library_key': _libraryB.globalKey, @@ -193,12 +224,23 @@ final class _Harness { return _Harness(libraries: libraries, hiddenLibraries: hiddenLibraries, multiServer: multiServer); } - Future pump(WidgetTester tester, {ValueChanged? onLibrarySelected, bool settle = true}) async { + Future pump( + WidgetTester tester, { + ValueChanged? onLibrarySelected, + bool settle = true, + VoidCallback? onFirstPostFrame, + }) async { tester.view.physicalSize = const Size(1280, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); + // Registered before the mounting frame, so it runs ahead of the callback + // LibrariesScreen.initState adds during that frame's build. + if (onFirstPostFrame != null) { + WidgetsBinding.instance.addPostFrameCallback((_) => onFirstPostFrame()); + } + await tester.pumpWidget( MultiProvider( providers: [