From c9db6e528e974ab7c177915f4472badb7a4cbb0d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:03:52 +0200 Subject: [PATCH] feat: more consistent poster sizing close #798 --- lib/providers/settings_provider.dart | 19 +--- .../libraries/adaptive_media_grid.dart | 4 +- lib/screens/livetv/tabs/whats_on_tab.dart | 24 +---- lib/screens/media_detail_screen.dart | 26 ++--- .../settings/appearance_settings_screen.dart | 37 ++++--- lib/screens/settings/settings_screen.dart | 2 +- lib/services/settings_service.dart | 34 +++++-- lib/theme/mono_theme.dart | 8 ++ lib/utils/grid_size_calculator.dart | 99 ++++++------------- lib/utils/layout_constants.dart | 20 ---- lib/widgets/hub_section.dart | 28 +----- lib/widgets/media_card.dart | 57 ++--------- lib/widgets/media_grid_delegate.dart | 3 +- 13 files changed, 125 insertions(+), 236 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index c03f57eb..5524bac9 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -4,7 +4,7 @@ import '../services/settings_service.dart'; class SettingsProvider extends ChangeNotifier { SettingsService? _settingsService; - LibraryDensity _libraryDensity = LibraryDensity.normal; + int _libraryDensity = LibraryDensity.defaultValue; ViewMode _viewMode = ViewMode.grid; EpisodePosterMode _episodePosterMode = EpisodePosterMode.seriesPoster; bool _showHeroSection = true; @@ -49,7 +49,7 @@ class SettingsProvider extends ChangeNotifier { /// Whether the provider has completed initialization bool get isInitialized => _isInitialized; - LibraryDensity get libraryDensity => _libraryDensity; + int get libraryDensity => _libraryDensity; ViewMode get viewMode => _viewMode; @@ -86,8 +86,8 @@ class SettingsProvider extends ChangeNotifier { } } - Future setLibraryDensity(LibraryDensity density) => _updateSetting( - current: _libraryDensity, value: density, + Future setLibraryDensity(int density) => _updateSetting( + current: _libraryDensity, value: density.clamp(LibraryDensity.min, LibraryDensity.max), setLocal: (v) => _libraryDensity = v, persist: _settingsService!.setLibraryDensity, ); @@ -152,17 +152,6 @@ class SettingsProvider extends ChangeNotifier { persist: _settingsService!.setLiveTvDefaultFavorites, ); - String get libraryDensityDisplayName { - switch (_libraryDensity) { - case LibraryDensity.compact: - return 'Compact'; - case LibraryDensity.normal: - return 'Normal'; - case LibraryDensity.comfortable: - return 'Comfortable'; - } - } - String get episodePosterModeDisplayName { switch (_episodePosterMode) { case EpisodePosterMode.seriesPoster: diff --git a/lib/screens/libraries/adaptive_media_grid.dart b/lib/screens/libraries/adaptive_media_grid.dart index 90b826c3..fe778e02 100644 --- a/lib/screens/libraries/adaptive_media_grid.dart +++ b/lib/screens/libraries/adaptive_media_grid.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/settings_provider.dart'; -import '../../services/settings_service.dart' show ViewMode, LibraryDensity; +import '../../services/settings_service.dart' show ViewMode; import '../../utils/grid_size_calculator.dart'; import '../../utils/layout_constants.dart'; import '../main_screen.dart'; @@ -84,7 +84,7 @@ class AdaptiveMediaGrid extends StatelessWidget { } /// Builds either a list or grid view based on the view mode - Widget _buildItemsView(BuildContext context, ViewMode viewMode, LibraryDensity density) { + 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); diff --git a/lib/screens/livetv/tabs/whats_on_tab.dart b/lib/screens/livetv/tabs/whats_on_tab.dart index 1641d17d..26a96d53 100644 --- a/lib/screens/livetv/tabs/whats_on_tab.dart +++ b/lib/screens/livetv/tabs/whats_on_tab.dart @@ -14,10 +14,9 @@ import '../../../models/livetv_hub_result.dart'; import '../../../models/plex_metadata.dart'; import '../../../providers/multi_server_provider.dart'; import '../../../providers/settings_provider.dart'; -import '../../../services/settings_service.dart' show LibraryDensity; +import '../../../utils/grid_size_calculator.dart'; import '../../../theme/mono_tokens.dart'; import '../../../utils/app_logger.dart'; -import '../../../utils/layout_constants.dart'; import '../../../utils/live_tv_player_navigation.dart'; import '../../../utils/plex_image_helper.dart'; import '../../../utils/provider_extensions.dart'; @@ -470,12 +469,6 @@ class _LiveTvHubSectionState extends State<_LiveTvHubSection> { Widget build(BuildContext context) { final hasFocus = _hubFocusNode.hasFocus; final settings = context.watch(); - final densityScale = switch (settings.libraryDensity) { - LibraryDensity.compact => 0.8, - LibraryDensity.normal => 1.0, - LibraryDensity.comfortable => 1.15, - }; - return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -507,20 +500,7 @@ class _LiveTvHubSectionState extends State<_LiveTvHubSection> { onKeyEvent: _handleKeyEvent, child: LayoutBuilder( builder: (context, constraints) { - final screenWidth = constraints.maxWidth; - double baseWidth; - if (ScreenBreakpoints.isLargeDesktop(screenWidth)) { - baseWidth = 220.0; - } else if (ScreenBreakpoints.isDesktop(screenWidth)) { - baseWidth = 200.0; - } else if (ScreenBreakpoints.isWideTablet(screenWidth)) { - baseWidth = 190.0; - } else { - baseWidth = 160.0; - } - final baseCardWidth = baseWidth * densityScale; - - final cardWidth = baseCardWidth; + final cardWidth = GridSizeCalculator.getCellWidth(constraints.maxWidth, context, settings.libraryDensity); final posterWidth = cardWidth - 16; final posterHeight = posterWidth * 1.5; // 2:3 aspect final containerHeight = posterHeight + 66; diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index 8cc65a57..238ad5c1 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -31,6 +31,8 @@ import '../models/download_models.dart'; import '../services/download_storage_service.dart'; import '../utils/download_version_utils.dart'; import '../providers/playback_state_provider.dart'; +import '../providers/settings_provider.dart'; +import '../utils/grid_size_calculator.dart'; import '../providers/download_provider.dart'; import '../providers/offline_watch_provider.dart'; import '../theme/mono_tokens.dart'; @@ -1512,13 +1514,12 @@ class _MediaDetailScreenState extends State return KeyEventResult.handled; // consume to prevent unwanted traversal } - /// Get the responsive card width used by seasons/extras/cast rows + /// Get the responsive card width used by seasons/extras/cast rows. + /// Uses the shared grid size calculator for consistency with library grids. double _getResponsiveCardWidth() { - final screenWidth = MediaQuery.of(context).size.width; - if (screenWidth >= 1400) return 220.0; - if (screenWidth >= 900) return 200.0; - if (screenWidth >= 700) return 190.0; - return 160.0; + final density = context.read().libraryDensity; + final availableWidth = MediaQuery.of(context).size.width; + return GridSizeCalculator.getCellWidth(availableWidth, context, density); } /// Handle key events for the overview section @@ -1759,7 +1760,7 @@ class _MediaDetailScreenState extends State if (key.isLeftKey) { if (_focusedCastIndex > 0) { setState(() => _focusedCastIndex--); - scrollListToIndex(_castScrollController, _focusedCastIndex, itemExtent: 120.0 + 8 + 4, leadingPadding: 0); + scrollListToIndex(_castScrollController, _focusedCastIndex, itemExtent: _getResponsiveCardWidth() + 8 + 4, leadingPadding: 0); } return KeyEventResult.handled; } @@ -1768,7 +1769,7 @@ class _MediaDetailScreenState extends State if (key.isRightKey) { if (_focusedCastIndex < roleCount - 1) { setState(() => _focusedCastIndex++); - scrollListToIndex(_castScrollController, _focusedCastIndex, itemExtent: 120.0 + 8 + 4, leadingPadding: 0); + scrollListToIndex(_castScrollController, _focusedCastIndex, itemExtent: _getResponsiveCardWidth() + 8 + 4, leadingPadding: 0); } return KeyEventResult.handled; } @@ -2648,10 +2649,11 @@ class _MediaDetailScreenState extends State /// Build the cast section with locked focus pattern for D-pad navigation /// Uses same layout pattern as seasons/extras (ListView.builder + Padding(horizontal: 2)) Widget _buildCastSection(PlexMetadata metadata) { - const cardWidth = 120.0; + final cardWidth = _getResponsiveCardWidth(); const innerPadding = 4.0; + final imageSize = cardWidth; // image + inner padding + text area + outer list padding + focus scale headroom - const containerHeight = 120.0 + innerPadding * 2 + 66 + 16; + final containerHeight = imageSize + innerPadding * 2 + 66 + 16; final hasFocus = _castFocusNode.hasFocus; @@ -2690,8 +2692,8 @@ class _MediaDetailScreenState extends State child: PlexOptimizedImage( client: _getClientForMetadata(context), imagePath: actor.thumb, - width: 120, - height: 120, + width: imageSize, + height: imageSize, fit: BoxFit.cover, imageType: ImageType.avatar, fallbackIcon: Symbols.person_rounded, diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index 900cc438..5f04831f 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -137,16 +137,30 @@ class _AppearanceSettingsScreenState extends State { Widget _buildDensitySelector() { return Consumer( builder: (context, settingsProvider, child) { - return SegmentedSetting( - icon: Symbols.grid_view_rounded, - title: t.settings.libraryDensity, - segments: [ - ButtonSegment(value: settings.LibraryDensity.compact, label: Text(t.settings.compact)), - ButtonSegment(value: settings.LibraryDensity.normal, label: Text(t.settings.normal)), - ButtonSegment(value: settings.LibraryDensity.comfortable, label: Text(t.settings.comfortable)), - ], - selected: settingsProvider.libraryDensity, - onChanged: (value) => settingsProvider.setLibraryDensity(value), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const AppIcon(Symbols.grid_view_rounded, fill: 1), + const SizedBox(width: 16), + Text(t.settings.compact, style: const TextStyle(fontSize: 12, color: Colors.grey)), + Expanded( + child: Slider( + value: settingsProvider.libraryDensity.toDouble(), + min: 1, + max: 5, + divisions: 4, + onChanged: (value) => settingsProvider.setLibraryDensity(value.round()), + ), + ), + Text(t.settings.comfortable, style: const TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ], + ), ); }, ); @@ -178,8 +192,7 @@ class _AppearanceSettingsScreenState extends State { segments: [ ButtonSegment(value: settings.EpisodePosterMode.seriesPoster, label: Text(t.settings.seriesPoster)), ButtonSegment(value: settings.EpisodePosterMode.seasonPoster, label: Text(t.settings.seasonPoster)), - ButtonSegment( - value: settings.EpisodePosterMode.episodeThumbnail, label: Text(t.settings.episodeThumbnail)), + ButtonSegment(value: settings.EpisodePosterMode.episodeThumbnail, label: Text(t.settings.episodeThumbnail)), ], selected: settingsProvider.episodePosterMode, onChanged: (value) => settingsProvider.setEpisodePosterMode(value), diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index fdc61cf2..f9fc38e3 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -187,7 +187,7 @@ class _SettingsScreenState extends State with FocusableTab { Widget _buildAppearanceTile() { return Consumer2( builder: (context, themeProvider, settingsProvider, child) { - final summary = '${themeProvider.themeModeDisplayName} · ${settingsProvider.libraryDensityDisplayName}'; + final summary = '${themeProvider.themeModeDisplayName} · ${t.settings.libraryDensity} ${settingsProvider.libraryDensity}'; return ListTile( focusNode: _focusTracker.get(_kAppearance), leading: const AppIcon(Symbols.palette_rounded, fill: 1), diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 5abd87b6..00718e02 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -13,7 +13,16 @@ import '../utils/platform_detector.dart'; enum ThemeMode { system, light, dark, oled } -enum LibraryDensity { compact, normal, comfortable } +/// Library density is now an int 1–5 (1 = most compact, 5 = most comfortable). +/// Default is 3. +class LibraryDensity { + static const int min = 1; + static const int max = 5; + static const int defaultValue = 3; + + /// Returns a 0.0–1.0 factor for interpolation (0 = most compact, 1 = most comfortable). + static double factor(int density) => (density.clamp(min, max) - min) / (max - min); +} enum ViewMode { grid, list } @@ -209,13 +218,24 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getString(_keyPreferredAudioCodec) ?? 'auto'; } - // Library Density - Future setLibraryDensity(LibraryDensity density) async { - await prefs.setString(_keyLibraryDensity, density.name); + // Library Density (int 1–5) + Future setLibraryDensity(int density) async { + await prefs.setInt(_keyLibraryDensity, density.clamp(LibraryDensity.min, LibraryDensity.max)); } - LibraryDensity getLibraryDensity() { - return _getEnumValue(_keyLibraryDensity, LibraryDensity.values, LibraryDensity.normal); + int getLibraryDensity() { + // New int format + final intVal = prefs.getInt(_keyLibraryDensity); + if (intVal != null) return intVal.clamp(LibraryDensity.min, LibraryDensity.max); + // Migrate from old enum string format + final strVal = prefs.getString(_keyLibraryDensity); + final result = switch (strVal) { + 'compact' => 2, + 'comfortable' => 4, + _ => LibraryDensity.defaultValue, + }; + prefs.setInt(_keyLibraryDensity, result); + return result; } // View Mode @@ -1374,7 +1394,7 @@ class SettingsService extends BaseSharedPreferencesService { 'enableHardwareDecoding': getEnableHardwareDecoding(), 'preferredVideoCodec': getPreferredVideoCodec(), 'preferredAudioCodec': getPreferredAudioCodec(), - 'libraryDensity': getLibraryDensity().name, + 'libraryDensity': getLibraryDensity(), 'viewMode': getViewMode().name, 'episodePosterMode': getEpisodePosterMode().name, 'seekTimeSmall': getSeekTimeSmall(), diff --git a/lib/theme/mono_theme.dart b/lib/theme/mono_theme.dart index 016a9389..7e8e22c0 100644 --- a/lib/theme/mono_theme.dart +++ b/lib/theme/mono_theme.dart @@ -118,6 +118,14 @@ ThemeData monoTheme({required bool dark, bool oled = false}) { ), elevatedButtonTheme: ElevatedButtonThemeData(style: buttonStyle), filledButtonTheme: FilledButtonThemeData(style: buttonStyle), + sliderTheme: SliderThemeData( + trackHeight: 16, + trackGap: 6, + thumbSize: const WidgetStatePropertyAll(Size(4, 20)), + thumbShape: const HandleThumbShape(), + trackShape: const GappedSliderTrackShape(), + tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 2), + ), dividerTheme: DividerThemeData(space: 0, thickness: 1, color: c.outline), listTileTheme: ListTileThemeData( dense: true, diff --git a/lib/utils/grid_size_calculator.dart b/lib/utils/grid_size_calculator.dart index 814c274b..3ed9e01e 100644 --- a/lib/utils/grid_size_calculator.dart +++ b/lib/utils/grid_size_calculator.dart @@ -5,97 +5,47 @@ import 'platform_detector.dart'; /// Utility class for calculating consistent grid sizes across the app class GridSizeCalculator { - /// Calculates the maximum cross-axis extent for grid items based on screen size and density - static double getMaxCrossAxisExtent(BuildContext context, LibraryDensity density) { - final screenWidth = MediaQuery.of(context).size.width; - final isTV = PlatformDetector.isTV(); - final isDesktopOrLarger = ScreenBreakpoints.isDesktopOrLarger(screenWidth); - final isTablet = ScreenBreakpoints.isTablet(screenWidth); + static double _lerp(double min, double max, double t) => min + (max - min) * t; - switch (density) { - case LibraryDensity.comfortable: - if (isTV) return GridLayoutConstants.comfortableTV; - if (isDesktopOrLarger) return GridLayoutConstants.comfortableDesktop; - if (isTablet) return GridLayoutConstants.comfortableTablet; - return GridLayoutConstants.comfortableMobile; - case LibraryDensity.compact: - if (isTV) return GridLayoutConstants.compactTV; - if (isDesktopOrLarger) return GridLayoutConstants.compactDesktop; - if (isTablet) return GridLayoutConstants.compactTablet; - return GridLayoutConstants.compactMobile; - case LibraryDensity.normal: - if (isTV) return GridLayoutConstants.normalTV; - if (isDesktopOrLarger) return GridLayoutConstants.normalDesktop; - if (isTablet) return GridLayoutConstants.normalTablet; - return GridLayoutConstants.normalMobile; - } + /// Calculates the maximum cross-axis extent for grid items based on screen size and density. + /// [density] is an int 1–5 (1 = most compact, 5 = most comfortable). + static double getMaxCrossAxisExtent(BuildContext context, int density) { + final screenWidth = MediaQuery.of(context).size.width; + final f = LibraryDensity.factor(density); + + if (PlatformDetector.isTV()) return _lerp(120, 220, f); + if (ScreenBreakpoints.isDesktopOrLarger(screenWidth)) return _lerp(140, 280, f); + if (ScreenBreakpoints.isTablet(screenWidth)) return _lerp(120, 230, f); + return _lerp(100, 200, f); } /// Calculates the max cross-axis extent accounting for outer padding. - /// - /// Uses responsive strategies: - /// - Wide screens (>=900px): Divisor-based calculation with max item width - /// - Medium screens (600-899px): Fixed item count (4-6 items based on density) - /// - Small screens (<600px): Fixed item count (2-4 items based on density) + /// [density] is an int 1–5. static double getMaxCrossAxisExtentWithPadding( BuildContext context, - LibraryDensity density, + int density, double horizontalPadding, ) { final screenWidth = MediaQuery.of(context).size.width; final availableWidth = screenWidth - horizontalPadding; + final f = LibraryDensity.factor(density); // TV-specific sizing for 10ft viewing distance if (PlatformDetector.isTV()) { - double divisor; - double maxItemWidth; - switch (density) { - case LibraryDensity.comfortable: - divisor = 7.0; - maxItemWidth = 220; - case LibraryDensity.normal: - divisor = 9.0; - maxItemWidth = 190; - case LibraryDensity.compact: - divisor = 11.0; - maxItemWidth = 160; - } + final divisor = _lerp(12, 6, f); + final maxItemWidth = _lerp(140, 240, f); return (availableWidth / divisor).clamp(0, maxItemWidth); } if (ScreenBreakpoints.isWideTabletOrLarger(screenWidth)) { - // Wide screens (desktop/large tablet landscape): Responsive division - double divisor; - double maxItemWidth; - - switch (density) { - case LibraryDensity.comfortable: - divisor = 5.5; - maxItemWidth = 260; - case LibraryDensity.normal: - divisor = 6.5; - maxItemWidth = 230; - case LibraryDensity.compact: - divisor = 9.0; - maxItemWidth = 160; - } - + final divisor = _lerp(10, 5, f); + final maxItemWidth = _lerp(140, 280, f); return (availableWidth / divisor).clamp(0, maxItemWidth); } else if (ScreenBreakpoints.isTablet(screenWidth)) { - // Medium screens (tablets): Fixed 3-4-6 items - int targetItemCount = switch (density) { - LibraryDensity.comfortable => 3, - LibraryDensity.normal => 4, - LibraryDensity.compact => 6, - }; + final targetItemCount = _lerp(6, 3, f); return availableWidth / targetItemCount; } else { - // Small screens (phones): Fixed 2-3-4 items - int targetItemCount = switch (density) { - LibraryDensity.comfortable => 2, - LibraryDensity.normal => 3, - LibraryDensity.compact => 4, - }; + final targetItemCount = _lerp(5, 2, f); return availableWidth / targetItemCount; } } @@ -113,6 +63,15 @@ class GridSizeCalculator { return ((crossAxisExtent + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100); } + /// Computes the actual cell width that a grid with [getMaxCrossAxisExtent] would produce + /// for the given [availableWidth]. This matches SliverGridDelegateWithMaxCrossAxisExtent's + /// internal calculation, so horizontal scroll lists can use the same width as grids. + static double getCellWidth(double availableWidth, BuildContext context, int density) { + final maxExtent = getMaxCrossAxisExtent(context, density); + final columns = getColumnCount(availableWidth, maxExtent); + return availableWidth / columns; + } + /// Check if the given index is in the first row of a grid with given column count. static bool isFirstRow(int index, int columnCount) { return index < columnCount; diff --git a/lib/utils/layout_constants.dart b/lib/utils/layout_constants.dart index 566a282a..1f0e8376 100644 --- a/lib/utils/layout_constants.dart +++ b/lib/utils/layout_constants.dart @@ -44,26 +44,6 @@ class ScreenBreakpoints { /// Grid layout constants class GridLayoutConstants { - /// Maximum cross-axis extent for grid items in comfortable density mode - static const double comfortableDesktop = 250; - static const double comfortableTablet = 210; - static const double comfortableMobile = 180; - - /// Maximum cross-axis extent for grid items in compact density mode - static const double compactDesktop = 160; - static const double compactTablet = 140; - static const double compactMobile = 120; - - /// Maximum cross-axis extent for grid items in normal density mode - static const double normalDesktop = 220; - static const double normalTablet = 185; - static const double normalMobile = 155; - - /// Maximum cross-axis extent for grid items on TV (10ft viewing distance) - static const double comfortableTV = 200; - static const double normalTV = 170; - static const double compactTV = 140; - /// Default aspect ratio for media card grid cells (poster + text) static const double posterAspectRatio = 2 / 3.3; diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index 0687abff..fde0a102 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -10,9 +10,9 @@ import '../focus/focus_theme.dart'; import '../focus/input_mode_tracker.dart'; import '../focus/key_event_utils.dart'; import '../providers/settings_provider.dart'; -import '../services/settings_service.dart' show EpisodePosterMode, LibraryDensity; +import '../services/settings_service.dart' show EpisodePosterMode; +import '../utils/grid_size_calculator.dart'; import '../theme/mono_tokens.dart'; -import '../utils/layout_constants.dart'; import '../focus/locked_hub_controller.dart'; import '../models/plex_hub.dart'; import '../screens/hub_detail_screen.dart'; @@ -378,28 +378,10 @@ class HubSectionState extends State { Focus( focusNode: _hubFocusNode, onKeyEvent: _handleKeyEvent, - child: Builder( - builder: (context) { - // Use MediaQuery instead of LayoutBuilder to save 1 element level. - // HubSection is always full-width inside CustomScrollView. - final screenWidth = MediaQuery.of(context).size.width; + child: LayoutBuilder( + builder: (context, constraints) { final settings = context.watch(); - final densityScale = switch (settings.libraryDensity) { - LibraryDensity.compact => 0.8, - LibraryDensity.normal => 1.0, - LibraryDensity.comfortable => 1.15, - }; - double baseWidth; - if (ScreenBreakpoints.isLargeDesktop(screenWidth)) { - baseWidth = 220.0; - } else if (ScreenBreakpoints.isDesktop(screenWidth)) { - baseWidth = 200.0; - } else if (ScreenBreakpoints.isWideTablet(screenWidth)) { - baseWidth = 190.0; - } else { - baseWidth = 160.0; - } - final baseCardWidth = baseWidth * densityScale; + final baseCardWidth = GridSizeCalculator.getCellWidth(constraints.maxWidth, context, settings.libraryDensity); // Get episode poster mode setting final episodePosterMode = settings.episodePosterMode; diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 530efc16..5b96f418 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -194,7 +194,7 @@ class MediaCardState extends State { onLongPress: _showContextMenu, onSecondaryTapDown: _storeTapPosition, onSecondaryTap: _showContextMenu, - density: context.select((s) => s.libraryDensity), + density: context.select((s) => s.libraryDensity), isOffline: widget.isOffline, localPosterPath: localPosterPath, showServerName: widget.showServerName, @@ -317,7 +317,7 @@ class _MediaCardList extends StatelessWidget { final void Function(TapDownDetails)? onTapDown; final VoidCallback? onSecondaryTap; final void Function(TapDownDetails)? onSecondaryTapDown; - final LibraryDensity density; + final int density; final bool isOffline; final String? localPosterPath; final bool showServerName; @@ -337,14 +337,7 @@ class _MediaCardList extends StatelessWidget { }); double _basePosterWidth() { - switch (density) { - case LibraryDensity.compact: - return 80; - case LibraryDensity.normal: - return 100; - case LibraryDensity.comfortable: - return 120; - } + return 70 + LibraryDensity.factor(density) * 50; // 70–120 } double _posterWidth(BuildContext context) { @@ -372,54 +365,18 @@ class _MediaCardList extends StatelessWidget { return base * 1.5; // Default 2:3 aspect ratio } - double get _titleFontSize { - switch (density) { - case LibraryDensity.compact: - return 14; - case LibraryDensity.normal: - return 15; - case LibraryDensity.comfortable: - return 16; - } - } + double get _titleFontSize => 13 + LibraryDensity.factor(density) * 3; // 13–16 - double get _metadataFontSize { - switch (density) { - case LibraryDensity.compact: - return 11; - case LibraryDensity.normal: - return 12; - case LibraryDensity.comfortable: - return 13; - } - } + double get _metadataFontSize => 10 + LibraryDensity.factor(density) * 3; // 10–13 - double get _subtitleFontSize { - switch (density) { - case LibraryDensity.compact: - return 12; - case LibraryDensity.normal: - return 13; - case LibraryDensity.comfortable: - return 14; - } - } + double get _subtitleFontSize => 11 + LibraryDensity.factor(density) * 3; // 11–14 double get _summaryFontSize { // Summary uses the same sizing as metadata text return _metadataFontSize; } - int get _summaryMaxLines { - switch (density) { - case LibraryDensity.compact: - return 2; - case LibraryDensity.normal: - return 3; - case LibraryDensity.comfortable: - return 4; - } - } + int get _summaryMaxLines => density <= 2 ? 2 : density; // 2, 2, 3, 4, 5 String _buildMetadataLine() { final parts = []; diff --git a/lib/widgets/media_grid_delegate.dart b/lib/widgets/media_grid_delegate.dart index 3e400597..90645ddc 100644 --- a/lib/widgets/media_grid_delegate.dart +++ b/lib/widgets/media_grid_delegate.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../services/settings_service.dart'; import '../utils/grid_size_calculator.dart'; import '../utils/layout_constants.dart'; @@ -13,7 +12,7 @@ class MediaGridDelegate { /// Set [useWideAspectRatio] to true to use 16:9 aspect ratio for episode thumbnails. static SliverGridDelegateWithMaxCrossAxisExtent createDelegate({ required BuildContext context, - required LibraryDensity density, + required int density, bool usePaddingAware = false, double horizontalPadding = 16, bool useWideAspectRatio = false,