feat: more consistent poster sizing

close #798
This commit is contained in:
edde746
2026-04-03 17:04:20 +02:00
parent 062fd82de1
commit c9db6e528e
13 changed files with 125 additions and 236 deletions
+4 -15
View File
@@ -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<void> setLibraryDensity(LibraryDensity density) => _updateSetting(
current: _libraryDensity, value: density,
Future<void> 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:
@@ -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<T> 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);
+2 -22
View File
@@ -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<SettingsProvider>();
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;
+14 -12
View File
@@ -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<MediaDetailScreen>
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<SettingsProvider>().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<MediaDetailScreen>
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<MediaDetailScreen>
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<MediaDetailScreen>
/// 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<MediaDetailScreen>
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,
@@ -137,16 +137,30 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
Widget _buildDensitySelector() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SegmentedSetting<settings.LibraryDensity>(
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<AppearanceSettingsScreen> {
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),
+1 -1
View File
@@ -187,7 +187,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
Widget _buildAppearanceTile() {
return Consumer2<ThemeProvider, SettingsProvider>(
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),
+27 -7
View File
@@ -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 15 (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.01.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<void> setLibraryDensity(LibraryDensity density) async {
await prefs.setString(_keyLibraryDensity, density.name);
// Library Density (int 15)
Future<void> 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(),
+8
View File
@@ -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,
+29 -70
View File
@@ -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 15 (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 15.
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;
-20
View File
@@ -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;
+5 -23
View File
@@ -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<HubSection> {
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<SettingsProvider>();
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;
+7 -50
View File
@@ -194,7 +194,7 @@ class MediaCardState extends State<MediaCard> {
onLongPress: _showContextMenu,
onSecondaryTapDown: _storeTapPosition,
onSecondaryTap: _showContextMenu,
density: context.select<SettingsProvider, LibraryDensity>((s) => s.libraryDensity),
density: context.select<SettingsProvider, int>((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; // 70120
}
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; // 1316
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; // 1013
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; // 1114
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 = <String>[];
+1 -2
View File
@@ -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,