[tvOS] ItemTypeLibraryViewModel - Implement FilterViewModel (#1409)

* FilterViewModel only

* comments

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-01-26 12:24:24 -07:00 committed by GitHub
parent c9ae01e792
commit 35c39a8d0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 11 deletions

View File

@ -48,7 +48,10 @@ final class MainTabCoordinator: TabCoordinatable {
}
func makeTVShows() -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
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<LibraryCoordinator<BaseItemDto>> {
let viewModel = ItemTypeLibraryViewModel(itemTypes: [.movie])
let viewModel = ItemTypeLibraryViewModel(
itemTypes: [.movie],
filters: .default
)
return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel))
}

View File

@ -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)

View File

@ -56,9 +56,9 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
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 {

View File

@ -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<BaseItemDto> {
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<BaseItemDto> {
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<BaseItemDto> {
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<BaseItemDto> {
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
}
}

View File

@ -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<Element: Poster>: 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?,