feat: UI 2.0 and other Improvements (#357)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-06-01 10:37:19 +02:00 committed by GitHub
parent 9ca06eaa37
commit e7b5bb40ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 4584 additions and 3626 deletions

View file

@ -1,3 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/home_model.dart';
import 'package:fladder/models/item_base_model.dart';
@ -6,7 +8,6 @@ import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dashboardProvider = StateNotifierProvider<DashboardNotifier, HomeModel>((ref) {
return DashboardNotifier(ref);
@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.video],
enableTotalRecordCount: false,
@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.audio],
enableTotalRecordCount: false,
@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
],
mediaTypes: [MediaType.book],
enableTotalRecordCount: false,
@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
final nextResponse = await api.showsNextUpGet(
limit: 16,
nextUpDateCutoff: DateTime.now()
.subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
nextUpDateCutoff: DateTime.now().subtract(
ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
],
);

View file

@ -1,4 +1,6 @@
import 'package:chopper/chopper.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/favourites_model.dart';
import 'package:fladder/models/item_base_model.dart';
@ -6,7 +8,6 @@ import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final favouritesProvider = StateNotifierProvider<FavouritesNotifier, FavouritesModel>((ref) {
return FavouritesNotifier(ref);
@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
isFavorite: true,
limit: 10,
sortOrder: [SortOrder.ascending],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
);
final response2 = await api.itemsGet(
parentId: viewModel?.id,
@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier<FavouritesModel> {
limit: 10,
includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder],
sortOrder: [SortOrder.ascending],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded],
);
return [...?response.body?.items, ...?response2.body?.items];
}

View file

@ -1,174 +0,0 @@
import 'package:chopper/chopper.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/library_model.dart';
import 'package:fladder/models/recommended_model.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
bool _useFolders(ViewModel model) {
switch (model.collectionType) {
case CollectionType.boxsets:
case CollectionType.homevideos:
case CollectionType.folders:
return true;
default:
return false;
}
}
final libraryProvider = StateNotifierProvider.autoDispose.family<LibraryNotifier, LibraryModel?, String>((ref, id) {
return LibraryNotifier(ref);
});
class LibraryNotifier extends StateNotifier<LibraryModel?> {
LibraryNotifier(this.ref) : super(null);
final Ref ref;
late final JellyService api = ref.read(jellyApiProvider);
set loading(bool value) {
state = state?.copyWith(loading: value);
}
bool get loading => state?.loading ?? true;
Future<void> setupLibrary(ViewModel viewModel) async {
state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie);
}
Future<Response?> loadLibrary(ViewModel viewModel) async {
final response = await api.itemsGet(
parentId: viewModel.id,
sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear],
isMissing: false,
excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [],
fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid],
);
state = state?.copyWith(posters: response.body?.items);
loading = false;
return response;
}
Future<void> loadRecommendations(ViewModel viewModel) async {
loading = true;
//Clear recommendations because of all the copying
state = state?.copyWith(recommendations: []);
final latest = await api.usersUserIdItemsLatestGet(
parentId: viewModel.id,
limit: 14,
isPlayed: false,
imageTypeLimit: 1,
includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null,
);
state = state?.copyWith(
recommendations: [
...?state?.recommendations,
RecommendedModel(
name: "Latest",
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: "Latest",
),
],
);
if (viewModel.collectionType == CollectionType.movies) {
final response = await api.moviesRecommendationsGet(
parentId: viewModel.id,
categoryLimit: 6,
itemLimit: 8,
fields: [ItemFields.mediasourcecount],
);
state = state?.copyWith(recommendations: [
...?state?.recommendations,
...response.body?.map(
(e) => RecommendedModel(
name: e.baselineItemName ?? "",
posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: e.recommendationType.toString(),
),
) ??
[],
]);
loading = false;
} else {
final nextUp = await api.showsNextUpGet(
parentId: viewModel.id,
limit: 14,
imageTypeLimit: 1,
fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
);
state = state?.copyWith(recommendations: [
...?state?.recommendations,
...[
RecommendedModel(
name: "Next up",
posters: nextUp.body?.items
?.map(
(e) => ItemBaseModel.fromBaseDto(
e,
ref,
),
)
.toList() ??
[],
type: "Latest series")
],
]);
loading = false;
}
}
Future<Response?> loadFavourites(ViewModel viewModel) async {
loading = true;
final response = await api.itemsGet(
parentId: viewModel.id,
isFavorite: true,
recursive: true,
);
state = state?.copyWith(favourites: response.body?.items);
loading = false;
return response;
}
Future<Response?> loadTimeline(ViewModel viewModel) async {
loading = true;
final response = await api.itemsGet(
parentId: viewModel.id,
recursive: true,
fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated],
sortBy: [ItemSortBy.datecreated],
sortOrder: [SortOrder.descending],
includeItemTypes: [
BaseItemKind.photo,
BaseItemKind.video,
],
);
state = state?.copyWith(
timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(),
);
loading = false;
return response;
}
Future<Response?> loadGenres(ViewModel viewModel) async {
final genres = await api.genresGet(
sortBy: [ItemSortBy.sortname],
sortOrder: [SortOrder.ascending],
includeItemTypes: viewModel.collectionType == CollectionType.movies
? [BaseItemKind.movie]
: [
BaseItemKind.series,
],
parentId: viewModel.id,
);
state = state?.copyWith(
genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList());
return null;
}
}

View file

@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/collection_types.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/recommended_model.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/localization_helper.dart';
part 'library_screen_provider.freezed.dart';
part 'library_screen_provider.g.dart';
enum LibraryViewType {
recommended,
favourites,
genres;
const LibraryViewType();
String label(BuildContext context) => switch (this) {
LibraryViewType.recommended => context.localized.recommended,
LibraryViewType.favourites => context.localized.favorites,
LibraryViewType.genres => context.localized.genre(2),
};
IconData get icon => switch (this) {
LibraryViewType.recommended => IconsaxPlusLinear.star,
LibraryViewType.favourites => IconsaxPlusLinear.heart,
LibraryViewType.genres => IconsaxPlusLinear.hierarchy_3,
};
IconData get iconSelected => switch (this) {
LibraryViewType.recommended => IconsaxPlusBold.star,
LibraryViewType.favourites => IconsaxPlusBold.heart,
LibraryViewType.genres => IconsaxPlusBold.hierarchy_3,
};
}
@Freezed(fromJson: false, toJson: false)
class LibraryScreenModel with _$LibraryScreenModel {
factory LibraryScreenModel({
@Default([]) List<ViewModel> views,
ViewModel? selectedViewModel,
@Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set<LibraryViewType> viewType,
@Default([]) List<RecommendedModel> recommendations,
@Default([]) List<RecommendedModel> genres,
@Default([]) List<ItemBaseModel> favourites,
}) = _LibraryScreenModel;
}
@Riverpod(keepAlive: true)
class LibraryScreen extends _$LibraryScreen {
late final JellyService api = ref.read(jellyApiProvider);
@override
LibraryScreenModel build() => LibraryScreenModel();
Future<void> fetchAllLibraries() async {
final views = await ref.read(viewsProvider.notifier).fetchViews();
state = state.copyWith(views: views?.views ?? []);
if (state.views.isEmpty) return;
final viewModel = state.selectedViewModel ?? state.views.firstOrNull;
if (viewModel == null) return;
selectLibrary(viewModel);
await loadLibrary(viewModel);
}
Future<void> selectLibrary(ViewModel viewModel) async {
state = state.copyWith(selectedViewModel: viewModel);
}
Future<void> setViewType(Set<LibraryViewType> type) async {
state = state.copyWith(viewType: type);
}
Future<Response?> loadLibrary(ViewModel viewModel) async {
await loadRecommendations(viewModel);
await loadGenres(viewModel);
await loadFavourites(viewModel);
return null;
}
Future<void> loadRecommendations(ViewModel viewModel) async {
List<RecommendedModel> newRecommendations = [];
final latest = await api.usersUserIdItemsLatestGet(
parentId: viewModel.id,
limit: 14,
isPlayed: false,
imageTypeLimit: 1,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
);
newRecommendations = [
...newRecommendations,
RecommendedModel(
name: const Latest(),
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: null,
),
];
if (viewModel.collectionType == CollectionType.movies) {
final response = await api.moviesRecommendationsGet(
parentId: viewModel.id,
categoryLimit: 6,
itemLimit: 14,
fields: [ItemFields.mediasourcecount],
);
newRecommendations = [
...newRecommendations,
...(response.body?.map(
(e) => RecommendedModel.fromBaseDto(e, ref),
) ??
[])
];
} else {
final nextUp = await api.showsNextUpGet(
parentId: viewModel.id,
limit: 14,
imageTypeLimit: 1,
fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
);
newRecommendations = [
...newRecommendations,
RecommendedModel(
name: const NextUp(),
posters: nextUp.body?.items
?.map(
(e) => ItemBaseModel.fromBaseDto(
e,
ref,
),
)
.toList() ??
[],
type: null,
)
];
}
state = state.copyWith(
recommendations: newRecommendations,
);
}
Future<Response?> loadFavourites(ViewModel viewModel) async {
final response = await api.itemsGet(
parentId: viewModel.id,
isFavorite: true,
recursive: true,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
enableImageTypes: [ImageType.primary],
fields: [
ItemFields.primaryimageaspectratio,
ItemFields.mediasourcecount,
],
enableTotalRecordCount: false,
);
state = state.copyWith(favourites: response.body?.items ?? []);
return response;
}
Future<Response?> loadGenres(ViewModel viewModel) async {
final genres = await api.genresGet(
sortBy: [ItemSortBy.sortname],
sortOrder: [SortOrder.ascending],
includeItemTypes:
viewModel.collectionType == CollectionType.movies ? [BaseItemKind.movie] : [BaseItemKind.series],
parentId: viewModel.id,
);
final filteredGenres = (genres.body?.items?.map(
(item) => GenreItems(id: item.id ?? "", name: item.name ?? ""),
) ??
[])
.toList();
if (filteredGenres.isEmpty) return null;
final results = await Future.wait(filteredGenres.map((genre) async {
final response = await api.itemsGet(
parentId: viewModel.id,
genreIds: [genre.id],
limit: 9,
recursive: true,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
enableImageTypes: [ImageType.primary],
fields: [
ItemFields.primaryimageaspectratio,
ItemFields.mediasourcecount,
],
sortBy: [ItemSortBy.random],
enableTotalRecordCount: false,
imageTypeLimit: 1,
sortOrder: [SortOrder.ascending],
);
final items = response.body?.items;
if (items != null && items.isNotEmpty) {
return RecommendedModel(name: Other(genre.name), posters: items);
}
return null;
}));
state = state.copyWith(
genres: results.whereType<RecommendedModel>().toList(),
);
return null;
}
}

View file

@ -0,0 +1,301 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// 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_screen_provider.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$LibraryScreenModel {
List<ViewModel> get views => throw _privateConstructorUsedError;
ViewModel? get selectedViewModel => throw _privateConstructorUsedError;
Set<LibraryViewType> get viewType => throw _privateConstructorUsedError;
List<RecommendedModel> get recommendations =>
throw _privateConstructorUsedError;
List<RecommendedModel> get genres => throw _privateConstructorUsedError;
List<ItemBaseModel> get favourites => throw _privateConstructorUsedError;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LibraryScreenModelCopyWith<LibraryScreenModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LibraryScreenModelCopyWith<$Res> {
factory $LibraryScreenModelCopyWith(
LibraryScreenModel value, $Res Function(LibraryScreenModel) then) =
_$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>;
@useResult
$Res call(
{List<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> favourites});
}
/// @nodoc
class _$LibraryScreenModelCopyWithImpl<$Res, $Val extends LibraryScreenModel>
implements $LibraryScreenModelCopyWith<$Res> {
_$LibraryScreenModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? views = null,
Object? selectedViewModel = freezed,
Object? viewType = null,
Object? recommendations = null,
Object? genres = null,
Object? favourites = null,
}) {
return _then(_value.copyWith(
views: null == views
? _value.views
: views // ignore: cast_nullable_to_non_nullable
as List<ViewModel>,
selectedViewModel: freezed == selectedViewModel
? _value.selectedViewModel
: selectedViewModel // ignore: cast_nullable_to_non_nullable
as ViewModel?,
viewType: null == viewType
? _value.viewType
: viewType // ignore: cast_nullable_to_non_nullable
as Set<LibraryViewType>,
recommendations: null == recommendations
? _value.recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value.genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value.favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
) as $Val);
}
}
/// @nodoc
abstract class _$$LibraryScreenModelImplCopyWith<$Res>
implements $LibraryScreenModelCopyWith<$Res> {
factory _$$LibraryScreenModelImplCopyWith(_$LibraryScreenModelImpl value,
$Res Function(_$LibraryScreenModelImpl) then) =
__$$LibraryScreenModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<ViewModel> views,
ViewModel? selectedViewModel,
Set<LibraryViewType> viewType,
List<RecommendedModel> recommendations,
List<RecommendedModel> genres,
List<ItemBaseModel> favourites});
}
/// @nodoc
class __$$LibraryScreenModelImplCopyWithImpl<$Res>
extends _$LibraryScreenModelCopyWithImpl<$Res, _$LibraryScreenModelImpl>
implements _$$LibraryScreenModelImplCopyWith<$Res> {
__$$LibraryScreenModelImplCopyWithImpl(_$LibraryScreenModelImpl _value,
$Res Function(_$LibraryScreenModelImpl) _then)
: super(_value, _then);
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? views = null,
Object? selectedViewModel = freezed,
Object? viewType = null,
Object? recommendations = null,
Object? genres = null,
Object? favourites = null,
}) {
return _then(_$LibraryScreenModelImpl(
views: null == views
? _value._views
: views // ignore: cast_nullable_to_non_nullable
as List<ViewModel>,
selectedViewModel: freezed == selectedViewModel
? _value.selectedViewModel
: selectedViewModel // ignore: cast_nullable_to_non_nullable
as ViewModel?,
viewType: null == viewType
? _value._viewType
: viewType // ignore: cast_nullable_to_non_nullable
as Set<LibraryViewType>,
recommendations: null == recommendations
? _value._recommendations
: recommendations // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
genres: null == genres
? _value._genres
: genres // ignore: cast_nullable_to_non_nullable
as List<RecommendedModel>,
favourites: null == favourites
? _value._favourites
: favourites // ignore: cast_nullable_to_non_nullable
as List<ItemBaseModel>,
));
}
}
/// @nodoc
class _$LibraryScreenModelImpl implements _LibraryScreenModel {
_$LibraryScreenModelImpl(
{final List<ViewModel> views = const [],
this.selectedViewModel,
final Set<LibraryViewType> viewType = const {
LibraryViewType.recommended,
LibraryViewType.favourites
},
final List<RecommendedModel> recommendations = const [],
final List<RecommendedModel> genres = const [],
final List<ItemBaseModel> favourites = const []})
: _views = views,
_viewType = viewType,
_recommendations = recommendations,
_genres = genres,
_favourites = favourites;
final List<ViewModel> _views;
@override
@JsonKey()
List<ViewModel> get views {
if (_views is EqualUnmodifiableListView) return _views;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_views);
}
@override
final ViewModel? selectedViewModel;
final Set<LibraryViewType> _viewType;
@override
@JsonKey()
Set<LibraryViewType> get viewType {
if (_viewType is EqualUnmodifiableSetView) return _viewType;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_viewType);
}
final List<RecommendedModel> _recommendations;
@override
@JsonKey()
List<RecommendedModel> get recommendations {
if (_recommendations is EqualUnmodifiableListView) return _recommendations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recommendations);
}
final List<RecommendedModel> _genres;
@override
@JsonKey()
List<RecommendedModel> get genres {
if (_genres is EqualUnmodifiableListView) return _genres;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_genres);
}
final List<ItemBaseModel> _favourites;
@override
@JsonKey()
List<ItemBaseModel> get favourites {
if (_favourites is EqualUnmodifiableListView) return _favourites;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_favourites);
}
@override
String toString() {
return 'LibraryScreenModel(views: $views, selectedViewModel: $selectedViewModel, viewType: $viewType, recommendations: $recommendations, genres: $genres, favourites: $favourites)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LibraryScreenModelImpl &&
const DeepCollectionEquality().equals(other._views, _views) &&
(identical(other.selectedViewModel, selectedViewModel) ||
other.selectedViewModel == selectedViewModel) &&
const DeepCollectionEquality().equals(other._viewType, _viewType) &&
const DeepCollectionEquality()
.equals(other._recommendations, _recommendations) &&
const DeepCollectionEquality().equals(other._genres, _genres) &&
const DeepCollectionEquality()
.equals(other._favourites, _favourites));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_views),
selectedViewModel,
const DeepCollectionEquality().hash(_viewType),
const DeepCollectionEquality().hash(_recommendations),
const DeepCollectionEquality().hash(_genres),
const DeepCollectionEquality().hash(_favourites));
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
__$$LibraryScreenModelImplCopyWithImpl<_$LibraryScreenModelImpl>(
this, _$identity);
}
abstract class _LibraryScreenModel implements LibraryScreenModel {
factory _LibraryScreenModel(
{final List<ViewModel> views,
final ViewModel? selectedViewModel,
final Set<LibraryViewType> viewType,
final List<RecommendedModel> recommendations,
final List<RecommendedModel> genres,
final List<ItemBaseModel> favourites}) = _$LibraryScreenModelImpl;
@override
List<ViewModel> get views;
@override
ViewModel? get selectedViewModel;
@override
Set<LibraryViewType> get viewType;
@override
List<RecommendedModel> get recommendations;
@override
List<RecommendedModel> get genres;
@override
List<ItemBaseModel> get favourites;
/// Create a copy of LibraryScreenModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'library_screen_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c';
/// See also [LibraryScreen].
@ProviderFor(LibraryScreen)
final libraryScreenProvider =
NotifierProvider<LibraryScreen, LibraryScreenModel>.internal(
LibraryScreen.new,
name: r'libraryScreenProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$libraryScreenHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LibraryScreen = Notifier<LibraryScreenModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
.toSet()
.toList();
var tempState = state.copyWith();
final genres = mappedList
.expand((element) => element?.genres ?? <NameGuidPair>[])
.nonNulls
.sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel))))
.expand((element) => element)
.toSet()
.toList();
final tags = mappedList
.expand((element) => element?.tags ?? <String>[])
.sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
tempState = tempState.copyWith(
types: state.types.setAll(false).setKeys(enabledCollections, true),
genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres),
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),
);
@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
}
Future<List<GenreItems>> _loadGenres(ViewModel viewModel) async {
final response = await api.genresGet(parentId: viewModel.id);
return response.body?.items?.map((e) => GenreItems(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
}
Future<ServerQueryResult?> _loadLibrary(
{ViewModel? viewModel,
bool? recursive,

View file

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
final homeSettingsProvider = StateNotifierProvider<HomeSettingsNotifier, HomeSettingsModel>((ref) {
return HomeSettingsNotifier(ref);

View file

@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
// **************************************************************************
String _$backgroundDownloaderHash() =>
r'df72b6338a8e80178935985ba17c43bf720f4522';
r'dc27f708fc2f1695d37afcb99f8814bc024037af';
/// See also [BackgroundDownloader].
@ProviderFor(BackgroundDownloader)

View file

@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900';
String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a';
/// See also [User].
@ProviderFor(User)

View file

@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
late final JellyService api = ref.read(jellyApiProvider);
Future<void> fetchViews() async {
if (state.loading) return;
Future<ViewsModel?> fetchViews() async {
if (state.loading) return null;
final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes));
final response = await api.usersUserIdViewsGet(
includeExternalContent: showAllCollections,
@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
],
);
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
.where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true))
.toList(),
loading: false);
return state;
}
void clear() {