[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:
parent
34d64bca5d
commit
b9ac50c164
|
@ -47,7 +47,7 @@ struct ChevronAlertButton<Content>: View where Content: View {
|
|||
}
|
||||
}
|
||||
} message: {
|
||||
if let description = description {
|
||||
if let description {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
|
|
|
@ -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()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ struct AdminDashboardView: View {
|
|||
description: L10n.dashboardDescription
|
||||
)
|
||||
|
||||
ChevronButton(L10n.activeDevices)
|
||||
ChevronButton(L10n.sessions)
|
||||
.onSelect {
|
||||
router.route(to: \.activeSessions)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
)) */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.";
|
||||
|
|
Loading…
Reference in New Issue