jellyflood/Shared/ViewModels/HomeViewModel.swift

171 lines
4.9 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//
import Combine
import CoreStore
import Factory
import JellyfinAPI
import OrderedCollections
final class HomeViewModel: ViewModel, Stateful {
// MARK: Action
enum Action {
case error(JellyfinAPIError)
case refresh
}
// MARK: State
enum State: Equatable {
case content
case error(JellyfinAPIError)
case initial
case refreshing
}
@Published
var libraries: [LatestInLibraryViewModel] = []
@Published
var resumeItems: OrderedSet<BaseItemDto> = []
@Published
var state: State = .initial
private(set) var nextUpViewModel: NextUpLibraryViewModel = .init()
private(set) var recentlyAddedViewModel: RecentlyAddedLibraryViewModel = .init()
private var refreshTask: AnyCancellable?
func respond(to action: Action) -> State {
switch action {
case let .error(error):
return .error(error)
case .refresh:
cancellables.removeAll()
Task { [weak self] in
guard let self else { return }
do {
try await self.refresh()
guard !Task.isCancelled else { return }
await MainActor.run {
self.state = .content
}
} catch {
guard !Task.isCancelled else { return }
await MainActor.run {
self.send(.error(.init(error.localizedDescription)))
}
}
}
.store(in: &cancellables)
return .refreshing
}
}
private func refresh() async throws {
Task {
await nextUpViewModel.send(.refresh)
}
Task {
await recentlyAddedViewModel.send(.refresh)
}
let resumeItems = try await getResumeItems()
let libraries = try await getLibraries()
for library in libraries {
await library.send(.refresh)
}
await MainActor.run {
self.resumeItems.elements = resumeItems
self.libraries = libraries
}
}
private func getResumeItems() async throws -> [BaseItemDto] {
var parameters = Paths.GetResumeItemsParameters()
parameters.enableUserData = true
parameters.fields = .MinimumFields
parameters.includeItemTypes = [.movie, .episode]
parameters.limit = 20
let request = Paths.getResumeItems(userID: userSession.user.id, parameters: parameters)
let response = try await userSession.client.send(request)
return response.value.items ?? []
}
private func getLibraries() async throws -> [LatestInLibraryViewModel] {
let userViewsPath = Paths.getUserViews(userID: userSession.user.id)
async let userViews = userSession.client.send(userViewsPath)
async let excludedLibraryIDs = getExcludedLibraries()
return try await (userViews.value.items ?? [])
.intersection(["movies", "tvshows"], using: \.collectionType)
.subtracting(excludedLibraryIDs, using: \.id)
.map { LatestInLibraryViewModel(parent: $0) }
}
// TODO: eventually a more robust user/server information retrieval system
// will be in place. Replace with using the data from the remove user
private func getExcludedLibraries() async throws -> [String] {
let currentUserPath = Paths.getCurrentUser
let response = try await userSession.client.send(currentUserPath)
return response.value.configuration?.latestItemsExcludes ?? []
}
// TODO: fix
func markItemUnplayed(_ item: BaseItemDto) {
// guard resumeItems.contains(where: { $0.id == item.id! }) else { return }
//
// Task {
// let request = Paths.markUnplayedItem(
// userID: userSession.user.id,
// itemID: item.id!
// )
// let _ = try await userSession.client.send(request)
//
//// refreshResumeItems()
//
// try await nextUpViewModel.refresh()
// try await recentlyAddedViewModel.refresh()
// }
}
// TODO: fix
func markItemPlayed(_ item: BaseItemDto) {
// guard resumeItems.contains(where: { $0.id == item.id! }) else { return }
//
// Task {
// let request = Paths.markPlayedItem(
// userID: userSession.user.id,
// itemID: item.id!
// )
// let _ = try await userSession.client.send(request)
//
//// refreshResumeItems()
// try await nextUpViewModel.refresh()
// try await recentlyAddedViewModel.refresh()
// }
}
}