fix: use layout constraints for grid column count
This commit is contained in:
@@ -159,27 +159,35 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen<CollectionD
|
||||
Widget _buildFocusableGrid() {
|
||||
return Consumer<SettingsProvider>(
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<DownloadProvider, SettingsProvider>(
|
||||
@@ -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,
|
||||
|
||||
@@ -80,13 +80,6 @@ class AdaptiveMediaGrid<T> 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<T> 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,
|
||||
|
||||
@@ -723,24 +723,29 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
|
||||
);
|
||||
} else {
|
||||
// In grid view, calculate columns and pass to item builder
|
||||
final columnCount = GridSizeCalculator.getColumnCount(context, settingsProvider.libraryDensity);
|
||||
// Use 16:9 aspect ratio when browsing episodes with episode thumbnail mode
|
||||
final useWideRatio =
|
||||
_selectedGrouping == 'episodes' && settingsProvider.episodePosterMode == EpisodePosterMode.episodeThumbnail;
|
||||
final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(8, _gridTopPadding, 8, 8),
|
||||
sliver: 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),
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -784,29 +784,37 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen<PlaylistDetai
|
||||
Widget _buildSmartPlaylistGrid(bool isKeyboardMode) {
|
||||
return Consumer<SettingsProvider>(
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user