From 608046b4002b94254f94611861a2c3e4486a3975 Mon Sep 17 00:00:00 2001 From: mattsigal Date: Thu, 6 Aug 2026 23:34:48 -0700 Subject: [PATCH 1/2] Fix mobile crash on library browse initial load and scroll - Check for active items before passing getGridItemFocusNode(0) as topFocusNode in QuickReturnWrapper. - Add canRequestFocus check on topFocusNode in QuickReturnWrapper before requesting focus. - Add index bounds checking inside SliverChildBuilderDelegate in vertical and horizontal grid builders. - Add _scrollController.hasClients checks in _nearGridEnd and horizontal grid pointer scroll listeners. --- lib/ui/screens/browse/library_browse_screen.dart | 7 +++++-- lib/ui/widgets/quick_return_wrapper.dart | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/ui/screens/browse/library_browse_screen.dart b/lib/ui/screens/browse/library_browse_screen.dart index cf94d370a..4dbd0168f 100644 --- a/lib/ui/screens/browse/library_browse_screen.dart +++ b/lib/ui/screens/browse/library_browse_screen.dart @@ -208,6 +208,7 @@ class _LibraryBrowseScreenState extends State /// Whether the scroll view has settled metrics and is within /// [_kLoadMoreExtent] of its end. bool get _nearGridEnd { + if (!_scrollController.hasClients) return false; // Two grids briefly share the controller while one is swapped out. if (_scrollController.positions.length != 1) return false; final pos = _scrollController.position; @@ -729,7 +730,7 @@ class _LibraryBrowseScreenState extends State _vm.scrollDirection == LibraryScrollDirection.horizontal ? Axis.horizontal : Axis.vertical, - topFocusNode: getGridItemFocusNode(0), + topFocusNode: _vm.items.isNotEmpty ? getGridItemFocusNode(0) : null, child: _buildContent(context), ), ); @@ -1198,6 +1199,7 @@ class _LibraryBrowseScreenState extends State childAspectRatio: childAspectRatio, ), delegate: SliverChildBuilderDelegate((context, index) { + if (index < 0 || index >= itemsToDisplay.length) return null; final item = itemsToDisplay[index]; final itemAspectRatio = _itemAspectRatio(item); return _buildGridCard( @@ -1492,7 +1494,7 @@ class _LibraryBrowseScreenState extends State return Listener( onPointerSignal: (signal) { - if (signal is PointerScrollEvent) { + if (signal is PointerScrollEvent && _scrollController.hasClients) { final pos = _scrollController.position; final newOffset = (_scrollController.offset + signal.scrollDelta.dy) @@ -1518,6 +1520,7 @@ class _LibraryBrowseScreenState extends State ), delegate: SliverChildBuilderDelegate( (context, index) { + if (index < 0 || index >= _vm.items.length) return null; final item = _vm.items[index]; return MediaCard( title: item.name, diff --git a/lib/ui/widgets/quick_return_wrapper.dart b/lib/ui/widgets/quick_return_wrapper.dart index 578f3158d..440e1a0e5 100644 --- a/lib/ui/widgets/quick_return_wrapper.dart +++ b/lib/ui/widgets/quick_return_wrapper.dart @@ -167,7 +167,10 @@ class _QuickReturnWrapperState extends State curve: Curves.easeOutCubic, ); } - widget.topFocusNode?.requestFocus(); + final topNode = widget.topFocusNode; + if (topNode != null && topNode.canRequestFocus) { + topNode.requestFocus(); + } widget.onReturn?.call(); } From f844aaa8c7a2c6b25588e98514471474d095b11a Mon Sep 17 00:00:00 2001 From: RadicalMuffinMan <103554043+RadicalMuffinMan@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:35:48 -0400 Subject: [PATCH 2/2] Drop the guards that cant fire and extend the one for favorites --- lib/ui/screens/browse/favorites_screen.dart | 16 ++++++++++++---- lib/ui/screens/browse/library_browse_screen.dart | 7 +++++-- lib/ui/widgets/quick_return_wrapper.dart | 5 +---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/ui/screens/browse/favorites_screen.dart b/lib/ui/screens/browse/favorites_screen.dart index e70679afe..92f8a8334 100644 --- a/lib/ui/screens/browse/favorites_screen.dart +++ b/lib/ui/screens/browse/favorites_screen.dart @@ -104,10 +104,14 @@ class _FavoritesScreenState extends State with GridFocusNodeMix return _vm.rowItems[types[_selectedTab]] ?? const []; } + /// The list the grid focus nodes are keyed on. + List get _focusTrackedItems => + _vm.viewStyle == FavoritesViewStyle.library + ? _vm.gridItems + : _currentTabItems(); + void _maybeBumpGridVersion() { - final current = _vm.viewStyle == FavoritesViewStyle.library - ? _vm.gridItems - : _currentTabItems(); + final current = _focusTrackedItems; final length = current.length; final firstId = length == 0 ? null : current.first.id; if (length != _lastGridItemsLength || firstId != _lastGridFirstItemId) { @@ -353,7 +357,11 @@ class _FavoritesScreenState extends State with GridFocusNodeMix _vm.viewStyle == FavoritesViewStyle.home ? _tabsFocusNode : null, child: QuickReturnWrapper( scrollController: _scrollController, - topFocusNode: getGridItemFocusNode(0), + // cleanupGridFocusNodes disposes node 0 once the list empties, and the + // wrapper would keep holding it, so pass nothing instead. + topFocusNode: _focusTrackedItems.isNotEmpty + ? getGridItemFocusNode(0) + : null, child: _buildContent(context), ), ); diff --git a/lib/ui/screens/browse/library_browse_screen.dart b/lib/ui/screens/browse/library_browse_screen.dart index 4dbd0168f..d3b298fbc 100644 --- a/lib/ui/screens/browse/library_browse_screen.dart +++ b/lib/ui/screens/browse/library_browse_screen.dart @@ -730,6 +730,8 @@ class _LibraryBrowseScreenState extends State _vm.scrollDirection == LibraryScrollDirection.horizontal ? Axis.horizontal : Axis.vertical, + // cleanupGridFocusNodes disposes node 0 once the list empties, and + // the wrapper would keep holding it, so pass nothing instead. topFocusNode: _vm.items.isNotEmpty ? getGridItemFocusNode(0) : null, child: _buildContent(context), ), @@ -1199,7 +1201,6 @@ class _LibraryBrowseScreenState extends State childAspectRatio: childAspectRatio, ), delegate: SliverChildBuilderDelegate((context, index) { - if (index < 0 || index >= itemsToDisplay.length) return null; final item = itemsToDisplay[index]; final itemAspectRatio = _itemAspectRatio(item); return _buildGridCard( @@ -1520,7 +1521,9 @@ class _LibraryBrowseScreenState extends State ), delegate: SliverChildBuilderDelegate( (context, index) { - if (index < 0 || index >= _vm.items.length) return null; + // No childCount on this delegate, so returning null is + // what tells the sliver where the list ends. + if (index >= _vm.items.length) return null; final item = _vm.items[index]; return MediaCard( title: item.name, diff --git a/lib/ui/widgets/quick_return_wrapper.dart b/lib/ui/widgets/quick_return_wrapper.dart index 440e1a0e5..578f3158d 100644 --- a/lib/ui/widgets/quick_return_wrapper.dart +++ b/lib/ui/widgets/quick_return_wrapper.dart @@ -167,10 +167,7 @@ class _QuickReturnWrapperState extends State curve: Curves.easeOutCubic, ); } - final topNode = widget.topFocusNode; - if (topNode != null && topNode.canRequestFocus) { - topNode.requestFocus(); - } + widget.topFocusNode?.requestFocus(); widget.onReturn?.call(); }