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, copyWith: true) abstract class LibraryScreenModel with _$LibraryScreenModel { factory LibraryScreenModel({ @Default([]) List views, ViewModel? selectedViewModel, @Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set viewType, @Default([]) List recommendations, @Default([]) List genres, @Default([]) List favourites, }) = _LibraryScreenModel; } @Riverpod(keepAlive: true) class LibraryScreen extends _$LibraryScreen { late final JellyService api = ref.read(jellyApiProvider); @override LibraryScreenModel build() => LibraryScreenModel(); Future fetchAllLibraries() async { final views = await ref.read(viewsProvider.notifier).fetchViews(); 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; selectLibrary(viewModel); await loadLibrary(viewModel); } Future selectLibrary(ViewModel viewModel) async { state = state.copyWith(selectedViewModel: viewModel); } Future setViewType(Set type) async { state = state.copyWith(viewType: type); } Future loadLibrary(ViewModel viewModel) async { await loadRecommendations(viewModel); await loadGenres(viewModel); await loadFavourites(viewModel); return null; } Future loadRecommendations(ViewModel viewModel) async { List 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 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 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().toList(), ); return null; } }