jellyflood/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift

287 lines
8.4 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 Foundation
import JellyfinAPI
import OrderedCollections
final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiable {
// MARK: - Event
enum Event {
case error(JellyfinAPIError)
case updated
}
// MARK: - Action
enum Action: Equatable {
case cancel
case loadDetails
case loadLibraries(isHidden: Bool? = false)
case updatePolicy(UserPolicy)
case updateConfiguration(UserConfiguration)
case updateUsername(String)
}
// MARK: - Background State
enum BackgroundState: Hashable {
case updating
case refreshing
}
// MARK: - State
enum State: Hashable {
case initial
case content
case updating
case error(JellyfinAPIError)
}
// MARK: - Published Values
@Published
final var state: State = .initial
@Published
final var backgroundStates: OrderedSet<BackgroundState> = []
@Published
private(set) var user: UserDto
@Published
var libraries: [BaseItemDto] = []
private var userTaskCancellable: AnyCancellable?
private var eventSubject = PassthroughSubject<Event, Never>()
var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
// MARK: - Initialize
init(user: UserDto) {
self.user = user
}
// MARK: - Respond
func respond(to action: Action) -> State {
switch action {
case .cancel:
return .initial
case .loadDetails:
userTaskCancellable?.cancel()
userTaskCancellable = Task {
do {
await MainActor.run {
_ = backgroundStates.append(.refreshing)
}
try await loadDetails()
await MainActor.run {
state = .content
_ = backgroundStates.remove(.refreshing)
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
_ = backgroundStates.remove(.refreshing)
}
}
}
.asAnyCancellable()
return state
case let .loadLibraries(isHidden):
userTaskCancellable?.cancel()
userTaskCancellable = Task {
do {
await MainActor.run {
_ = backgroundStates.append(.refreshing)
}
try await loadLibraries(isHidden: isHidden)
await MainActor.run {
state = .content
_ = backgroundStates.remove(.refreshing)
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
_ = backgroundStates.remove(.refreshing)
}
}
}
.asAnyCancellable()
return state
case let .updatePolicy(policy):
userTaskCancellable?.cancel()
userTaskCancellable = Task {
do {
await MainActor.run {
_ = backgroundStates.append(.updating)
}
try await updatePolicy(policy: policy)
await MainActor.run {
state = .content
eventSubject.send(.updated)
_ = backgroundStates.remove(.updating)
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
_ = backgroundStates.remove(.updating)
}
}
}
.asAnyCancellable()
return state
case let .updateConfiguration(configuration):
userTaskCancellable?.cancel()
userTaskCancellable = Task {
do {
await MainActor.run {
_ = backgroundStates.append(.updating)
}
try await updateConfiguration(configuration: configuration)
await MainActor.run {
state = .content
eventSubject.send(.updated)
_ = backgroundStates.remove(.updating)
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
_ = backgroundStates.remove(.updating)
}
}
}
.asAnyCancellable()
return state
case let .updateUsername(username):
userTaskCancellable?.cancel()
userTaskCancellable = Task {
do {
await MainActor.run {
_ = backgroundStates.append(.updating)
}
try await updateUsername(username: username)
await MainActor.run {
state = .content
eventSubject.send(.updated)
_ = backgroundStates.remove(.updating)
}
} catch {
await MainActor.run {
state = .error(.init(error.localizedDescription))
eventSubject.send(.error(.init(error.localizedDescription)))
_ = backgroundStates.remove(.updating)
}
}
}
.asAnyCancellable()
return state
}
}
// MARK: - Load User Details
private func loadDetails() async throws {
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
let request = Paths.getUserByID(userID: userID)
let response = try await userSession.client.send(request)
await MainActor.run {
self.user = response.value
}
}
// MARK: - Load Libraries
private func loadLibraries(isHidden: Bool?) async throws {
let request = Paths.getMediaFolders(isHidden: isHidden)
let response = try await userSession.client.send(request)
await MainActor.run {
self.libraries = response.value.items ?? []
}
}
// MARK: - Update User Policy
private func updatePolicy(policy: UserPolicy) async throws {
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
let request = Paths.updateUserPolicy(userID: userID, policy)
try await userSession.client.send(request)
await MainActor.run {
self.user.policy = policy
}
}
// MARK: - Update User Configuration
private func updateConfiguration(configuration: UserConfiguration) async throws {
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
let request = Paths.updateUserConfiguration(userID: userID, configuration)
try await userSession.client.send(request)
await MainActor.run {
self.user.configuration = configuration
}
}
// MARK: - Update Username
private func updateUsername(username: String) async throws {
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
var updatedUser = user
updatedUser.name = username
let request = Paths.updateUser(userID: userID, updatedUser)
try await userSession.client.send(request)
await MainActor.run {
self.user.name = username
}
}
}