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

View File

@ -19,12 +19,24 @@ final class FilterViewModel: ViewModel {
var allFilters: ItemFilterCollection = .all var allFilters: ItemFilterCollection = .all
private let parent: (any LibraryParent)? private let parent: (any LibraryParent)?
private let itemTypes: [BaseItemKind]?
init( init(
parent: (any LibraryParent)? = nil, parent: (any LibraryParent)? = nil,
currentFilters: ItemFilterCollection = .default currentFilters: ItemFilterCollection = .default
) { ) {
self.parent = parent 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 self.currentFilters = currentFilters
super.init() super.init()
} }
@ -43,7 +55,8 @@ final class FilterViewModel: ViewModel {
private func getQueryFilters() async -> (genres: [ItemGenre], tags: [ItemTag], years: [ItemYear]) { private func getQueryFilters() async -> (genres: [ItemGenre], tags: [ItemTag], years: [ItemYear]) {
let parameters = Paths.GetQueryFiltersLegacyParameters( let parameters = Paths.GetQueryFiltersLegacyParameters(
userID: userSession.user.id, userID: userSession.user.id,
parentID: parent?.id as? String parentID: parent?.id as? String,
includeItemTypes: itemTypes
) )
let request = Paths.getQueryFiltersLegacy(parameters: parameters) let request = Paths.getQueryFiltersLegacy(parameters: parameters)

View File

@ -56,9 +56,9 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
var includeItemTypes: [BaseItemKind] = [.movie, .series, .boxSet] var includeItemTypes: [BaseItemKind] = [.movie, .series, .boxSet]
var isRecursive: Bool? = true var isRecursive: Bool? = true
// TODO: determine `includeItemTypes` better // TODO: this logic should be moved to a `LibraryParent` function
// - look at parent collection type if necessary // that transforms a `GetItemsByUserIDParameters` struct, instead
// - condense supported values // of having to do this case-by-case.
if let libraryType = parent?.libraryType, let id = parent?.id { if let libraryType = parent?.libraryType, let id = parent?.id {
switch libraryType { switch libraryType {

View File

@ -11,17 +11,28 @@ import Foundation
import Get import Get
import JellyfinAPI 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> { final class ItemTypeLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
let itemTypes: [BaseItemKind] let itemTypes: [BaseItemKind]
init(itemTypes: [BaseItemKind]) { // MARK: Initializer
init(
itemTypes: [BaseItemKind],
filters: ItemFilterCollection? = nil
) {
self.itemTypes = itemTypes self.itemTypes = itemTypes
super.init() super.init(
itemTypes: itemTypes,
filters: filters
)
} }
// MARK: Get Page
override func get(page: Int) async throws -> [BaseItemDto] { override func get(page: Int) async throws -> [BaseItemDto] {
let parameters = itemParameters(for: page) let parameters = itemParameters(for: page)
@ -31,6 +42,8 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
return response.value.items ?? [] return response.value.items ?? []
} }
// MARK: Item Parameters
func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters { func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters {
var parameters = Paths.GetItemsByUserIDParameters() var parameters = Paths.GetItemsByUserIDParameters()
@ -38,8 +51,6 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
parameters.includeItemTypes = itemTypes parameters.includeItemTypes = itemTypes
parameters.isRecursive = true parameters.isRecursive = true
parameters.sortBy = [ItemSortBy.name.rawValue]
parameters.sortOrder = [.ascending]
// Page size // Page size
if let page { if let page {
@ -47,6 +58,48 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
parameters.startIndex = page * pageSize 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 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? // on refresh. Should make bidirectional/offset index start?
// - use startIndex/index ranges instead of pages // - use startIndex/index ranges instead of pages
// - source of data doesn't guarantee that all items in 0 ..< startIndex exist // - 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 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( convenience init(
title: String, title: String,
id: String?, id: String?,