diff --git a/Shared/Components/ChevronAlertButton.swift b/Shared/Components/ChevronAlertButton.swift index 2844f878..91b6d0dc 100644 --- a/Shared/Components/ChevronAlertButton.swift +++ b/Shared/Components/ChevronAlertButton.swift @@ -47,7 +47,7 @@ struct ChevronAlertButton: View where Content: View { } } } message: { - if let description = description { + if let description { Text(description) } } diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index b70f4092..356b3b6a 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -17,37 +17,53 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { @Root var start = makeStart + // MARK: - Route: Active Sessions + @Route(.push) var activeSessions = makeActiveSessions @Route(.push) var activeDeviceDetails = makeActiveDeviceDetails - @Route(.push) - var tasks = makeTasks + + // MARK: - Route: Devices + @Route(.push) var devices = makeDevices @Route(.push) var deviceDetails = makeDeviceDetails + + // MARK: - Route: Server Tasks + @Route(.push) var editServerTask = makeEditServerTask + @Route(.push) + var tasks = makeTasks @Route(.modal) var addServerTaskTrigger = makeAddServerTaskTrigger + + // MARK: - Route: Server Logs + @Route(.push) var serverLogs = makeServerLogs + + // MARK: - Route: Users + @Route(.push) var users = makeUsers @Route(.push) var userDetails = makeUserDetails @Route(.modal) + var userPermissions = makeUserPermissions + @Route(.modal) var resetUserPassword = makeResetUserPassword @Route(.modal) var addServerUser = makeAddServerUser + + // MARK: - Route: API Keys + @Route(.push) var apiKeys = makeAPIKeys - @ViewBuilder - func makeAdminDashboard() -> some View { - AdminDashboardView() - } + // MARK: - Views: Active Sessions @ViewBuilder func makeActiveSessions() -> some View { @@ -59,21 +75,13 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { ActiveSessionDetailView(box: box) } + // MARK: - Views: Server Tasks + @ViewBuilder func makeTasks() -> some View { ServerTasksView() } - @ViewBuilder - func makeDevices() -> some View { - DevicesView() - } - - @ViewBuilder - func makeDeviceDetails(device: DeviceInfo) -> some View { - DeviceDetailsView(device: device) - } - @ViewBuilder func makeEditServerTask(observer: ServerTaskObserver) -> some View { EditServerTaskView(observer: observer) @@ -85,11 +93,27 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { } } + // MARK: - Views: Devices + + @ViewBuilder + func makeDevices() -> some View { + DevicesView() + } + + @ViewBuilder + func makeDeviceDetails(device: DeviceInfo) -> some View { + DeviceDetailsView(device: device) + } + + // MARK: - Views: Server Logs + @ViewBuilder func makeServerLogs() -> some View { ServerLogsView() } + // MARK: - Views: Users + @ViewBuilder func makeUsers() -> some View { ServerUsersView() @@ -106,17 +130,27 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { } } + func makeUserPermissions(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ServerUserPermissionsView(viewModel: viewModel) + } + } + func makeResetUserPassword(userID: String) -> NavigationViewCoordinator { NavigationViewCoordinator { ResetUserPasswordView(userID: userID, requiresCurrentPassword: false) } } + // MARK: - Views: API Keys + @ViewBuilder func makeAPIKeys() -> some View { APIKeysView() } + // MARK: - Views: Dashboard + @ViewBuilder func makeStart() -> some View { AdminDashboardView() diff --git a/Shared/Extensions/Binding.swift b/Shared/Extensions/Binding.swift new file mode 100644 index 00000000..cb41dc82 --- /dev/null +++ b/Shared/Extensions/Binding.swift @@ -0,0 +1,44 @@ +// +// 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 SwiftUI + +extension Binding { + + func clamp(min: Value, max: Value) -> Binding where Value: Comparable { + Binding( + get: { Swift.min(Swift.max(wrappedValue, min), max) }, + set: { wrappedValue = Swift.min(Swift.max($0, min), max) } + ) + } + + func coalesce(_ defaultValue: T) -> Binding where Value == T? { + Binding( + get: { wrappedValue ?? defaultValue }, + set: { wrappedValue = $0 } + ) + } + + func map(getter: @escaping (Value) -> V, setter: @escaping (V) -> Value) -> Binding { + Binding( + get: { getter(wrappedValue) }, + set: { wrappedValue = setter($0) } + ) + } + + func min(_ minValue: Value) -> Binding where Value: Comparable { + Binding( + get: { Swift.max(wrappedValue, minValue) }, + set: { wrappedValue = Swift.max($0, minValue) } + ) + } + + func negate() -> Binding where Value == Bool { + map(getter: { !$0 }, setter: { $0 }) + } +} diff --git a/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift new file mode 100644 index 00000000..1506ac82 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift @@ -0,0 +1,31 @@ +// +// 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 Foundation + +enum ActiveSessionsPolicy: Int, Displayable, CaseIterable { + + case unlimited = 0 + case custom = 1 // Default to 1 Active Session + + // MARK: - Display Title + + var displayTitle: String { + switch self { + case .unlimited: + return L10n.unlimited + case .custom: + return L10n.custom + } + } + + init?(rawValue: Int?) { + guard let rawValue else { return nil } + self.init(rawValue: rawValue) + } +} diff --git a/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift new file mode 100644 index 00000000..f8354c75 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift @@ -0,0 +1,27 @@ +// +// 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 Foundation + +enum LoginFailurePolicy: Int, Displayable, CaseIterable { + + case unlimited = -1 + case userDefault = 0 + case custom = 1 // Default to 1 + + var displayTitle: String { + switch self { + case .unlimited: + return L10n.unlimited + case .userDefault: + return L10n.default + case .custom: + return L10n.custom + } + } +} diff --git a/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift new file mode 100644 index 00000000..eb643517 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift @@ -0,0 +1,31 @@ +// +// 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 Foundation + +enum MaxBitratePolicy: Int, Displayable, CaseIterable { + + case unlimited = 0 + case custom = 10_000_000 // Default to 10mbps + + // MARK: - Display Title + + var displayTitle: String { + switch self { + case .unlimited: + return L10n.unlimited + case .custom: + return L10n.custom + } + } + + init?(rawValue: Int?) { + guard let rawValue else { return nil } + self.init(rawValue: rawValue) + } +} diff --git a/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift new file mode 100644 index 00000000..9e11c93e --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift @@ -0,0 +1,24 @@ +// +// 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 Foundation +import JellyfinAPI + +extension SyncPlayUserAccessType: Displayable { + + var displayTitle: String { + switch self { + case .createAndJoinGroups: + L10n.createAndJoinGroups + case .joinGroups: + L10n.joinGroups + case .none: + L10n.none + } + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index de3c575e..328e2209 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -20,8 +20,6 @@ internal enum L10n { internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility") /// Active internal static let active = L10n.tr("Localizable", "active", fallback: "Active") - /// ActiveSessionsView Header - internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices") /// Activity internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity") /// Add @@ -104,6 +102,8 @@ internal enum L10n { internal static let audioSampleRateNotSupported = L10n.tr("Localizable", "audioSampleRateNotSupported", fallback: "The audio sample rate is not supported") /// Audio Track internal static let audioTrack = L10n.tr("Localizable", "audioTrack", fallback: "Audio Track") + /// Audio transcoding + internal static let audioTranscoding = L10n.tr("Localizable", "audioTranscoding", fallback: "Audio transcoding") /// Authorize internal static let authorize = L10n.tr("Localizable", "authorize", fallback: "Authorize") /// PlaybackCompatibility Default Category @@ -260,6 +260,12 @@ internal enum L10n { internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue") /// Continue Watching internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: "Continue Watching") + /// Control other users + internal static let controlOtherUsers = L10n.tr("Localizable", "controlOtherUsers", fallback: "Control other users") + /// Control shared devices + internal static let controlSharedDevices = L10n.tr("Localizable", "controlSharedDevices", fallback: "Control shared devices") + /// Create & Join Groups + internal static let createAndJoinGroups = L10n.tr("Localizable", "createAndJoinGroups", fallback: "Create & Join Groups") /// Create API Key internal static let createAPIKey = L10n.tr("Localizable", "createAPIKey", fallback: "Create API Key") /// Enter the application name for the new API key. @@ -272,6 +278,10 @@ internal enum L10n { internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position") /// PlaybackCompatibility Custom Category internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom") + /// Custom bitrate + internal static let customBitrate = L10n.tr("Localizable", "customBitrate", fallback: "Custom bitrate") + /// Manually set the maximum number of connections a user can have to the server. + internal static let customConnectionsDescription = L10n.tr("Localizable", "customConnectionsDescription", fallback: "Manually set the maximum number of connections a user can have to the server.") /// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback. internal static let customDescription = L10n.tr("Localizable", "customDescription", fallback: "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.") /// Custom Device Name @@ -286,10 +296,16 @@ internal enum L10n { internal static let customDeviceProfileDescription = L10n.tr("Localizable", "customDeviceProfileDescription", fallback: "Dictates back to the Jellyfin Server what this device hardware is capable of playing.") /// Custom profile will replace the Existing Profiles internal static let customDeviceProfileReplace = L10n.tr("Localizable", "customDeviceProfileReplace", fallback: "The custom device profiles will replace the default Swiftfin device profiles.") + /// Manually set the number of failed login attempts allowed before locking the user. + internal static let customFailedLoginDescription = L10n.tr("Localizable", "customFailedLoginDescription", fallback: "Manually set the number of failed login attempts allowed before locking the user.") + /// Custom failed logins + internal static let customFailedLogins = L10n.tr("Localizable", "customFailedLogins", fallback: "Custom failed logins") /// Settings View - Customize internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize") /// Section Header for a Custom Device Profile internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile") + /// Custom sessions + internal static let customSessions = L10n.tr("Localizable", "customSessions", fallback: "Custom sessions") /// Daily internal static let daily = L10n.tr("Localizable", "daily", fallback: "Daily") /// Represents the dark theme setting @@ -304,6 +320,10 @@ internal enum L10n { internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week") /// Time Interval Help Text - Days internal static let days = L10n.tr("Localizable", "days", fallback: "Days") + /// Default + internal static let `default` = L10n.tr("Localizable", "default", fallback: "Default") + /// Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts. + internal static let defaultFailedLoginDescription = L10n.tr("Localizable", "defaultFailedLoginDescription", fallback: "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts.") /// Default Scheme internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme") /// Delete @@ -396,6 +416,12 @@ internal enum L10n { internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") /// Enabled internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled") + /// Enter custom bitrate in Mbps + internal static let enterCustomBitrate = L10n.tr("Localizable", "enterCustomBitrate", fallback: "Enter custom bitrate in Mbps") + /// Enter custom failed logins limit + internal static let enterCustomFailedLogins = L10n.tr("Localizable", "enterCustomFailedLogins", fallback: "Enter custom failed logins limit") + /// Enter custom max sessions + internal static let enterCustomMaxSessions = L10n.tr("Localizable", "enterCustomMaxSessions", fallback: "Enter custom max sessions") /// Episode Landscape Poster internal static let episodeLandscapePoster = L10n.tr("Localizable", "episodeLandscapePoster", fallback: "Episode Landscape Poster") /// Episode %1$@ @@ -422,10 +448,14 @@ internal enum L10n { internal static let existingUser = L10n.tr("Localizable", "existingUser", fallback: "Existing User") /// Experimental internal static let experimental = L10n.tr("Localizable", "experimental", fallback: "Experimental") + /// Failed logins + internal static let failedLogins = L10n.tr("Localizable", "failedLogins", fallback: "Failed logins") /// Favorited internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited") /// Favorites internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites") + /// Feature access + internal static let featureAccess = L10n.tr("Localizable", "featureAccess", fallback: "Feature access") /// File internal static let file = L10n.tr("Localizable", "file", fallback: "File") /// Filter Results @@ -436,6 +466,8 @@ internal enum L10n { internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing") /// Find missing metadata and images. internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.") + /// Force remote media transcoding + internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding") /// Transcode FPS internal static func fpsWithString(_ p1: Any) -> String { return L10n.tr("Localizable", "fpsWithString", String(describing: p1), fallback: "%@fps") @@ -454,6 +486,8 @@ internal enum L10n { internal static let hapticFeedback = L10n.tr("Localizable", "hapticFeedback", fallback: "Haptic Feedback") /// Hidden internal static let hidden = L10n.tr("Localizable", "hidden", fallback: "Hidden") + /// Hide user from login screen + internal static let hideUserFromLoginScreen = L10n.tr("Localizable", "hideUserFromLoginScreen", fallback: "Hide user from login screen") /// Home internal static let home = L10n.tr("Localizable", "home", fallback: "Home") /// Hours @@ -486,6 +520,8 @@ internal enum L10n { internal static let items = L10n.tr("Localizable", "items", fallback: "Items") /// General internal static let jellyfin = L10n.tr("Localizable", "jellyfin", fallback: "Jellyfin") + /// Join Groups + internal static let joinGroups = L10n.tr("Localizable", "joinGroups", fallback: "Join Groups") /// Jump internal static let jump = L10n.tr("Localizable", "jump", fallback: "Jump") /// Jump Backward @@ -538,10 +574,16 @@ internal enum L10n { internal static let list = L10n.tr("Localizable", "list", fallback: "List") /// Live TV internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV") + /// Live TV access + internal static let liveTvAccess = L10n.tr("Localizable", "liveTvAccess", fallback: "Live TV access") + /// Live TV recording management + internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management") /// Loading internal static let loading = L10n.tr("Localizable", "loading", fallback: "Loading") /// Local Servers internal static let localServers = L10n.tr("Localizable", "localServers", fallback: "Local Servers") + /// Locked users + internal static let lockedUsers = L10n.tr("Localizable", "lockedUsers", fallback: "Locked users") /// Login internal static let login = L10n.tr("Localizable", "login", fallback: "Login") /// Login to %@ @@ -552,12 +594,34 @@ internal enum L10n { internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs") /// Access the Jellyfin server logs for troubleshooting and monitoring purposes. internal static let logsDescription = L10n.tr("Localizable", "logsDescription", fallback: "Access the Jellyfin server logs for troubleshooting and monitoring purposes.") + /// Lyrics + internal static let lyrics = L10n.tr("Localizable", "lyrics", fallback: "Lyrics") + /// Management + internal static let management = L10n.tr("Localizable", "management", fallback: "Management") /// Option to set the maximum bitrate for playback internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate") + /// Limits the total number of connections a user can have to the server. + internal static let maximumConnectionsDescription = L10n.tr("Localizable", "maximumConnectionsDescription", fallback: "Limits the total number of connections a user can have to the server.") + /// Maximum failed login policy + internal static let maximumFailedLoginPolicy = L10n.tr("Localizable", "maximumFailedLoginPolicy", fallback: "Maximum failed login policy") + /// Sets the maximum failed login attempts before a user is locked out. + internal static let maximumFailedLoginPolicyDescription = L10n.tr("Localizable", "maximumFailedLoginPolicyDescription", fallback: "Sets the maximum failed login attempts before a user is locked out.") + /// Locked users must be re-enabled by an Administrator. + internal static let maximumFailedLoginPolicyReenable = L10n.tr("Localizable", "maximumFailedLoginPolicyReenable", fallback: "Locked users must be re-enabled by an Administrator.") + /// Maximum remote bitrate + internal static let maximumRemoteBitrate = L10n.tr("Localizable", "maximumRemoteBitrate", fallback: "Maximum remote bitrate") + /// Maximum sessions + internal static let maximumSessions = L10n.tr("Localizable", "maximumSessions", fallback: "Maximum sessions") + /// Maximum sessions policy + internal static let maximumSessionsPolicy = L10n.tr("Localizable", "maximumSessionsPolicy", fallback: "Maximum sessions policy") /// Playback May Fail 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 downloads + internal static let mediaDownloads = L10n.tr("Localizable", "mediaDownloads", fallback: "Media downloads") + /// Media playback + internal static let mediaPlayback = L10n.tr("Localizable", "mediaPlayback", fallback: "Media playback") /// Mbps internal static let megabitsPerSecond = L10n.tr("Localizable", "megabitsPerSecond", fallback: "Mbps") /// Menu Buttons @@ -690,6 +754,8 @@ internal enum L10n { internal static let pauseOnBackground = L10n.tr("Localizable", "pauseOnBackground", fallback: "Pause on background") /// People internal static let people = L10n.tr("Localizable", "people", fallback: "People") + /// Permissions + internal static let permissions = L10n.tr("Localizable", "permissions", fallback: "Permissions") /// Play internal static let play = L10n.tr("Localizable", "play", fallback: "Play") /// Play / Pause @@ -780,6 +846,10 @@ internal enum L10n { internal static let reload = L10n.tr("Localizable", "reload", fallback: "Reload") /// Remaining Time internal static let remainingTime = L10n.tr("Localizable", "remainingTime", fallback: "Remaining Time") + /// Remote connections + internal static let remoteConnections = L10n.tr("Localizable", "remoteConnections", fallback: "Remote connections") + /// Remote control + internal static let remoteControl = L10n.tr("Localizable", "remoteControl", fallback: "Remote control") /// Remove internal static let remove = L10n.tr("Localizable", "remove", fallback: "Remove") /// Remove All @@ -912,6 +982,8 @@ internal enum L10n { internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: "Server URL") /// The title for the session view internal static let session = L10n.tr("Localizable", "session", fallback: "Session") + /// Sessions + internal static let sessions = L10n.tr("Localizable", "sessions", fallback: "Sessions") /// Settings internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings") /// Show Cast & Crew @@ -1014,6 +1086,8 @@ internal enum L10n { internal static let supportsSync = L10n.tr("Localizable", "supportsSync", fallback: "Sync") /// Switch User internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User") + /// SyncPlay + internal static let syncPlay = L10n.tr("Localizable", "syncPlay", fallback: "SyncPlay") /// Represents the system theme setting internal static let system = L10n.tr("Localizable", "system", fallback: "System") /// System Control Gestures Enabled @@ -1096,6 +1170,12 @@ internal enum L10n { internal static let unknownError = L10n.tr("Localizable", "unknownError", fallback: "Unknown Error") /// TranscodeReason - Unknown Video Stream Info internal static let unknownVideoStreamInfo = L10n.tr("Localizable", "unknownVideoStreamInfo", fallback: "The video stream information is unknown") + /// Unlimited + internal static let unlimited = L10n.tr("Localizable", "unlimited", fallback: "Unlimited") + /// The user can connect to the server without any limits. + internal static let unlimitedConnectionsDescription = L10n.tr("Localizable", "unlimitedConnectionsDescription", fallback: "The user can connect to the server without any limits.") + /// Allows unlimited failed login attempts without locking the user. + internal static let unlimitedFailedLoginDescription = L10n.tr("Localizable", "unlimitedFailedLoginDescription", fallback: "Allows unlimited failed login attempts without locking the user.") /// Unplayed internal static let unplayed = L10n.tr("Localizable", "unplayed", fallback: "Unplayed") /// You have unsaved changes. Are you sure you want to discard them? @@ -1142,8 +1222,12 @@ internal enum L10n { internal static let videoProfileNotSupported = L10n.tr("Localizable", "videoProfileNotSupported", fallback: "The video profile is not supported") /// TranscodeReason - Video Range Type Not Supported internal static let videoRangeTypeNotSupported = L10n.tr("Localizable", "videoRangeTypeNotSupported", fallback: "The video range type is not supported") + /// Video remuxing + internal static let videoRemuxing = L10n.tr("Localizable", "videoRemuxing", fallback: "Video remuxing") /// TranscodeReason - Video Resolution Not Supported internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported") + /// Video transcoding + internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding") /// Weekly internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly") /// Who's watching? diff --git a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift index 57361de5..61d956e7 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift @@ -16,13 +16,8 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl // MARK: Event enum Event { - case success - } - - // MARK: BackgroundState - - enum BackgroundState { - case updating + case error(JellyfinAPIError) + case updated } // MARK: Action @@ -30,35 +25,42 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl enum Action: Equatable { case cancel case loadDetails - case resetPassword - case updatePassword(password: String) - case updatePolicy(policy: UserPolicy) - case updateConfiguration(configuration: UserConfiguration) + case updatePolicy(UserPolicy) + case updateConfiguration(UserConfiguration) + case updateUsername(String) + } + + // MARK: Background State + + enum BackgroundState: Hashable { + case updating } // MARK: State enum State: Hashable { - case error(JellyfinAPIError) case initial + case content + case updating + case error(JellyfinAPIError) } // MARK: Published Values + @Published + final var state: State = .initial + @Published + final var backgroundStates: OrderedSet = [] + @Published + private(set) var user: UserDto + var events: AnyPublisher { eventSubject .receive(on: RunLoop.main) .eraseToAnyPublisher() } - @Published - final var backgroundStates: OrderedSet = [] - @Published - final var state: State = .initial - @Published - private(set) var user: UserDto - - private var resetTask: AnyCancellable? + private var userTask: AnyCancellable? private var eventSubject: PassthroughSubject = .init() // MARK: Initialize from UserDto @@ -72,137 +74,76 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl func respond(to action: Action) -> State { switch action { case .cancel: - resetTask?.cancel() + userTask?.cancel() return .initial - case .resetPassword: - resetTask = Task { + case .loadDetails: + return performAction { + try await self.loadDetails() + } + + case let .updatePolicy(policy): + 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 { await MainActor.run { _ = self.backgroundStates.append(.updating) } - do { - try await resetPassword() - await MainActor.run { - self.state = .initial - self.eventSubject.send(.success) - } - } catch { - await MainActor.run { - let jellyfinError = JellyfinAPIError(error.localizedDescription) - self.state = .error(jellyfinError) - } + try await action() + + await MainActor.run { + self.state = .content + self.eventSubject.send(.updated) } await MainActor.run { _ = self.backgroundStates.remove(.updating) } - } - .asAnyCancellable() - - return .initial - - case .loadDetails: - resetTask = Task { - do { - try await loadDetails() - await MainActor.run { - self.state = .initial - self.eventSubject.send(.success) - } - } catch { - await MainActor.run { - let jellyfinError = JellyfinAPIError(error.localizedDescription) - self.state = .error(jellyfinError) - } + } catch { + let jellyfinError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(jellyfinError) + self.backgroundStates.remove(.updating) + self.eventSubject.send(.error(jellyfinError)) } } - .asAnyCancellable() - - return .initial - - case let .updatePassword(password): - resetTask = Task { - do { - try await updatePassword(password: password) - await MainActor.run { - self.state = .initial - self.eventSubject.send(.success) - } - } catch { - await MainActor.run { - let jellyfinError = JellyfinAPIError(error.localizedDescription) - self.state = .error(jellyfinError) - } - } - } - .asAnyCancellable() - - return .initial - - case let .updatePolicy(policy): - resetTask = Task { - do { - try await updatePolicy(policy: policy) - await MainActor.run { - self.state = .initial - self.eventSubject.send(.success) - } - } catch { - await MainActor.run { - let jellyfinError = JellyfinAPIError(error.localizedDescription) - self.state = .error(jellyfinError) - } - } - } - .asAnyCancellable() - - return .initial - - case let .updateConfiguration(configuration): - resetTask = Task { - do { - try await updateConfiguration(configuration: configuration) - await MainActor.run { - self.state = .initial - self.eventSubject.send(.success) - } - } catch { - await MainActor.run { - let jellyfinError = JellyfinAPIError(error.localizedDescription) - self.state = .error(jellyfinError) - } - } - } - .asAnyCancellable() - - return .initial } + .asAnyCancellable() + + return .updating } - // MARK: - Reset Password + // MARK: - Load User - private func resetPassword() async throws { - guard let userId = user.id else { return } - let parameters = UpdateUserPassword(isResetPassword: true) - let request = Paths.updateUserPassword(userID: userId, parameters) - try await userSession.client.send(request) - - await MainActor.run { - self.user.hasPassword = false - } - } - - // MARK: - Update Password - - private func updatePassword(password: String) async throws { + private func loadDetails() async throws { guard let userID = user.id else { return } - let parameters = UpdateUserPassword(newPw: password) - let request = Paths.updateUserPassword(userID: userID, parameters) - try await userSession.client.send(request) + let request = Paths.getUserByID(userID: userID) + let response = try await userSession.client.send(request) await MainActor.run { - self.user.hasPassword = (password != "") + self.user = response.value + self.state = .content } } @@ -230,15 +171,18 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl } } - // MARK: - Load User + // MARK: - Update User Name - private func loadDetails() async throws { + private func updateUsername(username: String) async throws { guard let userID = user.id else { return } - let request = Paths.getUserByID(userID: userID) - let response = try await userSession.client.send(request) + var updatedUser = user + updatedUser.name = username + + let request = Paths.updateUser(userID: userID, updatedUser) + try await userSession.client.send(request) await MainActor.run { - self.user = response.value + self.user.name = username } } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2065061e..fc719264 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0195E32CE04678007844F4 /* ItemSection.swift */; }; 4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; + 4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */; }; 4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; }; 4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; }; 4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10C8102CC030C90012CC9F /* DeviceDetailsView.swift */; }; @@ -59,6 +60,20 @@ 4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; + 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; + 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */; }; + 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */; }; + 4E49DED02CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */; }; + 4E49DED22CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */; }; + 4E49DED32CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */; }; + 4E49DED52CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; + 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; + 4E49DED82CE5509300352DCD /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED72CE5509000352DCD /* StatusSection.swift */; }; + 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */; }; + 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */; }; + 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; + 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; + 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; @@ -102,6 +117,12 @@ 4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */; }; 4EB4ECE32CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; }; 4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; }; + 4EB538B52CE3C77200EB72D5 /* ServerUserPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */; }; + 4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */; }; + 4EB538C12CE3CF0F00EB72D5 /* ManagementSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */; }; + 4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */; }; + 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */; }; + 4EB538C82CE3E8A600EB72D5 /* RemoteControlSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */; }; 4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; 4EB7C8D52CCED6E7000CC011 /* AddServerUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7C8D42CCED6E1000CC011 /* AddServerUserView.swift */; }; 4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; }; @@ -350,6 +371,8 @@ E10B1ECE2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1ECC2BD9AFD800A92EAF /* SwiftfinStore+V2.swift */; }; E10B1ED02BD9AFF200A92EAF /* V2UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1ECF2BD9AFF200A92EAF /* V2UserModel.swift */; }; E10B1ED12BD9AFF200A92EAF /* V2UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1ECF2BD9AFF200A92EAF /* V2UserModel.swift */; }; + E10E67B62CF515130095365B /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E67B52CF515130095365B /* Binding.swift */; }; + E10E67B72CF515130095365B /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E67B52CF515130095365B /* Binding.swift */; }; E10E842A29A587110064EA49 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842929A587110064EA49 /* LoadingView.swift */; }; E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842B29A589860064EA49 /* NonePosterButton.swift */; }; E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSize.swift */; }; @@ -522,10 +545,7 @@ E14A08CB28E6831D004FC984 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; }; E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */; }; E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */; }; - E14EA15E2BF6F72900DE757A /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EA15D2BF6F72900DE757A /* PhotoPicker.swift */; }; - E14EA1602BF6FF8900DE757A /* UserProfileImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EA15F2BF6FF8900DE757A /* UserProfileImagePicker.swift */; }; E14EA1652BF70A8E00DE757A /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = E14EA1642BF70A8E00DE757A /* Mantis */; }; - E14EA1672BF70F9C00DE757A /* SquareImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EA1662BF70F9C00DE757A /* SquareImageCropView.swift */; }; E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EA1682BF7330A00DE757A /* UserProfileImageViewModel.swift */; }; E14EA16A2BF7333B00DE757A /* UserProfileImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EA1682BF7330A00DE757A /* UserProfileImageViewModel.swift */; }; E14EDEC52B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */; }; @@ -545,7 +565,6 @@ E152107C2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E152107B2947ACA000375CC2 /* InvertedLightAppIcon.swift */; }; E152107D2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E152107B2947ACA000375CC2 /* InvertedLightAppIcon.swift */; }; E1523F822B132C350062821A /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1523F812B132C350062821A /* CollectionHStack */; }; - E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */; }; E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546776289AF46E00087E35 /* CollectionItemView.swift */; }; E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; }; E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549655296CA2EF00C4EF88 /* DownloadTask.swift */; }; @@ -1077,6 +1096,7 @@ /* Begin PBXFileReference section */ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; 4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = ""; }; + 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = ""; }; 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCompletionStatus.swift; sourceTree = ""; }; 4E10C8102CC030C90012CC9F /* DeviceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetailsView.swift; sourceTree = ""; }; 4E10C8162CC045530012CC9F /* CompatibilitiesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibilitiesSection.swift; sourceTree = ""; }; @@ -1112,6 +1132,16 @@ 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = ""; }; 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = ""; }; 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = ""; }; + 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; + 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = ""; }; + 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxBitratePolicy.swift; sourceTree = ""; }; + 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsPolicy.swift; sourceTree = ""; }; + 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFailurePolicy.swift; sourceTree = ""; }; + 4E49DED72CE5509000352DCD /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; + 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; + 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = ""; }; + 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlayUserAccessType.swift; sourceTree = ""; }; + 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = ""; }; 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; @@ -1147,6 +1177,12 @@ 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = ""; }; 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; + 4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserPermissionsView.swift; sourceTree = ""; }; + 4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaybackSection.swift; sourceTree = ""; }; + 4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementSection.swift; sourceTree = ""; }; + 4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlaySection.swift; sourceTree = ""; }; + 4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAccessSection.swift; sourceTree = ""; }; + 4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteControlSection.swift; sourceTree = ""; }; 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.swift; sourceTree = ""; }; 4EB7C8D42CCED6E1000CC011 /* AddServerUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserView.swift; sourceTree = ""; }; 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCompatibility.swift; sourceTree = ""; }; @@ -1352,6 +1388,7 @@ E10B1EC92BD9AF8200A92EAF /* SwiftfinStore+V1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+V1.swift"; sourceTree = ""; }; E10B1ECC2BD9AFD800A92EAF /* SwiftfinStore+V2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+V2.swift"; sourceTree = ""; }; E10B1ECF2BD9AFF200A92EAF /* V2UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2UserModel.swift; sourceTree = ""; }; + E10E67B52CF515130095365B /* Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = ""; }; E10E842929A587110064EA49 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; E10E842B29A589860064EA49 /* NonePosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonePosterButton.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = ""; }; @@ -1462,9 +1499,6 @@ E149CCAC2BE6ECC8008B9331 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = ""; }; E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLetter.swift; sourceTree = ""; }; - E14EA15D2BF6F72900DE757A /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; - E14EA15F2BF6FF8900DE757A /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = ""; }; - E14EA1662BF70F9C00DE757A /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = ""; }; E14EA1682BF7330A00DE757A /* UserProfileImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageViewModel.swift; sourceTree = ""; }; E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyItemFilter.swift; sourceTree = ""; }; E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFilterType.swift; sourceTree = ""; }; @@ -1472,7 +1506,6 @@ E150C0B92BFD44F500944FFA /* ImagePipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipeline.swift; sourceTree = ""; }; E150C0BC2BFD45BD00944FFA /* RedrawOnNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnNotificationView.swift; sourceTree = SOURCE_ROOT; }; E152107B2947ACA000375CC2 /* InvertedLightAppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedLightAppIcon.swift; sourceTree = ""; }; - E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = ""; }; E1546776289AF46E00087E35 /* CollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = ""; }; E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = ""; }; E1549655296CA2EF00C4EF88 /* DownloadTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadTask.swift; sourceTree = ""; }; @@ -2040,6 +2073,24 @@ path = PlaybackBitrate; sourceTree = ""; }; + 4E49DEDE2CE55F7F00352DCD /* Components */ = { + isa = PBXGroup; + children = ( + 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */, + 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4E49DEDF2CE55F7F00352DCD /* UserProfileImagePicker */ = { + isa = PBXGroup; + children = ( + 4E49DEDE2CE55F7F00352DCD /* Components */, + 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */, + ); + path = UserProfileImagePicker; + sourceTree = ""; + }; 4E5334A02CD1A27C00D59FA8 /* ActionButtons */ = { isa = PBXGroup; children = ( @@ -2055,17 +2106,18 @@ children = ( 4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */, 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */, - 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */, - 4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */, 4EB7C8D32CCED318000CC011 /* AddServerUserView */, 4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */, + 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */, + 4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */, E1DE64902CC6F06C00E423B6 /* Components */, 4E10C80F2CC030B20012CC9F /* DeviceDetailsView */, 4EED87492CBF824B002354D2 /* DevicesView */, 4E90F7622CC72B1F00417C31 /* EditServerTaskView */, - 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, 4E35CE622CBED3FF00DBD886 /* ServerLogsView */, + 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, + 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, ); path = AdminDashboardView; @@ -2250,6 +2302,38 @@ path = Components; sourceTree = ""; }; + 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */ = { + isa = PBXGroup; + children = ( + 4EB538B82CE3CB2400EB72D5 /* Components */, + 4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */, + ); + path = ServerUserPermissionsView; + sourceTree = ""; + }; + 4EB538B82CE3CB2400EB72D5 /* Components */ = { + isa = PBXGroup; + children = ( + 4EB538B92CE3CB2900EB72D5 /* Sections */, + ); + path = Components; + sourceTree = ""; + }; + 4EB538B92CE3CB2900EB72D5 /* Sections */ = { + isa = PBXGroup; + children = ( + 4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */, + 4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */, + 4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */, + 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */, + 4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */, + 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */, + 4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */, + 4E49DED72CE5509000352DCD /* StatusSection.swift */, + ); + path = Sections; + sourceTree = ""; + }; 4EB7C8D32CCED318000CC011 /* AddServerUserView */ = { isa = PBXGroup; children = ( @@ -2321,7 +2405,7 @@ 4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */ = { isa = PBXGroup; children = ( - E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */, + 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */, ); path = ResetUserPasswordView; sourceTree = ""; @@ -2790,6 +2874,7 @@ isa = PBXGroup; children = ( E1E1644028BB301900323B0A /* Array.swift */, + E10E67B52CF515130095365B /* Binding.swift */, E1E6C44F29B104840064123F /* Button.swift */, E1C8CE5A28FE512400DF5D7B /* CGPoint.swift */, E10EAA4E277BBCC4000269ED /* CGSize.swift */, @@ -3388,24 +3473,6 @@ path = StoredValue; sourceTree = ""; }; - E14EA1612BF6FF8D00DE757A /* UserProfileImagePicker */ = { - isa = PBXGroup; - children = ( - E14EA1622BF7008A00DE757A /* Components */, - E14EA15F2BF6FF8900DE757A /* UserProfileImagePicker.swift */, - ); - path = UserProfileImagePicker; - sourceTree = ""; - }; - E14EA1622BF7008A00DE757A /* Components */ = { - isa = PBXGroup; - children = ( - E14EA15D2BF6F72900DE757A /* PhotoPicker.swift */, - E14EA1662BF70F9C00DE757A /* SquareImageCropView.swift */, - ); - path = Components; - sourceTree = ""; - }; E14EDECA2B8FB66F000F00A4 /* ItemFilter */ = { isa = PBXGroup; children = ( @@ -3446,8 +3513,8 @@ isa = PBXGroup; children = ( 6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */, + 4E49DEDF2CE55F7F00352DCD /* UserProfileImagePicker */, E1EA09872BEE9CF3004CDE76 /* UserLocalSecurityView.swift */, - E14EA1612BF6FF8D00DE757A /* UserProfileImagePicker */, E1BE1CEF2BDB6C97008176A9 /* UserProfileSettingsView.swift */, ); path = UserProfileSettingsView; @@ -3892,6 +3959,7 @@ E1AD105226D96D5F003E4A08 /* JellyfinAPI */ = { isa = PBXGroup; children = ( + 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */, E1D37F5B2B9CF02600343D2B /* BaseItemDto */, E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */, E1002B632793CEE700E47059 /* ChapterInfo.swift */, @@ -3907,6 +3975,8 @@ E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E12A9EF729499E0100731C3A /* JellyfinClient.swift */, + 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */, + 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */, E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */, E122A9122788EAAD0060FA63 /* MediaStream.swift */, E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */, @@ -3917,6 +3987,7 @@ E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */, E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */, E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */, + 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */, 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */, 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */, 4E35CE632CBED69600DBD886 /* TaskTriggerType.swift */, @@ -4624,6 +4695,7 @@ E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E18E021E2887492B0022598C /* RowDivider.swift in Sources */, + 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */, 4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */, E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, 4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */, @@ -4692,6 +4764,7 @@ E102314E2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */, E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */, E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */, + 4E49DED22CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */, E1763A762BF3FF01004DF6AB /* AppLoadingView.swift in Sources */, E18121062CBE428000682985 /* ChevronButton.swift in Sources */, E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */, @@ -4699,6 +4772,7 @@ C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1575E95293E7B1E001665B1 /* Font.swift in Sources */, + 4E49DED02CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, 4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, @@ -4743,6 +4817,7 @@ E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */, E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */, + 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */, E1575E93293E7B1E001665B1 /* Double.swift in Sources */, E1B5784228F8AFCB00D42911 /* WrappedView.swift in Sources */, @@ -4922,6 +4997,7 @@ 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E1575E86293E7A00001665B1 /* AppIcons.swift in Sources */, E1AEFA382BE36C4900CFAFD8 /* SwiftinStore+UserState.swift in Sources */, + E10E67B62CF515130095365B /* Binding.swift in Sources */, E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, 4E8F74B12CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */, @@ -5053,7 +5129,9 @@ 4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */, E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */, 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */, + 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */, + 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */, E1AEFA372BE317E200CFAFD8 /* ListRowButton.swift in Sources */, 4EB7C8D52CCED6E7000CC011 /* AddServerUserView.swift in Sources */, E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, @@ -5065,8 +5143,10 @@ E11895AF2893840F0042947B /* NavigationBarOffsetView.swift in Sources */, E18E0208288749200022598C /* BlurView.swift in Sources */, E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */, + 4E49DED82CE5509300352DCD /* StatusSection.swift in Sources */, E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */, C46DD8D22A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */, + 4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */, E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */, E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, @@ -5101,6 +5181,7 @@ E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */, E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */, + 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */, E133328829538D8D00EE76AB /* Files.swift in Sources */, @@ -5164,6 +5245,7 @@ E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */, E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */, + 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, E18ACA922A15A32F00BB4F35 /* (null) in Sources */, E1A3E4C92BB74EA3005C59F8 /* LoadingCard.swift in Sources */, 4E71D6892C80910900A0174D /* EditCustomDeviceProfileView.swift in Sources */, @@ -5173,6 +5255,7 @@ 4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */, E139CC1D28EC836F00688DE2 /* ChapterOverlay.swift in Sources */, E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */, + 4EB538C12CE3CF0F00EB72D5 /* ManagementSection.swift in Sources */, E10B1EB42BD9803100A92EAF /* UserRow.swift in Sources */, E1E6C45029B104840064123F /* Button.swift in Sources */, 4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */, @@ -5193,6 +5276,7 @@ E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */, E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, + E10E67B72CF515130095365B /* Binding.swift in Sources */, E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */, E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */, E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */, @@ -5210,6 +5294,8 @@ E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */, E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, + 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */, + 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */, 4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */, 4E90F7652CC72B1F00417C31 /* EditServerTaskView.swift in Sources */, 4E90F7662CC72B1F00417C31 /* LastErrorSection.swift in Sources */, @@ -5331,12 +5417,12 @@ E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */, 4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */, 4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */, + 4E49DED32CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */, 4E35CE5E2CBED3F300DBD886 /* AddTaskTriggerView.swift in Sources */, 4E35CE5F2CBED3F300DBD886 /* IntervalRow.swift in Sources */, 4E35CE602CBED3F300DBD886 /* DayOfWeekRow.swift in Sources */, 4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, - E14EA1602BF6FF8900DE757A /* UserProfileImagePicker.swift in Sources */, 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */, @@ -5344,15 +5430,18 @@ E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */, E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, + 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */, E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */, 4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */, 4E35CE692CBED95F00DBD886 /* DayOfWeek.swift in Sources */, E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */, + 4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */, 4E10C8192CC045700012CC9F /* CustomDeviceNameSection.swift in Sources */, + 4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */, 4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */, E1366A222C826DA700A36DED /* EditCustomDeviceProfileCoordinator.swift in Sources */, E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */, @@ -5381,12 +5470,12 @@ E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */, + 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */, 4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */, E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */, E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */, BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, - E14EA1672BF70F9C00DE757A /* SquareImageCropView.swift in Sources */, E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */, BD3957772C112AD30078CEF8 /* SliderSection.swift in Sources */, E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */, @@ -5436,7 +5525,6 @@ E145EB422BE0A6EE003BF6F3 /* ServerSelectionMenu.swift in Sources */, 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */, E1DE64922CC6F0C900E423B6 /* DeviceSection.swift in Sources */, - E14EA15E2BF6F72900DE757A /* PhotoPicker.swift in Sources */, E19F6C5D28F5189300C5197E /* MediaStreamInfoView.swift in Sources */, E1D8429329340B8300D1041A /* Utilities.swift in Sources */, E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */, @@ -5448,10 +5536,10 @@ E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */, E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */, 4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */, - E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */, E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */, E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */, 4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */, + 4E49DED52CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */, E10B1EC72BD9AF6100A92EAF /* V2ServerModel.swift in Sources */, E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */, 4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */, @@ -5497,6 +5585,7 @@ E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */, E1E6C44029AECC6D0064123F /* ActionButtons.swift in Sources */, E103DF902BCF2F1C000229B2 /* MediaItem.swift in Sources */, + 4EB538B52CE3C77200EB72D5 /* ServerUserPermissionsView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, E1E2F83F2B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */, E15D4F072B1B12C300442DB8 /* Backport.swift in Sources */, @@ -5526,6 +5615,7 @@ 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */, E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */, 5377CBF5263B596A003A4E83 /* SwiftfinApp.swift in Sources */, + 4EB538C82CE3E8A600EB72D5 /* RemoteControlSection.swift in Sources */, E13DD4022717EE79009D4DAF /* SelectUserCoordinator.swift in Sources */, E11245B128D919CD00D8A977 /* Overlay.swift in Sources */, E145EB4D2BE1688E003BF6F3 /* SwiftinStore+UserState.swift in Sources */, diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift index ec55aec7..6d390cbb 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift @@ -68,7 +68,7 @@ struct ActiveSessionsView: View { } } .animation(.linear(duration: 0.2), value: viewModel.state) - .navigationTitle(L10n.activeDevices) + .navigationTitle(L10n.sessions) .onFirstAppear { viewModel.send(.refreshSessions) } diff --git a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift index ee37153b..93d67c70 100644 --- a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift +++ b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift @@ -23,7 +23,7 @@ struct AdminDashboardView: View { description: L10n.dashboardDescription ) - ChevronButton(L10n.activeDevices) + ChevronButton(L10n.sessions) .onSelect { router.route(to: \.activeSessions) } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 4a9367c1..123dd481 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -34,7 +34,9 @@ struct ServerUserDetailsView: View { AdminDashboardView.UserSection( user: viewModel.user, lastActivityDate: viewModel.user.lastActivityDate - ) + ) { + // TODO: Update Profile Picture & Username + } Section(L10n.advanced) { if let userId = viewModel.user.id { @@ -43,6 +45,19 @@ struct ServerUserDetailsView: View { router.route(to: \.resetUserPassword, userId) } } + + ChevronButton(L10n.permissions) + .onSelect { + router.route(to: \.userPermissions, viewModel) + } + + // TODO: Access: enabledFolders & enableAllFolders + + // TODO: Deletion: enableContentDeletion & enableContentDeletionFromFolders + + // TODO: Parental: accessSchedules, maxParentalRating, blockUnratedItems, blockedTags, blockUnratedItems & blockedMediaFolders + + // TODO: Live TV: enabledChannels & enableAllChannels } } .navigationTitle(L10n.user) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift new file mode 100644 index 00000000..099da752 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift @@ -0,0 +1,66 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct ExternalAccessSection: View { + + @Binding + var policy: UserPolicy + + // MARK: - Body + + var body: some View { + Section(L10n.remoteConnections) { + + Toggle( + L10n.remoteConnections, + isOn: $policy.enableRemoteAccess.coalesce(false) + ) + + CaseIterablePicker( + L10n.maximumRemoteBitrate, + selection: $policy.remoteClientBitrateLimit.map( + getter: { MaxBitratePolicy(rawValue: $0) ?? .custom }, + setter: { $0.rawValue } + ) + ) + + if policy.remoteClientBitrateLimit != MaxBitratePolicy.unlimited.rawValue { + ChevronAlertButton( + L10n.customBitrate, + subtitle: Text(policy.remoteClientBitrateLimit ?? 0, format: .bitRate), + description: L10n.enterCustomBitrate + ) { + MaxBitrateInput() + } + } + } + } + + // MARK: - Create Bitrate Input + + @ViewBuilder + private func MaxBitrateInput() -> some View { + let bitrateBinding = $policy.remoteClientBitrateLimit + .coalesce(0) + .map( + // Convert to Mbps + getter: { Double($0) / 1_000_000 }, + setter: { Int($0 * 1_000_000) } + ) + .min(0.001) // Minimum bitrate of 1 Kbps + + TextField(L10n.maximumBitrate, value: bitrateBinding, format: .number) + .keyboardType(.numbersAndPunctuation) + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift new file mode 100644 index 00000000..dea82fd3 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift @@ -0,0 +1,46 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct ManagementSection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.management) { + + Toggle( + L10n.administrator, + isOn: $policy.isAdministrator.coalesce(false) + ) + + // TODO: Enable for 10.9 + /* Toggle(L10n.collections, isOn: Binding( + get: { policy.enableCollectionManagement ?? false }, + set: { policy.enableCollectionManagement = $0 } + )) + + Toggle(L10n.subtitles, isOn: Binding( + get: { policy.enableSubtitleManagement ?? false }, + set: { policy.enableSubtitleManagement = $0 } + )) */ + + // TODO: Enable for 10.10 + /* Toggle(L10n.lyrics, isOn: Binding( + get: { policy.enableLyricManagement ?? false }, + set: { policy.enableLyricManagement = $0 } + )) */ + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift new file mode 100644 index 00000000..4efc03ae --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift @@ -0,0 +1,49 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct MediaPlaybackSection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.mediaPlayback) { + + Toggle( + L10n.mediaPlayback, + isOn: $policy.enableMediaPlayback.coalesce(false) + ) + + Toggle( + L10n.audioTranscoding, + isOn: $policy.enableAudioPlaybackTranscoding.coalesce(false) + ) + + Toggle( + L10n.videoTranscoding, + isOn: $policy.enableVideoPlaybackTranscoding.coalesce(false) + ) + + Toggle( + L10n.videoRemuxing, + isOn: $policy.enablePlaybackRemuxing.coalesce(false) + ) + + Toggle( + L10n.forceRemoteTranscoding, + isOn: $policy.isForceRemoteSourceTranscoding.coalesce(false) + ) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift new file mode 100644 index 00000000..39fe395b --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift @@ -0,0 +1,34 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct PermissionSection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.permissions) { + + Toggle( + L10n.mediaDownloads, + isOn: $policy.enableContentDownloading.coalesce(false) + ) + + Toggle( + L10n.hideUserFromLoginScreen, + isOn: $policy.isHidden.coalesce(false) + ) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift new file mode 100644 index 00000000..99d75e22 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift @@ -0,0 +1,34 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct RemoteControlSection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.remoteControl) { + + Toggle( + L10n.controlOtherUsers, + isOn: $policy.enableRemoteControlOfOtherUsers.coalesce(false) + ) + + Toggle( + L10n.controlSharedDevices, + isOn: $policy.enableSharedDeviceControl.coalesce(false) + ) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift new file mode 100644 index 00000000..86760e46 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift @@ -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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct SessionsSection: View { + + @Binding + var policy: UserPolicy + + // MARK: - Body + + var body: some View { + FailedLoginsView + MaxSessionsView + } + + // MARK: - Failed Login Selection View + + @ViewBuilder + private var FailedLoginsView: some View { + Section { + CaseIterablePicker( + L10n.maximumFailedLoginPolicy, + selection: $policy.loginAttemptsBeforeLockout + .coalesce(0) + .map( + getter: { LoginFailurePolicy(rawValue: $0) ?? .custom }, + setter: { $0.rawValue } + ) + ) + + if let loginAttempts = policy.loginAttemptsBeforeLockout, loginAttempts > 0 { + MaxFailedLoginsButton() + } + + } header: { + Text(L10n.sessions) + } footer: { + VStack(alignment: .leading) { + Text(L10n.maximumFailedLoginPolicyDescription) + + LearnMoreButton(L10n.maximumFailedLoginPolicy) { + TextPair( + title: L10n.lockedUsers, + subtitle: L10n.maximumFailedLoginPolicyReenable + ) + TextPair( + title: L10n.unlimited, + subtitle: L10n.unlimitedFailedLoginDescription + ) + TextPair( + title: L10n.default, + subtitle: L10n.defaultFailedLoginDescription + ) + TextPair( + title: L10n.custom, + subtitle: L10n.customFailedLoginDescription + ) + } + } + } + } + + // MARK: - Failed Login Selection Button + + @ViewBuilder + private func MaxFailedLoginsButton() -> some View { + ChevronAlertButton( + L10n.customFailedLogins, + subtitle: Text(policy.loginAttemptsBeforeLockout ?? 1, format: .number), + description: L10n.enterCustomFailedLogins + ) { + TextField( + L10n.failedLogins, + value: $policy.loginAttemptsBeforeLockout + .coalesce(1) + .clamp(min: 1, max: 1000), + format: .number + ) + .keyboardType(.numberPad) + } + } + + // MARK: - Failed Login Validation + + @ViewBuilder + private var MaxSessionsView: some View { + Section { + CaseIterablePicker( + L10n.maximumSessionsPolicy, + selection: $policy.maxActiveSessions.map( + getter: { ActiveSessionsPolicy(rawValue: $0) ?? .custom }, + setter: { $0.rawValue } + ) + ) + + if policy.maxActiveSessions != ActiveSessionsPolicy.unlimited.rawValue { + MaxSessionsButton() + } + + } footer: { + VStack(alignment: .leading) { + Text(L10n.maximumConnectionsDescription) + + LearnMoreButton(L10n.maximumSessionsPolicy) { + TextPair( + title: L10n.unlimited, + subtitle: L10n.unlimitedConnectionsDescription + ) + TextPair( + title: L10n.custom, + subtitle: L10n.customConnectionsDescription + ) + } + } + } + } + + @ViewBuilder + private func MaxSessionsButton() -> some View { + ChevronAlertButton( + L10n.customSessions, + subtitle: Text(policy.maxActiveSessions ?? 1, format: .number), + description: L10n.enterCustomMaxSessions + ) { + TextField( + L10n.maximumSessions, + value: $policy.maxActiveSessions + .coalesce(1) + .clamp(min: 1, max: 1000), + format: .number + ) + .keyboardType(.numberPad) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift new file mode 100644 index 00000000..0e045ed3 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift @@ -0,0 +1,29 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct StatusSection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.status) { + + Toggle(L10n.active, isOn: Binding( + get: { !(policy.isDisabled ?? false) }, + set: { policy.isDisabled = !$0 } + )) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift new file mode 100644 index 00000000..8db9c1e9 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift @@ -0,0 +1,29 @@ +// +// 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 JellyfinAPI +import SwiftUI + +extension ServerUserPermissionsView { + + struct SyncPlaySection: View { + + @Binding + var policy: UserPolicy + + var body: some View { + Section(L10n.syncPlay) { + + CaseIterablePicker( + L10n.permissions, + selection: $policy.syncPlayAccess.coalesce(.none) + ) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift new file mode 100644 index 00000000..4dd6df05 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift @@ -0,0 +1,121 @@ +// +// 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 Defaults +import JellyfinAPI +import SwiftUI + +struct ServerUserPermissionsView: View { + + // MARK: - Environment + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + // MARK: - ViewModel + + @ObservedObject + 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 = ObservedObject(wrappedValue: viewModel) + self.tempPolicy = viewModel.user.policy ?? UserPolicy() + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.permissions) + .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) + } + } + + // MARK: - Content View + + @ViewBuilder + var contentView: some View { + switch viewModel.state { + case let .error(error): + ErrorView(error: error) + case .initial: + ErrorView(error: JellyfinAPIError("Loading user failed")) + default: + permissionsListView + } + } + + // MARK: - Permissions List View + + @ViewBuilder + var permissionsListView: some View { + List { + StatusSection(policy: $tempPolicy) + + ManagementSection(policy: $tempPolicy) + + MediaPlaybackSection(policy: $tempPolicy) + + ExternalAccessSection(policy: $tempPolicy) + + SyncPlaySection(policy: $tempPolicy) + + RemoteControlSection(policy: $tempPolicy) + + PermissionSection(policy: $tempPolicy) + + SessionsSection(policy: $tempPolicy) + } + } +} diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index b46b2794..27eaf958 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -547,9 +547,6 @@ /* Restart Warning Label */ "restartWarning" = "Are you sure you want to restart the server?"; -/* ActiveSessionsView Header */ -"activeDevices" = "Active Devices"; - /* UserDashboardView Header */ "dashboard" = "Dashboard"; @@ -1129,7 +1126,6 @@ // Appears in the views with eventful to indicate a task did not fail "success" = "Success"; - // Trigger Already Exists - // Message to indicate that a Task Trigger already exists // Appears in AddServerTask when there is an existing task with the same configuration @@ -1210,6 +1206,201 @@ // Used as the button label in the options menu when there are users to edit "editUsers" = "Edit Users"; +// Bits Per Second - Unit +// Represents a speed in bits per second +// Used for bandwidth display +"bitsPerSecond" = "bps"; + +// Kilobits Per Second - Unit +// Represents a speed in kilobits per second +// Used for bandwidth display +"kilobitsPerSecond" = "kbps"; + +// Megabits Per Second - Unit +// Represents a speed in megabits per second +// Used for bandwidth display +"megabitsPerSecond" = "Mbps"; + +// Gigabits Per Second - Unit +// Represents a speed in gigabits per second +// Used for bandwidth display +"gigabitsPerSecond" = "Gbps"; + +// Terabits Per Second - Unit +// Represents a speed in terabits per second +// Used for bandwidth display +"terabitsPerSecond" = "Tbps"; + +// Default - Setting +// Represents the default policy or limit +// Used for setting user policies to default values +"default" = "Default"; + +// Unlimited - Setting +// Represents no restriction or unlimited policy +// Used for setting user policies with no limits +"unlimited" = "Unlimited"; + +// Create & Join Groups - Action +// Allows the user to create and join groups +// Used for setting user permissions related to groups +"createAndJoinGroups" = "Create & Join Groups"; + +// Join Groups - Action +// Allows the user to join existing groups +// Used for setting user permissions related to group joining +"joinGroups" = "Join Groups"; + +// Permissions - Section +// Represents access control settings for users +// Used for managing user permissions in various sections +"permissions" = "Permissions"; + +// SyncPlay - Feature +// Represents the synchronized playback feature across multiple devices +// Used for enabling or managing synchronized streaming sessions +"syncPlay" = "SyncPlay"; + +// Remote connections - Section & Toggle +// Represents settings related to remote access +// Used in the external access section of user permissions +"remoteConnections" = "Remote connections"; + +// Maximum remote bitrate - Picker +// Represents the maximum bitrate allowed for remote connections +// Used in the external access section +"maximumRemoteBitrate" = "Maximum remote bitrate"; + +// Custom bitrate - Button +// Opens an alert to enter a custom bitrate value +// Used in the external access section +"customBitrate" = "Custom bitrate"; + +// Enter custom bitrate in Mbps - Description +// Describes the purpose of the custom bitrate entry +// Used in the custom bitrate alert +"enterCustomBitrate" = "Enter custom bitrate in Mbps"; + +// Feature access - Section +// Represents settings related to feature access for users +// Used in the feature access section of user permissions +"featureAccess" = "Feature access"; + +// Live TV access - Toggle +// Toggles access to live TV content +// Used in the feature access section +"liveTvAccess" = "Live TV access"; + +// Live TV recording management - Toggle +// Toggles management of live TV recordings +// Used in the feature access section +"liveTvRecordingManagement" = "Live TV recording management"; + +// Management - Section +// Represents settings related to management permissions +// Used in the management section of user permissions +"management" = "Management"; + +// Lyrics - Toggle +// Toggles permission to manage lyrics +// Used in the management section +"lyrics" = "Lyrics"; + +// Media playback - Section & Toggle +// Represents settings related to media playback permissions +// Used in the media playback section of user permissions +"mediaPlayback" = "Media playback"; + +// Audio transcoding - Toggle +// Toggles permission for audio transcoding +// Used in the media playback section +"audioTranscoding" = "Audio transcoding"; + +// Video transcoding - Toggle +// Toggles permission for video transcoding +// Used in the media playback section +"videoTranscoding" = "Video transcoding"; + +// Video remuxing - Toggle +// Toggles permission for video remuxing +// Used in the media playback section +"videoRemuxing" = "Video remuxing"; + +// Force remote media transcoding - Toggle +// Toggles whether remote media transcoding is forced +// Used in the media playback section +"forceRemoteTranscoding" = "Force remote media transcoding"; + +// Media downloads - Toggle +// Toggles permission to download media content +// Used in the permission section +"mediaDownloads" = "Media downloads"; + +// Hide user from login screen - Toggle +// Toggles whether the user is hidden from the login screen +// Used in the permission section +"hideUserFromLoginScreen" = "Hide user from login screen"; + +// Remote control - Section +// Represents settings related to remote control permissions +// Used in the remote control section of user permissions +"remoteControl" = "Remote control"; + +// Control other users - Toggle +// Toggles permission to control other users' sessions +// Used in the remote control section +"controlOtherUsers" = "Control other users"; + +// Control shared devices - Toggle +// Toggles permission to control shared devices +// Used in the remote control section +"controlSharedDevices" = "Control shared devices"; + +// Sessions - Section +// Represents settings related to session control +// Used in the sessions section of user permissions +"sessions" = "Sessions"; + +// Maximum failed login policy - Picker +// Represents the policy for maximum failed login attempts +// Used in the sessions section +"maximumFailedLoginPolicy" = "Maximum failed login policy"; + +// Maximum sessions policy - Picker +// Represents the policy for maximum active sessions +// Used in the sessions section +"maximumSessionsPolicy" = "Maximum sessions policy"; + +// Custom failed logins - Button +// Opens an alert to enter a custom failed login limit +// Used in the sessions section +"customFailedLogins" = "Custom failed logins"; + +// Enter custom failed logins limit - Description +// Describes the purpose of the custom failed logins entry +// Used in the custom failed logins alert +"enterCustomFailedLogins" = "Enter custom failed logins limit"; + +// Failed logins - Text Field +// Represents the input field for custom failed logins +// Used in the custom failed logins section +"failedLogins" = "Failed logins"; + +// Custom sessions - Button +// Opens an alert to enter a custom maximum session limit +// Used in the sessions section +"customSessions" = "Custom sessions"; + +// Enter custom max sessions - Description +// Describes the purpose of the custom max sessions entry +// Used in the custom sessions alert +"enterCustomMaxSessions" = "Enter custom max sessions"; + +// Maximum sessions - Text Field +// Represents the input field for custom maximum sessions +// Used in the custom sessions section +"maximumSessions" = "Maximum sessions"; + // Refresh - Button // Button title for the menu to refresh metadata // Used as the label for the refresh metadata button @@ -1379,3 +1570,48 @@ // Represents a speed in terabits per second // Used for bandwidth display "terabitsPerSecond" = "Tbps"; + +// Maximum Failed Login Policy - Description +// Explanation of the maximum failed login attempts policy +// Used in the user settings view +"maximumFailedLoginPolicyDescription" = "Sets the maximum failed login attempts before a user is locked out."; + +// Maximum Failed Login Policy Re-enable - Description +// Explanation of the resetting locked users +// Used in the user settings view +"maximumFailedLoginPolicyReenable" = "Locked users must be re-enabled by an Administrator."; + +// Locked Users - Title +// Section title for description on Locked Users +// Used in the user settings view +"lockedUsers" = "Locked users"; + +// Unlimited - Description +// Explanation of the unlimited login attempts policy +// Used in the user settings view +"unlimitedFailedLoginDescription" = "Allows unlimited failed login attempts without locking the user."; + +// Default - Description +// Explanation of the default login attempts policy +// Used in the user settings view +"defaultFailedLoginDescription" = "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts."; + +// Custom - Description +// Explanation of the custom login attempts policy +// Used in the user settings view +"customFailedLoginDescription" = "Manually set the number of failed login attempts allowed before locking the user."; + +// Maximum Connections Policy - Description +// Explanation of the maximum connections policy +// Used in the user settings view +"maximumConnectionsDescription" = "Limits the total number of connections a user can have to the server."; + +// Unlimited Connections - Description +// Explanation of unlimited connections policy +// Used in the user settings view +"unlimitedConnectionsDescription" = "The user can connect to the server without any limits."; + +// Custom Connections - Description +// Explanation of custom connections policy +// Used in the user settings view +"customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server.";