From 088501513a859725917dfaa42e5799dac6e02225 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:44:52 +0200 Subject: [PATCH] fix(catalog): keep the watchlist action focusable while membership loads Keep the action enabled and let a press retry the snapshot, as the media detail action bar already does. A disabled sole action left the detail screen with no initial D-pad focus on TV. --- lib/screens/catalog_item_detail_screen.dart | 13 +++++++++---- test/screens/catalog_item_detail_screen_test.dart | 15 ++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/screens/catalog_item_detail_screen.dart b/lib/screens/catalog_item_detail_screen.dart index a7923138..deca4f40 100644 --- a/lib/screens/catalog_item_detail_screen.dart +++ b/lib/screens/catalog_item_detail_screen.dart @@ -289,8 +289,15 @@ class _CatalogItemDetailScreenState extends State { Future _toggleWatchlist() async { final source = _watchlistSource; + if (source == null || _mutatingWatchlist) return; final current = _isOnWatchlist; - if (source == null || current == null || _mutatingWatchlist) return; + // Parity with lib/screens/media_detail/action_buttons.dart: the action + // stays focusable while membership is unknown, and a press kicks the + // snapshot load rather than toggling a state we haven't read yet. + if (current == null) { + unawaited(source.ensureWatchlistLoaded()); + return; + } _mutatingWatchlist = true; try { if (current) { @@ -564,9 +571,7 @@ class _CatalogItemDetailScreenState extends State { tooltip: onWatchlist ?? false ? t.explore.removeFromWatchlist : t.explore.addToWatchlist, - onPressed: onWatchlist == null - ? null - : () => unawaited(_toggleWatchlist()), + onPressed: () => unawaited(_toggleWatchlist()), ), if (_requestSource case final SeerrCatalogSource seerr) FocusableAction( diff --git a/test/screens/catalog_item_detail_screen_test.dart b/test/screens/catalog_item_detail_screen_test.dart index 04678b1e..01e89146 100644 --- a/test/screens/catalog_item_detail_screen_test.dart +++ b/test/screens/catalog_item_detail_screen_test.dart @@ -243,31 +243,24 @@ void main() { expect(FocusManager.instance.primaryFocus?.debugLabel, 'catalog_library_match_1'); }); - testWidgets('loading watchlist action cannot receive focus or activate', (tester) async { + testWidgets('pending watchlist action keeps initial focus and its press retries the snapshot', (tester) async { final source = _FakeCatalogSource(watchlistLoading: true); await _pumpDetail(tester, source); - final actionBar = tester.widget(find.byType(FocusableActionBar)); - expect(actionBar.actions.single.onPressed, isNull); + expect(FocusManager.instance.primaryFocus?.debugLabel, 'ActionBar[0]'); final actionNode = tester .widgetList(find.descendant(of: find.byType(FocusableActionBar), matching: find.byType(Focus))) .map((widget) => widget.focusNode) .whereType() .singleWhere((node) => node.debugLabel == 'ActionBar[0]'); - expect(actionNode.canRequestFocus, isFalse); + expect(actionNode.canRequestFocus, isTrue); - actionNode.requestFocus(); - await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.select); - expect(actionNode.hasFocus, isFalse); + await tester.pump(); expect(source.addToWatchlistCalls, 0); source.completeWatchlistLoad(); await tester.pump(); - final loadedActionBar = tester.widget(find.byType(FocusableActionBar)); - expect(loadedActionBar.actions.single.onPressed, isNotNull); - actionNode.requestFocus(); - await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.select); await tester.pump(); expect(source.addToWatchlistCalls, 1);