154 lines
5.7 KiB
Dart
154 lines
5.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../../providers/settings_provider.dart';
|
|
import '../../services/settings_service.dart' show ViewMode;
|
|
import '../../utils/grid_size_calculator.dart';
|
|
import '../../utils/layout_constants.dart';
|
|
import '../main_screen.dart';
|
|
|
|
/// Context passed to the item builder with navigation information.
|
|
class GridItemContext {
|
|
/// Whether this item is in the first row of the grid.
|
|
final bool isFirstRow;
|
|
|
|
/// Whether this item is in the first column of the grid.
|
|
final bool isFirstColumn;
|
|
|
|
/// Whether items are displayed in list mode (single column).
|
|
final bool isListMode;
|
|
|
|
/// Callback to navigate to the sidebar (for first-column items).
|
|
final VoidCallback? navigateToSidebar;
|
|
|
|
const GridItemContext({
|
|
required this.isFirstRow,
|
|
required this.isFirstColumn,
|
|
this.isListMode = false,
|
|
this.navigateToSidebar,
|
|
});
|
|
}
|
|
|
|
/// A widget that automatically switches between grid and list view
|
|
/// based on user settings, providing a consistent layout pattern
|
|
/// across all library screens.
|
|
///
|
|
/// Generic type T: The type of items being displayed
|
|
class AdaptiveMediaGrid<T> extends StatelessWidget {
|
|
/// The list of items to display
|
|
final List<T> items;
|
|
|
|
/// Builder function for each item in the grid/list.
|
|
/// Receives the item, index, and optional grid context with navigation info.
|
|
final Widget Function(BuildContext context, T item, int index, [GridItemContext? gridContext]) itemBuilder;
|
|
|
|
/// Callback when the list needs to be refreshed
|
|
final VoidCallback? onRefresh;
|
|
|
|
/// Optional padding around the grid/list
|
|
final EdgeInsets? padding;
|
|
|
|
/// Child aspect ratio for grid items (width / height)
|
|
final double? childAspectRatio;
|
|
|
|
/// Optional focus node for the first item (for programmatic focus)
|
|
final FocusNode? firstItemFocusNode;
|
|
|
|
/// Callback when back button is pressed (for hierarchical navigation)
|
|
final VoidCallback? onBack;
|
|
|
|
/// Whether to enable sidebar navigation for first-column items.
|
|
final bool enableSidebarNavigation;
|
|
|
|
const AdaptiveMediaGrid({
|
|
super.key,
|
|
required this.items,
|
|
required this.itemBuilder,
|
|
this.onRefresh,
|
|
this.padding,
|
|
this.childAspectRatio,
|
|
this.firstItemFocusNode,
|
|
this.onBack,
|
|
this.enableSidebarNavigation = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<SettingsProvider>(
|
|
builder: (context, settingsProvider, child) {
|
|
return _buildItemsView(context, settingsProvider.viewMode, settingsProvider.libraryDensity);
|
|
},
|
|
);
|
|
}
|
|
|
|
// Extra top padding for focus decoration (scale + border extends beyond item bounds)
|
|
static const double _focusDecorationPadding = 3.0;
|
|
|
|
/// Navigate focus to the sidebar
|
|
void _navigateToSidebar(BuildContext context) {
|
|
MainScreenFocusScope.of(context)?.focusSidebar();
|
|
}
|
|
|
|
/// Builds either a list or grid view based on the view mode
|
|
Widget _buildItemsView(BuildContext context, ViewMode viewMode, int density) {
|
|
final basePadding = padding ?? GridLayoutConstants.gridPadding;
|
|
// Add extra top padding for focus decoration of first row items
|
|
final effectivePadding = basePadding.copyWith(top: basePadding.top + _focusDecorationPadding);
|
|
final effectiveAspectRatio = childAspectRatio ?? GridLayoutConstants.posterAspectRatio;
|
|
|
|
if (viewMode == ViewMode.list) {
|
|
// In list view, all items are in a single column (first column)
|
|
return ListView.builder(
|
|
padding: effectivePadding,
|
|
// Allow focus decoration to render outside scroll bounds
|
|
clipBehavior: Clip.none,
|
|
itemCount: items.length,
|
|
itemBuilder: (ctx, index) {
|
|
final gridContext = enableSidebarNavigation
|
|
? GridItemContext(
|
|
isFirstRow: index == 0,
|
|
isFirstColumn: true, // List view = single column
|
|
isListMode: true,
|
|
navigateToSidebar: () => _navigateToSidebar(context),
|
|
)
|
|
: null;
|
|
return itemBuilder(ctx, items[index], index, gridContext);
|
|
},
|
|
);
|
|
} else {
|
|
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
|
final horizontalPadding = effectivePadding.left + effectivePadding.right;
|
|
|
|
// Use LayoutBuilder to get the actual available width (accounting for sidebar, etc.)
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final availableWidth = constraints.maxWidth - horizontalPadding;
|
|
final columnCount = GridSizeCalculator.getColumnCount(availableWidth, maxCrossAxisExtent);
|
|
|
|
return GridView.builder(
|
|
padding: effectivePadding,
|
|
// Allow focus decoration to render outside scroll bounds
|
|
clipBehavior: Clip.none,
|
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
|
childAspectRatio: effectiveAspectRatio,
|
|
crossAxisSpacing: GridLayoutConstants.crossAxisSpacing,
|
|
mainAxisSpacing: GridLayoutConstants.mainAxisSpacing,
|
|
),
|
|
itemCount: items.length,
|
|
itemBuilder: (ctx, index) {
|
|
final gridContext = enableSidebarNavigation
|
|
? GridItemContext(
|
|
isFirstRow: GridSizeCalculator.isFirstRow(index, columnCount),
|
|
isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount),
|
|
navigateToSidebar: () => _navigateToSidebar(context),
|
|
)
|
|
: null;
|
|
return itemBuilder(ctx, items[index], index, gridContext);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|