[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)
|
||||
var userDetails = makeUserDetails
|
||||
@Route(.modal)
|
||||
var userMediaAccess = makeUserMediaAccess
|
||||
@Route(.modal)
|
||||
var userPermissions = makeUserPermissions
|
||||
@Route(.modal)
|
||||
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> {
|
||||
NavigationViewCoordinator {
|
||||
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> {
|
||||
Binding<V>(
|
||||
get: { getter(wrappedValue) },
|
||||
|
|
|
@ -18,6 +18,8 @@ internal enum L10n {
|
|||
internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color")
|
||||
/// 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
|
||||
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
||||
/// Active
|
||||
|
@ -400,6 +402,8 @@ internal enum L10n {
|
|||
}
|
||||
/// 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
|
||||
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
|
||||
/// Details
|
||||
|
@ -452,6 +456,8 @@ internal enum L10n {
|
|||
internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users")
|
||||
/// 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
|
||||
internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled")
|
||||
/// 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.")
|
||||
/// 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
|
||||
internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads")
|
||||
/// Media playback
|
||||
|
|
|
@ -13,30 +13,32 @@ import OrderedCollections
|
|||
|
||||
final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiable {
|
||||
|
||||
// MARK: Event
|
||||
// MARK: - Event
|
||||
|
||||
enum Event {
|
||||
case error(JellyfinAPIError)
|
||||
case updated
|
||||
}
|
||||
|
||||
// MARK: Action
|
||||
// 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
|
||||
// MARK: - Background State
|
||||
|
||||
enum BackgroundState: Hashable {
|
||||
case updating
|
||||
case refreshing
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
// MARK: - State
|
||||
|
||||
enum State: Hashable {
|
||||
case initial
|
||||
|
@ -45,112 +47,208 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
|||
case error(JellyfinAPIError)
|
||||
}
|
||||
|
||||
// MARK: Published Values
|
||||
// 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()
|
||||
}
|
||||
|
||||
private var userTask: AnyCancellable?
|
||||
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
||||
|
||||
// MARK: Initialize from UserDto
|
||||
// MARK: - Initialize
|
||||
|
||||
init(user: UserDto) {
|
||||
self.user = user
|
||||
}
|
||||
|
||||
// MARK: Respond
|
||||
// MARK: - Respond
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .cancel:
|
||||
userTask?.cancel()
|
||||
return .initial
|
||||
|
||||
case .loadDetails:
|
||||
return performAction {
|
||||
try await self.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):
|
||||
return performAction {
|
||||
try await self.updatePolicy(policy: 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):
|
||||
return performAction {
|
||||
try await self.updateConfiguration(configuration: 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):
|
||||
return performAction {
|
||||
try await self.updateUsername(username: 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: - Perform Action
|
||||
|
||||
private func performAction(action: @escaping () async throws -> Void) -> State {
|
||||
userTask?.cancel()
|
||||
|
||||
userTask = Task {
|
||||
do {
|
||||
await MainActor.run {
|
||||
_ = self.backgroundStates.append(.updating)
|
||||
}
|
||||
|
||||
try await action()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
self.eventSubject.send(.updated)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
_ = self.backgroundStates.remove(.updating)
|
||||
}
|
||||
} catch {
|
||||
let jellyfinError = JellyfinAPIError(error.localizedDescription)
|
||||
await MainActor.run {
|
||||
self.state = .error(jellyfinError)
|
||||
self.backgroundStates.remove(.updating)
|
||||
self.eventSubject.send(.error(jellyfinError))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return .updating
|
||||
}
|
||||
|
||||
// MARK: - Load User
|
||||
// MARK: - Load User Details
|
||||
|
||||
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 response = try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
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
|
||||
|
||||
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)
|
||||
try await userSession.client.send(request)
|
||||
|
||||
|
@ -162,7 +260,7 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
|||
// MARK: - Update User Configuration
|
||||
|
||||
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)
|
||||
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 {
|
||||
guard let userID = user.id else { return }
|
||||
guard let userID = user.id else { throw JellyfinAPIError("User ID is missing") }
|
||||
var updatedUser = user
|
||||
updatedUser.name = username
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@
|
|||
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
||||
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.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 */; };
|
||||
4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -2169,6 +2171,7 @@
|
|||
4E90F7622CC72B1F00417C31 /* EditServerTaskView */,
|
||||
4E35CE622CBED3FF00DBD886 /* ServerLogsView */,
|
||||
4E182C9A2C94991800FBEFD5 /* ServerTasksView */,
|
||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */,
|
||||
4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */,
|
||||
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */,
|
||||
4EC2B1992CC96E5E00D866BE /* ServerUsersView */,
|
||||
|
@ -2527,6 +2530,14 @@
|
|||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */,
|
||||
);
|
||||
path = ServerUserAccessView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -5718,6 +5729,7 @@
|
|||
E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */,
|
||||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */,
|
||||
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
||||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.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 {
|
||||
List {
|
||||
|
||||
// TODO: Replace with Update Profile Picture & Username
|
||||
AdminDashboardView.UserSection(
|
||||
user: viewModel.user,
|
||||
lastActivityDate: viewModel.user.lastActivityDate
|
||||
) {
|
||||
// TODO: Update Profile Picture & Username
|
||||
}
|
||||
)
|
||||
|
||||
Section(L10n.advanced) {
|
||||
Section {
|
||||
if let userId = viewModel.user.id {
|
||||
ChevronButton(L10n.password)
|
||||
.onSelect {
|
||||
router.route(to: \.resetUserPassword, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
ChevronButton(L10n.mediaAccess)
|
||||
.onSelect {
|
||||
router.route(to: \.userMediaAccess, viewModel)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.permissions)
|
||||
.onSelect {
|
||||
|
|
|
@ -1881,3 +1881,19 @@
|
|||
// Custom Connections - Description
|
||||
// Explanation of custom connections policy
|
||||
"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