fix(libraries): keep a sidebar library selection that mounts the Libraries tab
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
This commit is contained in:
@@ -136,10 +136,22 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
|
||||
/// 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<void> _initializeWithLibraries() async {
|
||||
if (_selectedLibraryGlobalKey != null) return;
|
||||
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
|
||||
await hiddenLibrariesProvider.ensureInitialized();
|
||||
if (!mounted || _selectedLibraryGlobalKey != null) return;
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
|
||||
if (allLibraries.isEmpty) {
|
||||
@@ -151,6 +163,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
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<LibrariesScreen>
|
||||
libraryGlobalKeyToLoad = visibleLibraries.first.globalKey;
|
||||
}
|
||||
|
||||
if (libraryGlobalKeyToLoad != null && mounted) {
|
||||
if (libraryGlobalKeyToLoad != null) {
|
||||
unawaited(_loadLibraryContent(libraryGlobalKeyToLoad));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = <String>[];
|
||||
|
||||
// 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<BaseLibraryTab>()
|
||||
.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<void> pump(WidgetTester tester, {ValueChanged<String>? onLibrarySelected, bool settle = true}) async {
|
||||
Future<void> pump(
|
||||
WidgetTester tester, {
|
||||
ValueChanged<String>? 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: [
|
||||
|
||||
Reference in New Issue
Block a user