jellyflood/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift

261 lines
6.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) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Foundation
import IdentifiedCollections
import JellyfinAPI
import SwiftUI
final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable {
// MARK: Event
enum Event {
case deleted
case error(JellyfinAPIError)
}
// MARK: Actions
enum Action: Equatable {
case refreshUser(String)
case getUsers(isHidden: Bool = false, isDisabled: Bool = false)
case deleteUsers([String])
case appendUser(UserDto)
}
// MARK: - BackgroundState
enum BackgroundState: Hashable {
case gettingUsers
case deletingUsers
case appendingUsers
}
// MARK: - State
enum State: Hashable {
case content
case error(JellyfinAPIError)
case initial
}
// MARK: Published Values
@Published
var backgroundStates: Set<BackgroundState> = []
@Published
var users: IdentifiedArrayOf<UserDto> = []
@Published
var state: State = .initial
var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
private var userTask: AnyCancellable?
private var eventSubject: PassthroughSubject<Event, Never> = .init()
// MARK: - Initializer
override init() {
super.init()
Notifications[.didChangeUserProfile]
.publisher
.sink { userID in
Task {
await self.send(.refreshUser(userID))
}
}
.store(in: &cancellables)
}
// MARK: - Respond to Action
func respond(to action: Action) -> State {
switch action {
case let .refreshUser(userID):
userTask?.cancel()
backgroundStates.insert(.gettingUsers)
userTask = Task {
do {
try await refreshUser(userID)
await MainActor.run {
state = .content
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.gettingUsers)
}
}
.asAnyCancellable()
return state
case let .getUsers(isHidden, isDisabled):
userTask?.cancel()
backgroundStates.insert(.gettingUsers)
userTask = Task {
do {
try await loadUsers(isHidden: isHidden, isDisabled: isDisabled)
await MainActor.run {
state = .content
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.gettingUsers)
}
}
.asAnyCancellable()
return state
case let .deleteUsers(ids):
userTask?.cancel()
backgroundStates.insert(.deletingUsers)
userTask = Task {
do {
try await self.deleteUsers(ids: ids)
await MainActor.run {
self.state = .content
self.eventSubject.send(.deleted)
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
self.eventSubject.send(.error(.init(error.localizedDescription)))
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.deletingUsers)
}
}
.asAnyCancellable()
return state
case let .appendUser(user):
userTask?.cancel()
backgroundStates.insert(.appendingUsers)
userTask = Task {
do {
await self.appendUser(user: user)
await MainActor.run {
self.state = .content
self.eventSubject.send(.deleted)
}
}
await MainActor.run {
_ = self.backgroundStates.remove(.appendingUsers)
}
}
.asAnyCancellable()
return state
}
}
// MARK: - Refresh User
private func refreshUser(_ userID: String) async throws {
let request = Paths.getUserByID(userID: userID)
let response = try await userSession.client.send(request)
let newUser = response.value
await MainActor.run {
if let index = self.users.firstIndex(where: { $0.id == userID }) {
self.users[index] = newUser
}
}
}
// MARK: - Load Users
private func loadUsers(isHidden: Bool, isDisabled: Bool) async throws {
let request = Paths.getUsers(isHidden: isHidden ? true : nil, isDisabled: isDisabled ? true : nil)
let response = try await userSession.client.send(request)
let newUsers = response.value
.sorted(using: \.name)
await MainActor.run {
self.users = IdentifiedArray(uniqueElements: newUsers)
}
}
// MARK: - Delete Users
private func deleteUsers(ids: [String]) async throws {
guard ids.isNotEmpty else {
return
}
// Don't allow self-deletion
let userIdsToDelete = ids.filter { $0 != userSession.user.id }
try await withThrowingTaskGroup(of: Void.self) { group in
for userId in userIdsToDelete {
group.addTask {
try await self.deleteUser(id: userId)
}
}
try await group.waitForAll()
}
await MainActor.run {
self.users.removeAll(where: { userIdsToDelete.contains($0.id ?? "") })
}
}
// MARK: - Delete User
private func deleteUser(id: String) async throws {
let request = Paths.deleteUser(userID: id)
try await userSession.client.send(request)
}
// MARK: - Append User
private func appendUser(user: UserDto) async {
await MainActor.run {
users.append(user)
users.sort(by: { $0.name ?? "" < $1.name ?? "" })
}
}
}