diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 255af5158..71159a29d 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -48,7 +48,10 @@ final class MainTabCoordinator: TabCoordinatable { } func makeTVShows() -> NavigationViewCoordinator> { - let viewModel = ItemTypeLibraryViewModel(itemTypes: [.series]) + let viewModel = ItemTypeLibraryViewModel( + itemTypes: [.series], + filters: .default + ) return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel)) } @@ -62,7 +65,10 @@ final class MainTabCoordinator: TabCoordinatable { } func makeMovies() -> NavigationViewCoordinator> { - let viewModel = ItemTypeLibraryViewModel(itemTypes: [.movie]) + let viewModel = ItemTypeLibraryViewModel( + itemTypes: [.movie], + filters: .default + ) return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel)) } diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift index 87799f1bc..b400e1554 100644 --- a/Shared/ViewModels/FilterViewModel.swift +++ b/Shared/ViewModels/FilterViewModel.swift @@ -19,12 +19,24 @@ final class FilterViewModel: ViewModel { var allFilters: ItemFilterCollection = .all private let parent: (any LibraryParent)? + private let itemTypes: [BaseItemKind]? init( parent: (any LibraryParent)? = nil, currentFilters: ItemFilterCollection = .default ) { self.parent = parent + self.itemTypes = nil + self.currentFilters = currentFilters + super.init() + } + + init( + itemTypes: [BaseItemKind], + currentFilters: ItemFilterCollection = .default + ) { + self.parent = nil + self.itemTypes = itemTypes self.currentFilters = currentFilters super.init() } @@ -43,7 +55,8 @@ final class FilterViewModel: ViewModel { private func getQueryFilters() async -> (genres: [ItemGenre], tags: [ItemTag], years: [ItemYear]) { let parameters = Paths.GetQueryFiltersLegacyParameters( userID: userSession.user.id, - parentID: parent?.id as? String + parentID: parent?.id as? String, + includeItemTypes: itemTypes ) let request = Paths.getQueryFiltersLegacy(parameters: parameters) diff --git a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift index 24fc3f31c..2ac76e410 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift @@ -56,9 +56,9 @@ final class ItemLibraryViewModel: PagingLibraryViewModel { var includeItemTypes: [BaseItemKind] = [.movie, .series, .boxSet] var isRecursive: Bool? = true - // TODO: determine `includeItemTypes` better - // - look at parent collection type if necessary - // - condense supported values + // TODO: this logic should be moved to a `LibraryParent` function + // that transforms a `GetItemsByUserIDParameters` struct, instead + // of having to do this case-by-case. if let libraryType = parent?.libraryType, let id = parent?.id { switch libraryType { diff --git a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift index aa5fd8f0e..739544883 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift @@ -11,17 +11,28 @@ import Foundation import Get import JellyfinAPI -// TODO: atow, this is only really used for tvOS tabs +// TODO: filtering on `itemTypes` should be moved to `ItemFilterCollection`, +// but there is additional logic based on the parent type, mainly `.folder`. final class ItemTypeLibraryViewModel: PagingLibraryViewModel { let itemTypes: [BaseItemKind] - init(itemTypes: [BaseItemKind]) { + // MARK: Initializer + + init( + itemTypes: [BaseItemKind], + filters: ItemFilterCollection? = nil + ) { self.itemTypes = itemTypes - super.init() + super.init( + itemTypes: itemTypes, + filters: filters + ) } + // MARK: Get Page + override func get(page: Int) async throws -> [BaseItemDto] { let parameters = itemParameters(for: page) @@ -31,6 +42,8 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { return response.value.items ?? [] } + // MARK: Item Parameters + func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters { var parameters = Paths.GetItemsByUserIDParameters() @@ -38,8 +51,6 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { parameters.fields = .MinimumFields parameters.includeItemTypes = itemTypes parameters.isRecursive = true - parameters.sortBy = [ItemSortBy.name.rawValue] - parameters.sortOrder = [.ascending] // Page size if let page { @@ -47,6 +58,48 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { parameters.startIndex = page * pageSize } + // Filters + if let filterViewModel { + let filters = filterViewModel.currentFilters + parameters.filters = filters.traits + parameters.genres = filters.genres.map(\.value) + parameters.sortBy = filters.sortBy.map(\.rawValue) + parameters.sortOrder = filters.sortOrder + parameters.tags = filters.tags.map(\.value) + parameters.years = filters.years.compactMap { Int($0.value) } + + if filters.letter.first?.value == "#" { + parameters.nameLessThan = "A" + } else { + parameters.nameStartsWith = filters.letter + .map(\.value) + .filter { $0 != "#" } + .first + } + + // Random sort won't take into account previous items, so + // manual exclusion is necessary. This could possibly be + // a performance issue for loading pages after already loading + // many items, but there's nothing we can do about that. + if filters.sortBy.first == ItemSortBy.random { + parameters.excludeItemIDs = elements.compactMap(\.id) + } + } + return parameters } + + // MARK: Get Random Item + + override func getRandomItem() async -> BaseItemDto? { + + var parameters = itemParameters(for: nil) + parameters.limit = 1 + parameters.sortBy = [ItemSortBy.random.rawValue] + + let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters) + let response = try? await userSession.client.send(request) + + return response?.value.items?.first + } } diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index b780f662b..11c370288 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -49,6 +49,8 @@ protocol LibraryIdentifiable: Identifiable { // on refresh. Should make bidirectional/offset index start? // - use startIndex/index ranges instead of pages // - source of data doesn't guarantee that all items in 0 ..< startIndex exist +// TODO: have `filterViewModel` be private to the parent and the `get_` overrides recieve the +// current filters as a parameter /* Note: if `rememberSort == true`, then will override given filters with stored sorts @@ -218,6 +220,52 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { } } + // paging item type + init( + itemTypes: [BaseItemKind], + filters: ItemFilterCollection? = nil, + pageSize: Int = DefaultPageSize + ) { + self.elements = IdentifiedArray([], id: \.unwrappedIDHashOrZero, uniquingIDsWith: { x, _ in x }) + self.isStatic = false + self.pageSize = pageSize + + self.parent = nil + + if let filters { + self.filterViewModel = .init( + itemTypes: itemTypes, + currentFilters: filters + ) + } else { + self.filterViewModel = nil + } + + super.init() + + Notifications[.didDeleteItem] + .publisher + .sink { id in + self.elements.remove(id: id.hashValue) + } + .store(in: &cancellables) + + if let filterViewModel { + filterViewModel.$currentFilters + .dropFirst() + .debounce(for: 1, scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] _ in + guard let self else { return } + + Task { @MainActor in + self.send(.refresh) + } + } + .store(in: &cancellables) + } + } + convenience init( title: String, id: String?,