diff --git a/lib/models/items/images_models.dart b/lib/models/items/images_models.dart index 58bdd2554..fe82e4b14 100644 --- a/lib/models/items/images_models.dart +++ b/lib/models/items/images_models.dart @@ -15,21 +15,23 @@ import 'package:fladder/util/custom_cache_manager.dart'; class ImagesData { final ImageData? primary; + final ImageData? thumb; final List? backDrop; final ImageData? logo; ImagesData({ this.primary, + this.thumb, this.backDrop, this.logo, }); bool get isEmpty { - if (primary == null && backDrop == null) return true; + if (primary == null && thumb == null && backDrop == null) return true; return false; } ImageData? get firstOrNull { - return primary ?? backDrop?.firstOrNull; + return primary ?? thumb ?? backDrop?.firstOrNull; } ImageData? get randomBackDrop => (backDrop?..shuffle())?.firstOrNull ?? primary; @@ -38,6 +40,7 @@ class ImagesData { dto.BaseItemDto item, Ref ref, { Size backDrop = const Size(2000, 2000), + Size thumb = const Size(1200, 1200), Size logo = const Size(500, 500), Size primary = const Size(600, 600), bool getOriginalSize = false, @@ -64,6 +67,23 @@ class ImagesData { hash: item.imageBlurHashes?.primary?[item.imageTags?['Primary']] ?? "", ) : null, + thumb: item.imageTags?['Thumb'] != null + ? ImageData( + path: getOriginalSize + ? imageProvider.getItemsOrigImageUrl( + itemid, + type: enums.ImageType.thumb, + ) + : imageProvider.getItemsImageUrl( + itemid, + type: enums.ImageType.thumb, + maxHeight: thumb.height.toInt(), + maxWidth: thumb.width.toInt(), + ), + key: "${itemid}_thumb_${item.imageTags?['Thumb']}", + hash: item.imageBlurHashes?.thumb?[item.imageTags?['Thumb']] ?? "", + ) + : null, logo: ImageData( path: getOriginalSize ? imageProvider.getItemsOrigImageUrl( @@ -111,6 +131,7 @@ class ImagesData { dto.BaseItemDto item, Ref ref, { Size backDrop = const Size(2000, 2000), + Size thumb = const Size(1200, 1200), Size logo = const Size(500, 500), Size primary = const Size(600, 600), }) { @@ -130,6 +151,19 @@ class ImagesData { key: "${item.seriesId}_primary_${item.seriesPrimaryImageTag ?? ""}", hash: item.imageBlurHashes?.primary?[item.seriesPrimaryImageTag] ?? "") : null, + thumb: ((item.seriesThumbImageTag ?? item.parentThumbImageTag) != null) + ? ImageData( + path: imageProvider.getItemsImageUrl( + item.parentThumbItemId ?? item.seriesId ?? item.parentId, + type: enums.ImageType.thumb, + maxHeight: thumb.height.toInt(), + maxWidth: thumb.width.toInt(), + ), + key: + "${item.parentThumbItemId ?? item.seriesId ?? item.parentId}_thumb_${item.seriesThumbImageTag ?? item.parentThumbImageTag ?? ""}", + hash: item.imageBlurHashes?.thumb?[item.seriesThumbImageTag ?? item.parentThumbImageTag] ?? "", + ) + : null, logo: ImageData( path: imageProvider.getItemsImageUrl( item.seriesId, @@ -183,21 +217,24 @@ class ImagesData { key: "${item.id ?? ""}_primary_${item.primaryImageTag ?? ''}", hash: item.imageBlurHashes?.primary?[item.primaryImageTag] ?? '') : null, + thumb: null, logo: null, backDrop: null, ); } @override - String toString() => 'ImagesData(primary: $primary, backDrop: $backDrop, logo: $logo)'; + String toString() => 'ImagesData(primary: $primary, thumb: $thumb, backDrop: $backDrop, logo: $logo)'; ImagesData copyWith({ ValueGetter? primary, + ValueGetter? thumb, ValueGetter?>? backDrop, ValueGetter? logo, }) { return ImagesData( primary: primary != null ? primary() : this.primary, + thumb: thumb != null ? thumb() : this.thumb, backDrop: backDrop != null ? backDrop() : this.backDrop, logo: logo != null ? logo() : this.logo, ); @@ -206,6 +243,7 @@ class ImagesData { Map toMap() { return { 'primary': primary?.toMap(), + 'thumb': thumb?.toMap(), 'backDrop': backDrop?.map((x) => x.toMap()).toList(), 'logo': logo?.toMap(), }; @@ -214,6 +252,7 @@ class ImagesData { factory ImagesData.fromMap(Map map) { return ImagesData( primary: map['primary'] != null ? ImageData.fromMap(map['primary']) : null, + thumb: map['thumb'] != null ? ImageData.fromMap(map['thumb']) : null, backDrop: map['backDrop'] != null ? List.from(map['backDrop']?.map((x) => ImageData.fromMap(x))) : null, logo: map['logo'] != null ? ImageData.fromMap(map['logo']) : null, diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart index 1de310f4f..6ed573143 100644 --- a/lib/providers/dashboard_provider.dart +++ b/lib/providers/dashboard_provider.dart @@ -31,6 +31,7 @@ class DashboardNotifier extends StateNotifier { final imagesToFetch = { ImageType.logo, + ImageType.thumb, ImageType.primary, ImageType.backdrop, ImageType.banner, @@ -117,6 +118,8 @@ class DashboardNotifier extends StateNotifier { nextUpDateCutoff: DateTime.now().subtract( ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), fields: fieldsToFetch.toList(), + enableImageTypes: imagesToFetch, + imageTypeLimit: 1, ); final next = nextResponse.body?.items diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index c856f1ec8..fce361bb5 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -173,7 +173,7 @@ class _LibraryScreenState extends ConsumerState with SingleTicker tvMode: useTVExpandedLayout, contentPadding: padding, posters: element.posters, - primaryPosters: element.name is Continue, + collectionAspectRatio: element.name is Continue ? 1.2 : null, label: element.type != null ? "${element.type?.label(context.localized)} - ${element.name.label(context.localized)}" : element.name.label(context.localized), diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 784f9338f..100a9ecc4 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -1,3 +1,5 @@ +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as jelly; import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -35,7 +37,7 @@ class PosterImage extends ConsumerWidget { final Function(ItemBaseModel newItem)? onItemUpdated; final Function(ItemBaseModel oldItem)? onItemRemoved; final Function(Function() action, ItemBaseModel item)? onPressed; - final bool primaryPosters; + final List? imagePriority; final Function(bool focus)? onFocusChanged; final bool showSyncStatus; @@ -50,12 +52,33 @@ class PosterImage extends ConsumerWidget { this.otherActions = const [], this.onPressed, this.onUserDataChanged, - this.primaryPosters = false, + this.imagePriority, this.onFocusChanged, this.showSyncStatus = false, super.key, }); + ImageData? _resolveImage() { + final source = poster.getPosters; + final fallback = poster.images; + + final effectivePriority = + imagePriority ?? const [jelly.ImageType.primary, jelly.ImageType.thumb, jelly.ImageType.backdrop]; + + for (final type in effectivePriority) { + final image = switch (type) { + jelly.ImageType.primary => source?.primary ?? fallback?.primary, + jelly.ImageType.thumb => source?.thumb ?? fallback?.thumb, + jelly.ImageType.backdrop => source?.backDrop?.lastOrNull ?? fallback?.backDrop?.lastOrNull, + _ => null, + }; + + if (image != null) return image; + } + + return null; + } + @override Widget build(BuildContext context, WidgetRef ref) { final radius = FladderTheme.smallShape.borderRadius; @@ -95,9 +118,7 @@ class PosterImage extends ConsumerWidget { border: Border.all(width: 1, color: Colors.white.withAlpha(45)), ), child: FladderImage( - image: primaryPosters - ? poster.images?.primary - : poster.getPosters?.primary ?? poster.getPosters?.backDrop?.lastOrNull, + image: _resolveImage(), placeHolder: PosterPlaceholder(item: poster), ), ), diff --git a/lib/screens/shared/media/detailed_banner.dart b/lib/screens/shared/media/detailed_banner.dart index e4b9070b3..235ed28af 100644 --- a/lib/screens/shared/media/detailed_banner.dart +++ b/lib/screens/shared/media/detailed_banner.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as jelly; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/details_screens/components/overview_header.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; @@ -110,7 +111,11 @@ class _DetailedBannerState extends ConsumerState { return FocusProvider( autoFocus: true, child: PosterRow( - primaryPosters: true, + imagePriority: const [ + jelly.ImageType.thumb, + jelly.ImageType.backdrop, + jelly.ImageType.primary, + ], label: context.localized.nextUp, posters: widget.posters, onFocused: (poster) { diff --git a/lib/screens/shared/media/poster_row.dart b/lib/screens/shared/media/poster_row.dart index 421d758be..948f9b186 100644 --- a/lib/screens/shared/media/poster_row.dart +++ b/lib/screens/shared/media/poster_row.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as jelly; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; @@ -18,7 +19,7 @@ class PosterRow extends ConsumerWidget { final Function()? onLabelClick; final EdgeInsets contentPadding; final Function(ItemBaseModel focused)? onFocused; - final bool primaryPosters; + final List? imagePriority; final bool tvMode; final bool showSyncStatus; const PosterRow({ @@ -28,7 +29,7 @@ class PosterRow extends ConsumerWidget { this.collectionAspectRatio, this.onLabelClick, this.onFocused, - this.primaryPosters = false, + this.imagePriority, this.tvMode = false, this.showSyncStatus = false, super.key, @@ -36,7 +37,7 @@ class PosterRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final dominantRatio = primaryPosters ? 1.2 : collectionAspectRatio ?? posters.getMostCommonType.aspectRatio; + final dominantRatio = collectionAspectRatio ?? posters.getMostCommonType.aspectRatio; if (tvMode) { return TVPosterRow( posters: posters, @@ -45,7 +46,6 @@ class PosterRow extends ConsumerWidget { contentPadding: contentPadding, onLabelClick: onLabelClick, onFocused: onFocused, - primaryPosters: primaryPosters, autoFocus: ref.read(argumentsStateProvider).htpcMode ? FocusProvider.autoFocusOf(context) : false, ); } @@ -69,8 +69,8 @@ class PosterRow extends ConsumerWidget { key: Key(poster.id), poster: poster, aspectRatio: dominantRatio, - primaryPosters: primaryPosters, showSyncStatus: showSyncStatus, + imagePriority: imagePriority, ); }, ); diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index 41073de29..ba27a42d6 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as jelly; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/album_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; @@ -30,7 +31,7 @@ class PosterWidget extends ConsumerWidget { final Function(ItemBaseModel newItem)? onItemUpdated; final Function(ItemBaseModel oldItem)? onItemRemoved; final Function(VoidCallback action, ItemBaseModel item)? onPressed; - final bool primaryPosters; + final List? imagePriority; final Function(bool focus)? onFocusChanged; final bool showSyncStatus; @@ -48,7 +49,7 @@ class PosterWidget extends ConsumerWidget { this.onItemUpdated, this.onItemRemoved, this.onPressed, - this.primaryPosters = false, + this.imagePriority, this.onFocusChanged, this.showSyncStatus = false, super.key, @@ -80,7 +81,7 @@ class PosterWidget extends ConsumerWidget { onItemRemoved: onItemRemoved, onItemUpdated: onItemUpdated, onPressed: onPressed, - primaryPosters: primaryPosters, + imagePriority: imagePriority, onFocusChanged: onFocusChanged, showSyncStatus: showSyncStatus, ), diff --git a/lib/screens/shared/media/tv_poster_row.dart b/lib/screens/shared/media/tv_poster_row.dart index 713482c20..e1adc8366 100644 --- a/lib/screens/shared/media/tv_poster_row.dart +++ b/lib/screens/shared/media/tv_poster_row.dart @@ -35,7 +35,6 @@ class TVPosterRow extends ConsumerStatefulWidget { final EdgeInsets contentPadding; final Function()? onLabelClick; final Function(ItemBaseModel focused)? onFocused; - final bool primaryPosters; final bool autoFocus; const TVPosterRow({ @@ -45,7 +44,6 @@ class TVPosterRow extends ConsumerStatefulWidget { this.primaryRatio = 0.67, this.onLabelClick, this.onFocused, - this.primaryPosters = false, this.autoFocus = false, super.key, }); @@ -143,7 +141,6 @@ class _TVPosterRowState extends ConsumerState { widget.onFocused?.call(poster); } }, - primaryPosters: isFocused || widget.primaryPosters, onTap: () => poster.navigateTo(context, ref: ref), ); }, @@ -172,7 +169,6 @@ class _TVPosterItem extends ConsumerWidget { final bool selected; final bool focused; final Function(bool focused)? onFocusChanged; - final bool primaryPosters; final VoidCallback onTap; const _TVPosterItem({ @@ -182,7 +178,6 @@ class _TVPosterItem extends ConsumerWidget { required this.selected, required this.focused, this.onFocusChanged, - required this.primaryPosters, required this.onTap, });