diff --git a/android/app/build.gradle b/android/app/build.gradle index db2ec0a..2026b86 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { applicationId = "nl.jknaapen.fladder" - minSdk = 23 + minSdkVersion flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/lib/models/account_model.dart b/lib/models/account_model.dart index ada9b28..53c14fb 100644 --- a/lib/models/account_model.dart +++ b/lib/models/account_model.dart @@ -31,7 +31,7 @@ abstract class AccountModel with _$AccountModel { @Default([]) List latestItemsExcludes, @Default([]) List searchQueryHistory, @Default(false) bool quickConnectState, - @Default([]) List savedFilters, + @Default([]) List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration, @JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration, diff --git a/lib/models/account_model.freezed.dart b/lib/models/account_model.freezed.dart index 1ef860f..9220d6f 100644 --- a/lib/models/account_model.freezed.dart +++ b/lib/models/account_model.freezed.dart @@ -24,7 +24,7 @@ mixin _$AccountModel implements DiagnosticableTreeMixin { List get latestItemsExcludes; List get searchQueryHistory; bool get quickConnectState; - List get savedFilters; + List get libraryFilters; @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? get policy; @JsonKey(includeFromJson: false, includeToJson: false) @@ -58,7 +58,7 @@ mixin _$AccountModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('latestItemsExcludes', latestItemsExcludes)) ..add(DiagnosticsProperty('searchQueryHistory', searchQueryHistory)) ..add(DiagnosticsProperty('quickConnectState', quickConnectState)) - ..add(DiagnosticsProperty('savedFilters', savedFilters)) + ..add(DiagnosticsProperty('libraryFilters', libraryFilters)) ..add(DiagnosticsProperty('policy', policy)) ..add(DiagnosticsProperty('serverConfiguration', serverConfiguration)) ..add(DiagnosticsProperty('userConfiguration', userConfiguration)) @@ -67,7 +67,7 @@ mixin _$AccountModel implements DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration, userSettings: $userSettings)'; + return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, libraryFilters: $libraryFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration, userSettings: $userSettings)'; } } @@ -88,7 +88,7 @@ abstract mixin class $AccountModelCopyWith<$Res> { List latestItemsExcludes, List searchQueryHistory, bool quickConnectState, - List savedFilters, + List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration, @@ -121,7 +121,7 @@ class _$AccountModelCopyWithImpl<$Res> implements $AccountModelCopyWith<$Res> { Object? latestItemsExcludes = null, Object? searchQueryHistory = null, Object? quickConnectState = null, - Object? savedFilters = null, + Object? libraryFilters = null, Object? policy = freezed, Object? serverConfiguration = freezed, Object? userConfiguration = freezed, @@ -168,9 +168,9 @@ class _$AccountModelCopyWithImpl<$Res> implements $AccountModelCopyWith<$Res> { ? _self.quickConnectState : quickConnectState // ignore: cast_nullable_to_non_nullable as bool, - savedFilters: null == savedFilters - ? _self.savedFilters - : savedFilters // ignore: cast_nullable_to_non_nullable + libraryFilters: null == libraryFilters + ? _self.libraryFilters + : libraryFilters // ignore: cast_nullable_to_non_nullable as List, policy: freezed == policy ? _self.policy @@ -310,7 +310,7 @@ extension AccountModelPatterns on AccountModel { List latestItemsExcludes, List searchQueryHistory, bool quickConnectState, - List savedFilters, + List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) @@ -335,7 +335,7 @@ extension AccountModelPatterns on AccountModel { _that.latestItemsExcludes, _that.searchQueryHistory, _that.quickConnectState, - _that.savedFilters, + _that.libraryFilters, _that.policy, _that.serverConfiguration, _that.userConfiguration, @@ -371,7 +371,7 @@ extension AccountModelPatterns on AccountModel { List latestItemsExcludes, List searchQueryHistory, bool quickConnectState, - List savedFilters, + List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) @@ -395,7 +395,7 @@ extension AccountModelPatterns on AccountModel { _that.latestItemsExcludes, _that.searchQueryHistory, _that.quickConnectState, - _that.savedFilters, + _that.libraryFilters, _that.policy, _that.serverConfiguration, _that.userConfiguration, @@ -430,7 +430,7 @@ extension AccountModelPatterns on AccountModel { List latestItemsExcludes, List searchQueryHistory, bool quickConnectState, - List savedFilters, + List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) @@ -454,7 +454,7 @@ extension AccountModelPatterns on AccountModel { _that.latestItemsExcludes, _that.searchQueryHistory, _that.quickConnectState, - _that.savedFilters, + _that.libraryFilters, _that.policy, _that.serverConfiguration, _that.userConfiguration, @@ -479,7 +479,7 @@ class _AccountModel extends AccountModel with DiagnosticableTreeMixin { final List latestItemsExcludes = const [], final List searchQueryHistory = const [], this.quickConnectState = false, - final List savedFilters = const [], + final List libraryFilters = const [], @JsonKey(includeFromJson: false, includeToJson: false) this.policy, @JsonKey(includeFromJson: false, includeToJson: false) this.serverConfiguration, @@ -488,7 +488,7 @@ class _AccountModel extends AccountModel with DiagnosticableTreeMixin { this.userSettings}) : _latestItemsExcludes = latestItemsExcludes, _searchQueryHistory = searchQueryHistory, - _savedFilters = savedFilters, + _libraryFilters = libraryFilters, super._(); factory _AccountModel.fromJson(Map json) => _$AccountModelFromJson(json); @@ -532,13 +532,13 @@ class _AccountModel extends AccountModel with DiagnosticableTreeMixin { @override @JsonKey() final bool quickConnectState; - final List _savedFilters; + final List _libraryFilters; @override @JsonKey() - List get savedFilters { - if (_savedFilters is EqualUnmodifiableListView) return _savedFilters; + List get libraryFilters { + if (_libraryFilters is EqualUnmodifiableListView) return _libraryFilters; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_savedFilters); + return EqualUnmodifiableListView(_libraryFilters); } @override @@ -582,7 +582,7 @@ class _AccountModel extends AccountModel with DiagnosticableTreeMixin { ..add(DiagnosticsProperty('latestItemsExcludes', latestItemsExcludes)) ..add(DiagnosticsProperty('searchQueryHistory', searchQueryHistory)) ..add(DiagnosticsProperty('quickConnectState', quickConnectState)) - ..add(DiagnosticsProperty('savedFilters', savedFilters)) + ..add(DiagnosticsProperty('libraryFilters', libraryFilters)) ..add(DiagnosticsProperty('policy', policy)) ..add(DiagnosticsProperty('serverConfiguration', serverConfiguration)) ..add(DiagnosticsProperty('userConfiguration', userConfiguration)) @@ -591,7 +591,7 @@ class _AccountModel extends AccountModel with DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration, userSettings: $userSettings)'; + return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, libraryFilters: $libraryFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration, userSettings: $userSettings)'; } } @@ -614,7 +614,7 @@ abstract mixin class _$AccountModelCopyWith<$Res> List latestItemsExcludes, List searchQueryHistory, bool quickConnectState, - List savedFilters, + List libraryFilters, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration, @@ -649,7 +649,7 @@ class __$AccountModelCopyWithImpl<$Res> Object? latestItemsExcludes = null, Object? searchQueryHistory = null, Object? quickConnectState = null, - Object? savedFilters = null, + Object? libraryFilters = null, Object? policy = freezed, Object? serverConfiguration = freezed, Object? userConfiguration = freezed, @@ -696,9 +696,9 @@ class __$AccountModelCopyWithImpl<$Res> ? _self.quickConnectState : quickConnectState // ignore: cast_nullable_to_non_nullable as bool, - savedFilters: null == savedFilters - ? _self._savedFilters - : savedFilters // ignore: cast_nullable_to_non_nullable + libraryFilters: null == libraryFilters + ? _self._libraryFilters + : libraryFilters // ignore: cast_nullable_to_non_nullable as List, policy: freezed == policy ? _self.policy diff --git a/lib/models/account_model.g.dart b/lib/models/account_model.g.dart index b08538d..457550a 100644 --- a/lib/models/account_model.g.dart +++ b/lib/models/account_model.g.dart @@ -26,7 +26,7 @@ _AccountModel _$AccountModelFromJson(Map json) => .toList() ?? const [], quickConnectState: json['quickConnectState'] as bool? ?? false, - savedFilters: (json['savedFilters'] as List?) + libraryFilters: (json['libraryFilters'] as List?) ?.map((e) => LibraryFiltersModel.fromJson(e as Map)) .toList() ?? @@ -48,7 +48,7 @@ Map _$AccountModelToJson(_AccountModel instance) => 'latestItemsExcludes': instance.latestItemsExcludes, 'searchQueryHistory': instance.searchQueryHistory, 'quickConnectState': instance.quickConnectState, - 'savedFilters': instance.savedFilters, + 'libraryFilters': instance.libraryFilters, 'userSettings': instance.userSettings, }; diff --git a/lib/models/collection_types.dart b/lib/models/collection_types.dart index 282f430..c9e3ce6 100644 --- a/lib/models/collection_types.dart +++ b/lib/models/collection_types.dart @@ -30,6 +30,11 @@ extension CollectionTypeExtension on CollectionType { } } + bool get searchRecursive => switch (this) { + CollectionType.homevideos || CollectionType.photos => false, + _ => true, + }; + IconData getIconType(bool outlined) { switch (this) { case CollectionType.music: diff --git a/lib/models/library_filter_model.dart b/lib/models/library_filter_model.dart new file mode 100644 index 0000000..e9e27de --- /dev/null +++ b/lib/models/library_filter_model.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/library_search/library_search_options.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/util/map_bool_helper.dart'; + +part 'library_filter_model.freezed.dart'; +part 'library_filter_model.g.dart'; + +@Freezed(copyWith: true) +abstract class LibraryFilterModel with _$LibraryFilterModel { + const LibraryFilterModel._(); + + const factory LibraryFilterModel({ + @Default({}) Map genres, + @Default({ + ItemFilter.isplayed: false, + ItemFilter.isunplayed: false, + ItemFilter.isresumable: false, + }) + @Default({}) + Map itemFilters, + @StudioEncoder() @Default({}) Map studios, + @Default({}) Map tags, + @Default({}) Map years, + @Default({}) Map officialRatings, + @Default({ + FladderItemType.audio: false, + FladderItemType.boxset: false, + FladderItemType.book: false, + FladderItemType.collectionFolder: false, + FladderItemType.episode: false, + FladderItemType.folder: false, + FladderItemType.movie: false, + FladderItemType.musicAlbum: false, + FladderItemType.musicVideo: false, + FladderItemType.photo: false, + FladderItemType.person: false, + FladderItemType.photoAlbum: false, + FladderItemType.series: false, + FladderItemType.video: false, + }) + Map types, + @Default(SortingOptions.sortName) SortingOptions sortingOption, + @Default(SortingOrder.ascending) SortingOrder sortOrder, + @Default(false) bool favourites, + @Default(true) bool hideEmptyShows, + @Default(true) bool recursive, + @Default(GroupBy.none) GroupBy groupBy, + }) = _LibraryFilterModel; + + bool get hasActiveFilters { + return genres.hasEnabled || + studios.hasEnabled || + tags.hasEnabled || + years.hasEnabled || + officialRatings.hasEnabled || + hideEmptyShows || + itemFilters.hasEnabled || + !recursive || + favourites; + } + + LibraryFilterModel loadModel(LibraryFilterModel model) { + return copyWith( + genres: genres.replaceMap(model.genres), + itemFilters: itemFilters.replaceMap(model.itemFilters), + studios: studios.replaceMap(model.studios), + tags: tags.replaceMap(model.tags), + years: years.replaceMap(model.years), + officialRatings: officialRatings.replaceMap(model.officialRatings), + types: types.replaceMap(model.types), + sortingOption: model.sortingOption, + sortOrder: model.sortOrder, + favourites: model.favourites, + hideEmptyShows: model.hideEmptyShows, + recursive: model.recursive, + groupBy: model.groupBy, + ); + } + + factory LibraryFilterModel.fromJson(Map json) => _$LibraryFilterModelFromJson(json); + + @override + bool operator ==(covariant LibraryFilterModel other) { + if (identical(this, other)) return true; + return mapEquals(other.genres, genres) && + mapEquals(other.studios, studios) && + mapEquals(other.tags, tags) && + mapEquals(other.years, years) && + mapEquals(other.officialRatings, officialRatings) && + mapEquals(other.types, types) && + other.sortingOption == sortingOption && + other.sortOrder == sortOrder && + other.favourites == favourites && + other.recursive == recursive; + } + + @override + int get hashCode { + return itemFilters.hashCode ^ + genres.hashCode ^ + studios.hashCode ^ + tags.hashCode ^ + years.hashCode ^ + officialRatings.hashCode ^ + types.hashCode ^ + sortingOption.hashCode ^ + sortOrder.hashCode ^ + favourites.hashCode ^ + recursive.hashCode; + } + + LibraryFilterModel clear() { + return copyWith( + genres: genres.setAll(false), + tags: tags.setAll(false), + officialRatings: officialRatings.setAll(false), + years: years.setAll(false), + favourites: false, + recursive: true, + studios: studios.setAll(false), + itemFilters: itemFilters.setAll(false), + hideEmptyShows: false, + ); + } +} + +class StudioEncoder implements JsonConverter, String> { + const StudioEncoder(); + + @override + Map fromJson(String json) { + final decodedMap = jsonDecode(json) as Map; + final studios = decodedMap.map((key, value) => MapEntry(Studio.fromJson(key), value as bool)); + return studios; + } + + @override + String toJson(Map studios) => jsonEncode(studios.map((key, value) => MapEntry(key.toJson(), value))); +} + +extension LibrarySearchRouteExtension on LibrarySearchRoute { + LibrarySearchRoute withFilter(LibraryFilterModel model) { + return LibrarySearchRoute( + viewModelId: args?.viewModelId, + folderId: args?.folderId, + favourites: model.favourites, + sortOrder: model.sortOrder, + sortingOptions: model.sortingOption, + types: model.types, + genres: model.genres, + recursive: model.recursive, + ); + } +} diff --git a/lib/models/library_filter_model.freezed.dart b/lib/models/library_filter_model.freezed.dart new file mode 100644 index 0000000..f73fdc5 --- /dev/null +++ b/lib/models/library_filter_model.freezed.dart @@ -0,0 +1,691 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'library_filter_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$LibraryFilterModel implements DiagnosticableTreeMixin { + Map get genres; + Map get itemFilters; + @StudioEncoder() + Map get studios; + Map get tags; + Map get years; + Map get officialRatings; + Map get types; + SortingOptions get sortingOption; + SortingOrder get sortOrder; + bool get favourites; + bool get hideEmptyShows; + bool get recursive; + GroupBy get groupBy; + + /// Create a copy of LibraryFilterModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $LibraryFilterModelCopyWith get copyWith => + _$LibraryFilterModelCopyWithImpl( + this as LibraryFilterModel, _$identity); + + /// Serializes this LibraryFilterModel to a JSON map. + Map toJson(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'LibraryFilterModel')) + ..add(DiagnosticsProperty('genres', genres)) + ..add(DiagnosticsProperty('itemFilters', itemFilters)) + ..add(DiagnosticsProperty('studios', studios)) + ..add(DiagnosticsProperty('tags', tags)) + ..add(DiagnosticsProperty('years', years)) + ..add(DiagnosticsProperty('officialRatings', officialRatings)) + ..add(DiagnosticsProperty('types', types)) + ..add(DiagnosticsProperty('sortingOption', sortingOption)) + ..add(DiagnosticsProperty('sortOrder', sortOrder)) + ..add(DiagnosticsProperty('favourites', favourites)) + ..add(DiagnosticsProperty('hideEmptyShows', hideEmptyShows)) + ..add(DiagnosticsProperty('recursive', recursive)) + ..add(DiagnosticsProperty('groupBy', groupBy)); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'LibraryFilterModel(genres: $genres, itemFilters: $itemFilters, studios: $studios, tags: $tags, years: $years, officialRatings: $officialRatings, types: $types, sortingOption: $sortingOption, sortOrder: $sortOrder, favourites: $favourites, hideEmptyShows: $hideEmptyShows, recursive: $recursive, groupBy: $groupBy)'; + } +} + +/// @nodoc +abstract mixin class $LibraryFilterModelCopyWith<$Res> { + factory $LibraryFilterModelCopyWith( + LibraryFilterModel value, $Res Function(LibraryFilterModel) _then) = + _$LibraryFilterModelCopyWithImpl; + @useResult + $Res call( + {Map genres, + Map itemFilters, + @StudioEncoder() Map studios, + Map tags, + Map years, + Map officialRatings, + Map types, + SortingOptions sortingOption, + SortingOrder sortOrder, + bool favourites, + bool hideEmptyShows, + bool recursive, + GroupBy groupBy}); +} + +/// @nodoc +class _$LibraryFilterModelCopyWithImpl<$Res> + implements $LibraryFilterModelCopyWith<$Res> { + _$LibraryFilterModelCopyWithImpl(this._self, this._then); + + final LibraryFilterModel _self; + final $Res Function(LibraryFilterModel) _then; + + /// Create a copy of LibraryFilterModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? genres = null, + Object? itemFilters = null, + Object? studios = null, + Object? tags = null, + Object? years = null, + Object? officialRatings = null, + Object? types = null, + Object? sortingOption = null, + Object? sortOrder = null, + Object? favourites = null, + Object? hideEmptyShows = null, + Object? recursive = null, + Object? groupBy = null, + }) { + return _then(_self.copyWith( + genres: null == genres + ? _self.genres + : genres // ignore: cast_nullable_to_non_nullable + as Map, + itemFilters: null == itemFilters + ? _self.itemFilters + : itemFilters // ignore: cast_nullable_to_non_nullable + as Map, + studios: null == studios + ? _self.studios + : studios // ignore: cast_nullable_to_non_nullable + as Map, + tags: null == tags + ? _self.tags + : tags // ignore: cast_nullable_to_non_nullable + as Map, + years: null == years + ? _self.years + : years // ignore: cast_nullable_to_non_nullable + as Map, + officialRatings: null == officialRatings + ? _self.officialRatings + : officialRatings // ignore: cast_nullable_to_non_nullable + as Map, + types: null == types + ? _self.types + : types // ignore: cast_nullable_to_non_nullable + as Map, + sortingOption: null == sortingOption + ? _self.sortingOption + : sortingOption // ignore: cast_nullable_to_non_nullable + as SortingOptions, + sortOrder: null == sortOrder + ? _self.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as SortingOrder, + favourites: null == favourites + ? _self.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as bool, + hideEmptyShows: null == hideEmptyShows + ? _self.hideEmptyShows + : hideEmptyShows // ignore: cast_nullable_to_non_nullable + as bool, + recursive: null == recursive + ? _self.recursive + : recursive // ignore: cast_nullable_to_non_nullable + as bool, + groupBy: null == groupBy + ? _self.groupBy + : groupBy // ignore: cast_nullable_to_non_nullable + as GroupBy, + )); + } +} + +/// Adds pattern-matching-related methods to [LibraryFilterModel]. +extension LibraryFilterModelPatterns on LibraryFilterModel { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_LibraryFilterModel value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _LibraryFilterModel() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_LibraryFilterModel value) $default, + ) { + final _that = this; + switch (_that) { + case _LibraryFilterModel(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_LibraryFilterModel value)? $default, + ) { + final _that = this; + switch (_that) { + case _LibraryFilterModel() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + Map genres, + Map itemFilters, + @StudioEncoder() Map studios, + Map tags, + Map years, + Map officialRatings, + Map types, + SortingOptions sortingOption, + SortingOrder sortOrder, + bool favourites, + bool hideEmptyShows, + bool recursive, + GroupBy groupBy)? + $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _LibraryFilterModel() when $default != null: + return $default( + _that.genres, + _that.itemFilters, + _that.studios, + _that.tags, + _that.years, + _that.officialRatings, + _that.types, + _that.sortingOption, + _that.sortOrder, + _that.favourites, + _that.hideEmptyShows, + _that.recursive, + _that.groupBy); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function( + Map genres, + Map itemFilters, + @StudioEncoder() Map studios, + Map tags, + Map years, + Map officialRatings, + Map types, + SortingOptions sortingOption, + SortingOrder sortOrder, + bool favourites, + bool hideEmptyShows, + bool recursive, + GroupBy groupBy) + $default, + ) { + final _that = this; + switch (_that) { + case _LibraryFilterModel(): + return $default( + _that.genres, + _that.itemFilters, + _that.studios, + _that.tags, + _that.years, + _that.officialRatings, + _that.types, + _that.sortingOption, + _that.sortOrder, + _that.favourites, + _that.hideEmptyShows, + _that.recursive, + _that.groupBy); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function( + Map genres, + Map itemFilters, + @StudioEncoder() Map studios, + Map tags, + Map years, + Map officialRatings, + Map types, + SortingOptions sortingOption, + SortingOrder sortOrder, + bool favourites, + bool hideEmptyShows, + bool recursive, + GroupBy groupBy)? + $default, + ) { + final _that = this; + switch (_that) { + case _LibraryFilterModel() when $default != null: + return $default( + _that.genres, + _that.itemFilters, + _that.studios, + _that.tags, + _that.years, + _that.officialRatings, + _that.types, + _that.sortingOption, + _that.sortOrder, + _that.favourites, + _that.hideEmptyShows, + _that.recursive, + _that.groupBy); + case _: + return null; + } + } +} + +/// @nodoc +@JsonSerializable() +class _LibraryFilterModel extends LibraryFilterModel + with DiagnosticableTreeMixin { + const _LibraryFilterModel( + {final Map genres = const {}, + final Map itemFilters = const { + ItemFilter.isplayed: false, + ItemFilter.isunplayed: false, + ItemFilter.isresumable: false + }, + @StudioEncoder() final Map studios = const {}, + final Map tags = const {}, + final Map years = const {}, + final Map officialRatings = const {}, + final Map types = const { + FladderItemType.audio: false, + FladderItemType.boxset: false, + FladderItemType.book: false, + FladderItemType.collectionFolder: false, + FladderItemType.episode: false, + FladderItemType.folder: false, + FladderItemType.movie: false, + FladderItemType.musicAlbum: false, + FladderItemType.musicVideo: false, + FladderItemType.photo: false, + FladderItemType.person: false, + FladderItemType.photoAlbum: false, + FladderItemType.series: false, + FladderItemType.video: false + }, + this.sortingOption = SortingOptions.sortName, + this.sortOrder = SortingOrder.ascending, + this.favourites = false, + this.hideEmptyShows = true, + this.recursive = true, + this.groupBy = GroupBy.none}) + : _genres = genres, + _itemFilters = itemFilters, + _studios = studios, + _tags = tags, + _years = years, + _officialRatings = officialRatings, + _types = types, + super._(); + factory _LibraryFilterModel.fromJson(Map json) => + _$LibraryFilterModelFromJson(json); + + final Map _genres; + @override + @JsonKey() + Map get genres { + if (_genres is EqualUnmodifiableMapView) return _genres; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_genres); + } + + final Map _itemFilters; + @override + @JsonKey() + Map get itemFilters { + if (_itemFilters is EqualUnmodifiableMapView) return _itemFilters; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_itemFilters); + } + + final Map _studios; + @override + @JsonKey() + @StudioEncoder() + Map get studios { + if (_studios is EqualUnmodifiableMapView) return _studios; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_studios); + } + + final Map _tags; + @override + @JsonKey() + Map get tags { + if (_tags is EqualUnmodifiableMapView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_tags); + } + + final Map _years; + @override + @JsonKey() + Map get years { + if (_years is EqualUnmodifiableMapView) return _years; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_years); + } + + final Map _officialRatings; + @override + @JsonKey() + Map get officialRatings { + if (_officialRatings is EqualUnmodifiableMapView) return _officialRatings; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_officialRatings); + } + + final Map _types; + @override + @JsonKey() + Map get types { + if (_types is EqualUnmodifiableMapView) return _types; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_types); + } + + @override + @JsonKey() + final SortingOptions sortingOption; + @override + @JsonKey() + final SortingOrder sortOrder; + @override + @JsonKey() + final bool favourites; + @override + @JsonKey() + final bool hideEmptyShows; + @override + @JsonKey() + final bool recursive; + @override + @JsonKey() + final GroupBy groupBy; + + /// Create a copy of LibraryFilterModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$LibraryFilterModelCopyWith<_LibraryFilterModel> get copyWith => + __$LibraryFilterModelCopyWithImpl<_LibraryFilterModel>(this, _$identity); + + @override + Map toJson() { + return _$LibraryFilterModelToJson( + this, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'LibraryFilterModel')) + ..add(DiagnosticsProperty('genres', genres)) + ..add(DiagnosticsProperty('itemFilters', itemFilters)) + ..add(DiagnosticsProperty('studios', studios)) + ..add(DiagnosticsProperty('tags', tags)) + ..add(DiagnosticsProperty('years', years)) + ..add(DiagnosticsProperty('officialRatings', officialRatings)) + ..add(DiagnosticsProperty('types', types)) + ..add(DiagnosticsProperty('sortingOption', sortingOption)) + ..add(DiagnosticsProperty('sortOrder', sortOrder)) + ..add(DiagnosticsProperty('favourites', favourites)) + ..add(DiagnosticsProperty('hideEmptyShows', hideEmptyShows)) + ..add(DiagnosticsProperty('recursive', recursive)) + ..add(DiagnosticsProperty('groupBy', groupBy)); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'LibraryFilterModel(genres: $genres, itemFilters: $itemFilters, studios: $studios, tags: $tags, years: $years, officialRatings: $officialRatings, types: $types, sortingOption: $sortingOption, sortOrder: $sortOrder, favourites: $favourites, hideEmptyShows: $hideEmptyShows, recursive: $recursive, groupBy: $groupBy)'; + } +} + +/// @nodoc +abstract mixin class _$LibraryFilterModelCopyWith<$Res> + implements $LibraryFilterModelCopyWith<$Res> { + factory _$LibraryFilterModelCopyWith( + _LibraryFilterModel value, $Res Function(_LibraryFilterModel) _then) = + __$LibraryFilterModelCopyWithImpl; + @override + @useResult + $Res call( + {Map genres, + Map itemFilters, + @StudioEncoder() Map studios, + Map tags, + Map years, + Map officialRatings, + Map types, + SortingOptions sortingOption, + SortingOrder sortOrder, + bool favourites, + bool hideEmptyShows, + bool recursive, + GroupBy groupBy}); +} + +/// @nodoc +class __$LibraryFilterModelCopyWithImpl<$Res> + implements _$LibraryFilterModelCopyWith<$Res> { + __$LibraryFilterModelCopyWithImpl(this._self, this._then); + + final _LibraryFilterModel _self; + final $Res Function(_LibraryFilterModel) _then; + + /// Create a copy of LibraryFilterModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? genres = null, + Object? itemFilters = null, + Object? studios = null, + Object? tags = null, + Object? years = null, + Object? officialRatings = null, + Object? types = null, + Object? sortingOption = null, + Object? sortOrder = null, + Object? favourites = null, + Object? hideEmptyShows = null, + Object? recursive = null, + Object? groupBy = null, + }) { + return _then(_LibraryFilterModel( + genres: null == genres + ? _self._genres + : genres // ignore: cast_nullable_to_non_nullable + as Map, + itemFilters: null == itemFilters + ? _self._itemFilters + : itemFilters // ignore: cast_nullable_to_non_nullable + as Map, + studios: null == studios + ? _self._studios + : studios // ignore: cast_nullable_to_non_nullable + as Map, + tags: null == tags + ? _self._tags + : tags // ignore: cast_nullable_to_non_nullable + as Map, + years: null == years + ? _self._years + : years // ignore: cast_nullable_to_non_nullable + as Map, + officialRatings: null == officialRatings + ? _self._officialRatings + : officialRatings // ignore: cast_nullable_to_non_nullable + as Map, + types: null == types + ? _self._types + : types // ignore: cast_nullable_to_non_nullable + as Map, + sortingOption: null == sortingOption + ? _self.sortingOption + : sortingOption // ignore: cast_nullable_to_non_nullable + as SortingOptions, + sortOrder: null == sortOrder + ? _self.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as SortingOrder, + favourites: null == favourites + ? _self.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as bool, + hideEmptyShows: null == hideEmptyShows + ? _self.hideEmptyShows + : hideEmptyShows // ignore: cast_nullable_to_non_nullable + as bool, + recursive: null == recursive + ? _self.recursive + : recursive // ignore: cast_nullable_to_non_nullable + as bool, + groupBy: null == groupBy + ? _self.groupBy + : groupBy // ignore: cast_nullable_to_non_nullable + as GroupBy, + )); + } +} + +// dart format on diff --git a/lib/models/library_filter_model.g.dart b/lib/models/library_filter_model.g.dart new file mode 100644 index 0000000..3963db9 --- /dev/null +++ b/lib/models/library_filter_model.g.dart @@ -0,0 +1,152 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_filter_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_LibraryFilterModel _$LibraryFilterModelFromJson(Map json) => + _LibraryFilterModel( + genres: (json['genres'] as Map?)?.map( + (k, e) => MapEntry(k, e as bool), + ) ?? + const {}, + itemFilters: (json['itemFilters'] as Map?)?.map( + (k, e) => MapEntry($enumDecode(_$ItemFilterEnumMap, k), e as bool), + ) ?? + const { + ItemFilter.isplayed: false, + ItemFilter.isunplayed: false, + ItemFilter.isresumable: false + }, + studios: json['studios'] == null + ? const {} + : const StudioEncoder().fromJson(json['studios'] as String), + tags: (json['tags'] as Map?)?.map( + (k, e) => MapEntry(k, e as bool), + ) ?? + const {}, + years: (json['years'] as Map?)?.map( + (k, e) => MapEntry(int.parse(k), e as bool), + ) ?? + const {}, + officialRatings: (json['officialRatings'] as Map?)?.map( + (k, e) => MapEntry(k, e as bool), + ) ?? + const {}, + types: (json['types'] as Map?)?.map( + (k, e) => + MapEntry($enumDecode(_$FladderItemTypeEnumMap, k), e as bool), + ) ?? + const { + FladderItemType.audio: false, + FladderItemType.boxset: false, + FladderItemType.book: false, + FladderItemType.collectionFolder: false, + FladderItemType.episode: false, + FladderItemType.folder: false, + FladderItemType.movie: false, + FladderItemType.musicAlbum: false, + FladderItemType.musicVideo: false, + FladderItemType.photo: false, + FladderItemType.person: false, + FladderItemType.photoAlbum: false, + FladderItemType.series: false, + FladderItemType.video: false + }, + sortingOption: + $enumDecodeNullable(_$SortingOptionsEnumMap, json['sortingOption']) ?? + SortingOptions.sortName, + sortOrder: + $enumDecodeNullable(_$SortingOrderEnumMap, json['sortOrder']) ?? + SortingOrder.ascending, + favourites: json['favourites'] as bool? ?? false, + hideEmptyShows: json['hideEmptyShows'] as bool? ?? true, + recursive: json['recursive'] as bool? ?? true, + groupBy: $enumDecodeNullable(_$GroupByEnumMap, json['groupBy']) ?? + GroupBy.none, + ); + +Map _$LibraryFilterModelToJson(_LibraryFilterModel instance) => + { + 'genres': instance.genres, + 'itemFilters': instance.itemFilters + .map((k, e) => MapEntry(_$ItemFilterEnumMap[k], e)), + 'studios': const StudioEncoder().toJson(instance.studios), + 'tags': instance.tags, + 'years': instance.years.map((k, e) => MapEntry(k.toString(), e)), + 'officialRatings': instance.officialRatings, + 'types': instance.types + .map((k, e) => MapEntry(_$FladderItemTypeEnumMap[k]!, e)), + 'sortingOption': _$SortingOptionsEnumMap[instance.sortingOption]!, + 'sortOrder': _$SortingOrderEnumMap[instance.sortOrder]!, + 'favourites': instance.favourites, + 'hideEmptyShows': instance.hideEmptyShows, + 'recursive': instance.recursive, + 'groupBy': _$GroupByEnumMap[instance.groupBy]!, + }; + +const _$ItemFilterEnumMap = { + ItemFilter.swaggerGeneratedUnknown: null, + ItemFilter.isfolder: 'IsFolder', + ItemFilter.isnotfolder: 'IsNotFolder', + ItemFilter.isunplayed: 'IsUnplayed', + ItemFilter.isplayed: 'IsPlayed', + ItemFilter.isfavorite: 'IsFavorite', + ItemFilter.isresumable: 'IsResumable', + ItemFilter.likes: 'Likes', + ItemFilter.dislikes: 'Dislikes', + ItemFilter.isfavoriteorlikes: 'IsFavoriteOrLikes', +}; + +const _$FladderItemTypeEnumMap = { + FladderItemType.baseType: 'baseType', + FladderItemType.audio: 'audio', + FladderItemType.musicAlbum: 'musicAlbum', + FladderItemType.musicVideo: 'musicVideo', + FladderItemType.collectionFolder: 'collectionFolder', + FladderItemType.video: 'video', + FladderItemType.movie: 'movie', + FladderItemType.series: 'series', + FladderItemType.season: 'season', + FladderItemType.episode: 'episode', + FladderItemType.photo: 'photo', + FladderItemType.person: 'person', + FladderItemType.photoAlbum: 'photoAlbum', + FladderItemType.folder: 'folder', + FladderItemType.boxset: 'boxset', + FladderItemType.playlist: 'playlist', + FladderItemType.book: 'book', +}; + +const _$SortingOptionsEnumMap = { + SortingOptions.sortName: 'sortName', + SortingOptions.communityRating: 'communityRating', + SortingOptions.parentalRating: 'parentalRating', + SortingOptions.dateAdded: 'dateAdded', + SortingOptions.dateLastContentAdded: 'dateLastContentAdded', + SortingOptions.favorite: 'favorite', + SortingOptions.datePlayed: 'datePlayed', + SortingOptions.folders: 'folders', + SortingOptions.playCount: 'playCount', + SortingOptions.releaseDate: 'releaseDate', + SortingOptions.runTime: 'runTime', + SortingOptions.random: 'random', +}; + +const _$SortingOrderEnumMap = { + SortingOrder.ascending: 'ascending', + SortingOrder.descending: 'descending', +}; + +const _$GroupByEnumMap = { + GroupBy.none: 'none', + GroupBy.name: 'name', + GroupBy.genres: 'genres', + GroupBy.dateAdded: 'dateAdded', + GroupBy.tags: 'tags', + GroupBy.releaseDate: 'releaseDate', + GroupBy.rating: 'rating', + GroupBy.type: 'type', +}; diff --git a/lib/models/library_filters_model.dart b/lib/models/library_filters_model.dart index 9ba2dcf..fe2995a 100644 --- a/lib/models/library_filters_model.dart +++ b/lib/models/library_filters_model.dart @@ -1,14 +1,8 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:xid/xid.dart'; -import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; -import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; -import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; -import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/util/map_bool_helper.dart'; part 'library_filters_model.freezed.dart'; @@ -22,20 +16,8 @@ abstract class LibraryFiltersModel with _$LibraryFiltersModel { required String id, required String name, required bool isFavourite, - required List ids, - required Map genres, - required Map filters, - @StudioEncoder() required Map studios, - required Map tags, - required Map years, - required Map officialRatings, - required Map types, - required SortingOptions sortingOption, - required SortingOrder sortOrder, - required bool favourites, - required bool hideEmptyShows, - required bool recursive, - required GroupBy groupBy, + @Default([]) List ids, + @Default(LibraryFilterModel()) LibraryFilterModel filter, }) = _LibraryFiltersModel; factory LibraryFiltersModel.fromJson(Map json) => _$LibraryFiltersModelFromJson(json); @@ -51,35 +33,9 @@ abstract class LibraryFiltersModel with _$LibraryFiltersModel { name: name, isFavourite: isFavourite ?? false, ids: searchModel.views.included.map((e) => e.id).toList(), - genres: searchModel.genres, - filters: searchModel.filters, - studios: searchModel.studios, - tags: searchModel.tags, - years: searchModel.years, - officialRatings: searchModel.officialRatings, - types: searchModel.types, - sortingOption: searchModel.sortingOption, - sortOrder: searchModel.sortOrder, - favourites: searchModel.favourites, - hideEmptyShows: searchModel.hideEmptyShows, - recursive: searchModel.recursive, - groupBy: searchModel.groupBy, + filter: searchModel.filters, ); } bool containsSameIds(List otherIds) => ids.length == otherIds.length && Set.from(ids).containsAll(otherIds); } - -class StudioEncoder implements JsonConverter, String> { - const StudioEncoder(); - - @override - Map fromJson(String json) { - final decodedMap = jsonDecode(json) as Map; - final studios = decodedMap.map((key, value) => MapEntry(Studio.fromJson(key), value as bool)); - return studios; - } - - @override - String toJson(Map studios) => jsonEncode(studios.map((key, value) => MapEntry(key.toJson(), value))); -} diff --git a/lib/models/library_filters_model.freezed.dart b/lib/models/library_filters_model.freezed.dart index 93bb215..114a4e4 100644 --- a/lib/models/library_filters_model.freezed.dart +++ b/lib/models/library_filters_model.freezed.dart @@ -18,20 +18,7 @@ mixin _$LibraryFiltersModel { String get name; bool get isFavourite; List get ids; - Map get genres; - Map get filters; - @StudioEncoder() - Map get studios; - Map get tags; - Map get years; - Map get officialRatings; - Map get types; - SortingOptions get sortingOption; - SortingOrder get sortOrder; - bool get favourites; - bool get hideEmptyShows; - bool get recursive; - GroupBy get groupBy; + LibraryFilterModel get filter; /// Create a copy of LibraryFiltersModel /// with the given fields replaced by the non-null parameter values. @@ -46,7 +33,7 @@ mixin _$LibraryFiltersModel { @override String toString() { - return 'LibraryFiltersModel(id: $id, name: $name, isFavourite: $isFavourite, ids: $ids, genres: $genres, filters: $filters, studios: $studios, tags: $tags, years: $years, officialRatings: $officialRatings, types: $types, sortingOption: $sortingOption, sortOrder: $sortOrder, favourites: $favourites, hideEmptyShows: $hideEmptyShows, recursive: $recursive, groupBy: $groupBy)'; + return 'LibraryFiltersModel(id: $id, name: $name, isFavourite: $isFavourite, ids: $ids, filter: $filter)'; } } @@ -61,19 +48,9 @@ abstract mixin class $LibraryFiltersModelCopyWith<$Res> { String name, bool isFavourite, List ids, - Map genres, - Map filters, - @StudioEncoder() Map studios, - Map tags, - Map years, - Map officialRatings, - Map types, - SortingOptions sortingOption, - SortingOrder sortOrder, - bool favourites, - bool hideEmptyShows, - bool recursive, - GroupBy groupBy}); + LibraryFilterModel filter}); + + $LibraryFilterModelCopyWith<$Res> get filter; } /// @nodoc @@ -93,19 +70,7 @@ class _$LibraryFiltersModelCopyWithImpl<$Res> Object? name = null, Object? isFavourite = null, Object? ids = null, - Object? genres = null, - Object? filters = null, - Object? studios = null, - Object? tags = null, - Object? years = null, - Object? officialRatings = null, - Object? types = null, - Object? sortingOption = null, - Object? sortOrder = null, - Object? favourites = null, - Object? hideEmptyShows = null, - Object? recursive = null, - Object? groupBy = null, + Object? filter = null, }) { return _then(_self.copyWith( id: null == id @@ -124,60 +89,22 @@ class _$LibraryFiltersModelCopyWithImpl<$Res> ? _self.ids : ids // ignore: cast_nullable_to_non_nullable as List, - genres: null == genres - ? _self.genres - : genres // ignore: cast_nullable_to_non_nullable - as Map, - filters: null == filters - ? _self.filters - : filters // ignore: cast_nullable_to_non_nullable - as Map, - studios: null == studios - ? _self.studios - : studios // ignore: cast_nullable_to_non_nullable - as Map, - tags: null == tags - ? _self.tags - : tags // ignore: cast_nullable_to_non_nullable - as Map, - years: null == years - ? _self.years - : years // ignore: cast_nullable_to_non_nullable - as Map, - officialRatings: null == officialRatings - ? _self.officialRatings - : officialRatings // ignore: cast_nullable_to_non_nullable - as Map, - types: null == types - ? _self.types - : types // ignore: cast_nullable_to_non_nullable - as Map, - sortingOption: null == sortingOption - ? _self.sortingOption - : sortingOption // ignore: cast_nullable_to_non_nullable - as SortingOptions, - sortOrder: null == sortOrder - ? _self.sortOrder - : sortOrder // ignore: cast_nullable_to_non_nullable - as SortingOrder, - favourites: null == favourites - ? _self.favourites - : favourites // ignore: cast_nullable_to_non_nullable - as bool, - hideEmptyShows: null == hideEmptyShows - ? _self.hideEmptyShows - : hideEmptyShows // ignore: cast_nullable_to_non_nullable - as bool, - recursive: null == recursive - ? _self.recursive - : recursive // ignore: cast_nullable_to_non_nullable - as bool, - groupBy: null == groupBy - ? _self.groupBy - : groupBy // ignore: cast_nullable_to_non_nullable - as GroupBy, + filter: null == filter + ? _self.filter + : filter // ignore: cast_nullable_to_non_nullable + as LibraryFilterModel, )); } + + /// Create a copy of LibraryFiltersModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LibraryFilterModelCopyWith<$Res> get filter { + return $LibraryFilterModelCopyWith<$Res>(_self.filter, (value) { + return _then(_self.copyWith(filter: value)); + }); + } } /// Adds pattern-matching-related methods to [LibraryFiltersModel]. @@ -273,24 +200,8 @@ extension LibraryFiltersModelPatterns on LibraryFiltersModel { @optionalTypeArgs TResult maybeWhen( - TResult Function( - String id, - String name, - bool isFavourite, - List ids, - Map genres, - Map filters, - @StudioEncoder() Map studios, - Map tags, - Map years, - Map officialRatings, - Map types, - SortingOptions sortingOption, - SortingOrder sortOrder, - bool favourites, - bool hideEmptyShows, - bool recursive, - GroupBy groupBy)? + TResult Function(String id, String name, bool isFavourite, List ids, + LibraryFilterModel filter)? $default, { required TResult orElse(), }) { @@ -298,23 +209,7 @@ extension LibraryFiltersModelPatterns on LibraryFiltersModel { switch (_that) { case _LibraryFiltersModel() when $default != null: return $default( - _that.id, - _that.name, - _that.isFavourite, - _that.ids, - _that.genres, - _that.filters, - _that.studios, - _that.tags, - _that.years, - _that.officialRatings, - _that.types, - _that.sortingOption, - _that.sortOrder, - _that.favourites, - _that.hideEmptyShows, - _that.recursive, - _that.groupBy); + _that.id, _that.name, _that.isFavourite, _that.ids, _that.filter); case _: return orElse(); } @@ -335,47 +230,15 @@ extension LibraryFiltersModelPatterns on LibraryFiltersModel { @optionalTypeArgs TResult when( - TResult Function( - String id, - String name, - bool isFavourite, - List ids, - Map genres, - Map filters, - @StudioEncoder() Map studios, - Map tags, - Map years, - Map officialRatings, - Map types, - SortingOptions sortingOption, - SortingOrder sortOrder, - bool favourites, - bool hideEmptyShows, - bool recursive, - GroupBy groupBy) + TResult Function(String id, String name, bool isFavourite, List ids, + LibraryFilterModel filter) $default, ) { final _that = this; switch (_that) { case _LibraryFiltersModel(): return $default( - _that.id, - _that.name, - _that.isFavourite, - _that.ids, - _that.genres, - _that.filters, - _that.studios, - _that.tags, - _that.years, - _that.officialRatings, - _that.types, - _that.sortingOption, - _that.sortOrder, - _that.favourites, - _that.hideEmptyShows, - _that.recursive, - _that.groupBy); + _that.id, _that.name, _that.isFavourite, _that.ids, _that.filter); case _: throw StateError('Unexpected subclass'); } @@ -395,47 +258,15 @@ extension LibraryFiltersModelPatterns on LibraryFiltersModel { @optionalTypeArgs TResult? whenOrNull( - TResult? Function( - String id, - String name, - bool isFavourite, - List ids, - Map genres, - Map filters, - @StudioEncoder() Map studios, - Map tags, - Map years, - Map officialRatings, - Map types, - SortingOptions sortingOption, - SortingOrder sortOrder, - bool favourites, - bool hideEmptyShows, - bool recursive, - GroupBy groupBy)? + TResult? Function(String id, String name, bool isFavourite, + List ids, LibraryFilterModel filter)? $default, ) { final _that = this; switch (_that) { case _LibraryFiltersModel() when $default != null: return $default( - _that.id, - _that.name, - _that.isFavourite, - _that.ids, - _that.genres, - _that.filters, - _that.studios, - _that.tags, - _that.years, - _that.officialRatings, - _that.types, - _that.sortingOption, - _that.sortOrder, - _that.favourites, - _that.hideEmptyShows, - _that.recursive, - _that.groupBy); + _that.id, _that.name, _that.isFavourite, _that.ids, _that.filter); case _: return null; } @@ -449,28 +280,9 @@ class _LibraryFiltersModel extends LibraryFiltersModel { {required this.id, required this.name, required this.isFavourite, - required final List ids, - required final Map genres, - required final Map filters, - @StudioEncoder() required final Map studios, - required final Map tags, - required final Map years, - required final Map officialRatings, - required final Map types, - required this.sortingOption, - required this.sortOrder, - required this.favourites, - required this.hideEmptyShows, - required this.recursive, - required this.groupBy}) + final List ids = const [], + this.filter = const LibraryFilterModel()}) : _ids = ids, - _genres = genres, - _filters = filters, - _studios = studios, - _tags = tags, - _years = years, - _officialRatings = officialRatings, - _types = types, super._(); factory _LibraryFiltersModel.fromJson(Map json) => _$LibraryFiltersModelFromJson(json); @@ -483,81 +295,16 @@ class _LibraryFiltersModel extends LibraryFiltersModel { final bool isFavourite; final List _ids; @override + @JsonKey() List get ids { if (_ids is EqualUnmodifiableListView) return _ids; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_ids); } - final Map _genres; @override - Map get genres { - if (_genres is EqualUnmodifiableMapView) return _genres; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_genres); - } - - final Map _filters; - @override - Map get filters { - if (_filters is EqualUnmodifiableMapView) return _filters; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_filters); - } - - final Map _studios; - @override - @StudioEncoder() - Map get studios { - if (_studios is EqualUnmodifiableMapView) return _studios; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_studios); - } - - final Map _tags; - @override - Map get tags { - if (_tags is EqualUnmodifiableMapView) return _tags; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_tags); - } - - final Map _years; - @override - Map get years { - if (_years is EqualUnmodifiableMapView) return _years; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_years); - } - - final Map _officialRatings; - @override - Map get officialRatings { - if (_officialRatings is EqualUnmodifiableMapView) return _officialRatings; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_officialRatings); - } - - final Map _types; - @override - Map get types { - if (_types is EqualUnmodifiableMapView) return _types; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_types); - } - - @override - final SortingOptions sortingOption; - @override - final SortingOrder sortOrder; - @override - final bool favourites; - @override - final bool hideEmptyShows; - @override - final bool recursive; - @override - final GroupBy groupBy; + @JsonKey() + final LibraryFilterModel filter; /// Create a copy of LibraryFiltersModel /// with the given fields replaced by the non-null parameter values. @@ -577,7 +324,7 @@ class _LibraryFiltersModel extends LibraryFiltersModel { @override String toString() { - return 'LibraryFiltersModel(id: $id, name: $name, isFavourite: $isFavourite, ids: $ids, genres: $genres, filters: $filters, studios: $studios, tags: $tags, years: $years, officialRatings: $officialRatings, types: $types, sortingOption: $sortingOption, sortOrder: $sortOrder, favourites: $favourites, hideEmptyShows: $hideEmptyShows, recursive: $recursive, groupBy: $groupBy)'; + return 'LibraryFiltersModel(id: $id, name: $name, isFavourite: $isFavourite, ids: $ids, filter: $filter)'; } } @@ -594,19 +341,10 @@ abstract mixin class _$LibraryFiltersModelCopyWith<$Res> String name, bool isFavourite, List ids, - Map genres, - Map filters, - @StudioEncoder() Map studios, - Map tags, - Map years, - Map officialRatings, - Map types, - SortingOptions sortingOption, - SortingOrder sortOrder, - bool favourites, - bool hideEmptyShows, - bool recursive, - GroupBy groupBy}); + LibraryFilterModel filter}); + + @override + $LibraryFilterModelCopyWith<$Res> get filter; } /// @nodoc @@ -626,19 +364,7 @@ class __$LibraryFiltersModelCopyWithImpl<$Res> Object? name = null, Object? isFavourite = null, Object? ids = null, - Object? genres = null, - Object? filters = null, - Object? studios = null, - Object? tags = null, - Object? years = null, - Object? officialRatings = null, - Object? types = null, - Object? sortingOption = null, - Object? sortOrder = null, - Object? favourites = null, - Object? hideEmptyShows = null, - Object? recursive = null, - Object? groupBy = null, + Object? filter = null, }) { return _then(_LibraryFiltersModel( id: null == id @@ -657,60 +383,22 @@ class __$LibraryFiltersModelCopyWithImpl<$Res> ? _self._ids : ids // ignore: cast_nullable_to_non_nullable as List, - genres: null == genres - ? _self._genres - : genres // ignore: cast_nullable_to_non_nullable - as Map, - filters: null == filters - ? _self._filters - : filters // ignore: cast_nullable_to_non_nullable - as Map, - studios: null == studios - ? _self._studios - : studios // ignore: cast_nullable_to_non_nullable - as Map, - tags: null == tags - ? _self._tags - : tags // ignore: cast_nullable_to_non_nullable - as Map, - years: null == years - ? _self._years - : years // ignore: cast_nullable_to_non_nullable - as Map, - officialRatings: null == officialRatings - ? _self._officialRatings - : officialRatings // ignore: cast_nullable_to_non_nullable - as Map, - types: null == types - ? _self._types - : types // ignore: cast_nullable_to_non_nullable - as Map, - sortingOption: null == sortingOption - ? _self.sortingOption - : sortingOption // ignore: cast_nullable_to_non_nullable - as SortingOptions, - sortOrder: null == sortOrder - ? _self.sortOrder - : sortOrder // ignore: cast_nullable_to_non_nullable - as SortingOrder, - favourites: null == favourites - ? _self.favourites - : favourites // ignore: cast_nullable_to_non_nullable - as bool, - hideEmptyShows: null == hideEmptyShows - ? _self.hideEmptyShows - : hideEmptyShows // ignore: cast_nullable_to_non_nullable - as bool, - recursive: null == recursive - ? _self.recursive - : recursive // ignore: cast_nullable_to_non_nullable - as bool, - groupBy: null == groupBy - ? _self.groupBy - : groupBy // ignore: cast_nullable_to_non_nullable - as GroupBy, + filter: null == filter + ? _self.filter + : filter // ignore: cast_nullable_to_non_nullable + as LibraryFilterModel, )); } + + /// Create a copy of LibraryFiltersModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LibraryFilterModelCopyWith<$Res> get filter { + return $LibraryFilterModelCopyWith<$Res>(_self.filter, (value) { + return _then(_self.copyWith(filter: value)); + }); + } } // dart format on diff --git a/lib/models/library_filters_model.g.dart b/lib/models/library_filters_model.g.dart index cf63b26..7778bc5 100644 --- a/lib/models/library_filters_model.g.dart +++ b/lib/models/library_filters_model.g.dart @@ -11,27 +11,11 @@ _LibraryFiltersModel _$LibraryFiltersModelFromJson(Map json) => id: json['id'] as String, name: json['name'] as String, isFavourite: json['isFavourite'] as bool, - ids: (json['ids'] as List).map((e) => e as String).toList(), - genres: Map.from(json['genres'] as Map), - filters: (json['filters'] as Map).map( - (k, e) => MapEntry($enumDecode(_$ItemFilterEnumMap, k), e as bool), - ), - studios: const StudioEncoder().fromJson(json['studios'] as String), - tags: Map.from(json['tags'] as Map), - years: (json['years'] as Map).map( - (k, e) => MapEntry(int.parse(k), e as bool), - ), - officialRatings: Map.from(json['officialRatings'] as Map), - types: (json['types'] as Map).map( - (k, e) => MapEntry($enumDecode(_$FladderItemTypeEnumMap, k), e as bool), - ), - sortingOption: - $enumDecode(_$SortingOptionsEnumMap, json['sortingOption']), - sortOrder: $enumDecode(_$SortingOrderEnumMap, json['sortOrder']), - favourites: json['favourites'] as bool, - hideEmptyShows: json['hideEmptyShows'] as bool, - recursive: json['recursive'] as bool, - groupBy: $enumDecode(_$GroupByEnumMap, json['groupBy']), + ids: (json['ids'] as List?)?.map((e) => e as String).toList() ?? + const [], + filter: json['filter'] == null + ? const LibraryFilterModel() + : LibraryFilterModel.fromJson(json['filter'] as Map), ); Map _$LibraryFiltersModelToJson( @@ -41,83 +25,5 @@ Map _$LibraryFiltersModelToJson( 'name': instance.name, 'isFavourite': instance.isFavourite, 'ids': instance.ids, - 'genres': instance.genres, - 'filters': - instance.filters.map((k, e) => MapEntry(_$ItemFilterEnumMap[k], e)), - 'studios': const StudioEncoder().toJson(instance.studios), - 'tags': instance.tags, - 'years': instance.years.map((k, e) => MapEntry(k.toString(), e)), - 'officialRatings': instance.officialRatings, - 'types': instance.types - .map((k, e) => MapEntry(_$FladderItemTypeEnumMap[k]!, e)), - 'sortingOption': _$SortingOptionsEnumMap[instance.sortingOption]!, - 'sortOrder': _$SortingOrderEnumMap[instance.sortOrder]!, - 'favourites': instance.favourites, - 'hideEmptyShows': instance.hideEmptyShows, - 'recursive': instance.recursive, - 'groupBy': _$GroupByEnumMap[instance.groupBy]!, + 'filter': instance.filter, }; - -const _$ItemFilterEnumMap = { - ItemFilter.swaggerGeneratedUnknown: null, - ItemFilter.isfolder: 'IsFolder', - ItemFilter.isnotfolder: 'IsNotFolder', - ItemFilter.isunplayed: 'IsUnplayed', - ItemFilter.isplayed: 'IsPlayed', - ItemFilter.isfavorite: 'IsFavorite', - ItemFilter.isresumable: 'IsResumable', - ItemFilter.likes: 'Likes', - ItemFilter.dislikes: 'Dislikes', - ItemFilter.isfavoriteorlikes: 'IsFavoriteOrLikes', -}; - -const _$FladderItemTypeEnumMap = { - FladderItemType.baseType: 'baseType', - FladderItemType.audio: 'audio', - FladderItemType.musicAlbum: 'musicAlbum', - FladderItemType.musicVideo: 'musicVideo', - FladderItemType.collectionFolder: 'collectionFolder', - FladderItemType.video: 'video', - FladderItemType.movie: 'movie', - FladderItemType.series: 'series', - FladderItemType.season: 'season', - FladderItemType.episode: 'episode', - FladderItemType.photo: 'photo', - FladderItemType.person: 'person', - FladderItemType.photoAlbum: 'photoAlbum', - FladderItemType.folder: 'folder', - FladderItemType.boxset: 'boxset', - FladderItemType.playlist: 'playlist', - FladderItemType.book: 'book', -}; - -const _$SortingOptionsEnumMap = { - SortingOptions.sortName: 'sortName', - SortingOptions.communityRating: 'communityRating', - SortingOptions.parentalRating: 'parentalRating', - SortingOptions.dateAdded: 'dateAdded', - SortingOptions.dateLastContentAdded: 'dateLastContentAdded', - SortingOptions.favorite: 'favorite', - SortingOptions.datePlayed: 'datePlayed', - SortingOptions.folders: 'folders', - SortingOptions.playCount: 'playCount', - SortingOptions.releaseDate: 'releaseDate', - SortingOptions.runTime: 'runTime', - SortingOptions.random: 'random', -}; - -const _$SortingOrderEnumMap = { - SortingOrder.ascending: 'ascending', - SortingOrder.descending: 'descending', -}; - -const _$GroupByEnumMap = { - GroupBy.none: 'none', - GroupBy.name: 'name', - GroupBy.genres: 'genres', - GroupBy.dateAdded: 'dateAdded', - GroupBy.tags: 'tags', - GroupBy.releaseDate: 'releaseDate', - GroupBy.rating: 'rating', - GroupBy.type: 'type', -}; diff --git a/lib/models/library_search/library_search_model.dart b/lib/models/library_search/library_search_model.dart index 3e9df53..1cc66f0 100644 --- a/lib/models/library_search/library_search_model.dart +++ b/lib/models/library_search/library_search_model.dart @@ -2,102 +2,36 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; -import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/library_search/library_search_options.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; -part 'library_search_model.mapper.dart'; +part 'library_search_model.freezed.dart'; -@MappableClass() -class LibrarySearchModel with LibrarySearchModelMappable { - final bool loading; - final bool selecteMode; - final String searchQuery; - final List folderOverwrite; - final Map views; - final List posters; - final List selectedPosters; - final Map filters; - final Map genres; - final Map studios; - final Map tags; - final Map years; - final Map officialRatings; - final Map types; - final SortingOptions sortingOption; - final SortingOrder sortOrder; - final bool favourites; - final bool hideEmptyShows; - final bool recursive; - final GroupBy groupBy; - final Map lastIndices; - final Map libraryItemCounts; - final bool fetchingItems; +@Freezed(copyWith: true) +abstract class LibrarySearchModel with _$LibrarySearchModel { + const factory LibrarySearchModel({ + @Default(false) bool loading, + @Default(false) bool selecteMode, + @Default([]) List folderOverwrite, + @Default("") String searchQuery, + @Default({}) Map views, + @Default([]) List posters, + @Default([]) List selectedPosters, + @Default(LibraryFilterModel()) LibraryFilterModel filters, + @Default({}) Map lastIndices, + @Default({}) Map libraryItemCounts, + @Default(false) bool fetchingItems, + }) = _LibrarySearchModel; +} - const LibrarySearchModel({ - this.loading = false, - this.selecteMode = false, - this.folderOverwrite = const [], - this.searchQuery = "", - this.views = const {}, - this.posters = const [], - this.selectedPosters = const [], - this.filters = const { - ItemFilter.isplayed: false, - ItemFilter.isunplayed: false, - ItemFilter.isresumable: false, - }, - this.genres = const {}, - this.studios = const {}, - this.tags = const {}, - this.years = const {}, - this.officialRatings = const {}, - this.types = const { - FladderItemType.audio: false, - FladderItemType.boxset: false, - FladderItemType.book: false, - FladderItemType.collectionFolder: false, - FladderItemType.episode: false, - FladderItemType.folder: false, - FladderItemType.movie: true, - FladderItemType.musicAlbum: false, - FladderItemType.musicVideo: false, - FladderItemType.photo: false, - FladderItemType.person: false, - FladderItemType.photoAlbum: false, - FladderItemType.series: true, - FladderItemType.video: true, - }, - this.favourites = false, - this.sortingOption = SortingOptions.sortName, - this.sortOrder = SortingOrder.ascending, - this.hideEmptyShows = true, - this.recursive = false, - this.groupBy = GroupBy.none, - this.lastIndices = const {}, - this.libraryItemCounts = const {}, - this.fetchingItems = false, - }); - - bool get hasActiveFilters { - return genres.hasEnabled || - studios.hasEnabled || - tags.hasEnabled || - years.hasEnabled || - officialRatings.hasEnabled || - hideEmptyShows || - filters.hasEnabled || - favourites || - searchQuery.isNotEmpty; - } +extension LibrarySearchModelX on LibrarySearchModel { + bool get hasActiveFilters => filters.hasActiveFilters || searchQuery.isNotEmpty; int get totalItemCount { if (libraryItemCounts.isEmpty) return posters.length; @@ -137,16 +71,16 @@ class LibrarySearchModel with LibrarySearchModelMappable { bool get showPlayButtons { if (totalItemCount == 0) return false; - return types.included.isEmpty || - types.included.containsAny( + return filters.types.included.isEmpty || + filters.types.included.containsAny( {...FladderItemType.playable, FladderItemType.folder}, ); } bool get showGalleryButtons { if (totalItemCount == 0) return false; - return types.included.isEmpty || - types.included.containsAny( + return filters.types.included.isEmpty || + filters.types.included.containsAny( {...FladderItemType.galleryItem, FladderItemType.photoAlbum, FladderItemType.folder}, ); } @@ -170,55 +104,8 @@ class LibrarySearchModel with LibrarySearchModelMappable { LibrarySearchModel setFiltersToDefault() { return copyWith( - genres: const {}, - tags: const {}, - officialRatings: const {}, - years: const {}, searchQuery: '', - favourites: false, - recursive: false, - studios: const {}, - hideEmptyShows: true, + filters: const LibraryFilterModel(), ); } - - @override - bool operator ==(covariant LibrarySearchModel other) { - if (identical(this, other)) return true; - - return other.searchQuery == searchQuery && - listEquals(other.folderOverwrite, folderOverwrite) && - mapEquals(other.views, views) && - mapEquals(other.filters, filters) && - mapEquals(other.genres, genres) && - mapEquals(other.studios, studios) && - mapEquals(other.tags, tags) && - mapEquals(other.years, years) && - mapEquals(other.officialRatings, officialRatings) && - mapEquals(other.types, types) && - other.sortingOption == sortingOption && - other.sortOrder == sortOrder && - other.favourites == favourites && - other.recursive == recursive; - } - - @override - int get hashCode { - return searchQuery.hashCode ^ - folderOverwrite.hashCode ^ - views.hashCode ^ - posters.hashCode ^ - selectedPosters.hashCode ^ - filters.hashCode ^ - genres.hashCode ^ - studios.hashCode ^ - tags.hashCode ^ - years.hashCode ^ - officialRatings.hashCode ^ - types.hashCode ^ - sortingOption.hashCode ^ - sortOrder.hashCode ^ - favourites.hashCode ^ - recursive.hashCode; - } } diff --git a/lib/models/library_search/library_search_model.freezed.dart b/lib/models/library_search/library_search_model.freezed.dart new file mode 100644 index 0000000..0cbd542 --- /dev/null +++ b/lib/models/library_search/library_search_model.freezed.dart @@ -0,0 +1,627 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'library_search_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$LibrarySearchModel implements DiagnosticableTreeMixin { + bool get loading; + bool get selecteMode; + List get folderOverwrite; + String get searchQuery; + Map get views; + List get posters; + List get selectedPosters; + LibraryFilterModel get filters; + Map get lastIndices; + Map get libraryItemCounts; + bool get fetchingItems; + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $LibrarySearchModelCopyWith get copyWith => + _$LibrarySearchModelCopyWithImpl( + this as LibrarySearchModel, _$identity); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'LibrarySearchModel')) + ..add(DiagnosticsProperty('loading', loading)) + ..add(DiagnosticsProperty('selecteMode', selecteMode)) + ..add(DiagnosticsProperty('folderOverwrite', folderOverwrite)) + ..add(DiagnosticsProperty('searchQuery', searchQuery)) + ..add(DiagnosticsProperty('views', views)) + ..add(DiagnosticsProperty('posters', posters)) + ..add(DiagnosticsProperty('selectedPosters', selectedPosters)) + ..add(DiagnosticsProperty('filters', filters)) + ..add(DiagnosticsProperty('lastIndices', lastIndices)) + ..add(DiagnosticsProperty('libraryItemCounts', libraryItemCounts)) + ..add(DiagnosticsProperty('fetchingItems', fetchingItems)); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'LibrarySearchModel(loading: $loading, selecteMode: $selecteMode, folderOverwrite: $folderOverwrite, searchQuery: $searchQuery, views: $views, posters: $posters, selectedPosters: $selectedPosters, filters: $filters, lastIndices: $lastIndices, libraryItemCounts: $libraryItemCounts, fetchingItems: $fetchingItems)'; + } +} + +/// @nodoc +abstract mixin class $LibrarySearchModelCopyWith<$Res> { + factory $LibrarySearchModelCopyWith( + LibrarySearchModel value, $Res Function(LibrarySearchModel) _then) = + _$LibrarySearchModelCopyWithImpl; + @useResult + $Res call( + {bool loading, + bool selecteMode, + List folderOverwrite, + String searchQuery, + Map views, + List posters, + List selectedPosters, + LibraryFilterModel filters, + Map lastIndices, + Map libraryItemCounts, + bool fetchingItems}); + + $LibraryFilterModelCopyWith<$Res> get filters; +} + +/// @nodoc +class _$LibrarySearchModelCopyWithImpl<$Res> + implements $LibrarySearchModelCopyWith<$Res> { + _$LibrarySearchModelCopyWithImpl(this._self, this._then); + + final LibrarySearchModel _self; + final $Res Function(LibrarySearchModel) _then; + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? selecteMode = null, + Object? folderOverwrite = null, + Object? searchQuery = null, + Object? views = null, + Object? posters = null, + Object? selectedPosters = null, + Object? filters = null, + Object? lastIndices = null, + Object? libraryItemCounts = null, + Object? fetchingItems = null, + }) { + return _then(_self.copyWith( + loading: null == loading + ? _self.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + selecteMode: null == selecteMode + ? _self.selecteMode + : selecteMode // ignore: cast_nullable_to_non_nullable + as bool, + folderOverwrite: null == folderOverwrite + ? _self.folderOverwrite + : folderOverwrite // ignore: cast_nullable_to_non_nullable + as List, + searchQuery: null == searchQuery + ? _self.searchQuery + : searchQuery // ignore: cast_nullable_to_non_nullable + as String, + views: null == views + ? _self.views + : views // ignore: cast_nullable_to_non_nullable + as Map, + posters: null == posters + ? _self.posters + : posters // ignore: cast_nullable_to_non_nullable + as List, + selectedPosters: null == selectedPosters + ? _self.selectedPosters + : selectedPosters // ignore: cast_nullable_to_non_nullable + as List, + filters: null == filters + ? _self.filters + : filters // ignore: cast_nullable_to_non_nullable + as LibraryFilterModel, + lastIndices: null == lastIndices + ? _self.lastIndices + : lastIndices // ignore: cast_nullable_to_non_nullable + as Map, + libraryItemCounts: null == libraryItemCounts + ? _self.libraryItemCounts + : libraryItemCounts // ignore: cast_nullable_to_non_nullable + as Map, + fetchingItems: null == fetchingItems + ? _self.fetchingItems + : fetchingItems // ignore: cast_nullable_to_non_nullable + as bool, + )); + } + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LibraryFilterModelCopyWith<$Res> get filters { + return $LibraryFilterModelCopyWith<$Res>(_self.filters, (value) { + return _then(_self.copyWith(filters: value)); + }); + } +} + +/// Adds pattern-matching-related methods to [LibrarySearchModel]. +extension LibrarySearchModelPatterns on LibrarySearchModel { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_LibrarySearchModel value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _LibrarySearchModel() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_LibrarySearchModel value) $default, + ) { + final _that = this; + switch (_that) { + case _LibrarySearchModel(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_LibrarySearchModel value)? $default, + ) { + final _that = this; + switch (_that) { + case _LibrarySearchModel() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + bool loading, + bool selecteMode, + List folderOverwrite, + String searchQuery, + Map views, + List posters, + List selectedPosters, + LibraryFilterModel filters, + Map lastIndices, + Map libraryItemCounts, + bool fetchingItems)? + $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _LibrarySearchModel() when $default != null: + return $default( + _that.loading, + _that.selecteMode, + _that.folderOverwrite, + _that.searchQuery, + _that.views, + _that.posters, + _that.selectedPosters, + _that.filters, + _that.lastIndices, + _that.libraryItemCounts, + _that.fetchingItems); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function( + bool loading, + bool selecteMode, + List folderOverwrite, + String searchQuery, + Map views, + List posters, + List selectedPosters, + LibraryFilterModel filters, + Map lastIndices, + Map libraryItemCounts, + bool fetchingItems) + $default, + ) { + final _that = this; + switch (_that) { + case _LibrarySearchModel(): + return $default( + _that.loading, + _that.selecteMode, + _that.folderOverwrite, + _that.searchQuery, + _that.views, + _that.posters, + _that.selectedPosters, + _that.filters, + _that.lastIndices, + _that.libraryItemCounts, + _that.fetchingItems); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function( + bool loading, + bool selecteMode, + List folderOverwrite, + String searchQuery, + Map views, + List posters, + List selectedPosters, + LibraryFilterModel filters, + Map lastIndices, + Map libraryItemCounts, + bool fetchingItems)? + $default, + ) { + final _that = this; + switch (_that) { + case _LibrarySearchModel() when $default != null: + return $default( + _that.loading, + _that.selecteMode, + _that.folderOverwrite, + _that.searchQuery, + _that.views, + _that.posters, + _that.selectedPosters, + _that.filters, + _that.lastIndices, + _that.libraryItemCounts, + _that.fetchingItems); + case _: + return null; + } + } +} + +/// @nodoc + +class _LibrarySearchModel + with DiagnosticableTreeMixin + implements LibrarySearchModel { + const _LibrarySearchModel( + {this.loading = false, + this.selecteMode = false, + final List folderOverwrite = const [], + this.searchQuery = "", + final Map views = const {}, + final List posters = const [], + final List selectedPosters = const [], + this.filters = const LibraryFilterModel(), + final Map lastIndices = const {}, + final Map libraryItemCounts = const {}, + this.fetchingItems = false}) + : _folderOverwrite = folderOverwrite, + _views = views, + _posters = posters, + _selectedPosters = selectedPosters, + _lastIndices = lastIndices, + _libraryItemCounts = libraryItemCounts; + + @override + @JsonKey() + final bool loading; + @override + @JsonKey() + final bool selecteMode; + final List _folderOverwrite; + @override + @JsonKey() + List get folderOverwrite { + if (_folderOverwrite is EqualUnmodifiableListView) return _folderOverwrite; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_folderOverwrite); + } + + @override + @JsonKey() + final String searchQuery; + final Map _views; + @override + @JsonKey() + Map get views { + if (_views is EqualUnmodifiableMapView) return _views; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_views); + } + + final List _posters; + @override + @JsonKey() + List get posters { + if (_posters is EqualUnmodifiableListView) return _posters; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_posters); + } + + final List _selectedPosters; + @override + @JsonKey() + List get selectedPosters { + if (_selectedPosters is EqualUnmodifiableListView) return _selectedPosters; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_selectedPosters); + } + + @override + @JsonKey() + final LibraryFilterModel filters; + final Map _lastIndices; + @override + @JsonKey() + Map get lastIndices { + if (_lastIndices is EqualUnmodifiableMapView) return _lastIndices; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_lastIndices); + } + + final Map _libraryItemCounts; + @override + @JsonKey() + Map get libraryItemCounts { + if (_libraryItemCounts is EqualUnmodifiableMapView) + return _libraryItemCounts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_libraryItemCounts); + } + + @override + @JsonKey() + final bool fetchingItems; + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$LibrarySearchModelCopyWith<_LibrarySearchModel> get copyWith => + __$LibrarySearchModelCopyWithImpl<_LibrarySearchModel>(this, _$identity); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'LibrarySearchModel')) + ..add(DiagnosticsProperty('loading', loading)) + ..add(DiagnosticsProperty('selecteMode', selecteMode)) + ..add(DiagnosticsProperty('folderOverwrite', folderOverwrite)) + ..add(DiagnosticsProperty('searchQuery', searchQuery)) + ..add(DiagnosticsProperty('views', views)) + ..add(DiagnosticsProperty('posters', posters)) + ..add(DiagnosticsProperty('selectedPosters', selectedPosters)) + ..add(DiagnosticsProperty('filters', filters)) + ..add(DiagnosticsProperty('lastIndices', lastIndices)) + ..add(DiagnosticsProperty('libraryItemCounts', libraryItemCounts)) + ..add(DiagnosticsProperty('fetchingItems', fetchingItems)); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'LibrarySearchModel(loading: $loading, selecteMode: $selecteMode, folderOverwrite: $folderOverwrite, searchQuery: $searchQuery, views: $views, posters: $posters, selectedPosters: $selectedPosters, filters: $filters, lastIndices: $lastIndices, libraryItemCounts: $libraryItemCounts, fetchingItems: $fetchingItems)'; + } +} + +/// @nodoc +abstract mixin class _$LibrarySearchModelCopyWith<$Res> + implements $LibrarySearchModelCopyWith<$Res> { + factory _$LibrarySearchModelCopyWith( + _LibrarySearchModel value, $Res Function(_LibrarySearchModel) _then) = + __$LibrarySearchModelCopyWithImpl; + @override + @useResult + $Res call( + {bool loading, + bool selecteMode, + List folderOverwrite, + String searchQuery, + Map views, + List posters, + List selectedPosters, + LibraryFilterModel filters, + Map lastIndices, + Map libraryItemCounts, + bool fetchingItems}); + + @override + $LibraryFilterModelCopyWith<$Res> get filters; +} + +/// @nodoc +class __$LibrarySearchModelCopyWithImpl<$Res> + implements _$LibrarySearchModelCopyWith<$Res> { + __$LibrarySearchModelCopyWithImpl(this._self, this._then); + + final _LibrarySearchModel _self; + final $Res Function(_LibrarySearchModel) _then; + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? loading = null, + Object? selecteMode = null, + Object? folderOverwrite = null, + Object? searchQuery = null, + Object? views = null, + Object? posters = null, + Object? selectedPosters = null, + Object? filters = null, + Object? lastIndices = null, + Object? libraryItemCounts = null, + Object? fetchingItems = null, + }) { + return _then(_LibrarySearchModel( + loading: null == loading + ? _self.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + selecteMode: null == selecteMode + ? _self.selecteMode + : selecteMode // ignore: cast_nullable_to_non_nullable + as bool, + folderOverwrite: null == folderOverwrite + ? _self._folderOverwrite + : folderOverwrite // ignore: cast_nullable_to_non_nullable + as List, + searchQuery: null == searchQuery + ? _self.searchQuery + : searchQuery // ignore: cast_nullable_to_non_nullable + as String, + views: null == views + ? _self._views + : views // ignore: cast_nullable_to_non_nullable + as Map, + posters: null == posters + ? _self._posters + : posters // ignore: cast_nullable_to_non_nullable + as List, + selectedPosters: null == selectedPosters + ? _self._selectedPosters + : selectedPosters // ignore: cast_nullable_to_non_nullable + as List, + filters: null == filters + ? _self.filters + : filters // ignore: cast_nullable_to_non_nullable + as LibraryFilterModel, + lastIndices: null == lastIndices + ? _self._lastIndices + : lastIndices // ignore: cast_nullable_to_non_nullable + as Map, + libraryItemCounts: null == libraryItemCounts + ? _self._libraryItemCounts + : libraryItemCounts // ignore: cast_nullable_to_non_nullable + as Map, + fetchingItems: null == fetchingItems + ? _self.fetchingItems + : fetchingItems // ignore: cast_nullable_to_non_nullable + as bool, + )); + } + + /// Create a copy of LibrarySearchModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LibraryFilterModelCopyWith<$Res> get filters { + return $LibraryFilterModelCopyWith<$Res>(_self.filters, (value) { + return _then(_self.copyWith(filters: value)); + }); + } +} + +// dart format on diff --git a/lib/models/library_search/library_search_model.mapper.dart b/lib/models/library_search/library_search_model.mapper.dart deleted file mode 100644 index c775159..0000000 --- a/lib/models/library_search/library_search_model.mapper.dart +++ /dev/null @@ -1,395 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'library_search_model.dart'; - -class LibrarySearchModelMapper extends ClassMapperBase { - LibrarySearchModelMapper._(); - - static LibrarySearchModelMapper? _instance; - static LibrarySearchModelMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = LibrarySearchModelMapper._()); - ItemBaseModelMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'LibrarySearchModel'; - - static bool _$loading(LibrarySearchModel v) => v.loading; - static const Field _f$loading = - Field('loading', _$loading, opt: true, def: false); - static bool _$selecteMode(LibrarySearchModel v) => v.selecteMode; - static const Field _f$selecteMode = - Field('selecteMode', _$selecteMode, opt: true, def: false); - static List _$folderOverwrite(LibrarySearchModel v) => - v.folderOverwrite; - static const Field> - _f$folderOverwrite = - Field('folderOverwrite', _$folderOverwrite, opt: true, def: const []); - static String _$searchQuery(LibrarySearchModel v) => v.searchQuery; - static const Field _f$searchQuery = - Field('searchQuery', _$searchQuery, opt: true, def: ""); - static Map _$views(LibrarySearchModel v) => v.views; - static const Field> _f$views = - Field('views', _$views, opt: true, def: const {}); - static List _$posters(LibrarySearchModel v) => v.posters; - static const Field> _f$posters = - Field('posters', _$posters, opt: true, def: const []); - static List _$selectedPosters(LibrarySearchModel v) => - v.selectedPosters; - static const Field> - _f$selectedPosters = - Field('selectedPosters', _$selectedPosters, opt: true, def: const []); - static Map _$filters(LibrarySearchModel v) => v.filters; - static const Field> _f$filters = - Field('filters', _$filters, opt: true, def: const { - ItemFilter.isplayed: false, - ItemFilter.isunplayed: false, - ItemFilter.isresumable: false - }); - static Map _$genres(LibrarySearchModel v) => v.genres; - static const Field> _f$genres = - Field('genres', _$genres, opt: true, def: const {}); - static Map _$studios(LibrarySearchModel v) => v.studios; - static const Field> _f$studios = - Field('studios', _$studios, opt: true, def: const {}); - static Map _$tags(LibrarySearchModel v) => v.tags; - static const Field> _f$tags = - Field('tags', _$tags, opt: true, def: const {}); - static Map _$years(LibrarySearchModel v) => v.years; - static const Field> _f$years = - Field('years', _$years, opt: true, def: const {}); - static Map _$officialRatings(LibrarySearchModel v) => - v.officialRatings; - static const Field> _f$officialRatings = - Field('officialRatings', _$officialRatings, opt: true, def: const {}); - static Map _$types(LibrarySearchModel v) => v.types; - static const Field> _f$types = - Field('types', _$types, opt: true, def: const { - FladderItemType.audio: false, - FladderItemType.boxset: false, - FladderItemType.book: false, - FladderItemType.collectionFolder: false, - FladderItemType.episode: false, - FladderItemType.folder: false, - FladderItemType.movie: true, - FladderItemType.musicAlbum: false, - FladderItemType.musicVideo: false, - FladderItemType.photo: false, - FladderItemType.person: false, - FladderItemType.photoAlbum: false, - FladderItemType.series: true, - FladderItemType.video: true - }); - static bool _$favourites(LibrarySearchModel v) => v.favourites; - static const Field _f$favourites = - Field('favourites', _$favourites, opt: true, def: false); - static SortingOptions _$sortingOption(LibrarySearchModel v) => - v.sortingOption; - static const Field _f$sortingOption = - Field('sortingOption', _$sortingOption, - opt: true, def: SortingOptions.sortName); - static SortingOrder _$sortOrder(LibrarySearchModel v) => v.sortOrder; - static const Field _f$sortOrder = - Field('sortOrder', _$sortOrder, opt: true, def: SortingOrder.ascending); - static bool _$hideEmptyShows(LibrarySearchModel v) => v.hideEmptyShows; - static const Field _f$hideEmptyShows = - Field('hideEmptyShows', _$hideEmptyShows, opt: true, def: true); - static bool _$recursive(LibrarySearchModel v) => v.recursive; - static const Field _f$recursive = - Field('recursive', _$recursive, opt: true, def: false); - static GroupBy _$groupBy(LibrarySearchModel v) => v.groupBy; - static const Field _f$groupBy = - Field('groupBy', _$groupBy, opt: true, def: GroupBy.none); - static Map _$lastIndices(LibrarySearchModel v) => v.lastIndices; - static const Field> _f$lastIndices = - Field('lastIndices', _$lastIndices, opt: true, def: const {}); - static Map _$libraryItemCounts(LibrarySearchModel v) => - v.libraryItemCounts; - static const Field> - _f$libraryItemCounts = - Field('libraryItemCounts', _$libraryItemCounts, opt: true, def: const {}); - static bool _$fetchingItems(LibrarySearchModel v) => v.fetchingItems; - static const Field _f$fetchingItems = - Field('fetchingItems', _$fetchingItems, opt: true, def: false); - - @override - final MappableFields fields = const { - #loading: _f$loading, - #selecteMode: _f$selecteMode, - #folderOverwrite: _f$folderOverwrite, - #searchQuery: _f$searchQuery, - #views: _f$views, - #posters: _f$posters, - #selectedPosters: _f$selectedPosters, - #filters: _f$filters, - #genres: _f$genres, - #studios: _f$studios, - #tags: _f$tags, - #years: _f$years, - #officialRatings: _f$officialRatings, - #types: _f$types, - #favourites: _f$favourites, - #sortingOption: _f$sortingOption, - #sortOrder: _f$sortOrder, - #hideEmptyShows: _f$hideEmptyShows, - #recursive: _f$recursive, - #groupBy: _f$groupBy, - #lastIndices: _f$lastIndices, - #libraryItemCounts: _f$libraryItemCounts, - #fetchingItems: _f$fetchingItems, - }; - @override - final bool ignoreNull = true; - - static LibrarySearchModel _instantiate(DecodingData data) { - return LibrarySearchModel( - loading: data.dec(_f$loading), - selecteMode: data.dec(_f$selecteMode), - folderOverwrite: data.dec(_f$folderOverwrite), - searchQuery: data.dec(_f$searchQuery), - views: data.dec(_f$views), - posters: data.dec(_f$posters), - selectedPosters: data.dec(_f$selectedPosters), - filters: data.dec(_f$filters), - genres: data.dec(_f$genres), - studios: data.dec(_f$studios), - tags: data.dec(_f$tags), - years: data.dec(_f$years), - officialRatings: data.dec(_f$officialRatings), - types: data.dec(_f$types), - favourites: data.dec(_f$favourites), - sortingOption: data.dec(_f$sortingOption), - sortOrder: data.dec(_f$sortOrder), - hideEmptyShows: data.dec(_f$hideEmptyShows), - recursive: data.dec(_f$recursive), - groupBy: data.dec(_f$groupBy), - lastIndices: data.dec(_f$lastIndices), - libraryItemCounts: data.dec(_f$libraryItemCounts), - fetchingItems: data.dec(_f$fetchingItems)); - } - - @override - final Function instantiate = _instantiate; -} - -mixin LibrarySearchModelMappable { - LibrarySearchModelCopyWith - get copyWith => _LibrarySearchModelCopyWithImpl(this as LibrarySearchModel, $identity, $identity); -} - -extension LibrarySearchModelValueCopy<$R, $Out> - on ObjectCopyWith<$R, LibrarySearchModel, $Out> { - LibrarySearchModelCopyWith<$R, LibrarySearchModel, $Out> - get $asLibrarySearchModel => $base.as( - (v, t, t2) => _LibrarySearchModelCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class LibrarySearchModelCopyWith<$R, $In extends LibrarySearchModel, - $Out> implements ClassCopyWith<$R, $In, $Out> { - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> - get folderOverwrite; - MapCopyWith<$R, ViewModel, bool, ObjectCopyWith<$R, bool, bool>> get views; - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get posters; - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> - get selectedPosters; - MapCopyWith<$R, ItemFilter, bool, ObjectCopyWith<$R, bool, bool>> get filters; - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get genres; - MapCopyWith<$R, Studio, bool, ObjectCopyWith<$R, bool, bool>> get studios; - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get tags; - MapCopyWith<$R, int, bool, ObjectCopyWith<$R, bool, bool>> get years; - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> - get officialRatings; - MapCopyWith<$R, FladderItemType, bool, ObjectCopyWith<$R, bool, bool>> - get types; - MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> get lastIndices; - MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> - get libraryItemCounts; - $R call( - {bool? loading, - bool? selecteMode, - List? folderOverwrite, - String? searchQuery, - Map? views, - List? posters, - List? selectedPosters, - Map? filters, - Map? genres, - Map? studios, - Map? tags, - Map? years, - Map? officialRatings, - Map? types, - bool? favourites, - SortingOptions? sortingOption, - SortingOrder? sortOrder, - bool? hideEmptyShows, - bool? recursive, - GroupBy? groupBy, - Map? lastIndices, - Map? libraryItemCounts, - bool? fetchingItems}); - LibrarySearchModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t); -} - -class _LibrarySearchModelCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, LibrarySearchModel, $Out> - implements LibrarySearchModelCopyWith<$R, LibrarySearchModel, $Out> { - _LibrarySearchModelCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - LibrarySearchModelMapper.ensureInitialized(); - @override - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> - get folderOverwrite => ListCopyWith($value.folderOverwrite, - (v, t) => v.copyWith.$chain(t), (v) => call(folderOverwrite: v)); - @override - MapCopyWith<$R, ViewModel, bool, ObjectCopyWith<$R, bool, bool>> get views => - MapCopyWith($value.views, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(views: v)); - @override - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> - get posters => ListCopyWith($value.posters, - (v, t) => v.copyWith.$chain(t), (v) => call(posters: v)); - @override - ListCopyWith<$R, ItemBaseModel, - ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> - get selectedPosters => ListCopyWith($value.selectedPosters, - (v, t) => v.copyWith.$chain(t), (v) => call(selectedPosters: v)); - @override - MapCopyWith<$R, ItemFilter, bool, ObjectCopyWith<$R, bool, bool>> - get filters => MapCopyWith($value.filters, - (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(filters: v)); - @override - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get genres => - MapCopyWith($value.genres, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(genres: v)); - @override - MapCopyWith<$R, Studio, bool, ObjectCopyWith<$R, bool, bool>> get studios => - MapCopyWith($value.studios, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(studios: v)); - @override - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get tags => - MapCopyWith($value.tags, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(tags: v)); - @override - MapCopyWith<$R, int, bool, ObjectCopyWith<$R, bool, bool>> get years => - MapCopyWith($value.years, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(years: v)); - @override - MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> - get officialRatings => MapCopyWith( - $value.officialRatings, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(officialRatings: v)); - @override - MapCopyWith<$R, FladderItemType, bool, ObjectCopyWith<$R, bool, bool>> - get types => MapCopyWith($value.types, - (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(types: v)); - @override - MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> get lastIndices => - MapCopyWith($value.lastIndices, (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(lastIndices: v)); - @override - MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> - get libraryItemCounts => MapCopyWith( - $value.libraryItemCounts, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(libraryItemCounts: v)); - @override - $R call( - {bool? loading, - bool? selecteMode, - List? folderOverwrite, - String? searchQuery, - Map? views, - List? posters, - List? selectedPosters, - Map? filters, - Map? genres, - Map? studios, - Map? tags, - Map? years, - Map? officialRatings, - Map? types, - bool? favourites, - SortingOptions? sortingOption, - SortingOrder? sortOrder, - bool? hideEmptyShows, - bool? recursive, - GroupBy? groupBy, - Map? lastIndices, - Map? libraryItemCounts, - bool? fetchingItems}) => - $apply(FieldCopyWithData({ - if (loading != null) #loading: loading, - if (selecteMode != null) #selecteMode: selecteMode, - if (folderOverwrite != null) #folderOverwrite: folderOverwrite, - if (searchQuery != null) #searchQuery: searchQuery, - if (views != null) #views: views, - if (posters != null) #posters: posters, - if (selectedPosters != null) #selectedPosters: selectedPosters, - if (filters != null) #filters: filters, - if (genres != null) #genres: genres, - if (studios != null) #studios: studios, - if (tags != null) #tags: tags, - if (years != null) #years: years, - if (officialRatings != null) #officialRatings: officialRatings, - if (types != null) #types: types, - if (favourites != null) #favourites: favourites, - if (sortingOption != null) #sortingOption: sortingOption, - if (sortOrder != null) #sortOrder: sortOrder, - if (hideEmptyShows != null) #hideEmptyShows: hideEmptyShows, - if (recursive != null) #recursive: recursive, - if (groupBy != null) #groupBy: groupBy, - if (lastIndices != null) #lastIndices: lastIndices, - if (libraryItemCounts != null) #libraryItemCounts: libraryItemCounts, - if (fetchingItems != null) #fetchingItems: fetchingItems - })); - @override - LibrarySearchModel $make(CopyWithData data) => LibrarySearchModel( - loading: data.get(#loading, or: $value.loading), - selecteMode: data.get(#selecteMode, or: $value.selecteMode), - folderOverwrite: data.get(#folderOverwrite, or: $value.folderOverwrite), - searchQuery: data.get(#searchQuery, or: $value.searchQuery), - views: data.get(#views, or: $value.views), - posters: data.get(#posters, or: $value.posters), - selectedPosters: data.get(#selectedPosters, or: $value.selectedPosters), - filters: data.get(#filters, or: $value.filters), - genres: data.get(#genres, or: $value.genres), - studios: data.get(#studios, or: $value.studios), - tags: data.get(#tags, or: $value.tags), - years: data.get(#years, or: $value.years), - officialRatings: data.get(#officialRatings, or: $value.officialRatings), - types: data.get(#types, or: $value.types), - favourites: data.get(#favourites, or: $value.favourites), - sortingOption: data.get(#sortingOption, or: $value.sortingOption), - sortOrder: data.get(#sortOrder, or: $value.sortOrder), - hideEmptyShows: data.get(#hideEmptyShows, or: $value.hideEmptyShows), - recursive: data.get(#recursive, or: $value.recursive), - groupBy: data.get(#groupBy, or: $value.groupBy), - lastIndices: data.get(#lastIndices, or: $value.lastIndices), - libraryItemCounts: - data.get(#libraryItemCounts, or: $value.libraryItemCounts), - fetchingItems: data.get(#fetchingItems, or: $value.fetchingItems)); - - @override - LibrarySearchModelCopyWith<$R2, LibrarySearchModel, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t) => - _LibrarySearchModelCopyWithImpl<$R2, $Out2>($value, $cast, t); -} diff --git a/lib/providers/favourites_provider.dart b/lib/providers/favourites_provider.dart index 907bf46..8fb3d4c 100644 --- a/lib/providers/favourites_provider.dart +++ b/lib/providers/favourites_provider.dart @@ -44,23 +44,33 @@ class FavouritesNotifier extends StateNotifier { } Future?> _loadLibrary({ViewModel? viewModel}) async { - final response = await api.itemsGet( - parentId: viewModel?.id, - isFavorite: true, - limit: 10, - sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], - ); - final response2 = await api.itemsGet( - parentId: viewModel?.id, - isFavorite: true, - recursive: true, - limit: 10, - includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder], - sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], - ); - return [...?response.body?.items, ...?response2.body?.items]; + final kinds = [ + BaseItemKind.movie, + BaseItemKind.episode, + BaseItemKind.series, + BaseItemKind.video, + BaseItemKind.photo, + BaseItemKind.book, + BaseItemKind.photoalbum, + ]; + final futures = kinds.map((kind) => fetchTypes(viewModel?.id, [kind])).toList(); + final results = await Future.wait(futures); + return results.expand((list) => list).toList(); + } + + Future> fetchTypes(String? id, List? includeItemTypes) async { + return (await api.itemsGet( + parentId: id, + isFavorite: true, + recursive: true, + limit: 15, + includeItemTypes: includeItemTypes, + sortOrder: [SortOrder.ascending], + sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], + )) + .body + ?.items ?? + []; } Future>?> _fetchPeople() async { diff --git a/lib/providers/library_filters_provider.dart b/lib/providers/library_filters_provider.dart index 364405d..fe178b7 100644 --- a/lib/providers/library_filters_provider.dart +++ b/lib/providers/library_filters_provider.dart @@ -10,7 +10,7 @@ class LibraryFilters extends _$LibraryFilters { @override List build(List ids) => ref.watch( userProvider - .select((value) => (value?.savedFilters ?? []).where((element) => element.containsSameIds(ids)).toList()), + .select((value) => (value?.libraryFilters ?? []).where((element) => element.containsSameIds(ids)).toList()), ); void removeFilter(LibraryFiltersModel model) => ref.read(userProvider.notifier).removeFilter(model); diff --git a/lib/providers/library_filters_provider.g.dart b/lib/providers/library_filters_provider.g.dart index 35e2992..7e9331f 100644 --- a/lib/providers/library_filters_provider.g.dart +++ b/lib/providers/library_filters_provider.g.dart @@ -6,7 +6,7 @@ part of 'library_filters_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$libraryFiltersHash() => r'fd98699d8d7c1db6daefa6e53d5d90f989a8f776'; +String _$libraryFiltersHash() => r'2fae16f2bf8f9aee08b047d8615a47ee794b30b0'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/providers/library_screen_provider.dart b/lib/providers/library_screen_provider.dart index 676c46e..1f90c25 100644 --- a/lib/providers/library_screen_provider.dart +++ b/lib/providers/library_screen_provider.dart @@ -66,7 +66,8 @@ class LibraryScreen extends _$LibraryScreen { Future fetchAllLibraries() async { final views = await ref.read(viewsProvider.notifier).fetchViews(); - state = state.copyWith(views: views?.views ?? []); + state = state.copyWith( + views: views?.views.where((element) => element.collectionType != CollectionType.folders).toList() ?? []); if (state.views.isEmpty) return; final viewModel = state.selectedViewModel ?? state.views.firstOrNull; if (viewModel == null) return; diff --git a/lib/providers/library_search_provider.dart b/lib/providers/library_search_provider.dart index 4c76d1f..9f6489b 100644 --- a/lib/providers/library_search_provider.dart +++ b/lib/providers/library_search_provider.dart @@ -13,6 +13,7 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/folder_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/photos_model.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/library_filters_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; @@ -49,15 +50,14 @@ class LibrarySearchNotifier extends StateNotifier { set loading(bool loading) => state = state.copyWith(loading: loading); bool loadedFilters = false; + bool wasInitialized = false; bool get loading => state.loading; Future initRefresh( List? folderId, String? viewModelId, - bool? favourites, - SortingOrder? sortOrder, - SortingOptions? sortingOptions, + LibraryFilterModel filters, ) async { loading = true; state = state.resetLazyLoad(); @@ -65,12 +65,23 @@ class LibrarySearchNotifier extends StateNotifier { if (folderId != null) { await loadFolders(folderId: folderId); } else { - await loadViews(viewModelId, favourites, sortOrder, sortingOptions); + await loadViews(viewModelId, filters); } } - await loadFilters(); + + if (!wasInitialized) { + wasInitialized = true; + state = state.copyWith( + filters: state.filters.copyWith( + types: state.filters.types.replaceMap(filters.types, enabledOnly: true), + genres: state.filters.genres.replaceMap(filters.genres, enabledOnly: true), + ), + ); + } + await loadMore(init: true); + loading = false; } @@ -125,11 +136,11 @@ class LibrarySearchNotifier extends StateNotifier { List newPosters = results.nonNulls.expand((element) => element.items).toList(); if (state.views.included.length > 1) { - if (state.sortingOption == SortingOptions.random) { + if (state.filters.sortingOption == SortingOptions.random) { newPosters = newPosters.random(); } else { newPosters = newPosters.sorted( - (a, b) => sortItems(a, b, state.sortingOption, state.sortOrder), + (a, b) => sortItems(a, b, state.filters.sortingOption, state.filters.sortOrder), ); } } @@ -145,7 +156,7 @@ class LibrarySearchNotifier extends StateNotifier { } else if (state.views.hasEnabled) { await handleViewLoading(); } else { - if (state.searchQuery.isEmpty && !state.favourites) { + if (state.searchQuery.isEmpty && !state.filters.favourites) { state = state.copyWith(posters: []); } else { final response = await _loadLibrary(recursive: true); @@ -156,12 +167,9 @@ class LibrarySearchNotifier extends StateNotifier { loading = false; } - //Pas viewmodel otherwise select first Future loadViews( String? viewModelId, - bool? favourites, - SortingOrder? sortOrder, - SortingOptions? sortingOptions, + LibraryFilterModel filters, ) async { final response = await api.usersUserIdViewsGet(includeHidden: false); final createdViews = response.body?.items?.map((e) => ViewModel.fromBodyDto(e, ref)); @@ -178,34 +186,28 @@ class LibrarySearchNotifier extends StateNotifier { views: views, ); - if (sortOrder == null && sortingOptions == null && favourites == null) { - final findFavouriteFilter = ref - .read(libraryFiltersProvider(views.included.map((e) => e.id).toList())) - .firstWhereOrNull((element) => element.isFavourite); - if (findFavouriteFilter != null) { - loadModel(findFavouriteFilter); - } + final findFavouriteFilter = ref + .read(libraryFiltersProvider(views.included.map((e) => e.id).toList())) + .firstWhereOrNull((element) => element.isFavourite); + if (findFavouriteFilter != null) { + loadModel(findFavouriteFilter.filter); } else { - state = state.copyWith( - sortOrder: sortOrder, - sortingOption: sortingOptions, - favourites: favourites, - ); + loadModel(filters); } } Future loadFolders({List? folderId}) async { final response = await api.itemsGet( ids: folderId ?? state.folderOverwrite.map((e) => e.id).toList(), - sortBy: state.sortingOption.toSortBy, - sortOrder: [state.sortOrder.sortOrder], + sortBy: state.filters.sortingOption.toSortBy, + sortOrder: [state.filters.sortOrder.sortOrder], fields: [ ItemFields.parentid, ItemFields.primaryimageaspectratio, ], ); - state = state.copyWith(folderOverwrite: response.body?.items.toList()); + state = state.copyWith(folderOverwrite: response.body?.items.toList() ?? []); } Future loadFilters() async { @@ -225,11 +227,15 @@ class LibrarySearchNotifier extends StateNotifier { final tags = mappedList .expand((element) => element?.tags ?? []) .sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + var tempFilters = tempState.filters; tempState = tempState.copyWith( - types: state.types.setAll(false).setKeys(enabledCollections, true), - genres: {for (var element in genres) element.name: false}.replaceMap(tempState.genres), - studios: {for (var element in studios) element: false}.replaceMap(tempState.studios), - tags: {for (var element in tags) element: false}.replaceMap(tempState.tags), + filters: tempFilters.copyWith( + types: tempFilters.types.setAll(false).setKeys(enabledCollections, true), + genres: {for (var element in genres) element.name: false}.replaceMap(tempFilters.genres), + studios: {for (var element in studios) element: false}.replaceMap(tempFilters.studios), + tags: {for (var element in tags) element: false}.replaceMap(tempFilters.tags), + recursive: state.views.included.firstOrNull?.collectionType.searchRecursive ?? true, + ), ); state = tempState; } @@ -261,18 +267,18 @@ class LibrarySearchNotifier extends StateNotifier { final response = await api.itemsGet( parentId: viewModel?.id ?? id, searchTerm: searchString, - genres: state.genres.included, - tags: state.tags.included, - recursive: searchString?.isNotEmpty == true ? true : recursive ?? state.recursive, - officialRatings: state.officialRatings.included, - years: state.years.included, + genres: state.filters.genres.included, + tags: state.filters.tags.included, + recursive: searchString?.isNotEmpty == true ? true : recursive ?? state.filters.recursive, + officialRatings: state.filters.officialRatings.included, + years: state.filters.years.included, isMissing: false, limit: (limit ?? 0) > 0 ? limit : null, startIndex: (limit ?? 0) > 0 ? startIndex : null, collapseBoxSetItems: false, - studioIds: state.studios.included.map((e) => e.id).toList(), - sortBy: shuffle == true ? [ItemSortBy.random] : state.sortingOption.toSortBy, - sortOrder: [state.sortOrder.sortOrder], + studioIds: state.filters.studios.included.map((e) => e.id).toList(), + sortBy: shuffle == true ? [ItemSortBy.random] : state.filters.sortingOption.toSortBy, + sortOrder: [state.filters.sortOrder.sortOrder], fields: { ItemFields.genres, ItemFields.parentid, @@ -286,10 +292,10 @@ class LibrarySearchNotifier extends StateNotifier { if (viewModel?.collectionType == CollectionType.tvshows) ItemFields.childcount, }.toList(), filters: [ - ...state.filters.included, - if (state.favourites) ItemFilter.isfavorite, + ...state.filters.itemFilters.included, + if (state.filters.favourites) ItemFilter.isfavorite, ], - includeItemTypes: state.types.included.map((e) => e.dtoKind).toList(), + includeItemTypes: state.filters.types.included.map((e) => e.dtoKind).toList(), ); return response.body; } @@ -344,53 +350,58 @@ class LibrarySearchNotifier extends StateNotifier { ref.read(userProvider.notifier).addSearchQuery(query); } - void toggleFavourite() => state = state.copyWith(favourites: !state.favourites); - void toggleRecursive() => state = state.copyWith(recursive: !state.recursive); - void toggleType(FladderItemType type) => state = state.copyWith(types: state.types.toggleKey(type)); + void toggleFavourite() => + state = state.copyWith(filters: state.filters.copyWith(favourites: !state.filters.favourites)); + void toggleRecursive() => + state = state.copyWith(filters: state.filters.copyWith(recursive: !state.filters.recursive)); + void toggleType(FladderItemType type) => + state = state.copyWith(filters: state.filters.copyWith(types: state.filters.types.toggleKey(type))); void toggleView(ViewModel view) => state = state.copyWith(views: state.views.toggleKey(view)); - void toggleGenre(String genre) => state = state.copyWith(genres: state.genres.toggleKey(genre)); - void toggleStudio(Studio studio) => state = state.copyWith(studios: state.studios.toggleKey(studio)); - void toggleTag(String tag) => state = state.copyWith(tags: state.tags.toggleKey(tag)); - void toggleRatings(String officialRatings) => - state = state.copyWith(officialRatings: state.officialRatings.toggleKey(officialRatings)); - void toggleYears(int year) => state = state.copyWith(years: state.years.toggleKey(year)); - void toggleFilters(ItemFilter filter) => state = state.copyWith(filters: state.filters.toggleKey(filter)); + void toggleGenre(String genre) => + state = state.copyWith(filters: state.filters.copyWith(genres: state.filters.genres.toggleKey(genre))); + void toggleStudio(Studio studio) => + state = state.copyWith(filters: state.filters.copyWith(studios: state.filters.studios.toggleKey(studio))); + void toggleTag(String tag) => + state = state.copyWith(filters: state.filters.copyWith(tags: state.filters.tags.toggleKey(tag))); + void toggleRatings(String officialRatings) => state = state.copyWith( + filters: state.filters.copyWith(officialRatings: state.filters.officialRatings.toggleKey(officialRatings))); + void toggleYears(int year) => + state = state.copyWith(filters: state.filters.copyWith(years: state.filters.years.toggleKey(year))); + void toggleFilters(ItemFilter filter) => + state = state.copyWith(filters: state.filters.copyWith(itemFilters: state.filters.itemFilters.toggleKey(filter))); void setViews(Map views) { loadedFilters = false; state = state.copyWith(views: views).setFiltersToDefault(); } - void setGenres(Map genres) => state = state.copyWith(genres: genres); - void setStudios(Map studios) => state = state.copyWith(studios: studios); - void setTags(Map tags) => state = state.copyWith(tags: tags); - void setTypes(Map types) => state = state.copyWith(types: types); - void setRatings(Map officialRatings) => state = state.copyWith(officialRatings: officialRatings); - void setYears(Map years) => state = state.copyWith(years: years); - void setFilters(Map filters) => state = state.copyWith(filters: filters); + void setGenres(Map genres) => state = state.copyWith(filters: state.filters.copyWith(genres: genres)); + void setStudios(Map studios) => + state = state.copyWith(filters: state.filters.copyWith(studios: studios)); + void setTags(Map tags) => state = state.copyWith(filters: state.filters.copyWith(tags: tags)); + void setTypes(Map types) => + state = state.copyWith(filters: state.filters.copyWith(types: types)); + void setRatings(Map officialRatings) => + state = state.copyWith(filters: state.filters.copyWith(officialRatings: officialRatings)); + void setYears(Map years) => state = state.copyWith(filters: state.filters.copyWith(years: years)); + void setFilters(Map filters) => + state = state.copyWith(filters: state.filters.copyWith(itemFilters: filters)); + + void setSortBy(SortingOptions e) => state = state.copyWith(filters: state.filters.copyWith(sortingOption: e)); + + void setSortOrder(SortingOrder e) => state = state.copyWith(filters: state.filters.copyWith(sortOrder: e)); + + void toggleEmptyShows() => + state = state.copyWith(filters: state.filters.copyWith(hideEmptyShows: !state.filters.hideEmptyShows)); + void setGroupBy(GroupBy groupBy) => state = state.copyWith(filters: state.filters.copyWith(groupBy: groupBy)); void clearAllFilters() { state = state.copyWith( - genres: state.genres.setAll(false), - tags: state.tags.setAll(false), - officialRatings: state.officialRatings.setAll(false), - years: state.years.setAll(false), searchQuery: '', - favourites: false, - recursive: false, - studios: state.studios.setAll(false), - filters: state.filters.setAll(false), - hideEmptyShows: false, + filters: state.filters.clear(), ); } - void setSortBy(SortingOptions e) => state = state.copyWith(sortingOption: e); - - void setSortOrder(SortingOrder e) => state = state.copyWith(sortOrder: e); - - void setHideEmpty(bool value) => state = state.copyWith(hideEmptyShows: value); - void setGroupBy(GroupBy groupBy) => state = state.copyWith(groupBy: groupBy); - void setFolderId(ItemBaseModel item) { if (state.folderOverwrite.contains(item)) return; state = state.copyWith(folderOverwrite: [...state.folderOverwrite, item]); @@ -402,7 +413,7 @@ class LibrarySearchNotifier extends StateNotifier { void clearFolderOverWrite() => state = state.copyWith(folderOverwrite: []); void toggleSelectMode() => - state = state.copyWith(selecteMode: !state.selecteMode, selectedPosters: !state.selecteMode == false ? [] : null); + state = state.copyWith(selecteMode: !state.selecteMode, selectedPosters: !state.selecteMode == false ? [] : []); void toggleSelection(ItemBaseModel item) { if (state.selectedPosters.contains(item)) { @@ -539,11 +550,11 @@ class LibrarySearchNotifier extends StateNotifier { List newPosters = results.nonNulls.expand((element) => element.items).toList(); if (state.views.included.length > 1) { - if (shuffle || state.sortingOption == SortingOptions.random) { + if (shuffle || state.filters.sortingOption == SortingOptions.random) { newPosters = newPosters.random(); } else { newPosters = newPosters.sorted( - (a, b) => sortItems(a, b, state.sortingOption, state.sortOrder), + (a, b) => sortItems(a, b, state.filters.sortingOption, state.filters.sortOrder), ); } } @@ -556,7 +567,7 @@ class LibrarySearchNotifier extends StateNotifier { } else if (state.views.hasEnabled) { await handleViewLoading(); } else { - if (state.searchQuery.isEmpty && !state.favourites) { + if (state.searchQuery.isEmpty && !state.filters.favourites) { itemsToPlay = []; } else { final response = await _loadLibrary(recursive: true, shuffle: shuffle); @@ -601,7 +612,8 @@ class LibrarySearchNotifier extends StateNotifier { List albumItems = []; - if (!state.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) && state.recursive) { + if (!state.filters.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) && + state.filters.recursive) { for (var album in itemsToPlay.where( (element) => element is PhotoAlbumModel || element is FolderModel, )) { @@ -622,8 +634,8 @@ class LibrarySearchNotifier extends StateNotifier { ItemFields.primaryimageaspectratio, }.toList(), filters: [ - ...state.filters.included, - if (state.favourites) ItemFilter.isfavorite, + ...state.filters.itemFilters.included, + if (state.filters.favourites) ItemFilter.isfavorite, ], sortBy: shuffle ? [ItemSortBy.random] : null, ); @@ -683,23 +695,7 @@ class LibrarySearchNotifier extends StateNotifier { state = state.copyWith(); } - void loadModel(LibraryFiltersModel model) { - state = state.copyWith( - genres: state.genres.replaceMap(model.genres), - filters: state.filters.replaceMap(model.filters), - studios: state.studios.replaceMap(model.studios), - tags: state.tags.replaceMap(model.tags), - years: state.years.replaceMap(model.years), - officialRatings: state.officialRatings.replaceMap(model.officialRatings), - types: state.types.replaceMap(model.types), - sortingOption: model.sortingOption, - sortOrder: model.sortOrder, - favourites: model.favourites, - hideEmptyShows: model.hideEmptyShows, - recursive: model.recursive, - groupBy: model.groupBy, - ); - } + void loadModel(LibraryFilterModel model) => state = state.copyWith(filters: state.filters.loadModel(model)); void saveFiltersNew(String newName) => ref.read(filterProvider.notifier).saveFilter(LibraryFiltersModel.fromLibrarySearch(newName, state)); diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 036f91e..673c668 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -189,16 +189,16 @@ class User extends _$User { } void removeFilter(LibraryFiltersModel model) { - final currentList = ((state?.savedFilters ?? [])).toList(growable: true); + final currentList = ((state?.libraryFilters ?? [])).toList(growable: true); currentList.remove(model); - state = state?.copyWith(savedFilters: currentList); + userState = state?.copyWith(libraryFilters: currentList); } void saveFilter(LibraryFiltersModel model) { - final currentList = (state?.savedFilters ?? []).toList(growable: true); + final currentList = (state?.libraryFilters ?? []).toList(growable: true); if (currentList.firstWhereOrNull((value) => value.id == model.id) != null) { - state = state?.copyWith( - savedFilters: currentList.map( + userState = state?.copyWith( + libraryFilters: currentList.map( (e) { if (e.id == model.id) { return model; @@ -210,11 +210,11 @@ class User extends _$User { }, ).toList()); } else { - state = state?.copyWith(savedFilters: [model, ...currentList]); + userState = state?.copyWith(libraryFilters: [model, ...currentList]); } } - void deleteAllFilters() => state = state?.copyWith(savedFilters: []); + void deleteAllFilters() => userState = state?.copyWith(libraryFilters: []); String? createDownloadUrl(ItemBaseModel item) => Uri.encodeFull("${state?.server}/Items/${item.id}/Download?api_key=${state?.credentials.token}"); diff --git a/lib/providers/user_provider.g.dart b/lib/providers/user_provider.g.dart index da5edae..2b4e7db 100644 --- a/lib/providers/user_provider.g.dart +++ b/lib/providers/user_provider.g.dart @@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef; -String _$userHash() => r'24b34a88eae11aec1e377a82d1e507f293b7816a'; +String _$userHash() => r'2b2a9f10a3bbb2c7ce51a926e7fb6e807a0e6377'; /// See also [User]. @ProviderFor(User) diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 840e506..11dd40a 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -198,6 +198,9 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { bool? favourites, _i21.SortingOrder? sortOrder, _i21.SortingOptions? sortingOptions, + Map<_i19.FladderItemType, bool>? types, + Map? genres, + bool recursive = true, _i22.PhotoModel? photoToView, _i20.Key? key, List<_i18.PageRouteInfo>? children, @@ -209,6 +212,9 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { favourites: favourites, sortOrder: sortOrder, sortingOptions: sortingOptions, + types: types, + genres: genres, + recursive: recursive, photoToView: photoToView, key: key, ), @@ -218,6 +224,9 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { 'favourites': favourites, 'sortOrder': sortOrder, 'sortOptions': sortingOptions, + 'itemTypes': types, + 'genres': genres, + 'recursive': recursive, }, initialChildren: children, ); @@ -235,6 +244,9 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { favourites: queryParams.optBool('favourites'), sortOrder: queryParams.get('sortOrder'), sortingOptions: queryParams.get('sortOptions'), + types: queryParams.get('itemTypes'), + genres: queryParams.get('genres'), + recursive: queryParams.getBool('recursive', true), ), ); return _i8.LibrarySearchScreen( @@ -243,6 +255,9 @@ class LibrarySearchRoute extends _i18.PageRouteInfo { favourites: args.favourites, sortOrder: args.sortOrder, sortingOptions: args.sortingOptions, + types: args.types, + genres: args.genres, + recursive: args.recursive, photoToView: args.photoToView, key: args.key, ); @@ -257,6 +272,9 @@ class LibrarySearchRouteArgs { this.favourites, this.sortOrder, this.sortingOptions, + this.types, + this.genres, + this.recursive = true, this.photoToView, this.key, }); @@ -271,13 +289,19 @@ class LibrarySearchRouteArgs { final _i21.SortingOptions? sortingOptions; + final Map<_i19.FladderItemType, bool>? types; + + final Map? genres; + + final bool recursive; + final _i22.PhotoModel? photoToView; final _i20.Key? key; @override String toString() { - return 'LibrarySearchRouteArgs{viewModelId: $viewModelId, folderId: $folderId, favourites: $favourites, sortOrder: $sortOrder, sortingOptions: $sortingOptions, photoToView: $photoToView, key: $key}'; + return 'LibrarySearchRouteArgs{viewModelId: $viewModelId, folderId: $folderId, favourites: $favourites, sortOrder: $sortOrder, sortingOptions: $sortingOptions, types: $types, genres: $genres, recursive: $recursive, photoToView: $photoToView, key: $key}'; } @override @@ -289,6 +313,9 @@ class LibrarySearchRouteArgs { favourites == other.favourites && sortOrder == other.sortOrder && sortingOptions == other.sortingOptions && + const _i23.MapEquality().equals(types, other.types) && + const _i23.MapEquality().equals(genres, other.genres) && + recursive == other.recursive && photoToView == other.photoToView && key == other.key; } @@ -300,6 +327,9 @@ class LibrarySearchRouteArgs { favourites.hashCode ^ sortOrder.hashCode ^ sortingOptions.hashCode ^ + const _i23.MapEquality().hash(types) ^ + const _i23.MapEquality().hash(genres) ^ + recursive.hashCode ^ photoToView.hashCode ^ key.hashCode; } diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 1329647..d8e202d 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/dashboard_provider.dart'; @@ -177,19 +178,26 @@ class _DashboardScreenState extends ConsumerState { contentPadding: padding, label: context.localized.dashboardRecentlyAdded(view.name), collectionAspectRatio: view.collectionType.aspectRatio, - onLabelClick: () => context.router.push(LibrarySearchRoute( - viewModelId: view.id, - sortingOptions: switch (view.collectionType) { - CollectionType.tvshows || - CollectionType.books || - CollectionType.boxsets || - CollectionType.folders || - CollectionType.music => - SortingOptions.dateLastContentAdded, - _ => SortingOptions.dateAdded, - }, - sortOrder: SortingOrder.descending, - )), + onLabelClick: () => context.router.push( + LibrarySearchRoute( + viewModelId: view.id, + types: switch (view.collectionType) { + CollectionType.tvshows => { + FladderItemType.episode: true, + }, + _ => {}, + }, + sortingOptions: switch (view.collectionType) { + CollectionType.books || + CollectionType.boxsets || + CollectionType.folders || + CollectionType.music => + SortingOptions.dateLastContentAdded, + _ => SortingOptions.dateAdded, + }, + sortOrder: SortingOrder.descending, + ), + ), posters: view.recentlyAdded, ), )), diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index bf3e94b..3ae7155 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/providers/favourites_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -56,6 +57,15 @@ class FavouritesScreen extends ConsumerWidget { (e) => SliverToBoxAdapter( child: PosterRow( contentPadding: padding, + onLabelClick: () => context.pushRoute( + LibrarySearchRoute().withFilter( + LibraryFilterModel( + favourites: true, + types: {e.key: true}, + recursive: true, + ), + ), + ), label: e.key.label(context), posters: e.value, ), diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index b051b07..cbad727 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -6,6 +6,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/recommended_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/providers/library_screen_provider.dart'; @@ -15,6 +16,7 @@ import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -159,6 +161,16 @@ class _LibraryScreenState extends ConsumerState with SingleTicker padding: const EdgeInsets.symmetric(vertical: 8.0), child: PosterRow( contentPadding: padding, + onLabelClick: () => context.pushRoute( + LibrarySearchRoute( + viewModelId: libraryScreenState.selectedViewModel?.id ?? "", + ).withFilter( + const LibraryFilterModel( + favourites: true, + recursive: true, + ), + ), + ), posters: favourites, label: context.localized.favorites, ), @@ -173,6 +185,16 @@ class _LibraryScreenState extends ConsumerState with SingleTicker child: PosterRow( contentPadding: padding, posters: element.posters, + onLabelClick: () => context.pushRoute( + LibrarySearchRoute( + viewModelId: libraryScreenState.selectedViewModel?.id ?? "", + ).withFilter( + LibraryFilterModel( + recursive: true, + genres: {(element.name as Other).customLabel: true}, + ), + ), + ), label: element.type != null ? "${element.type?.label(context)} - ${element.name.label(context)}" : element.name.label(context), @@ -210,7 +232,7 @@ class LibraryRow extends ConsumerWidget { label: context.localized.library(views.length), items: views, startIndex: selectedView != null ? views.indexOf(selectedView!) : null, - height: 165, + height: 125, contentPadding: padding, itemBuilder: (context, index) { final view = views[index]; @@ -240,52 +262,51 @@ class LibraryRow extends ConsumerWidget { items: viewActions.popupMenuItems(useIcons: true), ); }, - child: Card( - color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null, - shadowColor: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - SizedBox( - width: 200, - child: Card( - child: AspectRatio( - aspectRatio: 1.60, - child: FladderImage( - image: view.imageData?.primary, - fit: BoxFit.cover, - placeHolder: Center( - child: Text( - view.name, - style: Theme.of(context).textTheme.titleMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - ), - ), + child: Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 250), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: isSelected ? 4 : 0, + color: isSelected ? Theme.of(context).colorScheme.primary : Colors.transparent, + ), + borderRadius: FladderTheme.defaultShape.borderRadius, + ), + clipBehavior: Clip.hardEdge, + width: 200, + child: ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: AspectRatio( + aspectRatio: 1.60, + child: FladderImage( + image: view.imageData?.primary, + fit: BoxFit.cover, + placeHolder: Center( + child: Text( + view.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, ), ), ), ), - Expanded( + ), + ), + Positioned.fill( + child: Align( + alignment: Alignment.bottomLeft, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + opacity: isSelected ? 0 : 1, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), + padding: const EdgeInsets.all(6), child: Row( spacing: 8, children: [ - if (isSelected) - Container( - height: 12, - width: 12, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - ), - ), Text( view.name, style: Theme.of(context).textTheme.titleMedium, @@ -297,9 +318,9 @@ class LibraryRow extends ConsumerWidget { ), ), ), - ], + ), ), - ), + ], ), ); }, diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart index dbd6352..5dd9ec4 100644 --- a/lib/screens/library_search/library_search_screen.dart +++ b/lib/screens/library_search/library_search_screen.dart @@ -9,6 +9,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; +import 'package:fladder/models/library_filter_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/playlist_model.dart'; @@ -44,7 +45,6 @@ import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; import 'package:fladder/widgets/shared/scroll_position.dart'; -import 'package:fladder/widgets/shared/shapes.dart'; @RoutePage() class LibrarySearchScreen extends ConsumerStatefulWidget { @@ -53,6 +53,9 @@ class LibrarySearchScreen extends ConsumerStatefulWidget { final List? folderId; final SortingOrder? sortOrder; final SortingOptions? sortingOptions; + final Map? types; + final Map? genres; + final bool recursive; final PhotoModel? photoToView; const LibrarySearchScreen({ @QueryParam("parentId") this.viewModelId, @@ -60,6 +63,9 @@ class LibrarySearchScreen extends ConsumerStatefulWidget { @QueryParam("favourites") this.favourites, @QueryParam("sortOrder") this.sortOrder, @QueryParam("sortOptions") this.sortingOptions, + @QueryParam("itemTypes") this.types, + @QueryParam("genres") this.genres, + @QueryParam("recursive") this.recursive = true, this.photoToView, super.key, }); @@ -69,7 +75,6 @@ class LibrarySearchScreen extends ConsumerStatefulWidget { } class _LibrarySearchScreenState extends ConsumerState { - final SearchController searchController = SearchController(); final Debouncer debouncer = Debouncer(const Duration(seconds: 1)); final GlobalKey refreshKey = GlobalKey(); final ScrollController scrollController = ScrollController(); @@ -93,32 +98,24 @@ class _LibrarySearchScreenState extends ConsumerState { @override void initState() { super.initState(); - initLibrary(); + WidgetsBinding.instance.addPostFrameCallback((value) { + initLibrary(); + }); } - void initLibrary() { - searchController.addListener(() { - debouncer.run(() { - ref.read(providerKey.notifier).setSearch(searchController.text); - }); - }); - - Future.microtask( - () async { - await refreshKey.currentState?.show(); - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.edgeToEdge, - overlays: [], - ); - - if (context.mounted && widget.photoToView != null) { - libraryProvider.viewGallery(context, selected: widget.photoToView); - } - scrollController.addListener(() { - scrollPosition(); - }); - }, + Future initLibrary() async { + await refreshKey.currentState?.show(); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: [], ); + + if (context.mounted && widget.photoToView != null) { + libraryProvider.viewGallery(context, selected: widget.photoToView); + } + scrollController.addListener(() { + scrollPosition(); + }); } void scrollPosition() { @@ -127,19 +124,27 @@ class _LibrarySearchScreenState extends ConsumerState { } } + Future refreshSearch() async { + await refreshKey.currentState?.show(); + scrollController.jumpTo(0); + } + @override Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null; final librarySearchResults = ref.watch(providerKey); - final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows); + final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.filters.hideEmptyShows); final libraryViewType = ref.watch(libraryViewTypeProvider); + final floatingAppBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; + ref.listen( providerKey, (previous, next) { - if (previous != next) { - refreshKey.currentState?.show(); - scrollController.jumpTo(0); + if (previous?.filters != next.filters) { + refreshSearch(); } }, ); @@ -153,20 +158,17 @@ class _LibrarySearchScreenState extends ConsumerState { } }, child: NestedScaffold( - background: BackgroundImage(items: librarySearchResults.activePosters), + background: BackgroundImage(images: postersList.map((e) => e.images).nonNulls.toList()), body: Padding( padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth), child: Scaffold( extendBody: true, + backgroundColor: Colors.transparent, extendBodyBehindAppBar: true, floatingActionButton: HideOnScroll( controller: scrollController, - visibleBuilder: (visible) => Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (librarySearchResults.activePosters.isNotEmpty) - FloatingActionButtonAnimated( + visibleBuilder: (visible) => librarySearchResults.activePosters.isNotEmpty + ? FloatingActionButtonAnimated( key: Key(context.localized.playLabel), isExtended: visible, tooltip: context.localized.playVideos, @@ -192,12 +194,12 @@ class _LibrarySearchScreenState extends ConsumerState { }, label: Text(context.localized.playLabel), icon: const Icon(IconsaxPlusBold.play), - ), - ].addInBetween(const SizedBox(height: 10)), - ), + ) + : null, ), bottomNavigationBar: HideOnScroll( - controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, + controller: scrollController, + canHide: !floatingAppBar, child: IgnorePointer( ignoring: librarySearchResults.fetchingItems, child: _LibrarySearchBottomBar( @@ -209,319 +211,306 @@ class _LibrarySearchScreenState extends ConsumerState { ), ), ), - body: Stack( - fit: StackFit.expand, - children: [ - Positioned.fill( - child: PinchPosterZoom( - scaleDifference: (difference) => - ref.read(clientSettingsProvider.notifier).addPosterSize(difference), - child: ClipRRect( - borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop - ? BorderRadius.circular(15) - : BorderRadius.circular(0), - child: FladderScrollbar( - visible: AdaptiveLayout.of(context).inputDevice != InputDevice.pointer, - controller: scrollController, - child: PullToRefresh( - refreshKey: refreshKey, - autoFocus: false, - contextRefresh: false, - onRefresh: () async { - if (libraryProvider.mounted) { - return libraryProvider.initRefresh( - widget.folderId, - widget.viewModelId, - widget.favourites, - widget.sortOrder, - widget.sortingOptions, - ); - } - }, - refreshOnStart: false, - child: CustomScrollView( - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverAppBar( - floating: !AdaptiveLayout.of(context).isDesktop, - collapsedHeight: 80, - automaticallyImplyLeading: false, - pinned: AdaptiveLayout.of(context).isDesktop, - primary: true, - elevation: 5, - leading: context.router.backButton(), - surfaceTintColor: Colors.transparent, - shadowColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.surface, - shape: AppBarShape(), - titleSpacing: 4, - leadingWidth: 48, - actions: [ - const SizedBox(width: 4), - Builder(builder: (context) { - final isFavorite = - librarySearchResults.nestedCurrentItem?.userData.isFavourite == true; - final itemActions = librarySearchResults.nestedCurrentItem?.generateActions( - context, - ref, - exclude: { - ItemActions.details, - ItemActions.markPlayed, - ItemActions.markUnplayed, - }, - onItemUpdated: (item) { - libraryProvider.updateParentItem(item); - }, - onUserDataChanged: (userData) { - libraryProvider.updateUserDataMain(userData); - }, - ) ?? - []; - final itemCountWidget = ItemActionButton( - label: Text(context.localized.itemCount(librarySearchResults.totalItemCount)), - icon: const Icon(IconsaxPlusBold.document_1), - ); - final refreshAction = ItemActionButton( - label: Text(context.localized.forceRefresh), - action: () => refreshKey.currentState?.show(), - icon: const Icon(IconsaxPlusLinear.refresh), - ); - final showSavedFiltersDialogue = ItemActionButton( - label: Text(context.localized.filter(2)), - action: () => showSavedFilters(context, librarySearchResults, libraryProvider), - icon: const Icon(IconsaxPlusLinear.refresh), - ); - final itemViewAction = ItemActionButton( - label: Text(context.localized.selectViewType), - icon: Icon(libraryViewType.icon), - action: () { - showAdaptiveDialog( - context: context, - builder: (context) => AlertDialog( - content: Consumer( - builder: (context, ref, child) { - final currentType = ref.watch(libraryViewTypeProvider); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(context.localized.selectViewType, - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 12), - ...LibraryViewTypes.values - .map( - (e) => FilledButton.tonal( - style: FilledButtonTheme.of(context).style?.copyWith( - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric( - horizontal: 12, vertical: 24)), - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (e != currentType) { - return Colors.transparent; - } - return null; - }, - ), - ), - onPressed: () { - ref.read(libraryViewTypeProvider.notifier).state = e; + body: PinchPosterZoom( + scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference), + child: FladderScrollbar( + visible: AdaptiveLayout.of(context).inputDevice != InputDevice.pointer, + controller: scrollController, + child: PullToRefresh( + refreshKey: refreshKey, + autoFocus: false, + contextRefresh: false, + onRefresh: () async { + final defaultFilter = const LibraryFilterModel(); + if (libraryProvider.mounted) { + return libraryProvider.initRefresh( + widget.folderId, + widget.viewModelId, + defaultFilter.copyWith( + favourites: widget.favourites ?? defaultFilter.favourites, + sortOrder: widget.sortOrder ?? defaultFilter.sortOrder, + sortingOption: widget.sortingOptions ?? defaultFilter.sortingOption, + types: widget.types ?? {}, + genres: widget.genres ?? {}, + recursive: widget.recursive, + ), + ); + } + }, + refreshOnStart: false, + child: CustomScrollView( + controller: scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverAppBar( + floating: !floatingAppBar, + collapsedHeight: 80, + automaticallyImplyLeading: false, + primary: true, + pinned: floatingAppBar, + elevation: 5, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + backgroundColor: Colors.transparent, + titleSpacing: 4, + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + surfaceColor.withValues(alpha: 0.8), + surfaceColor.withValues(alpha: 0.75), + surfaceColor.withValues(alpha: 0.5), + surfaceColor.withValues(alpha: 0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + )), + ), + actions: [ + Builder(builder: (context) { + final isFavorite = librarySearchResults.nestedCurrentItem?.userData.isFavourite == true; + final itemActions = librarySearchResults.nestedCurrentItem?.generateActions( + context, + ref, + exclude: { + ItemActions.details, + ItemActions.markPlayed, + ItemActions.markUnplayed, + }, + onItemUpdated: (item) { + libraryProvider.updateParentItem(item); + }, + onUserDataChanged: (userData) { + libraryProvider.updateUserDataMain(userData); + }, + ) ?? + []; + final itemCountWidget = ItemActionButton( + label: Text(context.localized.itemCount(librarySearchResults.totalItemCount)), + icon: const Icon(IconsaxPlusBold.document_1), + ); + final refreshAction = ItemActionButton( + label: Text(context.localized.forceRefresh), + action: () => refreshKey.currentState?.show(), + icon: const Icon(IconsaxPlusLinear.refresh), + ); + final showSavedFiltersDialogue = ItemActionButton( + label: Text(context.localized.filter(2)), + action: () => showSavedFilters(context, uniqueKey), + icon: const Icon(IconsaxPlusLinear.refresh), + ); + final itemViewAction = ItemActionButton( + label: Text(context.localized.selectViewType), + icon: Icon(libraryViewType.icon), + action: () { + showAdaptiveDialog( + context: context, + builder: (context) => AlertDialog( + content: Consumer( + builder: (context, ref, child) { + final currentType = ref.watch(libraryViewTypeProvider); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(context.localized.selectViewType, + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 12), + ...LibraryViewTypes.values + .map( + (e) => FilledButton.tonal( + style: FilledButtonTheme.of(context).style?.copyWith( + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 12, vertical: 24)), + backgroundColor: WidgetStateProperty.resolveWith( + (states) { + if (e != currentType) { + return Colors.transparent; + } + return null; }, - child: Row( - children: [ - Icon(e.icon), - const SizedBox(width: 12), - Text( - e.label(context), - ) - ], - ), ), + ), + onPressed: () { + ref.read(libraryViewTypeProvider.notifier).state = e; + }, + child: Row( + children: [ + Icon(e.icon), + const SizedBox(width: 12), + Text( + e.label(context), ) - .toList() - .addInBetween(const SizedBox(height: 12)), - ], - ); - }, - ), - ), + ], + ), + ), + ) + .toList() + .addInBetween(const SizedBox(height: 12)), + ], ); - }); - return Card( - elevation: 0, - child: Tooltip( - message: librarySearchResults.nestedCurrentItem?.type.label(context) ?? - context.localized.library(1), - child: InkWell( - onTapUp: (details) async { - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) { - double left = details.globalPosition.dx; - double top = details.globalPosition.dy; - await showMenu( - context: context, - position: RelativeRect.fromLTRB(left, top, 40, 100), - items: [ - PopupMenuItem( - child: Text( - librarySearchResults.nestedCurrentItem?.type.label(context) ?? - context.localized.library(0))), - itemCountWidget.toPopupMenuItem(useIcons: true), - refreshAction.toPopupMenuItem(useIcons: true), - itemViewAction.toPopupMenuItem(useIcons: true), - if (librarySearchResults.views.hasEnabled == true) - showSavedFiltersDialogue.toPopupMenuItem(useIcons: true), - if (itemActions.isNotEmpty) ItemActionDivider().toPopupMenuItem(), - ...itemActions.popupMenuItems(useIcons: true), - ], - elevation: 8.0, - ); - } else { - await showBottomSheetPill( - context: context, - content: (context, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: [ - itemCountWidget.toListItem(context, useIcons: true), - refreshAction.toListItem(context, useIcons: true), - itemViewAction.toListItem(context, useIcons: true), - if (librarySearchResults.views.hasEnabled == true) - showSavedFiltersDialogue.toPopupMenuItem(useIcons: true), - if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context), - ...itemActions.listTileItems(context, useIcons: true), - ], - ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon( - isFavorite - ? librarySearchResults.nestedCurrentItem?.type.selectedicon - : librarySearchResults.nestedCurrentItem?.type.icon ?? - IconsaxPlusLinear.document, - color: isFavorite ? Theme.of(context).colorScheme.primary : null, - ), - ), - ), + }, ), - ); - }), - if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[ - const SizedBox(width: 6), - const SizedBox.square(dimension: 46, child: SettingsUserIcon()), - ], - const SizedBox(width: 12) - ], - title: Hero( - tag: "PrimarySearch", - child: SuggestionSearchBar( - autoFocus: isEmptySearchScreen, - key: uniqueKey, - title: librarySearchResults.searchBarTitle(context), - debounceDuration: const Duration(seconds: 1), - onItem: (value) async { - await value.navigateTo(context); - refreshKey.currentState?.show(); - }, - onSubmited: (value) async { - if (librarySearchResults.searchQuery != value) { - libraryProvider.setSearch(value); - refreshKey.currentState?.show(); - } - }, - ), - ), - bottom: PreferredSize( - preferredSize: const Size(0, 50), - child: Transform.translate( - offset: Offset(0, AdaptiveLayout.of(context).isDesktop ? -20 : -15), - child: IgnorePointer( - ignoring: librarySearchResults.loading, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Opacity( - opacity: librarySearchResults.loading ? 0.5 : 1, - child: SingleChildScrollView( - padding: const EdgeInsets.all(8), - scrollDirection: Axis.horizontal, - child: LibraryFilterChips( - key: uniqueKey, - ), - ), - ), + ), + ); + }); + return Card( + elevation: 0, + child: Tooltip( + message: librarySearchResults.nestedCurrentItem?.type.label(context) ?? + context.localized.library(1), + child: InkWell( + onTapUp: (details) async { + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) { + double left = details.globalPosition.dx; + double top = details.globalPosition.dy; + await showMenu( + context: context, + position: RelativeRect.fromLTRB(left, top, 40, 100), + items: [ + PopupMenuItem( + child: Text(librarySearchResults.nestedCurrentItem?.type.label(context) ?? + context.localized.library(0))), + itemCountWidget.toPopupMenuItem(useIcons: true), + refreshAction.toPopupMenuItem(useIcons: true), + itemViewAction.toPopupMenuItem(useIcons: true), + if (librarySearchResults.views.hasEnabled == true) + showSavedFiltersDialogue.toPopupMenuItem(useIcons: true), + if (itemActions.isNotEmpty) ItemActionDivider().toPopupMenuItem(), + ...itemActions.popupMenuItems(useIcons: true), ], - ), + elevation: 8.0, + ); + } else { + await showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: [ + itemCountWidget.toListItem(context, useIcons: true), + refreshAction.toListItem(context, useIcons: true), + itemViewAction.toListItem(context, useIcons: true), + if (librarySearchResults.views.hasEnabled == true) + showSavedFiltersDialogue.toPopupMenuItem(useIcons: true), + if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context), + ...itemActions.listTileItems(context, useIcons: true), + ], + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon( + isFavorite + ? librarySearchResults.nestedCurrentItem?.type.selectedicon + : librarySearchResults.nestedCurrentItem?.type.icon ?? + IconsaxPlusLinear.document, + color: isFavorite ? Theme.of(context).colorScheme.primary : null, ), ), ), ), - if (AdaptiveLayout.of(context).isDesktop) - const SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - PosterSizeWidget(), - ], - ), + ); + }), + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[ + const SizedBox(width: 6), + const SizedBox.square(dimension: 46, child: SettingsUserIcon()), + ], + const SizedBox(width: 12) + ], + title: Row( + spacing: 2, + children: [ + const SizedBox(width: 2), + Center( + child: SizedBox.square( + dimension: 47, + child: Card( + child: context.router.backButton(), ), - if (postersList.isNotEmpty) - SliverPadding( - padding: EdgeInsets.only( - left: MediaQuery.of(context).padding.left, - right: MediaQuery.of(context).padding.right), - sliver: LibraryViews( - key: uniqueKey, - items: postersList, - groupByType: librarySearchResults.groupBy, - ), - ) - else - SliverFillRemaining( - child: Center( - child: Text(context.localized.noItemsToShow), - ), + ), + ), + Flexible( + child: Hero( + tag: "PrimarySearch", + child: SuggestionSearchBar( + autoFocus: isEmptySearchScreen, + key: uniqueKey, + title: librarySearchResults.searchBarTitle(context), + debounceDuration: const Duration(seconds: 1), + onItem: (value) async { + await value.navigateTo(context); + refreshKey.currentState?.show(); + }, + onSubmited: (value) async { + if (librarySearchResults.searchQuery != value) { + libraryProvider.setSearch(value); + refreshKey.currentState?.show(); + } + }, ), - SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20)) - ], + ), + ), + ], + ), + bottom: PreferredSize( + preferredSize: const Size(0, 50), + child: Transform.translate( + offset: Offset(0, AdaptiveLayout.of(context).isDesktop ? -20 : -15), + child: IgnorePointer( + ignoring: librarySearchResults.loading, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Opacity( + opacity: librarySearchResults.loading ? 0.5 : 1, + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + scrollDirection: Axis.horizontal, + child: LibraryFilterChips( + key: uniqueKey, + ), + ), + ), + ], + ), + ), ), ), ), - ), + if (AdaptiveLayout.of(context).isDesktop) + const SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PosterSizeWidget(), + ], + ), + ), + if (postersList.isNotEmpty) + SliverPadding( + padding: EdgeInsets.only( + left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right), + sliver: LibraryViews( + key: uniqueKey, + items: postersList, + groupByType: librarySearchResults.filters.groupBy, + ), + ) + else + SliverFillRemaining( + child: Center( + child: Text(context.localized.noItemsToShow), + ), + ), + SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20)) + ], ), ), - if (librarySearchResults.fetchingItems) ...[ - Container( - color: Colors.black.withValues(alpha: 0.1), - ), - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator.adaptive(), - Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), - IconButton( - onPressed: () => libraryProvider.cancelFetch(), - icon: const Icon(IconsaxPlusLinear.close_square), - ) - ].addInBetween(const SizedBox(width: 16)), - ), - ), - ), - ) - ], - ], + ), ), ), ), @@ -663,7 +652,7 @@ class _LibrarySearchBottomBar extends ConsumerWidget { context, libraryProvider: libraryProvider, uniqueKey: uniqueKey, - options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder), + options: (librarySearchResults.filters.sortingOption, librarySearchResults.filters.sortOrder), ); if (newOptions != null) { if (newOptions.$1 != null) { diff --git a/lib/screens/library_search/widgets/library_filter_chips.dart b/lib/screens/library_search/widgets/library_filter_chips.dart index 07487b0..43d497e 100644 --- a/lib/screens/library_search/widgets/library_filter_chips.dart +++ b/lib/screens/library_search/widgets/library_filter_chips.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; @@ -11,7 +12,9 @@ import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/screens/shared/chips/category_chip.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; +import 'package:fladder/util/position_provider.dart'; import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/widgets/shared/button_group.dart'; class LibraryFilterChips extends ConsumerStatefulWidget { const LibraryFilterChips({super.key}); @@ -25,133 +28,137 @@ class _LibraryFilterChipsState extends ConsumerState { Widget build(BuildContext context) { final uniqueKey = widget.key ?? UniqueKey(); final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier); - final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); - final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites)); - final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive)); - final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows)); + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.groupBy)); + final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.favourites)); + final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.recursive)); + final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.hideEmptyShows)); final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey)); + final chips = [ + if (librarySearchResults.folderOverwrite.isEmpty) + CategoryChip( + label: Text(context.localized.library(2)), + items: librarySearchResults.views.sortByKey((value) => value.name), + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setViews(value), + onCancel: () => libraryProvider.setViews(librarySearchResults.views), + onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), + ), + CategoryChip( + label: Text(context.localized.type(librarySearchResults.filters.types.length)), + items: librarySearchResults.filters.types.sortByKey((value) => value.label(context)), + activeIcon: IconsaxPlusBold.filter_tick, + labelBuilder: (item) => Row( + children: [ + Icon(item.icon), + const SizedBox(width: 12), + Text(item.label(context)), + ], + ), + onSave: (value) => libraryProvider.setTypes(value), + onClear: () => libraryProvider.setTypes(librarySearchResults.filters.types.setAll(false)), + ), + ExpressiveButton( + isSelected: favourites, + icon: favourites ? const Icon(IconsaxPlusBold.heart) : null, + label: Text(context.localized.favorites), + onPressed: () { + libraryProvider.toggleFavourite(); + context.refreshData(); + }, + ), + ExpressiveButton( + isSelected: recursive, + icon: recursive ? const Icon(IconsaxPlusBold.tick_circle) : null, + label: Text(context.localized.recursive), + onPressed: () { + libraryProvider.toggleRecursive(); + context.refreshData(); + }, + ), + if (librarySearchResults.filters.genres.isNotEmpty) + CategoryChip( + label: Text(context.localized.genre(librarySearchResults.filters.genres.length)), + activeIcon: IconsaxPlusBold.hierarchy_2, + items: librarySearchResults.filters.genres, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setGenres(value), + onCancel: () => libraryProvider.setGenres(librarySearchResults.filters.genres), + onClear: () => libraryProvider.setGenres(librarySearchResults.filters.genres.setAll(false)), + ), + if (librarySearchResults.filters.studios.isNotEmpty) + CategoryChip( + label: Text(context.localized.studio(librarySearchResults.filters.studios.length)), + activeIcon: IconsaxPlusBold.airdrop, + items: librarySearchResults.filters.studios, + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setStudios(value), + onCancel: () => libraryProvider.setStudios(librarySearchResults.filters.studios), + onClear: () => libraryProvider.setStudios(librarySearchResults.filters.studios.setAll(false)), + ), + if (librarySearchResults.filters.tags.isNotEmpty) + CategoryChip( + label: Text(context.localized.label(librarySearchResults.filters.tags.length)), + activeIcon: Icons.label_rounded, + items: librarySearchResults.filters.tags, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setTags(value), + onCancel: () => libraryProvider.setTags(librarySearchResults.filters.tags), + onClear: () => libraryProvider.setTags(librarySearchResults.filters.tags.setAll(false)), + ), + ExpressiveButton( + isSelected: groupBy != GroupBy.none, + icon: groupBy != GroupBy.none ? const Icon(IconsaxPlusBold.bag_tick) : null, + label: Text(context.localized.group), + onPressed: () { + _openGroupDialogue(context, ref, libraryProvider, uniqueKey); + }, + ), + CategoryChip( + label: Text(context.localized.filter(librarySearchResults.filters.itemFilters.length)), + items: librarySearchResults.filters.itemFilters, + labelBuilder: (item) => Text(item.label(context)), + onSave: (value) => libraryProvider.setFilters(value), + onClear: () => libraryProvider.setFilters(librarySearchResults.filters.itemFilters.setAll(false)), + ), + if (librarySearchResults.filters.types[FladderItemType.series] == true) + ExpressiveButton( + isSelected: !hideEmpty, + icon: !hideEmpty ? const Icon(IconsaxPlusBold.ghost) : null, + label: Text(!hideEmpty ? context.localized.hideEmpty : context.localized.showEmpty), + onPressed: libraryProvider.toggleEmptyShows, + ), + if (librarySearchResults.filters.officialRatings.isNotEmpty) + CategoryChip( + label: Text(context.localized.rating(librarySearchResults.filters.officialRatings.length)), + activeIcon: Icons.star_rate_rounded, + items: librarySearchResults.filters.officialRatings, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setRatings(value), + onCancel: () => libraryProvider.setRatings(librarySearchResults.filters.officialRatings), + onClear: () => libraryProvider.setRatings(librarySearchResults.filters.officialRatings.setAll(false)), + ), + if (librarySearchResults.filters.years.isNotEmpty) + CategoryChip( + label: Text(context.localized.year(librarySearchResults.filters.years.length)), + items: librarySearchResults.filters.years, + labelBuilder: (item) => Text(item.toString()), + onSave: (value) => libraryProvider.setYears(value), + onCancel: () => libraryProvider.setYears(librarySearchResults.filters.years), + onClear: () => libraryProvider.setYears(librarySearchResults.filters.years.setAll(false)), + ), + ]; + return Row( - spacing: 8, - children: [ - if (librarySearchResults.folderOverwrite.isEmpty) - CategoryChip( - label: Text(context.localized.library(2)), - items: librarySearchResults.views, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setViews(value), - onCancel: () => libraryProvider.setViews(librarySearchResults.views), - onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), - ), - CategoryChip( - label: Text(context.localized.type(librarySearchResults.types.length)), - items: librarySearchResults.types, - labelBuilder: (item) => Row( - children: [ - Icon(item.icon), - const SizedBox(width: 12), - Text(item.label(context)), - ], - ), - onSave: (value) => libraryProvider.setTypes(value), - onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)), - ), - FilterChip( - label: Text(context.localized.favorites), - avatar: Icon( - favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: favourites, - showCheckmark: false, - onSelected: (_) { - libraryProvider.toggleFavourite(); - context.refreshData(); - }, - ), - FilterChip( - label: Text(context.localized.recursive), - selected: recursive, - onSelected: (_) { - libraryProvider.toggleRecursive(); - context.refreshData(); - }, - ), - if (librarySearchResults.genres.isNotEmpty) - CategoryChip( - label: Text(context.localized.genre(librarySearchResults.genres.length)), - activeIcon: IconsaxPlusBold.hierarchy_2, - items: librarySearchResults.genres, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setGenres(value), - onCancel: () => libraryProvider.setGenres(librarySearchResults.genres), - onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)), - ), - if (librarySearchResults.studios.isNotEmpty) - CategoryChip( - label: Text(context.localized.studio(librarySearchResults.studios.length)), - activeIcon: IconsaxPlusBold.airdrop, - items: librarySearchResults.studios, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setStudios(value), - onCancel: () => libraryProvider.setStudios(librarySearchResults.studios), - onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)), - ), - if (librarySearchResults.tags.isNotEmpty) - CategoryChip( - label: Text(context.localized.label(librarySearchResults.tags.length)), - activeIcon: Icons.label_rounded, - items: librarySearchResults.tags, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setTags(value), - onCancel: () => libraryProvider.setTags(librarySearchResults.tags), - onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)), - ), - FilterChip( - label: Text(context.localized.group), - selected: groupBy != GroupBy.none, - onSelected: (_) { - _openGroupDialogue(context, ref, libraryProvider, uniqueKey); - }, - ), - CategoryChip( - label: Text(context.localized.filter(librarySearchResults.filters.length)), - items: librarySearchResults.filters, - labelBuilder: (item) => Text(item.label(context)), - onSave: (value) => libraryProvider.setFilters(value), - onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)), - ), - if (librarySearchResults.types[FladderItemType.series] == true) - FilterChip( - avatar: Icon( - hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: hideEmpty, - showCheckmark: false, - label: Text(context.localized.hideEmpty), - onSelected: libraryProvider.setHideEmpty, - ), - if (librarySearchResults.officialRatings.isNotEmpty) - CategoryChip( - label: Text(context.localized.rating(librarySearchResults.officialRatings.length)), - activeIcon: Icons.star_rate_rounded, - items: librarySearchResults.officialRatings, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setRatings(value), - onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings), - onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)), - ), - if (librarySearchResults.years.isNotEmpty) - CategoryChip( - label: Text(context.localized.year(librarySearchResults.years.length)), - items: librarySearchResults.years, - labelBuilder: (item) => Text(item.toString()), - onSave: (value) => libraryProvider.setYears(value), - onCancel: () => libraryProvider.setYears(librarySearchResults.years), - onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)), - ), - ], + spacing: 4, + children: chips.mapIndexed( + (index, element) { + final position = index == 0 + ? PositionContext.first + : (index == chips.length - 1 ? PositionContext.last : PositionContext.middle); + return PositionProvider(position: position, child: element); + }, + ).toList(), ); } @@ -164,7 +171,7 @@ class _LibraryFilterChipsState extends ConsumerState { showDialog( context: context, builder: (context) { - final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.groupBy)); return AlertDialog( content: SizedBox( width: MediaQuery.of(context).size.width * 0.65, diff --git a/lib/screens/library_search/widgets/library_saved_filters.dart b/lib/screens/library_search/widgets/library_saved_filters.dart index f99df2c..99a1e4c 100644 --- a/lib/screens/library_search/widgets/library_saved_filters.dart +++ b/lib/screens/library_search/widgets/library_saved_filters.dart @@ -3,41 +3,40 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; Future showSavedFilters( BuildContext context, - LibrarySearchModel model, - LibrarySearchNotifier provider, + Key providerKey, ) { return showDialog( context: context, builder: (context) => LibrarySavedFiltersDialogue( - searchModel: model, - provider: provider, + providerKey: providerKey, ), ); } class LibrarySavedFiltersDialogue extends ConsumerWidget { - final LibrarySearchModel searchModel; - final LibrarySearchNotifier provider; + final Key providerKey; + const LibrarySavedFiltersDialogue({ - required this.searchModel, - required this.provider, super.key, + required this.providerKey, }); @override Widget build(BuildContext context, WidgetRef ref) { final controller = TextEditingController(); + final provider = ref.watch(librarySearchProvider(providerKey).notifier); + final currentFilters = ref.watch(librarySearchProvider(providerKey).select((value) => value.filters)); final filters = ref.watch(provider.filterProvider); final filterProvider = ref.watch(provider.filterProvider.notifier); + final anyFilterSelected = filters.any((element) => element.filter == currentFilters); + return Dialog( child: Padding( padding: const EdgeInsets.all(16), @@ -57,68 +56,75 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget { children: [ ...filters.map( (filter) { + final isCurrentFilter = filter.filter == currentFilters; return Container( margin: const EdgeInsets.symmetric(vertical: 4), child: Card( - child: InkWell( - onTap: () => provider.loadModel(filter), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - children: [ - Expanded(child: Text(filter.name)), - IconButton.filledTonal( - tooltip: context.localized.defaultFilterForLibrary, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - filter.isFavourite - ? Colors.yellowAccent.shade700.withValues(alpha: 0.5) - : null, - ), - ), - onPressed: () => - filterProvider.saveFilter(filter.copyWith(isFavourite: !filter.isFavourite)), - icon: Icon( - color: filter.isFavourite ? Colors.yellowAccent : null, - filter.isFavourite ? IconsaxPlusBold.star_1 : IconsaxPlusLinear.star, - ), - ), - IconButton.filledTonal( - tooltip: context.localized.updateFilterForLibrary, - onPressed: () => provider.updateFilter(filter), - icon: const Icon(IconsaxPlusBold.refresh), - ), - IconButton.filledTonal( - tooltip: context.localized.delete, - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.removeFilterForLibrary(filter.name), - context.localized.deleteFilterConfirmation, - (context) { - filterProvider.removeFilter(filter); - Navigator.of(context).pop(); - }, - context.localized.delete, - (context) { - Navigator.of(context).pop(); - }, - context.localized.cancel, - ); - }, - style: ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), - iconColor: - WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - foregroundColor: - WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - ), - icon: const Icon(IconsaxPlusLinear.trash), - ), - ].addInBetween(const SizedBox(width: 8)), + color: isCurrentFilter + ? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.75) + : null, + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: Row(spacing: 8, children: [ + Expanded( + child: OutlinedTextField( + fillColor: Colors.transparent, + controller: TextEditingController(text: filter.name), + onSubmitted: (value) => provider.updateFilter(filter.copyWith(name: value)), + ), ), - ), + IconButton.filledTonal( + onPressed: isCurrentFilter ? null : () => provider.loadModel(filter.filter), + icon: const Icon(IconsaxPlusBold.filter_add), + ), + IconButton.filledTonal( + tooltip: context.localized.defaultFilterForLibrary, + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + filter.isFavourite ? Colors.yellowAccent.shade700.withValues(alpha: 0.5) : null, + ), + ), + onPressed: () => + filterProvider.saveFilter(filter.copyWith(isFavourite: !filter.isFavourite)), + icon: Icon( + color: filter.isFavourite ? Colors.yellowAccent : null, + filter.isFavourite ? IconsaxPlusBold.star_1 : IconsaxPlusLinear.star_1, + ), + ), + IconButton.filledTonal( + tooltip: context.localized.updateFilterForLibrary, + onPressed: + isCurrentFilter || anyFilterSelected ? null : () => provider.updateFilter(filter), + icon: const Icon(IconsaxPlusBold.refresh), + ), + IconButton.filledTonal( + tooltip: context.localized.delete, + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.removeFilterForLibrary(filter.name), + context.localized.deleteFilterConfirmation, + (context) { + filterProvider.removeFilter(filter); + Navigator.of(context).pop(); + }, + context.localized.delete, + (context) { + Navigator.of(context).pop(); + }, + context.localized.cancel, + ); + }, + style: ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), + iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + foregroundColor: + WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + ), + icon: const Icon(IconsaxPlusLinear.trash), + ), + ]), ), ), ); @@ -129,31 +135,42 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget { ), const Divider(), ], - if (filters.length < 10) + if (filters.length < 10 && !anyFilterSelected) StatefulBuilder(builder: (context, setState) { - return Row( - children: [ - Flexible( - child: OutlinedTextField( - controller: controller, - label: context.localized.name, - onChanged: (value) => setState(() {}), - onSubmitted: (value) => provider.saveFiltersNew(value), + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + child: Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Row( + spacing: 8, + children: [ + Expanded( + child: OutlinedTextField( + controller: controller, + label: context.localized.name, + onChanged: (value) => setState(() {}), + onSubmitted: (value) => provider.saveFiltersNew(value), + ), + ), + FilledButton( + onPressed: controller.text.isEmpty + ? null + : () { + provider.saveFiltersNew(controller.text); + }, + child: Row( + spacing: 8, + children: [Text(context.localized.save), const Icon(IconsaxPlusLinear.save_2)], + ), + ) + ], ), ), - const SizedBox(width: 6), - FilledButton.tonal( - onPressed: controller.text.isEmpty - ? null - : () { - provider.saveFiltersNew(controller.text); - }, - child: const Icon(IconsaxPlusLinear.save_2), - ), - ], + ), ); }) - else + else if (filters.length >= 10) Text(context.localized.libraryFiltersLimitReached), ElevatedButton( onPressed: () { diff --git a/lib/screens/library_search/widgets/library_views.dart b/lib/screens/library_search/widgets/library_views.dart index 2820529..d0d1ac2 100644 --- a/lib/screens/library_search/widgets/library_views.dart +++ b/lib/screens/library_search/widgets/library_views.dart @@ -14,6 +14,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; +import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/playlist_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; @@ -76,7 +77,7 @@ class LibraryViews extends ConsumerWidget { ref.watch(clientSettingsProvider.select((value) => value.posterSize))); final decimal = posterSize - posterSize.toInt(); - final sortingOptions = ref.watch(librarySearchProvider(key!).select((value) => value.sortingOption)); + final sortingOptions = ref.watch(librarySearchProvider(key!).select((value) => value.filters.sortingOption)); List otherActions(ItemBaseModel item) { return [ diff --git a/lib/screens/library_search/widgets/suggestion_search_bar.dart b/lib/screens/library_search/widgets/suggestion_search_bar.dart index 8acb64d..25633ea 100644 --- a/lib/screens/library_search/widgets/suggestion_search_bar.dart +++ b/lib/screens/library_search/widgets/suggestion_search_bar.dart @@ -66,7 +66,7 @@ class _SearchBarState extends ConsumerState { return Card( elevation: 2, shape: RoundedRectangleBorder( - borderRadius: FladderTheme.largeShape.borderRadius, + borderRadius: FladderTheme.smallShape.borderRadius, ), shadowColor: Colors.transparent, child: TypeAheadField( @@ -83,7 +83,7 @@ class _SearchBarState extends ConsumerState { decorationBuilder: (context, child) => DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: FladderTheme.largeShape.borderRadius, + borderRadius: FladderTheme.smallShape.borderRadius, ), child: child, ), diff --git a/lib/screens/shared/chips/category_chip.dart b/lib/screens/shared/chips/category_chip.dart index 0d07d83..7f4cb2b 100644 --- a/lib/screens/shared/chips/category_chip.dart +++ b/lib/screens/shared/chips/category_chip.dart @@ -7,6 +7,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; +import 'package:fladder/widgets/shared/button_group.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart'; @@ -20,7 +21,6 @@ class CategoryChip extends StatelessWidget { final VoidCallback? onCancel; final VoidCallback? onClear; final VoidCallback? onDismiss; - const CategoryChip({ required this.label, this.dialogueTitle, @@ -37,37 +37,21 @@ class CategoryChip extends StatelessWidget { @override Widget build(BuildContext context) { var selection = items.included.isNotEmpty; - return FilterChip( - selected: selection, - showCheckmark: activeIcon == null, + return ExpressiveButton( + isSelected: selection, + icon: selection ? Icon(activeIcon ?? IconsaxPlusBold.archive_tick) : null, label: Row( - mainAxisSize: MainAxisSize.min, + spacing: 6, children: [ - if (activeIcon != null) - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: selection - ? Padding( - padding: const EdgeInsets.only(right: 12), - child: Icon( - activeIcon!, - size: 20, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - : const SizedBox(), - ), label, - const SizedBox(width: 8), - Icon( - Icons.arrow_drop_down_rounded, - size: 20, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + const Icon( + IconsaxPlusLinear.arrow_down, + size: 16, + ) ], ), - onSelected: items.isNotEmpty - ? (_) async { + onPressed: items.isNotEmpty + ? () async { final newEntry = await openActionSheet(context); if (newEntry != null) { onSave?.call(newEntry); diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index f3a1e5c..5e6d580 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -189,7 +189,7 @@ class _DetailScaffoldState extends ConsumerState { onPressed: () => context.router.popBack(), icon: Padding( padding: EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4), - child: const Icon(IconsaxPlusLinear.arrow_left_2), + child: const BackButtonIcon(), ), ), const Spacer(), diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 93773fb..02db9da 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -83,6 +83,8 @@ class _PosterImageState extends ConsumerState { await widget.poster.navigateTo(context); } + final posterRadius = FladderTheme.smallShape.borderRadius; + @override Widget build(BuildContext context) { final poster = widget.poster; @@ -101,7 +103,7 @@ class _PosterImageState extends ConsumerState { width: 1.0, color: Colors.white.withValues(alpha: 0.10), ), - borderRadius: FladderTheme.defaultShape.borderRadius, + borderRadius: posterRadius, ), child: Stack( fit: StackFit.expand, @@ -135,7 +137,7 @@ class _PosterImageState extends ConsumerState { decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.15), border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), - borderRadius: FladderTheme.defaultShape.borderRadius, + borderRadius: posterRadius, ), clipBehavior: Clip.hardEdge, child: Stack( @@ -201,6 +203,102 @@ class _PosterImageState extends ConsumerState { ], ), ), + if (widget.poster.unWatched) + Align( + alignment: Alignment.topLeft, + child: StatusCard( + color: Colors.amber, + child: Padding( + padding: const EdgeInsets.all(10), + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.amber, + ), + ), + ), + ), + ), + if (widget.inlineTitle) + IgnorePointer( + child: Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + widget.poster.title.maxLength(limitTo: 25), + style: + Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + ), + if (widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) + IgnorePointer( + child: Align( + alignment: Alignment.topRight, + child: StatusCard( + color: Theme.of(context).colorScheme.primary, + useFittedBox: widget.poster.unPlayedItemCount != 0, + child: Padding( + padding: const EdgeInsets.all(6), + child: widget.poster.unPlayedItemCount != 0 + ? Container( + constraints: const BoxConstraints(minWidth: 16), + child: Text( + widget.poster.userData.unPlayedItemCount.toString(), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + overflow: TextOverflow.visible, + fontSize: 14, + ), + ), + ) + : Icon( + Icons.check_rounded, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ), + if (widget.poster.overview.runTime != null && + ((widget.poster is PhotoModel) && + (widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: padding, + child: Card( + elevation: 5, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.poster.overview.runTime.humanizeSmall ?? "", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(width: 2), + Icon( + Icons.play_arrow_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), + ), + ), + ), + ) + }, //Desktop overlay if (AdaptiveLayout.of(context).inputDevice != InputDevice.touch && widget.poster.type != FladderItemType.person) @@ -215,7 +313,7 @@ class _PosterImageState extends ConsumerState { decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.55), border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), - borderRadius: FladderTheme.defaultShape.borderRadius, + borderRadius: posterRadius, )), //Poster Button Focus( @@ -317,102 +415,6 @@ class _PosterImageState extends ConsumerState { }, ), ), - if (widget.poster.unWatched) - Align( - alignment: Alignment.topLeft, - child: StatusCard( - color: Colors.amber, - child: Padding( - padding: const EdgeInsets.all(10), - child: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.amber, - ), - ), - ), - ), - ), - if (widget.inlineTitle) - IgnorePointer( - child: Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - widget.poster.title.maxLength(limitTo: 25), - style: - Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ), - ), - if (widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) - IgnorePointer( - child: Align( - alignment: Alignment.topRight, - child: StatusCard( - color: Theme.of(context).colorScheme.primary, - useFittedBox: widget.poster.unPlayedItemCount != 0, - child: Padding( - padding: const EdgeInsets.all(6), - child: widget.poster.unPlayedItemCount != 0 - ? Container( - constraints: const BoxConstraints(minWidth: 16), - child: Text( - widget.poster.userData.unPlayedItemCount.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - overflow: TextOverflow.visible, - fontSize: 14, - ), - ), - ) - : Icon( - Icons.check_rounded, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ), - if (widget.poster.overview.runTime != null && - ((widget.poster is PhotoModel) && - (widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{ - Align( - alignment: Alignment.topRight, - child: Padding( - padding: padding, - child: Card( - elevation: 5, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.poster.overview.runTime.humanizeSmall ?? "", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(width: 2), - Icon( - Icons.play_arrow_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - ], - ), - ), - ), - ), - ) - } ], ), ), diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index 8e242bf..334163c 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -27,7 +27,7 @@ class NestedScaffold extends ConsumerWidget { end: Alignment.bottomCenter, colors: [ Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity), - Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity - 0.15), + Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity / 1.5), ], ), ), diff --git a/lib/screens/shared/outlined_text_field.dart b/lib/screens/shared/outlined_text_field.dart index cf6c96f..7ff30be 100644 --- a/lib/screens/shared/outlined_text_field.dart +++ b/lib/screens/shared/outlined_text_field.dart @@ -1,9 +1,11 @@ -import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/theme.dart'; + class OutlinedTextField extends ConsumerStatefulWidget { final String? label; final FocusNode? focusNode; diff --git a/lib/util/map_bool_helper.dart b/lib/util/map_bool_helper.dart index 1f34d59..4a252f0 100644 --- a/lib/util/map_bool_helper.dart +++ b/lib/util/map_bool_helper.dart @@ -37,15 +37,30 @@ extension MapExtensions on Map { bool get hasEnabled => values.any((element) => element == true); //Replaces only keys that exist with the new values - Map replaceMap(Map oldMap) { + Map replaceMap(Map oldMap, {bool enabledOnly = false}) { + if (oldMap.isEmpty) return this; + Map result = {}; forEach((key, value) { - result[key] = oldMap[key] ?? false; + if (enabledOnly) { + if (oldMap[key] == true) { + result[key] = true; + } else { + result[key] = value; + } + } else { + result[key] = oldMap[key] ?? false; + } }); return result; } + + Map sortByKey(String Function(T value) keySelector) { + final sortedEntries = entries.toList()..sort((a, b) => keySelector(a.key).compareTo(keySelector(b.key))); + return Map.fromEntries(sortedEntries); + } } extension MapExtensionsGeneric on Map { diff --git a/lib/util/position_provider.dart b/lib/util/position_provider.dart new file mode 100644 index 0000000..22d2b7c --- /dev/null +++ b/lib/util/position_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +enum PositionContext { first, middle, last } + +class PositionProvider extends InheritedWidget { + final PositionContext position; + + const PositionProvider({ + required this.position, + required super.child, + super.key, + }); + + static PositionContext of(BuildContext context) { + final provider = context.dependOnInheritedWidgetOfExactType(); + assert(provider != null, 'No PositionProvider found in context'); + return provider?.position ?? PositionContext.middle; + } + + @override + bool updateShouldNotify(PositionProvider oldWidget) => position != oldWidget.position; +} diff --git a/lib/widgets/navigation_scaffold/components/background_image.dart b/lib/widgets/navigation_scaffold/components/background_image.dart index fafba22..5b4b29a 100644 --- a/lib/widgets/navigation_scaffold/components/background_image.dart +++ b/lib/widgets/navigation_scaffold/components/background_image.dart @@ -10,6 +10,8 @@ import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/util/fladder_image.dart'; +final _backgroundImageProvider = StateProvider((ref) => null); + class BackgroundImage extends ConsumerStatefulWidget { final List items; final List images; @@ -20,7 +22,11 @@ class BackgroundImage extends ConsumerStatefulWidget { } class _BackgroundImageState extends ConsumerState { - ImageData? backgroundImage; + @override + void initState() { + super.initState(); + updateItems(); + } @override void didUpdateWidget(covariant BackgroundImage oldWidget) { @@ -30,55 +36,65 @@ class _BackgroundImageState extends ConsumerState { } } - @override - void initState() { - super.initState(); - updateItems(); - } - void updateItems() { - final enabled = - ref.read(clientSettingsProvider.select((value) => value.backgroundImage != BackgroundType.disabled)); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final enabled = ref.read( + clientSettingsProvider.select((value) => value.backgroundImage != BackgroundType.disabled), + ); + if (!enabled || !mounted) return; - WidgetsBinding.instance.addPostFrameCallback((value) async { - if (!enabled && mounted) return; + ImageData? newImage; if (widget.images.isNotEmpty) { - final image = widget.images.shuffled().firstOrNull?.primary; - if (mounted) setState(() => backgroundImage = image); - return; + newImage = widget.images.shuffled().firstOrNull?.primary; + } else if (widget.items.isNotEmpty) { + final randomItem = widget.items.shuffled().firstOrNull; + final itemId = switch (randomItem?.type) { + FladderItemType.folder => randomItem?.id, + FladderItemType.series => randomItem?.parentId ?? randomItem?.id, + _ => randomItem?.id, + }; + + if (itemId != null) { + final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId); + + newImage = apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? + apiResponse.body?.getPosters?.randomBackDrop ?? + apiResponse.body?.getPosters?.primary; + } } - if (widget.items.isEmpty) return; - - final randomItem = widget.items.shuffled().firstOrNull; - final itemId = switch (randomItem?.type) { - FladderItemType.folder => randomItem?.id, - FladderItemType.series => randomItem?.parentId ?? randomItem?.id, - _ => randomItem?.id, - }; - - if (itemId == null) return; - - final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId); - final image = apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? - apiResponse.body?.getPosters?.randomBackDrop ?? - apiResponse.body?.getPosters?.primary; - - if (mounted) setState(() => backgroundImage = image); + if (newImage != null && mounted) { + ref.read(_backgroundImageProvider.notifier).state = newImage; + } }); } @override Widget build(BuildContext context) { - final state = ref.watch(clientSettingsProvider.select((value) => value.backgroundImage)); - final enabled = state != BackgroundType.disabled; - return enabled - ? FladderImage( - image: backgroundImage, - fit: BoxFit.cover, - blurOnly: state == BackgroundType.blurred, - ) - : const SizedBox.shrink(); + final settings = ref.watch(clientSettingsProvider.select((value) => value.backgroundImage)); + final enabled = settings != BackgroundType.disabled; + final image = ref.watch(_backgroundImageProvider); + + if (!enabled || image == null) return const SizedBox.shrink(); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + layoutBuilder: (currentChild, previousChildren) { + return Stack( + fit: StackFit.expand, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: FladderImage( + key: ValueKey(image.key), + image: image, + fit: BoxFit.cover, + blurOnly: settings == BackgroundType.blurred, + ), + ); } } diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index cf67a0a..74d2e8d 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -64,7 +64,8 @@ class _SideNavigationBarState extends ConsumerState { children: [ AdaptiveLayoutBuilder( adaptiveLayout: AdaptiveLayout.of(context).copyWith( - sideBarWidth: fullyExpanded ? expandedWidth : collapsedWidth, + // -0.1 offset to fix single visible pixel line + sideBarWidth: (fullyExpanded ? expandedWidth : collapsedWidth) - 0.1, ), child: (context) => widget.child, ), diff --git a/lib/widgets/shared/button_group.dart b/lib/widgets/shared/button_group.dart index d7e54ab..2b69881 100644 --- a/lib/widgets/shared/button_group.dart +++ b/lib/widgets/shared/button_group.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:fladder/util/position_provider.dart'; + class ExpressiveButtonGroup extends StatelessWidget { final List> options; final Set selectedValues; @@ -20,44 +22,77 @@ class ExpressiveButtonGroup extends StatelessWidget { mainAxisSize: MainAxisSize.max, spacing: 2, crossAxisAlignment: CrossAxisAlignment.stretch, - children: List.generate(options.length, (index) { - final option = options[index]; - final isSelected = selectedValues.contains(option.value); - final isFirst = index == 0; - final isLast = index == options.length - 1; + children: List.generate( + options.length, + (index) { + final option = options[index]; + final isSelected = selectedValues.contains(option.value); - final borderRadius = BorderRadius.horizontal( - left: isSelected || isFirst ? const Radius.circular(20) : const Radius.circular(6), - right: isSelected || isLast ? const Radius.circular(20) : const Radius.circular(6), - ); + final position = index == 0 + ? PositionContext.first + : (index == options.length - 1 ? PositionContext.last : PositionContext.middle); - return ElevatedButton.icon( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - elevation: isSelected ? 3 : 0, - backgroundColor: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainerHighest, - foregroundColor: - isSelected ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, - textStyle: Theme.of(context).textTheme.labelLarge, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - ), - onPressed: () { - final newSet = Set.from(selectedValues); - if (multiSelection) { - isSelected ? newSet.remove(option.value) : newSet.add(option.value); - } else { - newSet - ..clear() - ..add(option.value); - } - onSelected(newSet); - }, - label: option.child, - icon: isSelected ? option.selected ?? const Icon(Icons.check_rounded) : option.icon, - ); - }), + return PositionProvider( + position: position, + child: ExpressiveButton( + isSelected: isSelected, + label: option.child, + icon: isSelected ? option.selected ?? const Icon(Icons.check_rounded) : option.icon, + onPressed: () { + final newSet = Set.from(selectedValues); + if (multiSelection) { + isSelected ? newSet.remove(option.value) : newSet.add(option.value); + } else { + newSet + ..clear() + ..add(option.value); + } + onSelected(newSet); + }, + ), + ); + }, + ), + ); + } +} + +class ExpressiveButton extends StatelessWidget { + const ExpressiveButton({ + super.key, + required this.isSelected, + required this.label, + this.icon, + required this.onPressed, + }); + + final bool isSelected; + final Widget label; + final Widget? icon; + final Function()? onPressed; + + @override + Widget build(BuildContext context) { + final position = PositionProvider.of(context); + final borderRadius = BorderRadius.horizontal( + left: isSelected || position == PositionContext.first ? const Radius.circular(16) : const Radius.circular(4), + right: isSelected || position == PositionContext.last ? const Radius.circular(16) : const Radius.circular(4), + ); + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: borderRadius), + elevation: isSelected ? 4 : 0, + backgroundColor: + isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: + isSelected ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, + textStyle: Theme.of(context).textTheme.labelLarge, + visualDensity: VisualDensity.comfortable, + padding: const EdgeInsets.all(12), + ), + onPressed: onPressed, + label: label, + icon: icon, ); } } diff --git a/lib/widgets/shared/hide_on_scroll.dart b/lib/widgets/shared/hide_on_scroll.dart index 31f4e0c..76f8889 100644 --- a/lib/widgets/shared/hide_on_scroll.dart +++ b/lib/widgets/shared/hide_on_scroll.dart @@ -11,6 +11,7 @@ class HideOnScroll extends ConsumerStatefulWidget { final double height; final Widget? Function(bool visible)? visibleBuilder; final Duration duration; + final bool canHide; final bool forceHide; const HideOnScroll({ this.child, @@ -18,6 +19,7 @@ class HideOnScroll extends ConsumerStatefulWidget { this.height = kBottomNavigationBarHeight, this.visibleBuilder, this.duration = const Duration(milliseconds: 200), + this.canHide = true, this.forceHide = false, super.key, }) : assert(child != null || visibleBuilder != null); @@ -46,6 +48,12 @@ class _HideOnScrollState extends ConsumerState { } void _onScroll() { + if (!widget.canHide) { + if (!isVisible) { + setState(() => isVisible = true); + } + return; + } final position = scrollController.position; final direction = position.userScrollDirection; @@ -78,9 +86,9 @@ class _HideOnScrollState extends ConsumerState { alignment: const Alignment(0, -1), heightFactor: widget.forceHide ? 0 - : isVisible - ? 1.0 - : 0, + : !isVisible && widget.canHide + ? 0.0 + : 1.0, duration: widget.duration, child: Wrap( children: [widget.child!],