Files
plezy/lib/screens/libraries/adaptive_media_grid.dart
T

162 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import '../../services/settings_service.dart';
import '../../widgets/settings_builder.dart';
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 SettingsBuilder(
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity],
builder: (context) {
final svc = SettingsService.instanceOrNull!;
return _buildItemsView(context, svc.read(SettingsService.viewMode), svc.read(SettingsService.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;
return CustomScrollView(
// Allow focus decoration to render outside scroll bounds
clipBehavior: Clip.none,
slivers: [
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
if (viewMode == ViewMode.list)
SliverPadding(
padding: effectivePadding,
sliver: SliverList.builder(
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
_buildGridSliver(context, density, effectivePadding, effectiveAspectRatio),
],
);
}
Widget _buildGridSliver(BuildContext context, int density, EdgeInsets effectivePadding, double effectiveAspectRatio) {
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
return SliverPadding(
padding: effectivePadding,
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
// crossAxisExtent is the post-padding inner width.
final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxCrossAxisExtent);
return SliverGrid.builder(
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);
},
);
},
),
);
}
}