[iOS] Admin Dashboard - Media Access / Deletion Settings (#1333)
* ServerUserAdminViewModel cleanup & testing. ServerUserAccessView. * Change the enableAllLibraries to use the binding extensions * Use coalesce for enableAllFolders & enableContentDeletion * use contains binding --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
da40f6a3b5
commit
2ac9283dfa
|
@ -52,6 +52,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var userDetails = makeUserDetails
|
var userDetails = makeUserDetails
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
|
var userMediaAccess = makeUserMediaAccess
|
||||||
|
@Route(.modal)
|
||||||
var userPermissions = makeUserPermissions
|
var userPermissions = makeUserPermissions
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var resetUserPassword = makeResetUserPassword
|
var resetUserPassword = makeResetUserPassword
|
||||||
|
@ -130,6 +132,12 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeUserMediaAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
NavigationViewCoordinator {
|
||||||
|
ServerUserAccessView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeUserPermissions(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeUserPermissions(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
NavigationViewCoordinator {
|
NavigationViewCoordinator {
|
||||||
ServerUserPermissionsView(viewModel: viewModel)
|
ServerUserPermissionsView(viewModel: viewModel)
|
||||||
|
|
|
@ -24,6 +24,19 @@ extension Binding {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contains<E: Equatable>(_ value: E) -> Binding<Bool> where Value == [E] {
|
||||||
|
Binding<Bool>(
|
||||||
|
get: { wrappedValue.contains(value) },
|
||||||
|
set: { shouldBeContained in
|
||||||
|
if shouldBeContained {
|
||||||
|
wrappedValue.append(value)
|
||||||
|
} else {
|
||||||
|
wrappedValue.removeAll { $0 == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func map<V>(getter: @escaping (Value) -> V, setter: @escaping (V) -> Value) -> Binding<V> {
|
func map<V>(getter: @escaping (Value) -> V, setter: @escaping (V) -> Value) -> Binding<V> {
|
||||||
Binding<V>(
|
Binding<V>(
|
||||||
get: { getter(wrappedValue) },
|
get: { getter(wrappedValue) },
|
||||||
|
|
|
@ -18,6 +18,8 @@ internal enum L10n {
|
||||||
internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color")
|
internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color")
|
||||||
/// Some views may need an app restart to update.
|
/// Some views may need an app restart to update.
|
||||||
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
|
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
|
||||||
|
/// Access
|
||||||
|
internal static let access = L10n.tr("Localizable", "access", fallback: "Access")
|
||||||
/// Accessibility
|
/// Accessibility
|
||||||
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
||||||
/// Active
|
/// Active
|
||||||
|
@ -400,6 +402,8 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
/// Are you sure you wish to delete this user?
|
/// Are you sure you wish to delete this user?
|
||||||
internal static let deleteUserWarning = L10n.tr("Localizable", "deleteUserWarning", fallback: "Are you sure you wish to delete this user?")
|
internal static let deleteUserWarning = L10n.tr("Localizable", "deleteUserWarning", fallback: "Are you sure you wish to delete this user?")
|
||||||
|
/// Deletion
|
||||||
|
internal static let deletion = L10n.tr("Localizable", "deletion", fallback: "Deletion")
|
||||||
/// Delivery
|
/// Delivery
|
||||||
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
|
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
|
||||||
/// Details
|
/// Details
|
||||||
|
@ -452,6 +456,8 @@ internal enum L10n {
|
||||||
internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users")
|
internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users")
|
||||||
/// Empty Next Up
|
/// Empty Next Up
|
||||||
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up")
|
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up")
|
||||||
|
/// Enable all libraries
|
||||||
|
internal static let enableAllLibraries = L10n.tr("Localizable", "enableAllLibraries", fallback: "Enable all libraries")
|
||||||
/// Enabled
|
/// Enabled
|
||||||
internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled")
|
internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled")
|
||||||
/// End Date
|
/// End Date
|
||||||
|
@ -692,6 +698,8 @@ internal enum L10n {
|
||||||
internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.")
|
internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.")
|
||||||
/// Media
|
/// Media
|
||||||
internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
|
internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
|
||||||
|
/// Media Access
|
||||||
|
internal static let mediaAccess = L10n.tr("Localizable", "mediaAccess", fallback: "Media Access")
|
||||||
/// Media downloads
|
/// Media downloads
|
||||||
internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads")
|
internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads")
|
||||||
/// Media playback
|
/// Media playback
|
||||||
|
|
|
@ -13,30 +13,32 @@ import OrderedCollections
|
||||||
|
|
||||||
final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiable {
|
final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiable {
|
||||||
|
|
||||||
// MARK: Event
|
// MARK: - Event
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
case error(JellyfinAPIError)
|
case error(JellyfinAPIError)
|
||||||
case updated
|
case updated
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Action
|
// MARK: - Action
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
case cancel
|
case cancel
|
||||||
case loadDetails
|
case loadDetails
|
||||||
|
case loadLibraries(isHidden: Bool? = false)
|
||||||
case updatePolicy(UserPolicy)
|
case updatePolicy(UserPolicy)
|
||||||
case updateConfiguration(UserConfiguration)
|
case updateConfiguration(UserConfiguration)
|
||||||
case updateUsername(String)
|
case updateUsername(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Background State
|
// MARK: - Background State
|
||||||
|
|
||||||
enum BackgroundState: Hashable {
|
enum BackgroundState: Hashable {
|
||||||
case updating
|
case updating
|
||||||
|
case refreshing
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: State
|
// MARK: - State
|
||||||
|
|
||||||
enum State: Hashable {
|
enum State: Hashable {
|
||||||
case initial
|
case initial
|
||||||
|
@ -45,112 +47,208 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
||||||
case error(JellyfinAPIError)
|
case error(JellyfinAPIError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Published Values
|
// MARK: - Published Values
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
final var state: State = .initial
|
final var state: State = .initial
|
||||||
@Published
|
@Published
|
||||||
final var backgroundStates: OrderedSet<BackgroundState> = []
|
final var backgroundStates: OrderedSet<BackgroundState> = []
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
private(set) var user: UserDto
|
private(set) var user: UserDto
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var libraries: [BaseItemDto] = []
|
||||||
|
|
||||||
|
private var userTaskCancellable: AnyCancellable?
|
||||||
|
private var eventSubject = PassthroughSubject<Event, Never>()
|
||||||
|
|
||||||
var events: AnyPublisher<Event, Never> {
|
var events: AnyPublisher<Event, Never> {
|
||||||
eventSubject
|
eventSubject
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var userTask: AnyCancellable?
|
// MARK: - Initialize
|
||||||
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
|
||||||
|
|
||||||
// MARK: Initialize from UserDto
|
|
||||||
|
|
||||||
init(user: UserDto) {
|
init(user: UserDto) {
|
||||||
self.user = user
|
self.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Respond
|
// MARK: - Respond
|
||||||
|
|
||||||
func respond(to action: Action) -> State {
|
func respond(to action: Action) -> State {
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
userTask?.cancel()
|
|
||||||
return .initial
|
return .initial
|
||||||
|
|
||||||
case .loadDetails:
|
case .loadDetails:
|
||||||
return performAction {
|
userTaskCancellable?.cancel()
|
||||||
try await self.loadDetails()
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .updatePolicy(policy):
|
userTaskCancellable = Task {
|
||||||
return performAction {
|
|
||||||
try await self.updatePolicy(policy: policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .updateConfiguration(configuration):
|
|
||||||
return performAction {
|
|
||||||
try await self.updateConfiguration(configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .updateUsername(username):
|
|
||||||
return performAction {
|
|
||||||
try await self.updateUsername(username: username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Perform Action
|
|
||||||
|
|
||||||
private func performAction(action: @escaping () async throws -> Void) -> State {
|
|
||||||
userTask?.cancel()
|
|
||||||
|
|
||||||
userTask = Task {
|
|
||||||
do {
|
do {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_ = self.backgroundStates.append(.updating)
|
_ = backgroundStates.append(.refreshing)
|
||||||
}
|
}
|
||||||
|
|
||||||
try await action()
|
try await loadDetails()
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.state = .content
|
state = .content
|
||||||
self.eventSubject.send(.updated)
|
_ = backgroundStates.remove(.refreshing)
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
_ = self.backgroundStates.remove(.updating)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
let jellyfinError = JellyfinAPIError(error.localizedDescription)
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.state = .error(jellyfinError)
|
state = .error(.init(error.localizedDescription))
|
||||||
self.backgroundStates.remove(.updating)
|
eventSubject.send(.error(.init(error.localizedDescription)))
|
||||||
self.eventSubject.send(.error(jellyfinError))
|
_ = backgroundStates.remove(.refreshing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.asAnyCancellable()
|
.asAnyCancellable()
|
||||||
|
|
||||||
return .updating
|
return state
|
||||||
|
|
||||||
|
case let .loadLibraries(isHidden):
|
||||||
|
userTaskCancellable?.cancel()
|
||||||
|
|
||||||
|
userTaskCancellable = Task {
|
||||||
|
do {
|
||||||
|
await MainActor.run {
|
||||||
|
_ = backgroundStates.append(.refreshing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Load User
|
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 {
|
private func loadDetails() async throws {
|
||||||
guard let userID = user.id else { return }
|
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
|
||||||
let request = Paths.getUserByID(userID: userID)
|
let request = Paths.getUserByID(userID: userID)
|
||||||
let response = try await userSession.client.send(request)
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.user = response.value
|
self.user = response.value
|
||||||
self.state = .content
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// MARK: - Update User Policy
|
||||||
|
|
||||||
private func updatePolicy(policy: UserPolicy) async throws {
|
private func updatePolicy(policy: UserPolicy) async throws {
|
||||||
guard let userID = user.id else { return }
|
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
|
||||||
let request = Paths.updateUserPolicy(userID: userID, policy)
|
let request = Paths.updateUserPolicy(userID: userID, policy)
|
||||||
try await userSession.client.send(request)
|
try await userSession.client.send(request)
|
||||||
|
|
||||||
|
@ -162,7 +260,7 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
||||||
// MARK: - Update User Configuration
|
// MARK: - Update User Configuration
|
||||||
|
|
||||||
private func updateConfiguration(configuration: UserConfiguration) async throws {
|
private func updateConfiguration(configuration: UserConfiguration) async throws {
|
||||||
guard let userID = user.id else { return }
|
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
|
||||||
let request = Paths.updateUserConfiguration(userID: userID, configuration)
|
let request = Paths.updateUserConfiguration(userID: userID, configuration)
|
||||||
try await userSession.client.send(request)
|
try await userSession.client.send(request)
|
||||||
|
|
||||||
|
@ -171,10 +269,10 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Update User Name
|
// MARK: - Update Username
|
||||||
|
|
||||||
private func updateUsername(username: String) async throws {
|
private func updateUsername(username: String) async throws {
|
||||||
guard let userID = user.id else { return }
|
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
|
||||||
var updatedUser = user
|
var updatedUser = user
|
||||||
updatedUser.name = username
|
updatedUser.name = username
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,7 @@
|
||||||
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
||||||
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
|
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
|
||||||
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
||||||
|
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; };
|
||||||
4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; };
|
4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; };
|
||||||
4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; };
|
4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; };
|
||||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
|
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
|
||||||
|
@ -1261,6 +1262,7 @@
|
||||||
4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
|
4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
|
||||||
4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = "<group>"; };
|
4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = "<group>"; };
|
||||||
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
||||||
|
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = "<group>"; };
|
||||||
4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = "<group>"; };
|
4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = "<group>"; };
|
||||||
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = "<group>"; };
|
4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = "<group>"; };
|
||||||
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2169,6 +2171,7 @@
|
||||||
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
||||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||||
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
||||||
|
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||||
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
||||||
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
||||||
|
@ -2527,6 +2530,14 @@
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */,
|
||||||
|
);
|
||||||
|
path = ServerUserAccessView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -5718,6 +5729,7 @@
|
||||||
E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */,
|
E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */,
|
||||||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||||
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||||
|
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */,
|
||||||
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
||||||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
||||||
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */,
|
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
//
|
||||||
|
// 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 Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerUserAccessView: View {
|
||||||
|
|
||||||
|
// MARK: - Environment
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
// MARK: - ViewModel
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
// MARK: - State Variables
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempPolicy: UserPolicy
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
@State
|
||||||
|
private var isPresentingError: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contentView
|
||||||
|
.navigationTitle(L10n.mediaAccess)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarCloseButton {
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
.topBarTrailing {
|
||||||
|
if viewModel.backgroundStates.contains(.updating) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
Button(L10n.save) {
|
||||||
|
if tempPolicy != viewModel.user.policy {
|
||||||
|
viewModel.send(.updatePolicy(tempPolicy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.toolbarPill)
|
||||||
|
.disabled(viewModel.user.policy == tempPolicy)
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .error(eventError):
|
||||||
|
UIDevice.feedback(.error)
|
||||||
|
error = eventError
|
||||||
|
isPresentingError = true
|
||||||
|
case .updated:
|
||||||
|
UIDevice.feedback(.success)
|
||||||
|
router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(
|
||||||
|
L10n.error.text,
|
||||||
|
isPresented: $isPresentingError,
|
||||||
|
presenting: error
|
||||||
|
) { _ in
|
||||||
|
Button(L10n.dismiss, role: .cancel) {}
|
||||||
|
} message: { error in
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
}
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.loadLibraries(isHidden: false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var contentView: some View {
|
||||||
|
List {
|
||||||
|
accessView
|
||||||
|
deletionView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Media Access View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var accessView: some View {
|
||||||
|
Section(L10n.access) {
|
||||||
|
Toggle(
|
||||||
|
L10n.enableAllLibraries,
|
||||||
|
isOn: $tempPolicy.enableAllFolders.coalesce(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tempPolicy.enableAllFolders == false {
|
||||||
|
Section {
|
||||||
|
ForEach(viewModel.libraries, id: \.id) { library in
|
||||||
|
Toggle(
|
||||||
|
library.displayTitle,
|
||||||
|
isOn: $tempPolicy.enabledFolders
|
||||||
|
.coalesce([])
|
||||||
|
.contains(library.id!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Media Deletion View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var deletionView: some View {
|
||||||
|
Section(L10n.deletion) {
|
||||||
|
Toggle(
|
||||||
|
L10n.enableAllLibraries,
|
||||||
|
isOn: $tempPolicy.enableContentDeletion.coalesce(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tempPolicy.enableContentDeletion == false {
|
||||||
|
Section {
|
||||||
|
ForEach(viewModel.libraries, id: \.id) { library in
|
||||||
|
Toggle(
|
||||||
|
library.displayTitle,
|
||||||
|
isOn: $tempPolicy.enableContentDeletionFromFolders
|
||||||
|
.coalesce([])
|
||||||
|
.contains(library.id!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,20 +31,27 @@ struct ServerUserDetailsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
|
||||||
|
// TODO: Replace with Update Profile Picture & Username
|
||||||
AdminDashboardView.UserSection(
|
AdminDashboardView.UserSection(
|
||||||
user: viewModel.user,
|
user: viewModel.user,
|
||||||
lastActivityDate: viewModel.user.lastActivityDate
|
lastActivityDate: viewModel.user.lastActivityDate
|
||||||
) {
|
)
|
||||||
// TODO: Update Profile Picture & Username
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(L10n.advanced) {
|
Section {
|
||||||
if let userId = viewModel.user.id {
|
if let userId = viewModel.user.id {
|
||||||
ChevronButton(L10n.password)
|
ChevronButton(L10n.password)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.resetUserPassword, userId)
|
router.route(to: \.resetUserPassword, userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
ChevronButton(L10n.mediaAccess)
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.userMediaAccess, viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
ChevronButton(L10n.permissions)
|
ChevronButton(L10n.permissions)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
|
|
|
@ -1881,3 +1881,19 @@
|
||||||
// Custom Connections - Description
|
// Custom Connections - Description
|
||||||
// Explanation of custom connections policy
|
// Explanation of custom connections policy
|
||||||
"customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server.";
|
"customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server.";
|
||||||
|
|
||||||
|
// Enable All Libraries - Toggle
|
||||||
|
// Toggle to enable a setting for all Libraries
|
||||||
|
"enableAllLibraries" = "Enable all libraries";
|
||||||
|
|
||||||
|
// Media Access - Section Title
|
||||||
|
// Section Title for Server User Media Access Editing
|
||||||
|
"mediaAccess" = "Media Access";
|
||||||
|
|
||||||
|
// Deletion - Section Description
|
||||||
|
// Section Title for Media Deletion
|
||||||
|
"deletion" = "Deletion";
|
||||||
|
|
||||||
|
// Access - Section Description
|
||||||
|
// Section Title for Media Access
|
||||||
|
"access" = "Access";
|
||||||
|
|
Loading…
Reference in New Issue