[iOS] Admin Dashboard - User Permissions (#1313)

* WIP

* WIP

* Localization and better planning. Remove the Username as this will end up in another section. Updated planning here: https://github.com/jellyfin/Swiftfin/discussions/1283 | 5 more views required!

* Initializing an optional variable with nil is redundant line

* Remove Live TV since that will go in another section

* Cleanup Coordinator / Merge with Main

* Remove all 'Allows' from strings

* Fix Merge Issues

* Use CaseIterablePicker, Binding.map

* BackgroundState == updating, change all of the buttons to visible when custom by process of elimination opposed to the default custom value. Make all of the input fields use temp values to make it less jarring.

* Update SessionsSection.swift

* Learn more!

* Validate > 0, don't allow inputs to be less than 1 and reset tempValues when the enum is updated.

* use new binding extensions

* String fixes

* Don't test against adminDefault for users or userDefault for admins.

* Linting indentation

* Default vs UserDefault + no more reason to have temporary variables.

* cleanup

* format

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2024-11-27 11:22:37 -07:00 committed by GitHub
parent 34d64bca5d
commit b9ac50c164
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1311 additions and 197 deletions

View File

@ -47,7 +47,7 @@ struct ChevronAlertButton<Content>: View where Content: View {
}
}
} message: {
if let description = description {
if let description {
Text(description)
}
}

View File

@ -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<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
ServerUserPermissionsView(viewModel: viewModel)
}
}
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
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()

View File

@ -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<Value> where Value: Comparable {
Binding<Value>(
get: { Swift.min(Swift.max(wrappedValue, min), max) },
set: { wrappedValue = Swift.min(Swift.max($0, min), max) }
)
}
func coalesce<T>(_ defaultValue: T) -> Binding<T> where Value == T? {
Binding<T>(
get: { wrappedValue ?? defaultValue },
set: { wrappedValue = $0 }
)
}
func map<V>(getter: @escaping (Value) -> V, setter: @escaping (V) -> Value) -> Binding<V> {
Binding<V>(
get: { getter(wrappedValue) },
set: { wrappedValue = setter($0) }
)
}
func min(_ minValue: Value) -> Binding<Value> where Value: Comparable {
Binding<Value>(
get: { Swift.max(wrappedValue, minValue) },
set: { wrappedValue = Swift.max($0, minValue) }
)
}
func negate() -> Binding<Bool> where Value == Bool {
map(getter: { !$0 }, setter: { $0 })
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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?

View File

@ -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<BackgroundState> = []
@Published
private(set) var user: UserDto
var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
@Published
final var backgroundStates: OrderedSet<BackgroundState> = []
@Published
final var state: State = .initial
@Published
private(set) var user: UserDto
private var resetTask: AnyCancellable?
private var userTask: AnyCancellable?
private var eventSubject: PassthroughSubject<Event, Never> = .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()
try await action()
await MainActor.run {
self.state = .initial
self.eventSubject.send(.success)
}
} catch {
await MainActor.run {
let jellyfinError = JellyfinAPIError(error.localizedDescription)
self.state = .error(jellyfinError)
}
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)
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
}
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
}
}
}

View File

@ -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 = "<group>"; };
4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCompletionStatus.swift; sourceTree = "<group>"; };
4E10C8102CC030C90012CC9F /* DeviceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetailsView.swift; sourceTree = "<group>"; };
4E10C8162CC045530012CC9F /* CompatibilitiesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibilitiesSection.swift; sourceTree = "<group>"; };
@ -1112,6 +1132,16 @@
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = "<group>"; };
4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = "<group>"; };
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = "<group>"; };
4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = "<group>"; };
4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = "<group>"; };
4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxBitratePolicy.swift; sourceTree = "<group>"; };
4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsPolicy.swift; sourceTree = "<group>"; };
4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFailurePolicy.swift; sourceTree = "<group>"; };
4E49DED72CE5509000352DCD /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = "<group>"; };
4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlayUserAccessType.swift; sourceTree = "<group>"; };
4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = "<group>"; };
4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
@ -1147,6 +1177,12 @@
4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = "<group>"; };
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = "<group>"; };
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserPermissionsView.swift; sourceTree = "<group>"; };
4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaybackSection.swift; sourceTree = "<group>"; };
4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementSection.swift; sourceTree = "<group>"; };
4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlaySection.swift; sourceTree = "<group>"; };
4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAccessSection.swift; sourceTree = "<group>"; };
4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteControlSection.swift; sourceTree = "<group>"; };
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.swift; sourceTree = "<group>"; };
4EB7C8D42CCED6E1000CC011 /* AddServerUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserView.swift; sourceTree = "<group>"; };
4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCompatibility.swift; sourceTree = "<group>"; };
@ -1352,6 +1388,7 @@
E10B1EC92BD9AF8200A92EAF /* SwiftfinStore+V1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+V1.swift"; sourceTree = "<group>"; };
E10B1ECC2BD9AFD800A92EAF /* SwiftfinStore+V2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+V2.swift"; sourceTree = "<group>"; };
E10B1ECF2BD9AFF200A92EAF /* V2UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2UserModel.swift; sourceTree = "<group>"; };
E10E67B52CF515130095365B /* Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = "<group>"; };
E10E842929A587110064EA49 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
E10E842B29A589860064EA49 /* NonePosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonePosterButton.swift; sourceTree = "<group>"; };
E10EAA4E277BBCC4000269ED /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = "<group>"; };
@ -1462,9 +1499,6 @@
E149CCAC2BE6ECC8008B9331 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = "<group>"; };
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLetter.swift; sourceTree = "<group>"; };
E14EA15D2BF6F72900DE757A /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
E14EA15F2BF6FF8900DE757A /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = "<group>"; };
E14EA1662BF70F9C00DE757A /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = "<group>"; };
E14EA1682BF7330A00DE757A /* UserProfileImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageViewModel.swift; sourceTree = "<group>"; };
E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyItemFilter.swift; sourceTree = "<group>"; };
E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFilterType.swift; sourceTree = "<group>"; };
@ -1472,7 +1506,6 @@
E150C0B92BFD44F500944FFA /* ImagePipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipeline.swift; sourceTree = "<group>"; };
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 = "<group>"; };
E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
E1546776289AF46E00087E35 /* CollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = "<group>"; };
E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = "<group>"; };
E1549655296CA2EF00C4EF88 /* DownloadTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadTask.swift; sourceTree = "<group>"; };
@ -2040,6 +2073,24 @@
path = PlaybackBitrate;
sourceTree = "<group>";
};
4E49DEDE2CE55F7F00352DCD /* Components */ = {
isa = PBXGroup;
children = (
4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */,
4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */,
);
path = Components;
sourceTree = "<group>";
};
4E49DEDF2CE55F7F00352DCD /* UserProfileImagePicker */ = {
isa = PBXGroup;
children = (
4E49DEDE2CE55F7F00352DCD /* Components */,
4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */,
);
path = UserProfileImagePicker;
sourceTree = "<group>";
};
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 = "<group>";
};
4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */ = {
isa = PBXGroup;
children = (
4EB538B82CE3CB2400EB72D5 /* Components */,
4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */,
);
path = ServerUserPermissionsView;
sourceTree = "<group>";
};
4EB538B82CE3CB2400EB72D5 /* Components */ = {
isa = PBXGroup;
children = (
4EB538B92CE3CB2900EB72D5 /* Sections */,
);
path = Components;
sourceTree = "<group>";
};
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 = "<group>";
};
4EB7C8D32CCED318000CC011 /* AddServerUserView */ = {
isa = PBXGroup;
children = (
@ -2321,7 +2405,7 @@
4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */ = {
isa = PBXGroup;
children = (
E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */,
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */,
);
path = ResetUserPasswordView;
sourceTree = "<group>";
@ -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 = "<group>";
};
E14EA1612BF6FF8D00DE757A /* UserProfileImagePicker */ = {
isa = PBXGroup;
children = (
E14EA1622BF7008A00DE757A /* Components */,
E14EA15F2BF6FF8900DE757A /* UserProfileImagePicker.swift */,
);
path = UserProfileImagePicker;
sourceTree = "<group>";
};
E14EA1622BF7008A00DE757A /* Components */ = {
isa = PBXGroup;
children = (
E14EA15D2BF6F72900DE757A /* PhotoPicker.swift */,
E14EA1662BF70F9C00DE757A /* SquareImageCropView.swift */,
);
path = Components;
sourceTree = "<group>";
};
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 */,

View File

@ -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)
}

View File

@ -23,7 +23,7 @@ struct AdminDashboardView: View {
description: L10n.dashboardDescription
)
ChevronButton(L10n.activeDevices)
ChevronButton(L10n.sessions)
.onSelect {
router.route(to: \.activeSessions)
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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 }
)) */
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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 }
))
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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.";