[tvOS] ItemTypeLibraryViewModel - Implement FilterViewModel (#1409)
* FilterViewModel only * comments --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
c9ae01e792
commit
35c39a8d0a
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?,
|
||||||
|
|
Loading…
Reference in New Issue