diff --git a/.vscode/settings.json b/.vscode/settings.json index b99d4a1..d6e744b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,9 @@ { "cSpell.words": [ + "ficonsax", + "jellyfin", "Jellyfin", - "jellyfin" + "LTRB" ], "dart.flutterSdkPath": ".fvm/versions/3.24.3", "search.exclude": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5dbc715..39b7049 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -99,9 +99,9 @@ "group": "build", "label": "dart: dart pub run build_runner watch", "detail": "", - "runOptions": { - "runOn": "folderOpen" - } + // "runOptions": { + // "runOn": "folderOpen" + // } } ], } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6ff27bb..ed3f1ea 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1119,5 +1119,7 @@ "clientSettingsRequireWifiTitle": "Require Wi-Fi", "clientSettingsRequireWifiDesc": "Only download when connected to a Wi-Fi network", "libraryShuffleAndPlayItems": "Shuffle and play items", - "libraryPlayItems": "Play items" + "libraryPlayItems": "Play items", + "clientSettingsShowAllCollectionsTitle": "Show all collection types", + "clientSettingsShowAllCollectionsDesc": "When enabled, show all collection types, including those not supported by Fladder" } diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index cbdc0fa..f6a8366 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -31,6 +31,7 @@ class ClientSettingsModel with _$ClientSettingsModel { @Default(false) bool pinchPosterZoom, @Default(false) bool mouseDragSupport, @Default(true) bool requireWifi, + @Default(false) bool showAllCollectionTypes, @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, int? libraryPageSize, }) = _ClientSettingsModel; diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index f820155..3bb7f64 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -37,6 +37,7 @@ mixin _$ClientSettingsModel { bool get pinchPosterZoom => throw _privateConstructorUsedError; bool get mouseDragSupport => throw _privateConstructorUsedError; bool get requireWifi => throw _privateConstructorUsedError; + bool get showAllCollectionTypes => throw _privateConstructorUsedError; DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; int? get libraryPageSize => throw _privateConstructorUsedError; @@ -73,6 +74,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> { bool pinchPosterZoom, bool mouseDragSupport, bool requireWifi, + bool showAllCollectionTypes, DynamicSchemeVariant schemeVariant, int? libraryPageSize}); } @@ -108,6 +110,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> Object? pinchPosterZoom = null, Object? mouseDragSupport = null, Object? requireWifi = null, + Object? showAllCollectionTypes = null, Object? schemeVariant = null, Object? libraryPageSize = freezed, }) { @@ -176,6 +179,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> ? _value.requireWifi : requireWifi // ignore: cast_nullable_to_non_nullable as bool, + showAllCollectionTypes: null == showAllCollectionTypes + ? _value.showAllCollectionTypes + : showAllCollectionTypes // ignore: cast_nullable_to_non_nullable + as bool, schemeVariant: null == schemeVariant ? _value.schemeVariant : schemeVariant // ignore: cast_nullable_to_non_nullable @@ -213,6 +220,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res> bool pinchPosterZoom, bool mouseDragSupport, bool requireWifi, + bool showAllCollectionTypes, DynamicSchemeVariant schemeVariant, int? libraryPageSize}); } @@ -246,6 +254,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> Object? pinchPosterZoom = null, Object? mouseDragSupport = null, Object? requireWifi = null, + Object? showAllCollectionTypes = null, Object? schemeVariant = null, Object? libraryPageSize = freezed, }) { @@ -314,6 +323,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> ? _value.requireWifi : requireWifi // ignore: cast_nullable_to_non_nullable as bool, + showAllCollectionTypes: null == showAllCollectionTypes + ? _value.showAllCollectionTypes + : showAllCollectionTypes // ignore: cast_nullable_to_non_nullable + as bool, schemeVariant: null == schemeVariant ? _value.schemeVariant : schemeVariant // ignore: cast_nullable_to_non_nullable @@ -347,6 +360,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel this.pinchPosterZoom = false, this.mouseDragSupport = false, this.requireWifi = true, + this.showAllCollectionTypes = false, this.schemeVariant = DynamicSchemeVariant.tonalSpot, this.libraryPageSize}) : super._(); @@ -401,13 +415,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel final bool requireWifi; @override @JsonKey() + final bool showAllCollectionTypes; + @override + @JsonKey() final DynamicSchemeVariant schemeVariant; @override final int? libraryPageSize; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, schemeVariant: $schemeVariant, libraryPageSize: $libraryPageSize)'; + return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, schemeVariant: $schemeVariant, libraryPageSize: $libraryPageSize)'; } @override @@ -431,6 +448,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel ..add(DiagnosticsProperty('pinchPosterZoom', pinchPosterZoom)) ..add(DiagnosticsProperty('mouseDragSupport', mouseDragSupport)) ..add(DiagnosticsProperty('requireWifi', requireWifi)) + ..add( + DiagnosticsProperty('showAllCollectionTypes', showAllCollectionTypes)) ..add(DiagnosticsProperty('schemeVariant', schemeVariant)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)); } @@ -470,6 +489,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel other.mouseDragSupport == mouseDragSupport) && (identical(other.requireWifi, requireWifi) || other.requireWifi == requireWifi) && + (identical(other.showAllCollectionTypes, showAllCollectionTypes) || + other.showAllCollectionTypes == showAllCollectionTypes) && (identical(other.schemeVariant, schemeVariant) || other.schemeVariant == schemeVariant) && (identical(other.libraryPageSize, libraryPageSize) || @@ -478,26 +499,28 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, - syncPath, - position, - size, - timeOut, - nextUpDateCutoff, - themeMode, - themeColor, - amoledBlack, - blurPlaceHolders, - blurUpcomingEpisodes, - selectedLocale, - enableMediaKeys, - posterSize, - pinchPosterZoom, - mouseDragSupport, - requireWifi, - schemeVariant, - libraryPageSize); + int get hashCode => Object.hashAll([ + runtimeType, + syncPath, + position, + size, + timeOut, + nextUpDateCutoff, + themeMode, + themeColor, + amoledBlack, + blurPlaceHolders, + blurUpcomingEpisodes, + selectedLocale, + enableMediaKeys, + posterSize, + pinchPosterZoom, + mouseDragSupport, + requireWifi, + showAllCollectionTypes, + schemeVariant, + libraryPageSize + ]); /// Create a copy of ClientSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -534,6 +557,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { final bool pinchPosterZoom, final bool mouseDragSupport, final bool requireWifi, + final bool showAllCollectionTypes, final DynamicSchemeVariant schemeVariant, final int? libraryPageSize}) = _$ClientSettingsModelImpl; _ClientSettingsModel._() : super._(); @@ -575,6 +599,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { @override bool get requireWifi; @override + bool get showAllCollectionTypes; + @override DynamicSchemeVariant get schemeVariant; @override int? get libraryPageSize; diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index cb33aee..d870f72 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -35,6 +35,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson( pinchPosterZoom: json['pinchPosterZoom'] as bool? ?? false, mouseDragSupport: json['mouseDragSupport'] as bool? ?? false, requireWifi: json['requireWifi'] as bool? ?? true, + showAllCollectionTypes: json['showAllCollectionTypes'] as bool? ?? false, schemeVariant: $enumDecodeNullable( _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? DynamicSchemeVariant.tonalSpot, @@ -60,6 +61,7 @@ Map _$$ClientSettingsModelImplToJson( 'pinchPosterZoom': instance.pinchPosterZoom, 'mouseDragSupport': instance.mouseDragSupport, 'requireWifi': instance.requireWifi, + 'showAllCollectionTypes': instance.showAllCollectionTypes, 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'libraryPageSize': instance.libraryPageSize, }; diff --git a/lib/providers/views_provider.dart b/lib/providers/views_provider.dart index 841f345..d5be586 100644 --- a/lib/providers/views_provider.dart +++ b/lib/providers/views_provider.dart @@ -1,11 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/models/views_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/service_provider.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; + +//Known supported collection types +const enableCollectionTypes = { + CollectionType.movies, + CollectionType.books, + CollectionType.tvshows, + CollectionType.homevideos, + CollectionType.boxsets, + CollectionType.playlists, + CollectionType.photos, + CollectionType.folders, +}; final viewsProvider = StateNotifierProvider((ref) { return ViewsNotifier(ref); @@ -20,8 +34,14 @@ class ViewsNotifier extends StateNotifier { Future fetchViews() async { if (state.loading) return; - final response = await api.usersUserIdViewsGet(); - final createdViews = response.body?.items?.map((e) => ViewModel.fromBodyDto(e, ref)); + final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes)); + final response = await api.usersUserIdViewsGet( + includeExternalContent: showAllCollections, + ); + final createdViews = response.body?.items?.map((e) => ViewModel.fromBodyDto(e, ref)).where((element) { + return showAllCollections ? true : enableCollectionTypes.contains(element.collectionType); + }); + List newList = []; if (createdViews != null) { @@ -32,6 +52,8 @@ class ViewsNotifier extends StateNotifier { parentId: e.id, imageTypeLimit: 1, limit: 16, + includeItemTypes: + (e.collectionType == CollectionType.books && !showAllCollections) ? [BaseItemKind.book] : null, enableImageTypes: [ ImageType.primary, ImageType.backdrop, diff --git a/lib/screens/details_screens/empty_item.dart b/lib/screens/details_screens/empty_item.dart index e40b065..53339b8 100644 --- a/lib/screens/details_screens/empty_item.dart +++ b/lib/screens/details_screens/empty_item.dart @@ -1,8 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/detail_scaffold.dart'; +import 'package:fladder/screens/shared/media/components/poster_placeholder.dart'; +import 'package:fladder/theme.dart'; +import 'package:fladder/util/fladder_image.dart'; +import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; +import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/util/router_extension.dart'; import 'package:fladder/util/string_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class EmptyItem extends ConsumerWidget { final ItemBaseModel item; @@ -12,8 +21,53 @@ class EmptyItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return DetailScaffold( label: "Empty", - content: (padding) => - Center(child: Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet.")), + item: item, + backDrops: item.images, + actions: (context) => item.generateActions( + context, + ref, + exclude: { + ItemActions.play, + ItemActions.playFromStart, + ItemActions.details, + }, + onDeleteSuccesFully: (item) { + if (context.mounted) { + context.router.popBack(); + } + }, + ), + content: (padding) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 350), + child: AspectRatio( + aspectRatio: 0.67, + child: Card( + elevation: 6, + color: Theme.of(context).colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.0, + color: Colors.white.withOpacity(0.10), + ), + borderRadius: FladderTheme.defaultShape.borderRadius, + ), + child: FladderImage( + image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull, + placeHolder: PosterPlaceholder(item: item), + ), + ), + ), + ), + Text( + item.title, + style: Theme.of(context).textTheme.titleLarge, + ), + Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."), + ].addInBetween(const SizedBox(height: 32)), + ), ); } } diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart index bb87805..2452b1f 100644 --- a/lib/screens/details_screens/movie_detail_screen.dart +++ b/lib/screens/details_screens/movie_detail_screen.dart @@ -1,4 +1,3 @@ -import 'package:fladder/util/router_extension.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -20,6 +19,7 @@ import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/util/router_extension.dart'; import 'package:fladder/util/widget_extensions.dart'; import 'package:fladder/widgets/shared/selectable_icon_button.dart'; diff --git a/lib/screens/settings/client_settings_page.dart b/lib/screens/settings/client_settings_page.dart index a56bc1f..3ac2323 100644 --- a/lib/screens/settings/client_settings_page.dart +++ b/lib/screens/settings/client_settings_page.dart @@ -237,6 +237,19 @@ class _ClientSettingsPageState extends ConsumerState { .toList(), ), ), + SettingsListTile( + label: Text(context.localized.clientSettingsShowAllCollectionsTitle), + subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), + trailing: Switch( + value: clientSettings.showAllCollectionTypes, + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: value)), + ), + ), const Divider(), SettingsLabelDivider(label: context.localized.settingsVisual), SettingsListTile( diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 4f166ba..00ccefc 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -9,6 +9,7 @@ import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; +import 'package:fladder/screens/shared/media/components/poster_placeholder.dart'; import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; @@ -57,12 +58,6 @@ class _PosterImageState extends ConsumerState { late String currentTag = widget.heroTag == true ? widget.poster.id : UniqueKey().toString(); bool hover = false; - Widget get placeHolder { - return Center( - child: Icon(widget.poster.type.icon), - ); - } - void pressedWidget() async { if (widget.heroTag == false) { setState(() { @@ -113,7 +108,7 @@ class _PosterImageState extends ConsumerState { children: [ FladderImage( image: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull, - placeHolder: placeHolder, + placeHolder: PosterPlaceholder(item: widget.poster), ), if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book) Align( diff --git a/lib/screens/shared/media/components/poster_placeholder.dart b/lib/screens/shared/media/components/poster_placeholder.dart new file mode 100644 index 0000000..9eb1e2c --- /dev/null +++ b/lib/screens/shared/media/components/poster_placeholder.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:fladder/models/item_base_model.dart'; + +class PosterPlaceholder extends StatelessWidget { + final ItemBaseModel item; + const PosterPlaceholder({required this.item, super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Opacity(opacity: 0.5, child: Icon(item.type.icon)), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + item.title, + maxLines: 2, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + softWrap: true, + ), + if (item.label(context) != null) ...[ + Opacity( + opacity: 0.75, + child: Text( + item.label(context)!, + maxLines: 2, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall, + softWrap: true, + ), + ), + ], + ], + ), + ), + ) + ], + ); + } +}