diff --git a/lib/screens/collection_detail_screen.dart b/lib/screens/collection_detail_screen.dart index f60df567..e3110cf0 100644 --- a/lib/screens/collection_detail_screen.dart +++ b/lib/screens/collection_detail_screen.dart @@ -159,27 +159,35 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen( builder: (context, settingsProvider, child) { - final columnCount = GridSizeCalculator.getColumnCount(context, settingsProvider.libraryDensity); + final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); return SliverPadding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - sliver: SliverGrid.builder( - gridDelegate: MediaGridDelegate.createDelegate(context: context, density: settingsProvider.libraryDensity), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - final inFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); - final focusNode = index == 0 ? firstItemFocusNode : getGridItemFocusNode(index); + sliver: SliverLayoutBuilder( + builder: (context, constraints) { + final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); + return SliverGrid.builder( + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settingsProvider.libraryDensity, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + final inFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); + final focusNode = index == 0 ? firstItemFocusNode : getGridItemFocusNode(index); - return FocusableMediaCard( - key: Key(item.ratingKey), - item: item, - focusNode: focusNode, - onRefresh: updateItem, - collectionId: widget.collection.ratingKey, - onListRefresh: loadItems, - onNavigateUp: inFirstRow ? navigateToAppBar : null, - onBack: handleBackFromContent, - onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus), + return FocusableMediaCard( + key: Key(item.ratingKey), + item: item, + focusNode: focusNode, + onRefresh: updateItem, + collectionId: widget.collection.ratingKey, + onListRefresh: loadItems, + onNavigateUp: inFirstRow ? navigateToAppBar : null, + onBack: handleBackFromContent, + onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus), + ); + }, ); }, ), diff --git a/lib/screens/downloads/downloads_screen.dart b/lib/screens/downloads/downloads_screen.dart index 80fd217d..b66e0fd6 100644 --- a/lib/screens/downloads/downloads_screen.dart +++ b/lib/screens/downloads/downloads_screen.dart @@ -7,7 +7,6 @@ import '../../providers/multi_server_provider.dart'; import '../../providers/settings_provider.dart'; import '../../services/gamepad_service.dart'; import '../../utils/grid_size_calculator.dart'; -import '../../utils/layout_constants.dart'; import '../../utils/platform_detector.dart'; import '../../widgets/desktop_app_bar.dart'; import '../../widgets/focusable_tab_chip.dart'; @@ -328,11 +327,6 @@ class _DownloadsGridContentState extends State<_DownloadsGridContent> { MainScreenFocusScope.of(context)?.focusSidebar(); } - /// Calculate column count based on actual available width. - int _calculateColumnCount(double availableWidth, double maxCrossAxisExtent, double crossAxisSpacing) { - return ((availableWidth + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100); - } - @override Widget build(BuildContext context) { return Consumer2( @@ -348,13 +342,12 @@ class _DownloadsGridContentState extends State<_DownloadsGridContent> { // Extra top padding for focus decoration (scale + border extends beyond item bounds) const effectivePadding = EdgeInsets.only(left: 8, right: 8, top: 8); final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); - const crossAxisSpacing = GridLayoutConstants.crossAxisSpacing; // Use LayoutBuilder to get actual available width (accounting for sidebar) return LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth - effectivePadding.left - effectivePadding.right; - final columnCount = _calculateColumnCount(availableWidth, maxCrossAxisExtent, crossAxisSpacing); + final columnCount = GridSizeCalculator.getColumnCount(availableWidth, maxCrossAxisExtent); return GridView.builder( padding: effectivePadding, diff --git a/lib/screens/libraries/adaptive_media_grid.dart b/lib/screens/libraries/adaptive_media_grid.dart index 2678c6a1..8cad8618 100644 --- a/lib/screens/libraries/adaptive_media_grid.dart +++ b/lib/screens/libraries/adaptive_media_grid.dart @@ -80,13 +80,6 @@ class AdaptiveMediaGrid extends StatelessWidget { MainScreenFocusScope.of(context)?.focusSidebar(); } - /// Calculate column count based on actual available width. - /// Uses the same formula as Flutter's SliverGridDelegateWithMaxCrossAxisExtent. - int _calculateColumnCount(double availableWidth, double maxCrossAxisExtent) { - final crossAxisSpacing = GridLayoutConstants.crossAxisSpacing; - return ((availableWidth + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100); - } - /// Builds either a list or grid view based on the view mode Widget _buildItemsView(BuildContext context, ViewMode viewMode, LibraryDensity density) { final basePadding = padding ?? GridLayoutConstants.gridPadding; @@ -120,7 +113,7 @@ class AdaptiveMediaGrid extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth - horizontalPadding; - final columnCount = _calculateColumnCount(availableWidth, maxCrossAxisExtent); + final columnCount = GridSizeCalculator.getColumnCount(availableWidth, maxCrossAxisExtent); return GridView.builder( padding: effectivePadding, diff --git a/lib/screens/libraries/tabs/library_browse_tab.dart b/lib/screens/libraries/tabs/library_browse_tab.dart index 2bf18e40..4cd1480c 100644 --- a/lib/screens/libraries/tabs/library_browse_tab.dart +++ b/lib/screens/libraries/tabs/library_browse_tab.dart @@ -723,24 +723,29 @@ class _LibraryBrowseTabState extends BaseLibraryTabState _buildMediaCardItem( - index, - isFirstRow: GridSizeCalculator.isFirstRow(index, columnCount), - isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount), - ), + sliver: SliverLayoutBuilder( + builder: (context, constraints) { + final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); + return SliverGrid.builder( + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settingsProvider.libraryDensity, + useWideAspectRatio: useWideRatio, + ), + itemCount: itemCount, + itemBuilder: (context, index) => _buildMediaCardItem( + index, + isFirstRow: GridSizeCalculator.isFirstRow(index, columnCount), + isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount), + ), + ); + }, ), ); } diff --git a/lib/screens/playlist/playlist_detail_screen.dart b/lib/screens/playlist/playlist_detail_screen.dart index 4b0ad271..a5323eb6 100644 --- a/lib/screens/playlist/playlist_detail_screen.dart +++ b/lib/screens/playlist/playlist_detail_screen.dart @@ -784,29 +784,37 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen( builder: (context, settingsProvider, child) { - final columnCount = GridSizeCalculator.getColumnCount(context, settingsProvider.libraryDensity); + final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); return SliverPadding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - sliver: SliverGrid.builder( - gridDelegate: MediaGridDelegate.createDelegate(context: context, density: settingsProvider.libraryDensity), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - final isFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); - final focusNode = index == 0 ? _firstItemFocusNode : _getGridItemFocusNode(index); + sliver: SliverLayoutBuilder( + builder: (context, constraints) { + final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); + return SliverGrid.builder( + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settingsProvider.libraryDensity, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + final isFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); + final focusNode = index == 0 ? _firstItemFocusNode : _getGridItemFocusNode(index); - return FocusableMediaCard( - key: Key(item.ratingKey), - item: item, - focusNode: focusNode, - onRefresh: updateItem, - onNavigateUp: isFirstRow ? _navigateToAppBar : null, - onBack: _handleBackFromContent, - onFocusChange: (hasFocus) { - if (hasFocus) { - _lastFocusedGridIndex = index; - _lastFocusedContentVersion = _contentVersion; - } + return FocusableMediaCard( + key: Key(item.ratingKey), + item: item, + focusNode: focusNode, + onRefresh: updateItem, + onNavigateUp: isFirstRow ? _navigateToAppBar : null, + onBack: _handleBackFromContent, + onFocusChange: (hasFocus) { + if (hasFocus) { + _lastFocusedGridIndex = index; + _lastFocusedContentVersion = _contentVersion; + } + }, + ); }, ); }, diff --git a/lib/utils/grid_size_calculator.dart b/lib/utils/grid_size_calculator.dart index b91818a0..eaf975fc 100644 --- a/lib/utils/grid_size_calculator.dart +++ b/lib/utils/grid_size_calculator.dart @@ -122,18 +122,17 @@ class GridSizeCalculator { return MediaQuery.of(context).size.width <= tabletBreakpoint; } - /// Calculates the number of columns in a grid based on screen width and density. + /// Calculates the number of columns for a given available width. /// /// Uses the same formula as Flutter's SliverGridDelegateWithMaxCrossAxisExtent: /// `((crossAxisExtent + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil()` /// - /// [horizontalPadding] should match the grid's total horizontal padding. - static int getColumnCount(BuildContext context, LibraryDensity density, {double horizontalPadding = 16}) { - final screenWidth = MediaQuery.of(context).size.width - horizontalPadding; - final maxCrossAxisExtent = getMaxCrossAxisExtent(context, density); + /// [crossAxisExtent] should come from layout constraints (e.g. `SliverLayoutBuilder` + /// or `LayoutBuilder`), not from `MediaQuery`, to account for sidebars or other + /// elements that reduce the grid's actual width. + static int getColumnCount(double crossAxisExtent, double maxCrossAxisExtent) { final crossAxisSpacing = GridLayoutConstants.crossAxisSpacing; - // Match Flutter's grid delegate calculation - return ((screenWidth + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100); + return ((crossAxisExtent + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100); } /// Check if the given index is in the first row of a grid with given column count.