[iOS & tvOS] Error Cleanup (#1357)
* Error Cleanup * Localize everything! * cleanup --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
a6c1908b87
commit
8f05169097
|
@ -34,6 +34,8 @@ internal enum L10n {
|
||||||
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
|
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
|
||||||
/// Add API key
|
/// Add API key
|
||||||
internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key")
|
internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key")
|
||||||
|
/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.
|
||||||
|
internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.")
|
||||||
/// Add Server
|
/// Add Server
|
||||||
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
||||||
/// Add trigger
|
/// Add trigger
|
||||||
|
@ -218,6 +220,8 @@ internal enum L10n {
|
||||||
internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: "Cast & Crew")
|
internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: "Cast & Crew")
|
||||||
/// Category
|
/// Category
|
||||||
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
||||||
|
/// Change Pin
|
||||||
|
internal static let changePin = L10n.tr("Localizable", "changePin", fallback: "Change Pin")
|
||||||
/// Change Server
|
/// Change Server
|
||||||
internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server")
|
internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server")
|
||||||
/// Changes not saved
|
/// Changes not saved
|
||||||
|
@ -314,6 +318,10 @@ internal enum L10n {
|
||||||
internal static let createAPIKey = L10n.tr("Localizable", "createAPIKey", fallback: "Create API Key")
|
internal static let createAPIKey = L10n.tr("Localizable", "createAPIKey", fallback: "Create API Key")
|
||||||
/// Enter the application name for the new API key.
|
/// Enter the application name for the new API key.
|
||||||
internal static let createAPIKeyMessage = L10n.tr("Localizable", "createAPIKeyMessage", fallback: "Enter the application name for the new API key.")
|
internal static let createAPIKeyMessage = L10n.tr("Localizable", "createAPIKeyMessage", fallback: "Enter the application name for the new API key.")
|
||||||
|
/// Create a pin to sign in to %@ on this device
|
||||||
|
internal static func createPinForUser(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "createPinForUser", String(describing: p1), fallback: "Create a pin to sign in to %@ on this device")
|
||||||
|
}
|
||||||
/// Creator
|
/// Creator
|
||||||
internal static let creator = L10n.tr("Localizable", "creator", fallback: "Creator")
|
internal static let creator = L10n.tr("Localizable", "creator", fallback: "Creator")
|
||||||
/// Critics
|
/// Critics
|
||||||
|
@ -422,10 +430,18 @@ internal enum L10n {
|
||||||
internal static let deleteUser = L10n.tr("Localizable", "deleteUser", fallback: "Delete User")
|
internal static let deleteUser = L10n.tr("Localizable", "deleteUser", fallback: "Delete User")
|
||||||
/// Failed to Delete User
|
/// Failed to Delete User
|
||||||
internal static let deleteUserFailed = L10n.tr("Localizable", "deleteUserFailed", fallback: "Failed to Delete User")
|
internal static let deleteUserFailed = L10n.tr("Localizable", "deleteUserFailed", fallback: "Failed to Delete User")
|
||||||
|
/// Are you sure you want to delete %d users?
|
||||||
|
internal static func deleteUserMultipleConfirmation(_ p1: Int) -> String {
|
||||||
|
return L10n.tr("Localizable", "deleteUserMultipleConfirmation", p1, fallback: "Are you sure you want to delete %d users?")
|
||||||
|
}
|
||||||
/// Cannot delete a user from the same user (%1$@).
|
/// Cannot delete a user from the same user (%1$@).
|
||||||
internal static func deleteUserSelfDeletion(_ p1: Any) -> String {
|
internal static func deleteUserSelfDeletion(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "deleteUserSelfDeletion", String(describing: p1), fallback: "Cannot delete a user from the same user (%1$@).")
|
return L10n.tr("Localizable", "deleteUserSelfDeletion", String(describing: p1), fallback: "Cannot delete a user from the same user (%1$@).")
|
||||||
}
|
}
|
||||||
|
/// Are you sure you want to delete %@?
|
||||||
|
internal static func deleteUserSingleConfirmation(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "deleteUserSingleConfirmation", String(describing: p1), fallback: "Are you sure you want to delete %@?")
|
||||||
|
}
|
||||||
/// Are you sure you wish to delete this user?
|
/// Are you sure you wish to delete this user?
|
||||||
internal static let deleteUserWarning = L10n.tr("Localizable", "deleteUserWarning", fallback: "Are you sure you wish to delete this user?")
|
internal static let deleteUserWarning = L10n.tr("Localizable", "deleteUserWarning", fallback: "Are you sure you wish to delete this user?")
|
||||||
/// Deletion
|
/// Deletion
|
||||||
|
@ -438,6 +454,8 @@ internal enum L10n {
|
||||||
internal static let device = L10n.tr("Localizable", "device", fallback: "Device")
|
internal static let device = L10n.tr("Localizable", "device", fallback: "Device")
|
||||||
/// Device Access
|
/// Device Access
|
||||||
internal static let deviceAccess = L10n.tr("Localizable", "deviceAccess", fallback: "Device Access")
|
internal static let deviceAccess = L10n.tr("Localizable", "deviceAccess", fallback: "Device Access")
|
||||||
|
/// Device authentication failed
|
||||||
|
internal static let deviceAuthFailed = L10n.tr("Localizable", "deviceAuthFailed", fallback: "Device authentication failed")
|
||||||
/// Device Profile
|
/// Device Profile
|
||||||
internal static let deviceProfile = L10n.tr("Localizable", "deviceProfile", fallback: "Device Profile")
|
internal static let deviceProfile = L10n.tr("Localizable", "deviceProfile", fallback: "Device Profile")
|
||||||
/// Decide which media plays natively or requires server transcoding for compatibility.
|
/// Decide which media plays natively or requires server transcoding for compatibility.
|
||||||
|
@ -462,6 +480,8 @@ internal enum L10n {
|
||||||
internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled")
|
internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled")
|
||||||
/// Discard Changes
|
/// Discard Changes
|
||||||
internal static let discardChanges = L10n.tr("Localizable", "discardChanges", fallback: "Discard Changes")
|
internal static let discardChanges = L10n.tr("Localizable", "discardChanges", fallback: "Discard Changes")
|
||||||
|
/// Disclaimer
|
||||||
|
internal static let disclaimer = L10n.tr("Localizable", "disclaimer", fallback: "Disclaimer")
|
||||||
/// Discovered Servers
|
/// Discovered Servers
|
||||||
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: "Discovered Servers")
|
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: "Discovered Servers")
|
||||||
/// Dismiss
|
/// Dismiss
|
||||||
|
@ -472,6 +492,12 @@ internal enum L10n {
|
||||||
internal static let done = L10n.tr("Localizable", "done", fallback: "Done")
|
internal static let done = L10n.tr("Localizable", "done", fallback: "Done")
|
||||||
/// Downloads
|
/// Downloads
|
||||||
internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads")
|
internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads")
|
||||||
|
/// Duplicate User
|
||||||
|
internal static let duplicateUser = L10n.tr("Localizable", "duplicateUser", fallback: "Duplicate User")
|
||||||
|
/// %@ is already saved
|
||||||
|
internal static func duplicateUserSaved(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "duplicateUserSaved", String(describing: p1), fallback: "%@ is already saved")
|
||||||
|
}
|
||||||
/// DVD
|
/// DVD
|
||||||
internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD")
|
internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD")
|
||||||
/// Edit
|
/// Edit
|
||||||
|
@ -506,6 +532,12 @@ internal enum L10n {
|
||||||
internal static let enterCustomMaxSessions = L10n.tr("Localizable", "enterCustomMaxSessions", fallback: "Enter custom max sessions")
|
internal static let enterCustomMaxSessions = L10n.tr("Localizable", "enterCustomMaxSessions", fallback: "Enter custom max sessions")
|
||||||
/// Enter the episode number.
|
/// Enter the episode number.
|
||||||
internal static let enterEpisodeNumber = L10n.tr("Localizable", "enterEpisodeNumber", fallback: "Enter the episode number.")
|
internal static let enterEpisodeNumber = L10n.tr("Localizable", "enterEpisodeNumber", fallback: "Enter the episode number.")
|
||||||
|
/// Enter Pin
|
||||||
|
internal static let enterPin = L10n.tr("Localizable", "enterPin", fallback: "Enter Pin")
|
||||||
|
/// Enter PIN for %@
|
||||||
|
internal static func enterPinForUser(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "enterPinForUser", String(describing: p1), fallback: "Enter PIN for %@")
|
||||||
|
}
|
||||||
/// Enter the season number.
|
/// Enter the season number.
|
||||||
internal static let enterSeasonNumber = L10n.tr("Localizable", "enterSeasonNumber", fallback: "Enter the season number.")
|
internal static let enterSeasonNumber = L10n.tr("Localizable", "enterSeasonNumber", fallback: "Enter the season number.")
|
||||||
/// Episode
|
/// Episode
|
||||||
|
@ -602,6 +634,8 @@ internal enum L10n {
|
||||||
internal static let hidden = L10n.tr("Localizable", "hidden", fallback: "Hidden")
|
internal static let hidden = L10n.tr("Localizable", "hidden", fallback: "Hidden")
|
||||||
/// Hide user from login screen
|
/// Hide user from login screen
|
||||||
internal static let hideUserFromLoginScreen = L10n.tr("Localizable", "hideUserFromLoginScreen", fallback: "Hide user from login screen")
|
internal static let hideUserFromLoginScreen = L10n.tr("Localizable", "hideUserFromLoginScreen", fallback: "Hide user from login screen")
|
||||||
|
/// Hint
|
||||||
|
internal static let hint = L10n.tr("Localizable", "hint", fallback: "Hint")
|
||||||
/// Home
|
/// Home
|
||||||
internal static let home = L10n.tr("Localizable", "home", fallback: "Home")
|
internal static let home = L10n.tr("Localizable", "home", fallback: "Home")
|
||||||
/// Hours
|
/// Hours
|
||||||
|
@ -678,6 +712,8 @@ internal enum L10n {
|
||||||
internal static func latestWithString(_ p1: Any) -> String {
|
internal static func latestWithString(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "latestWithString", String(describing: p1), fallback: "Latest %@")
|
return L10n.tr("Localizable", "latestWithString", String(describing: p1), fallback: "Latest %@")
|
||||||
}
|
}
|
||||||
|
/// Layout
|
||||||
|
internal static let layout = L10n.tr("Localizable", "layout", fallback: "Layout")
|
||||||
/// Learn more...
|
/// Learn more...
|
||||||
internal static let learnMoreEllipsis = L10n.tr("Localizable", "learnMoreEllipsis", fallback: "Learn more...")
|
internal static let learnMoreEllipsis = L10n.tr("Localizable", "learnMoreEllipsis", fallback: "Learn more...")
|
||||||
/// Left
|
/// Left
|
||||||
|
@ -704,6 +740,8 @@ internal enum L10n {
|
||||||
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
||||||
/// Loading
|
/// Loading
|
||||||
internal static let loading = L10n.tr("Localizable", "loading", fallback: "Loading")
|
internal static let loading = L10n.tr("Localizable", "loading", fallback: "Loading")
|
||||||
|
/// Loading user failed
|
||||||
|
internal static let loadingUserFailed = L10n.tr("Localizable", "loadingUserFailed", fallback: "Loading user failed")
|
||||||
/// Local Servers
|
/// Local Servers
|
||||||
internal static let localServers = L10n.tr("Localizable", "localServers", fallback: "Local Servers")
|
internal static let localServers = L10n.tr("Localizable", "localServers", fallback: "Local Servers")
|
||||||
/// Lock All Fields
|
/// Lock All Fields
|
||||||
|
@ -908,6 +946,8 @@ internal enum L10n {
|
||||||
internal static let peopleDescription = L10n.tr("Localizable", "peopleDescription", fallback: "People who helped create or perform specific media.")
|
internal static let peopleDescription = L10n.tr("Localizable", "peopleDescription", fallback: "People who helped create or perform specific media.")
|
||||||
/// Permissions
|
/// Permissions
|
||||||
internal static let permissions = L10n.tr("Localizable", "permissions", fallback: "Permissions")
|
internal static let permissions = L10n.tr("Localizable", "permissions", fallback: "Permissions")
|
||||||
|
/// Pin
|
||||||
|
internal static let pin = L10n.tr("Localizable", "pin", fallback: "Pin")
|
||||||
/// Play
|
/// Play
|
||||||
internal static let play = L10n.tr("Localizable", "play", fallback: "Play")
|
internal static let play = L10n.tr("Localizable", "play", fallback: "Play")
|
||||||
/// Play / Pause
|
/// Play / Pause
|
||||||
|
@ -966,6 +1006,8 @@ internal enum L10n {
|
||||||
internal static let quickConnect = L10n.tr("Localizable", "quickConnect", fallback: "Quick Connect")
|
internal static let quickConnect = L10n.tr("Localizable", "quickConnect", fallback: "Quick Connect")
|
||||||
/// Quick Connect code
|
/// Quick Connect code
|
||||||
internal static let quickConnectCode = L10n.tr("Localizable", "quickConnectCode", fallback: "Quick Connect code")
|
internal static let quickConnectCode = L10n.tr("Localizable", "quickConnectCode", fallback: "Quick Connect code")
|
||||||
|
/// Enter the 6 digit code from your other device.
|
||||||
|
internal static let quickConnectCodeInstruction = L10n.tr("Localizable", "quickConnectCodeInstruction", fallback: "Enter the 6 digit code from your other device.")
|
||||||
/// Invalid Quick Connect code
|
/// Invalid Quick Connect code
|
||||||
internal static let quickConnectInvalidError = L10n.tr("Localizable", "quickConnectInvalidError", fallback: "Invalid Quick Connect code")
|
internal static let quickConnectInvalidError = L10n.tr("Localizable", "quickConnectInvalidError", fallback: "Invalid Quick Connect code")
|
||||||
/// Note: Quick Connect not enabled
|
/// Note: Quick Connect not enabled
|
||||||
|
@ -1054,6 +1096,16 @@ internal enum L10n {
|
||||||
internal static let requestFeature = L10n.tr("Localizable", "requestFeature", fallback: "Request a Feature")
|
internal static let requestFeature = L10n.tr("Localizable", "requestFeature", fallback: "Request a Feature")
|
||||||
/// Required
|
/// Required
|
||||||
internal static let `required` = L10n.tr("Localizable", "required", fallback: "Required")
|
internal static let `required` = L10n.tr("Localizable", "required", fallback: "Required")
|
||||||
|
/// Require device authentication when signing in to the user.
|
||||||
|
internal static let requireDeviceAuthDescription = L10n.tr("Localizable", "requireDeviceAuthDescription", fallback: "Require device authentication when signing in to the user.")
|
||||||
|
/// Require device authentication to sign in to the Quick Connect user on this device.
|
||||||
|
internal static let requireDeviceAuthForQuickConnectUser = L10n.tr("Localizable", "requireDeviceAuthForQuickConnectUser", fallback: "Require device authentication to sign in to the Quick Connect user on this device.")
|
||||||
|
/// Require device authentication to sign in to %@ on this device.
|
||||||
|
internal static func requireDeviceAuthForUser(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "requireDeviceAuthForUser", String(describing: p1), fallback: "Require device authentication to sign in to %@ on this device.")
|
||||||
|
}
|
||||||
|
/// Require a local pin when signing in to the user. This pin is unrecoverable.
|
||||||
|
internal static let requirePinDescription = L10n.tr("Localizable", "requirePinDescription", fallback: "Require a local pin when signing in to the user. This pin is unrecoverable.")
|
||||||
/// Reset
|
/// Reset
|
||||||
internal static let reset = L10n.tr("Localizable", "reset", fallback: "Reset")
|
internal static let reset = L10n.tr("Localizable", "reset", fallback: "Reset")
|
||||||
/// Reset all settings back to defaults.
|
/// Reset all settings back to defaults.
|
||||||
|
@ -1086,6 +1138,8 @@ internal enum L10n {
|
||||||
internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right")
|
internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right")
|
||||||
/// Role
|
/// Role
|
||||||
internal static let role = L10n.tr("Localizable", "role", fallback: "Role")
|
internal static let role = L10n.tr("Localizable", "role", fallback: "Role")
|
||||||
|
/// Rotate
|
||||||
|
internal static let rotate = L10n.tr("Localizable", "rotate", fallback: "Rotate")
|
||||||
/// Run
|
/// Run
|
||||||
internal static let run = L10n.tr("Localizable", "run", fallback: "Run")
|
internal static let run = L10n.tr("Localizable", "run", fallback: "Run")
|
||||||
/// Running...
|
/// Running...
|
||||||
|
@ -1094,6 +1148,8 @@ internal enum L10n {
|
||||||
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
|
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
|
||||||
/// Save
|
/// Save
|
||||||
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
|
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
|
||||||
|
/// Save the user to this device without any local authentication.
|
||||||
|
internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.")
|
||||||
/// Scan All Libraries
|
/// Scan All Libraries
|
||||||
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
|
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
|
||||||
/// Scheduled Tasks
|
/// Scheduled Tasks
|
||||||
|
@ -1116,6 +1172,8 @@ internal enum L10n {
|
||||||
internal static let seasons = L10n.tr("Localizable", "seasons", fallback: "Seasons")
|
internal static let seasons = L10n.tr("Localizable", "seasons", fallback: "Seasons")
|
||||||
/// Secondary audio is not supported
|
/// Secondary audio is not supported
|
||||||
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
||||||
|
/// Security
|
||||||
|
internal static let security = L10n.tr("Localizable", "security", fallback: "Security")
|
||||||
/// See All
|
/// See All
|
||||||
internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: "See All")
|
internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: "See All")
|
||||||
/// Seek Slide Gesture Enabled
|
/// Seek Slide Gesture Enabled
|
||||||
|
@ -1132,9 +1190,9 @@ internal enum L10n {
|
||||||
internal static let seriesBackdrop = L10n.tr("Localizable", "seriesBackdrop", fallback: "Series Backdrop")
|
internal static let seriesBackdrop = L10n.tr("Localizable", "seriesBackdrop", fallback: "Series Backdrop")
|
||||||
/// Server
|
/// Server
|
||||||
internal static let server = L10n.tr("Localizable", "server", fallback: "Server")
|
internal static let server = L10n.tr("Localizable", "server", fallback: "Server")
|
||||||
/// Server %s is already connected
|
/// %@ is already connected.
|
||||||
internal static func serverAlreadyConnected(_ p1: UnsafePointer<CChar>) -> String {
|
internal static func serverAlreadyConnected(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "serverAlreadyConnected", p1, fallback: "Server %s is already connected")
|
return L10n.tr("Localizable", "serverAlreadyConnected", String(describing: p1), fallback: "%@ is already connected.")
|
||||||
}
|
}
|
||||||
/// Server %s already exists. Add new URL?
|
/// Server %s already exists. Add new URL?
|
||||||
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
|
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
|
||||||
|
@ -1162,6 +1220,14 @@ internal enum L10n {
|
||||||
internal static let session = L10n.tr("Localizable", "session", fallback: "Session")
|
internal static let session = L10n.tr("Localizable", "session", fallback: "Session")
|
||||||
/// Sessions
|
/// Sessions
|
||||||
internal static let sessions = L10n.tr("Localizable", "sessions", fallback: "Sessions")
|
internal static let sessions = L10n.tr("Localizable", "sessions", fallback: "Sessions")
|
||||||
|
/// Set
|
||||||
|
internal static let `set` = L10n.tr("Localizable", "set", fallback: "Set")
|
||||||
|
/// Set Pin
|
||||||
|
internal static let setPin = L10n.tr("Localizable", "setPin", fallback: "Set Pin")
|
||||||
|
/// Set pin for new user.
|
||||||
|
internal static let setPinForNewUser = L10n.tr("Localizable", "setPinForNewUser", fallback: "Set pin for new user.")
|
||||||
|
/// Set a hint when prompting for the pin.
|
||||||
|
internal static let setPinHintDescription = L10n.tr("Localizable", "setPinHintDescription", fallback: "Set a hint when prompting for the pin.")
|
||||||
/// Settings
|
/// Settings
|
||||||
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
|
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
|
||||||
/// Show Cast & Crew
|
/// Show Cast & Crew
|
||||||
|
@ -1356,6 +1422,10 @@ internal enum L10n {
|
||||||
internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer", fallback: "Unable to connect to server")
|
internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer", fallback: "Unable to connect to server")
|
||||||
/// Unable to find host
|
/// Unable to find host
|
||||||
internal static let unableToFindHost = L10n.tr("Localizable", "unableToFindHost", fallback: "Unable to find host")
|
internal static let unableToFindHost = L10n.tr("Localizable", "unableToFindHost", fallback: "Unable to find host")
|
||||||
|
/// Unable to perform device authentication
|
||||||
|
internal static let unableToPerformDeviceAuth = L10n.tr("Localizable", "unableToPerformDeviceAuth", fallback: "Unable to perform device authentication")
|
||||||
|
/// Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin.
|
||||||
|
internal static let unableToPerformDeviceAuthFaceID = L10n.tr("Localizable", "unableToPerformDeviceAuthFaceID", fallback: "Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin.")
|
||||||
/// Unaired
|
/// Unaired
|
||||||
internal static let unaired = L10n.tr("Localizable", "unaired", fallback: "Unaired")
|
internal static let unaired = L10n.tr("Localizable", "unaired", fallback: "Unaired")
|
||||||
/// Unauthorized
|
/// Unauthorized
|
||||||
|
@ -1396,10 +1466,18 @@ internal enum L10n {
|
||||||
internal static func userAlreadySignedIn(_ p1: UnsafePointer<CChar>) -> String {
|
internal static func userAlreadySignedIn(_ p1: UnsafePointer<CChar>) -> String {
|
||||||
return L10n.tr("Localizable", "userAlreadySignedIn", p1, fallback: "User %s is already signed in")
|
return L10n.tr("Localizable", "userAlreadySignedIn", p1, fallback: "User %s is already signed in")
|
||||||
}
|
}
|
||||||
|
/// This user will require device authentication.
|
||||||
|
internal static let userDeviceAuthRequiredDescription = L10n.tr("Localizable", "userDeviceAuthRequiredDescription", fallback: "This user will require device authentication.")
|
||||||
/// Username
|
/// Username
|
||||||
internal static let username = L10n.tr("Localizable", "username", fallback: "Username")
|
internal static let username = L10n.tr("Localizable", "username", fallback: "Username")
|
||||||
/// A username is required
|
/// A username is required
|
||||||
internal static let usernameRequired = L10n.tr("Localizable", "usernameRequired", fallback: "A username is required")
|
internal static let usernameRequired = L10n.tr("Localizable", "usernameRequired", fallback: "A username is required")
|
||||||
|
/// This user will require a pin.
|
||||||
|
internal static let userPinRequiredDescription = L10n.tr("Localizable", "userPinRequiredDescription", fallback: "This user will require a pin.")
|
||||||
|
/// User %@ requires device authentication
|
||||||
|
internal static func userRequiresDeviceAuthentication(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "userRequiresDeviceAuthentication", String(describing: p1), fallback: "User %@ requires device authentication")
|
||||||
|
}
|
||||||
/// Users
|
/// Users
|
||||||
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
|
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
|
||||||
/// Version
|
/// Version
|
||||||
|
|
|
@ -24,24 +24,24 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
case getDevices
|
case refresh
|
||||||
case deleteDevices(ids: [String])
|
case delete(ids: [String])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - BackgroundState
|
// MARK: - BackgroundState
|
||||||
|
|
||||||
enum BackgroundState: Hashable {
|
enum BackgroundState: Hashable {
|
||||||
case gettingDevices
|
case refreshing
|
||||||
case settingCustomName
|
case updating
|
||||||
case deletingDevices
|
case deleting
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - State
|
// MARK: - State
|
||||||
|
|
||||||
enum State: Hashable {
|
enum State: Hashable {
|
||||||
|
case initial
|
||||||
case content
|
case content
|
||||||
case error(JellyfinAPIError)
|
case error(JellyfinAPIError)
|
||||||
case initial
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Published Values
|
// MARK: Published Values
|
||||||
|
@ -66,10 +66,10 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
|
||||||
|
|
||||||
func respond(to action: Action) -> State {
|
func respond(to action: Action) -> State {
|
||||||
switch action {
|
switch action {
|
||||||
case .getDevices:
|
case .refresh:
|
||||||
deviceTask?.cancel()
|
deviceTask?.cancel()
|
||||||
|
|
||||||
backgroundStates.append(.gettingDevices)
|
backgroundStates.append(.refreshing)
|
||||||
|
|
||||||
deviceTask = Task { [weak self] in
|
deviceTask = Task { [weak self] in
|
||||||
do {
|
do {
|
||||||
|
@ -89,16 +89,16 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_ = self?.backgroundStates.remove(.gettingDevices)
|
_ = self?.backgroundStates.remove(.refreshing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.asAnyCancellable()
|
.asAnyCancellable()
|
||||||
|
|
||||||
return state
|
return state
|
||||||
case let .deleteDevices(ids):
|
case let .delete(ids):
|
||||||
deviceTask?.cancel()
|
deviceTask?.cancel()
|
||||||
|
|
||||||
backgroundStates.append(.deletingDevices)
|
backgroundStates.append(.deleting)
|
||||||
|
|
||||||
deviceTask = Task { [weak self] in
|
deviceTask = Task { [weak self] in
|
||||||
do {
|
do {
|
||||||
|
@ -116,7 +116,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_ = self?.backgroundStates.remove(.deletingDevices)
|
_ = self?.backgroundStates.remove(.deleting)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.asAnyCancellable()
|
.asAnyCancellable()
|
||||||
|
|
|
@ -43,7 +43,6 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
|
||||||
enum State: Hashable {
|
enum State: Hashable {
|
||||||
case initial
|
case initial
|
||||||
case content
|
case content
|
||||||
case updating
|
|
||||||
case error(JellyfinAPIError)
|
case error(JellyfinAPIError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,31 +12,47 @@ import SwiftUI
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
@EnvironmentObject
|
// MARK: - Focus Fields
|
||||||
private var router: SelectUserCoordinator.Router
|
|
||||||
|
|
||||||
@FocusState
|
@FocusState
|
||||||
private var isURLFocused: Bool
|
private var isURLFocused: Bool
|
||||||
|
|
||||||
@State
|
// MARK: - State & Environment Objects
|
||||||
private var duplicateServer: ServerState? = nil
|
|
||||||
@State
|
@EnvironmentObject
|
||||||
private var error: Error? = nil
|
private var router: SelectUserCoordinator.Router
|
||||||
@State
|
|
||||||
private var isPresentingDuplicateServer: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var url: String = ""
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel = ConnectToServerViewModel()
|
private var viewModel = ConnectToServerViewModel()
|
||||||
|
|
||||||
|
// MARK: - Connect to Server Variables
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var duplicateServer: ServerState? = nil
|
||||||
|
@State
|
||||||
|
private var url: String = ""
|
||||||
|
|
||||||
|
// MARK: - Dialog States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingDuplicateServer: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error States
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Connection Timer
|
||||||
|
|
||||||
private let timer = Timer.publish(every: 12, on: .main, in: .common).autoconnect()
|
private let timer = Timer.publish(every: 12, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
// MARK: - Connect Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var connectSection: some View {
|
private var connectSection: some View {
|
||||||
Section(L10n.connectToServer) {
|
Section(L10n.connectToServer) {
|
||||||
|
@ -73,6 +89,8 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Local Server Button
|
||||||
|
|
||||||
private func localServerButton(for server: ServerState) -> some View {
|
private func localServerButton(for server: ServerState) -> some View {
|
||||||
Button {
|
Button {
|
||||||
url = server.currentURL.absoluteString
|
url = server.currentURL.absoluteString
|
||||||
|
@ -100,6 +118,8 @@ struct ConnectToServerView: View {
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Local Servers Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var localServersSection: some View {
|
private var localServersSection: some View {
|
||||||
Section(L10n.localServers) {
|
Section(L10n.localServers) {
|
||||||
|
@ -116,6 +136,8 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -160,7 +182,6 @@ struct ConnectToServerView: View {
|
||||||
isPresentingDuplicateServer = true
|
isPresentingDuplicateServer = true
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
isURLFocused = true
|
isURLFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,15 +190,6 @@ struct ConnectToServerView: View {
|
||||||
|
|
||||||
viewModel.send(.searchForServers)
|
viewModel.send(.searchForServers)
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .destructive)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
L10n.server.text,
|
L10n.server.text,
|
||||||
isPresented: $isPresentingDuplicateServer,
|
isPresented: $isPresentingDuplicateServer,
|
||||||
|
@ -190,7 +202,8 @@ struct ConnectToServerView: View {
|
||||||
router.popLast()
|
router.popLast()
|
||||||
}
|
}
|
||||||
} message: { server in
|
} message: { server in
|
||||||
Text("\(server.name) is already connected.")
|
Text(L10n.serverAlreadyConnected(server.name))
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,38 +17,52 @@ import SwiftUI
|
||||||
|
|
||||||
struct SelectUserView: View {
|
struct SelectUserView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
|
@Default(.selectUserServerSelection)
|
||||||
|
private var serverSelection
|
||||||
|
|
||||||
|
// MARK: - User Grid Item Enum
|
||||||
|
|
||||||
private enum UserGridItem: Hashable {
|
private enum UserGridItem: Hashable {
|
||||||
case user(UserState, server: ServerState)
|
case user(UserState, server: ServerState)
|
||||||
case addUser
|
case addUser
|
||||||
}
|
}
|
||||||
|
|
||||||
@Default(.selectUserServerSelection)
|
// MARK: - State & Environment Objects
|
||||||
private var serverSelection
|
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: SelectUserCoordinator.Router
|
private var router: SelectUserCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = SelectUserViewModel()
|
||||||
|
|
||||||
|
// MARK: - Select User Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var contentSize: CGSize = .zero
|
private var contentSize: CGSize = .zero
|
||||||
@State
|
@State
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var gridItems: OrderedSet<UserGridItem> = []
|
private var gridItems: OrderedSet<UserGridItem> = []
|
||||||
@State
|
@State
|
||||||
private var gridItemSize: CGSize = .zero
|
private var gridItemSize: CGSize = .zero
|
||||||
@State
|
@State
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingServers: Bool = false
|
|
||||||
@State
|
|
||||||
private var padGridItemColumnCount: Int = 1
|
private var padGridItemColumnCount: Int = 1
|
||||||
@State
|
@State
|
||||||
private var scrollViewOffset: CGFloat = 0
|
private var scrollViewOffset: CGFloat = 0
|
||||||
@State
|
@State
|
||||||
private var splashScreenImageSource: ImageSource? = nil
|
private var splashScreenImageSource: ImageSource? = nil
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Dialog States
|
||||||
private var viewModel = SelectUserViewModel()
|
|
||||||
|
@State
|
||||||
|
private var isPresentingServers: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Selected Server
|
||||||
|
|
||||||
private var selectedServer: ServerState? {
|
private var selectedServer: ServerState? {
|
||||||
if case let SelectUserServerSelection.server(id: id) = serverSelection,
|
if case let SelectUserServerSelection.server(id: id) = serverSelection,
|
||||||
|
@ -60,6 +74,8 @@ struct SelectUserView: View {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Make Grid Items
|
||||||
|
|
||||||
private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> {
|
private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> {
|
||||||
switch serverSelection {
|
switch serverSelection {
|
||||||
case .all:
|
case .all:
|
||||||
|
@ -89,6 +105,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Make Splash Screen Image Source
|
||||||
|
|
||||||
// For all server selection, .all is random
|
// For all server selection, .all is random
|
||||||
private func makeSplashScreenImageSource(
|
private func makeSplashScreenImageSource(
|
||||||
serverSelection: SelectUserServerSelection,
|
serverSelection: SelectUserServerSelection,
|
||||||
|
@ -112,7 +130,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: grid
|
// MARK: - Grid Item Offset
|
||||||
|
|
||||||
private func gridItemOffset(index: Int) -> CGFloat {
|
private func gridItemOffset(index: Int) -> CGFloat {
|
||||||
let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)
|
let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)
|
||||||
|
@ -123,6 +141,8 @@ struct SelectUserView: View {
|
||||||
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
|
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Grid Content View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var gridContentView: some View {
|
private var gridContentView: some View {
|
||||||
let columns = Array(repeating: GridItem(.flexible(), spacing: EdgeInsets.edgePadding), count: 5)
|
let columns = Array(repeating: GridItem(.flexible(), spacing: EdgeInsets.edgePadding), count: 5)
|
||||||
|
@ -144,6 +164,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Grid Content View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func gridItemView(for item: UserGridItem) -> some View {
|
private func gridItemView(for item: UserGridItem) -> some View {
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -174,7 +196,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: userView
|
// MARK: - User View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var userView: some View {
|
private var userView: some View {
|
||||||
|
@ -221,7 +243,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: emptyView
|
// MARK: - Empty View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var emptyView: some View {
|
private var emptyView: some View {
|
||||||
|
@ -256,6 +278,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if viewModel.servers.isEmpty {
|
if viewModel.servers.isEmpty {
|
||||||
|
@ -303,7 +327,6 @@ struct SelectUserView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
self.error = eventError
|
self.error = eventError
|
||||||
self.isPresentingError = true
|
|
||||||
case let .signedIn(user):
|
case let .signedIn(user):
|
||||||
Defaults[.lastSignedInUserID] = .signedIn(userID: user.id)
|
Defaults[.lastSignedInUserID] = .signedIn(userID: user.id)
|
||||||
Container.shared.currentUserSession.reset()
|
Container.shared.currentUserSession.reset()
|
||||||
|
@ -332,5 +355,6 @@ struct SelectUserView: View {
|
||||||
// change splash screen selection if necessary
|
// change splash screen selection if necessary
|
||||||
// selectUserAllServersSplashscreen = serverSelection
|
// selectUserAllServersSplashscreen = serverSelection
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,40 +17,56 @@ import SwiftUI
|
||||||
|
|
||||||
struct UserSignInView: View {
|
struct UserSignInView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - Focus Fields
|
||||||
|
|
||||||
private enum FocusField: Hashable {
|
private enum FocusField: Hashable {
|
||||||
case username
|
case username
|
||||||
case password
|
case password
|
||||||
}
|
}
|
||||||
|
|
||||||
@Default(.accentColor)
|
@FocusState
|
||||||
private var accentColor
|
private var focusedTextField: FocusField?
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: UserSignInCoordinator.Router
|
private var router: UserSignInCoordinator.Router
|
||||||
|
|
||||||
@FocusState
|
@StateObject
|
||||||
private var focusedTextField: FocusField?
|
private var viewModel: UserSignInViewModel
|
||||||
|
|
||||||
|
// MARK: - User Sign In Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var duplicateUser: UserState? = nil
|
private var duplicateUser: UserState? = nil
|
||||||
@State
|
@State
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var isPresentingDuplicateUser: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var password: String = ""
|
private var password: String = ""
|
||||||
@State
|
@State
|
||||||
private var username: String = ""
|
private var username: String = ""
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Dialog State
|
||||||
private var viewModel: UserSignInViewModel
|
|
||||||
|
@State
|
||||||
|
private var isPresentingDuplicateUser: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
init(server: ServerState) {
|
init(server: ServerState) {
|
||||||
self._viewModel = StateObject(wrappedValue: UserSignInViewModel(server: server))
|
self._viewModel = StateObject(wrappedValue: UserSignInViewModel(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sign In Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var signInSection: some View {
|
private var signInSection: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -102,15 +118,17 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let disclaimer = viewModel.serverDisclaimer {
|
if let disclaimer = viewModel.serverDisclaimer {
|
||||||
Section("Disclaimer") {
|
Section(L10n.disclaimer) {
|
||||||
Text(disclaimer)
|
Text(disclaimer)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Users Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var publisUsersSection: some View {
|
private var publicUsersSection: some View {
|
||||||
Section(L10n.publicUsers) {
|
Section(L10n.publicUsers) {
|
||||||
if viewModel.publicUsers.isEmpty {
|
if viewModel.publicUsers.isEmpty {
|
||||||
L10n.noPublicUsers.text
|
L10n.noPublicUsers.text
|
||||||
|
@ -132,6 +150,8 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -156,7 +176,7 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
publisUsersSection
|
publicUsersSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +189,6 @@ struct UserSignInView: View {
|
||||||
isPresentingDuplicateUser = true
|
isPresentingDuplicateUser = true
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case let .signedIn(user):
|
case let .signedIn(user):
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
|
|
||||||
|
@ -183,7 +202,7 @@ struct UserSignInView: View {
|
||||||
viewModel.send(.getPublicData)
|
viewModel.send(.getPublicData)
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(
|
||||||
Text("Duplicate User"),
|
Text(L10n.duplicateUser),
|
||||||
isPresented: $isPresentingDuplicateUser,
|
isPresented: $isPresentingDuplicateUser,
|
||||||
presenting: duplicateUser
|
presenting: duplicateUser
|
||||||
) { _ in
|
) { _ in
|
||||||
|
@ -199,16 +218,8 @@ struct UserSignInView: View {
|
||||||
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
Button(L10n.dismiss, role: .cancel)
|
||||||
} message: { duplicateUser in
|
} message: { duplicateUser in
|
||||||
Text("\(duplicateUser.username) is already saved")
|
Text(L10n.duplicateUserSaved(duplicateUser.username))
|
||||||
}
|
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,20 +13,31 @@ import SwiftUI
|
||||||
|
|
||||||
struct AddServerUserView: View {
|
struct AddServerUserView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - Focus Fields
|
||||||
|
|
||||||
private enum Field: Hashable {
|
private enum Field: Hashable {
|
||||||
case username
|
case username
|
||||||
case password
|
case password
|
||||||
case confirmPassword
|
case confirmPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
@Default(.accentColor)
|
@FocusState
|
||||||
private var accentColor
|
private var focusedfield: Field?
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
@FocusState
|
@StateObject
|
||||||
private var focusedfield: Field?
|
private var viewModel = AddServerUserViewModel()
|
||||||
|
|
||||||
|
// MARK: - Element Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var username: String = ""
|
private var username: String = ""
|
||||||
|
@ -35,20 +46,24 @@ struct AddServerUserView: View {
|
||||||
@State
|
@State
|
||||||
private var confirmPassword: String = ""
|
private var confirmPassword: String = ""
|
||||||
|
|
||||||
@State
|
// MARK: - Dialog State
|
||||||
private var error: Error?
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
@State
|
||||||
private var isPresentingSuccess: Bool = false
|
private var isPresentingSuccess: Bool = false
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Error State
|
||||||
private var viewModel = AddServerUserViewModel()
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
|
// MARK: - Username is Valid
|
||||||
|
|
||||||
private var isValid: Bool {
|
private var isValid: Bool {
|
||||||
username.isNotEmpty && password == confirmPassword
|
username.isNotEmpty && password == confirmPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
|
||||||
|
@ -80,7 +95,7 @@ struct AddServerUserView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
UnmaskSecureField(L10n.confirmPassword, text: $confirmPassword) {}
|
UnmaskSecureField(L10n.confirmPassword, text: $confirmPassword)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
.textInputAutocapitalization(.none)
|
.textInputAutocapitalization(.none)
|
||||||
.focused($focusedfield, equals: .confirmPassword)
|
.focused($focusedfield, equals: .confirmPassword)
|
||||||
|
@ -108,12 +123,9 @@ struct AddServerUserView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case let .createdNewUser(newUser):
|
case let .createdNewUser(newUser):
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
router.dismissCoordinator {
|
router.dismissCoordinator {
|
||||||
Notifications[.didAddServerUser].post(newUser)
|
Notifications[.didAddServerUser].post(newUser)
|
||||||
}
|
}
|
||||||
|
@ -137,16 +149,8 @@ struct AddServerUserView: View {
|
||||||
.disabled(!isValid)
|
.disabled(!isValid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error) {
|
||||||
L10n.error,
|
focusedfield = .username
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {
|
|
||||||
focusedfield = .username
|
|
||||||
}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,27 +14,33 @@ import SwiftUI
|
||||||
|
|
||||||
struct DeviceDetailsView: View {
|
struct DeviceDetailsView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
// MARK: - Current Date
|
||||||
private var router: AdminDashboardCoordinator.Router
|
|
||||||
|
|
||||||
@CurrentDate
|
@CurrentDate
|
||||||
private var currentDate: Date
|
private var currentDate: Date
|
||||||
|
|
||||||
@State
|
// MARK: - State & Environment Objects
|
||||||
private var temporaryCustomName: String
|
|
||||||
@State
|
@EnvironmentObject
|
||||||
private var error: Error?
|
private var router: AdminDashboardCoordinator.Router
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingSuccess: Bool = false
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel: DeviceDetailViewModel
|
private var viewModel: DeviceDetailViewModel
|
||||||
|
|
||||||
private var device: DeviceInfo {
|
// MARK: - Custom Name Variable
|
||||||
viewModel.device
|
|
||||||
}
|
@State
|
||||||
|
private var temporaryCustomName: String
|
||||||
|
|
||||||
|
// MARK: - Dialog State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingSuccess: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
@ -43,23 +49,21 @@ struct DeviceDetailsView: View {
|
||||||
|
|
||||||
// TODO: Enable with SDK Change
|
// TODO: Enable with SDK Change
|
||||||
self.temporaryCustomName = device.name ?? "" // device.customName ?? device.name
|
self.temporaryCustomName = device.name ?? "" // device.customName ?? device.name
|
||||||
|
|
||||||
// _viewModel = StateObject(wrappedValue: DevicesViewModel(device.lastUserID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if let userID = device.lastUserID,
|
if let userID = viewModel.device.lastUserID,
|
||||||
let userName = device.lastUserName
|
let userName = viewModel.device.lastUserName
|
||||||
{
|
{
|
||||||
|
|
||||||
let user = UserDto(id: userID, name: userName)
|
let user = UserDto(id: userID, name: userName)
|
||||||
|
|
||||||
AdminDashboardView.UserSection(
|
AdminDashboardView.UserSection(
|
||||||
user: user,
|
user: user,
|
||||||
lastActivityDate: device.dateLastActivity
|
lastActivityDate: viewModel.device.dateLastActivity
|
||||||
) {
|
) {
|
||||||
router.route(to: \.userDetails, user)
|
router.route(to: \.userDetails, user)
|
||||||
}
|
}
|
||||||
|
@ -69,12 +73,12 @@ struct DeviceDetailsView: View {
|
||||||
// CustomDeviceNameSection(customName: $temporaryCustomName)
|
// CustomDeviceNameSection(customName: $temporaryCustomName)
|
||||||
|
|
||||||
AdminDashboardView.DeviceSection(
|
AdminDashboardView.DeviceSection(
|
||||||
client: device.appName,
|
client: viewModel.device.appName,
|
||||||
device: device.name,
|
device: viewModel.device.name,
|
||||||
version: device.appVersion
|
version: viewModel.device.appVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
CapabilitiesSection(device: device)
|
CapabilitiesSection(device: viewModel.device)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.device)
|
.navigationTitle(L10n.device)
|
||||||
.onReceive(viewModel.events) { event in
|
.onReceive(viewModel.events) { event in
|
||||||
|
@ -82,7 +86,6 @@ struct DeviceDetailsView: View {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .setCustomName:
|
case .setCustomName:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
isPresentingSuccess = true
|
isPresentingSuccess = true
|
||||||
|
@ -108,15 +111,6 @@ struct DeviceDetailsView: View {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
L10n.success.text,
|
L10n.success.text,
|
||||||
isPresented: $isPresentingSuccess
|
isPresented: $isPresentingSuccess
|
||||||
|
@ -125,5 +119,6 @@ struct DeviceDetailsView: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text(L10n.customDeviceNameSaved(temporaryCustomName))
|
Text(L10n.customDeviceNameSaved(temporaryCustomName))
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ struct DevicesView: View {
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
ErrorView(error: error)
|
ErrorView(error: error)
|
||||||
.onRetry {
|
.onRetry {
|
||||||
viewModel.send(.getDevices)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
case .initial:
|
case .initial:
|
||||||
DelayedProgressView()
|
DelayedProgressView()
|
||||||
|
@ -75,7 +75,7 @@ struct DevicesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
viewModel.send(.getDevices)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
L10n.deleteSelectedDevices,
|
L10n.deleteSelectedDevices,
|
||||||
|
@ -156,7 +156,7 @@ struct DevicesView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var navigationBarEditView: some View {
|
private var navigationBarEditView: some View {
|
||||||
if viewModel.backgroundStates.contains(.gettingDevices) {
|
if viewModel.backgroundStates.contains(.refreshing) {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
Button(isEditing ? L10n.cancel : L10n.edit) {
|
Button(isEditing ? L10n.cancel : L10n.edit) {
|
||||||
|
@ -194,7 +194,7 @@ struct DevicesView: View {
|
||||||
Button(L10n.cancel, role: .cancel) {}
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
|
|
||||||
Button(L10n.confirm, role: .destructive) {
|
Button(L10n.confirm, role: .destructive) {
|
||||||
viewModel.send(.deleteDevices(ids: Array(selectedDevices)))
|
viewModel.send(.delete(ids: Array(selectedDevices)))
|
||||||
isEditing = false
|
isEditing = false
|
||||||
selectedDevices.removeAll()
|
selectedDevices.removeAll()
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ struct DevicesView: View {
|
||||||
if deviceToDelete == viewModel.userSession.client.configuration.deviceID {
|
if deviceToDelete == viewModel.userSession.client.configuration.deviceID {
|
||||||
isPresentingSelfDeleteError = true
|
isPresentingSelfDeleteError = true
|
||||||
} else {
|
} else {
|
||||||
viewModel.send(.deleteDevices(ids: [deviceToDelete]))
|
viewModel.send(.delete(ids: [deviceToDelete]))
|
||||||
selectedDevices.removeAll()
|
selectedDevices.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,24 +12,23 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerUserMediaAccessView: View {
|
struct ServerUserMediaAccessView: View {
|
||||||
|
|
||||||
// MARK: - Environment
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
// MARK: - ViewModel
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
private var viewModel: ServerUserAdminViewModel
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
// MARK: - State Variables
|
// MARK: - Policy Variable
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var tempPolicy: UserPolicy
|
private var tempPolicy: UserPolicy
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var error: Error?
|
private var error: Error?
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
@ -64,24 +63,12 @@ struct ServerUserMediaAccessView: View {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .updated:
|
case .updated:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error)
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.onFirstAppear {
|
|
||||||
viewModel.send(.loadLibraries(isHidden: false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content View
|
// MARK: - Content View
|
||||||
|
|
|
@ -12,13 +12,16 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerUserDeviceAccessView: View {
|
struct ServerUserDeviceAccessView: View {
|
||||||
|
|
||||||
// MARK: - Environment
|
// MARK: - Current Date
|
||||||
|
|
||||||
|
@CurrentDate
|
||||||
|
private var currentDate: Date
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
// MARK: - ViewModel
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel: ServerUserAdminViewModel
|
private var viewModel: ServerUserAdminViewModel
|
||||||
@StateObject
|
@StateObject
|
||||||
|
@ -28,15 +31,11 @@ struct ServerUserDeviceAccessView: View {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var tempPolicy: UserPolicy
|
private var tempPolicy: UserPolicy
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var error: Error?
|
private var error: Error?
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Current Date
|
|
||||||
|
|
||||||
@CurrentDate
|
|
||||||
private var currentDate: Date
|
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
@ -71,24 +70,15 @@ struct ServerUserDeviceAccessView: View {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .updated:
|
case .updated:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
devicesViewModel.send(.getDevices)
|
devicesViewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content View
|
// MARK: - Content View
|
||||||
|
|
|
@ -12,30 +12,29 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerUserLiveTVAccessView: View {
|
struct ServerUserLiveTVAccessView: View {
|
||||||
|
|
||||||
// MARK: - Environment
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
|
||||||
|
|
||||||
// MARK: - ViewModel
|
|
||||||
|
|
||||||
@ObservedObject
|
|
||||||
private var viewModel: ServerUserAdminViewModel
|
|
||||||
|
|
||||||
// MARK: - State Variables
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var tempPolicy: UserPolicy
|
|
||||||
@State
|
|
||||||
private var error: Error?
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Current Date
|
// MARK: - Current Date
|
||||||
|
|
||||||
@CurrentDate
|
@CurrentDate
|
||||||
private var currentDate: Date
|
private var currentDate: Date
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
private var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
|
// MARK: - Policy Variable
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var tempPolicy: UserPolicy
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error?
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
init(viewModel: ServerUserAdminViewModel) {
|
init(viewModel: ServerUserAdminViewModel) {
|
||||||
|
@ -69,21 +68,12 @@ struct ServerUserLiveTVAccessView: View {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .updated:
|
case .updated:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error)
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content View
|
// MARK: - Content View
|
||||||
|
|
|
@ -13,24 +13,23 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerUserPermissionsView: View {
|
struct ServerUserPermissionsView: View {
|
||||||
|
|
||||||
// MARK: - Environment
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
// MARK: - ViewModel
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: ServerUserAdminViewModel
|
var viewModel: ServerUserAdminViewModel
|
||||||
|
|
||||||
// MARK: - State Variables
|
// MARK: - Policy Variable
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var tempPolicy: UserPolicy
|
private var tempPolicy: UserPolicy
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var error: Error?
|
private var error: Error?
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
@ -65,21 +64,12 @@ struct ServerUserPermissionsView: View {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .updated:
|
case .updated:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error)
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content View
|
// MARK: - Content View
|
||||||
|
@ -90,7 +80,7 @@ struct ServerUserPermissionsView: View {
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
ErrorView(error: error)
|
ErrorView(error: error)
|
||||||
case .initial:
|
case .initial:
|
||||||
ErrorView(error: JellyfinAPIError("Loading user failed"))
|
ErrorView(error: JellyfinAPIError(L10n.loadingUserFailed))
|
||||||
default:
|
default:
|
||||||
permissionsListView
|
permissionsListView
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,31 +12,47 @@ import SwiftUI
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
@EnvironmentObject
|
// MARK: - Focus Fields
|
||||||
private var router: SelectUserCoordinator.Router
|
|
||||||
|
|
||||||
@FocusState
|
@FocusState
|
||||||
private var isURLFocused: Bool
|
private var isURLFocused: Bool
|
||||||
|
|
||||||
@State
|
// MARK: - State & Environment Objects
|
||||||
private var duplicateServer: ServerState? = nil
|
|
||||||
@State
|
@EnvironmentObject
|
||||||
private var error: Error? = nil
|
private var router: SelectUserCoordinator.Router
|
||||||
@State
|
|
||||||
private var isPresentingDuplicateServer: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var url: String = ""
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel = ConnectToServerViewModel()
|
private var viewModel = ConnectToServerViewModel()
|
||||||
|
|
||||||
|
// MARK: - URL Variable
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var url: String = ""
|
||||||
|
|
||||||
|
// MARK: - Duplicate Server State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var duplicateServer: ServerState? = nil
|
||||||
|
@State
|
||||||
|
private var isPresentingDuplicateServer: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Connection Timer
|
||||||
|
|
||||||
private let timer = Timer.publish(every: 12, on: .main, in: .common).autoconnect()
|
private let timer = Timer.publish(every: 12, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
// MARK: - Handle Connection
|
||||||
|
|
||||||
private func handleConnection(_ event: ConnectToServerViewModel.Event) {
|
private func handleConnection(_ event: ConnectToServerViewModel.Event) {
|
||||||
switch event {
|
switch event {
|
||||||
case let .connected(server):
|
case let .connected(server):
|
||||||
|
@ -53,11 +69,12 @@ struct ConnectToServerView: View {
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
isURLFocused = true
|
isURLFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Connect Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var connectSection: some View {
|
private var connectSection: some View {
|
||||||
Section(L10n.connectToServer) {
|
Section(L10n.connectToServer) {
|
||||||
|
@ -87,6 +104,8 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Local Server Button
|
||||||
|
|
||||||
private func localServerButton(for server: ServerState) -> some View {
|
private func localServerButton(for server: ServerState) -> some View {
|
||||||
Button {
|
Button {
|
||||||
url = server.currentURL.absoluteString
|
url = server.currentURL.absoluteString
|
||||||
|
@ -114,6 +133,8 @@ struct ConnectToServerView: View {
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Local Servers Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var localServersSection: some View {
|
private var localServersSection: some View {
|
||||||
Section(L10n.localServers) {
|
Section(L10n.localServers) {
|
||||||
|
@ -130,6 +151,8 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
connectSection
|
connectSection
|
||||||
|
@ -159,15 +182,6 @@ struct ConnectToServerView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .destructive)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
L10n.server.text,
|
L10n.server.text,
|
||||||
isPresented: $isPresentingDuplicateServer,
|
isPresented: $isPresentingDuplicateServer,
|
||||||
|
@ -182,5 +196,6 @@ struct ConnectToServerView: View {
|
||||||
} message: { server in
|
} message: { server in
|
||||||
L10n.serverAlreadyExistsPrompt(server.name).text
|
L10n.serverAlreadyExistsPrompt(server.name).text
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,21 +13,27 @@ import SwiftUI
|
||||||
|
|
||||||
struct ResetUserPasswordView: View {
|
struct ResetUserPasswordView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - Focus Fields
|
||||||
|
|
||||||
private enum Field: Hashable {
|
private enum Field: Hashable {
|
||||||
case currentPassword
|
case currentPassword
|
||||||
case newPassword
|
case newPassword
|
||||||
case confirmNewPassword
|
case confirmNewPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
@Default(.accentColor)
|
@FocusState
|
||||||
private var accentColor
|
private var focusedField: Field?
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: BasicNavigationViewCoordinator.Router
|
private var router: BasicNavigationViewCoordinator.Router
|
||||||
|
|
||||||
@FocusState
|
|
||||||
private var focusedField: Field?
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel: ResetUserPasswordViewModel
|
private var viewModel: ResetUserPasswordViewModel
|
||||||
|
|
||||||
|
@ -40,16 +46,17 @@ struct ResetUserPasswordView: View {
|
||||||
@State
|
@State
|
||||||
private var confirmNewPassword: String = ""
|
private var confirmNewPassword: String = ""
|
||||||
|
|
||||||
// MARK: - State Variables
|
private let requiresCurrentPassword: Bool
|
||||||
|
|
||||||
|
// MARK: - Dialog States
|
||||||
|
|
||||||
@State
|
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
@State
|
||||||
private var isPresentingSuccess: Bool = false
|
private var isPresentingSuccess: Bool = false
|
||||||
|
|
||||||
private let requiresCurrentPassword: Bool
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
@ -144,12 +151,9 @@ struct ResetUserPasswordView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .success:
|
case .success:
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
isPresentingSuccess = true
|
isPresentingSuccess = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,17 +162,6 @@ struct ResetUserPasswordView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel) {
|
|
||||||
focusedField = .newPassword
|
|
||||||
}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
L10n.success,
|
L10n.success,
|
||||||
isPresented: $isPresentingSuccess
|
isPresented: $isPresentingSuccess
|
||||||
|
@ -179,5 +172,8 @@ struct ResetUserPasswordView: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text(L10n.passwordChangedMessage)
|
Text(L10n.passwordChangedMessage)
|
||||||
}
|
}
|
||||||
|
.errorMessage($error) {
|
||||||
|
focusedField = .newPassword
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct SelectUserView: View {
|
struct SelectUserView: View {
|
||||||
|
|
||||||
private enum UserGridItem: Hashable {
|
// MARK: - Defaults
|
||||||
case user(UserState, server: ServerState)
|
|
||||||
case addUser
|
|
||||||
}
|
|
||||||
|
|
||||||
@Default(.selectUserUseSplashscreen)
|
@Default(.selectUserUseSplashscreen)
|
||||||
private var selectUserUseSplashscreen
|
private var selectUserUseSplashscreen
|
||||||
|
@ -37,29 +34,37 @@ struct SelectUserView: View {
|
||||||
@Default(.selectUserDisplayType)
|
@Default(.selectUserDisplayType)
|
||||||
private var userListDisplayType
|
private var userListDisplayType
|
||||||
|
|
||||||
|
// MARK: - Environment Variable
|
||||||
|
|
||||||
@Environment(\.colorScheme)
|
@Environment(\.colorScheme)
|
||||||
private var colorScheme
|
private var colorScheme
|
||||||
|
|
||||||
|
// MARK: - Focus Fields
|
||||||
|
|
||||||
|
private enum UserGridItem: Hashable {
|
||||||
|
case user(UserState, server: ServerState)
|
||||||
|
case addUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: SelectUserCoordinator.Router
|
private var router: SelectUserCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = SelectUserViewModel()
|
||||||
|
|
||||||
|
// MARK: - Select Users Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var contentSize: CGSize = .zero
|
private var contentSize: CGSize = .zero
|
||||||
@State
|
@State
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var gridItems: OrderedSet<UserGridItem> = []
|
private var gridItems: OrderedSet<UserGridItem> = []
|
||||||
@State
|
@State
|
||||||
private var gridItemSize: CGSize = .zero
|
private var gridItemSize: CGSize = .zero
|
||||||
@State
|
@State
|
||||||
private var isEditingUsers: Bool = false
|
private var isEditingUsers: Bool = false
|
||||||
@State
|
@State
|
||||||
private var isPresentingConfirmDeleteUsers = false
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingLocalPin: Bool = false
|
|
||||||
@State
|
|
||||||
private var padGridItemColumnCount: Int = 1
|
private var padGridItemColumnCount: Int = 1
|
||||||
@State
|
@State
|
||||||
private var pin: String = ""
|
private var pin: String = ""
|
||||||
|
@ -68,8 +73,19 @@ struct SelectUserView: View {
|
||||||
@State
|
@State
|
||||||
private var splashScreenImageSources: [ImageSource] = []
|
private var splashScreenImageSources: [ImageSource] = []
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Dialog States
|
||||||
private var viewModel = SelectUserViewModel()
|
|
||||||
|
@State
|
||||||
|
private var isPresentingConfirmDeleteUsers = false
|
||||||
|
@State
|
||||||
|
private var isPresentingLocalPin: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Select Server
|
||||||
|
|
||||||
private var selectedServer: ServerState? {
|
private var selectedServer: ServerState? {
|
||||||
if case let SelectUserServerSelection.server(id: id) = serverSelection,
|
if case let SelectUserServerSelection.server(id: id) = serverSelection,
|
||||||
|
@ -81,6 +97,8 @@ struct SelectUserView: View {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Make Grid Items
|
||||||
|
|
||||||
private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> {
|
private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> {
|
||||||
switch serverSelection {
|
switch serverSelection {
|
||||||
case .all:
|
case .all:
|
||||||
|
@ -110,6 +128,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Make Splash Screen Image Source
|
||||||
|
|
||||||
// For all server selection, .all is random
|
// For all server selection, .all is random
|
||||||
private func makeSplashScreenImageSources(
|
private func makeSplashScreenImageSources(
|
||||||
serverSelection: SelectUserServerSelection,
|
serverSelection: SelectUserServerSelection,
|
||||||
|
@ -135,13 +155,15 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Select User(s)
|
||||||
|
|
||||||
private func select(user: UserState, needsPin: Bool = true) {
|
private func select(user: UserState, needsPin: Bool = true) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
selectedUsers.insert(user)
|
selectedUsers.insert(user)
|
||||||
|
|
||||||
switch user.accessPolicy {
|
switch user.accessPolicy {
|
||||||
case .requireDeviceAuthentication:
|
case .requireDeviceAuthentication:
|
||||||
try await performDeviceAuthentication(reason: "User \(user.username) requires device authentication")
|
try await performDeviceAuthentication(reason: L10n.userRequiresDeviceAuthentication(user.username))
|
||||||
case .requirePin:
|
case .requirePin:
|
||||||
if needsPin {
|
if needsPin {
|
||||||
isPresentingLocalPin = true
|
isPresentingLocalPin = true
|
||||||
|
@ -154,6 +176,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Perform Device Authentication
|
||||||
|
|
||||||
// error logging/presentation is handled within here, just
|
// error logging/presentation is handled within here, just
|
||||||
// use try+thrown error in local Task for early return
|
// use try+thrown error in local Task for early return
|
||||||
private func performDeviceAuthentication(reason: String) async throws {
|
private func performDeviceAuthentication(reason: String) async throws {
|
||||||
|
@ -166,13 +190,10 @@ struct SelectUserView: View {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self
|
self
|
||||||
.error =
|
.error =
|
||||||
JellyfinAPIError(
|
JellyfinAPIError(L10n.unableToPerformDeviceAuthFaceID)
|
||||||
"Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."
|
|
||||||
)
|
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -181,15 +202,14 @@ struct SelectUserView: View {
|
||||||
viewModel.logger.critical("\(error.localizedDescription)")
|
viewModel.logger.critical("\(error.localizedDescription)")
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.error = JellyfinAPIError("Unable to perform device authentication")
|
self.error = JellyfinAPIError(L10n.unableToPerformDeviceAuth)
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: advancedMenu
|
// MARK: - Advanced Menu
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var advancedMenu: some View {
|
private var advancedMenu: some View {
|
||||||
|
@ -197,7 +217,7 @@ struct SelectUserView: View {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if gridItems.count > 1 {
|
if gridItems.count > 1 {
|
||||||
Button("Edit Users", systemImage: "person.crop.circle") {
|
Button(L10n.editUsers, systemImage: "person.crop.circle") {
|
||||||
isEditingUsers.toggle()
|
isEditingUsers.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +230,7 @@ struct SelectUserView: View {
|
||||||
.tag($0)
|
.tag($0)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("Layout")
|
Text(L10n.layout)
|
||||||
Text(userListDisplayType.displayTitle)
|
Text(userListDisplayType.displayTitle)
|
||||||
Image(systemName: userListDisplayType.systemImage)
|
Image(systemName: userListDisplayType.systemImage)
|
||||||
}
|
}
|
||||||
|
@ -225,7 +245,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: grid
|
// MARK: - iPad Grid Item Offset
|
||||||
|
|
||||||
private func padGridItemOffset(index: Int) -> CGFloat {
|
private func padGridItemOffset(index: Int) -> CGFloat {
|
||||||
let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)
|
let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)
|
||||||
|
@ -236,6 +256,8 @@ struct SelectUserView: View {
|
||||||
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
|
return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - iPad Grid Content View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var padGridContentView: some View {
|
private var padGridContentView: some View {
|
||||||
let columns = [GridItem(.adaptive(minimum: 150, maximum: 300), spacing: EdgeInsets.edgePadding)]
|
let columns = [GridItem(.adaptive(minimum: 150, maximum: 300), spacing: EdgeInsets.edgePadding)]
|
||||||
|
@ -258,6 +280,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - iPhone Grid Content View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var phoneGridContentView: some View {
|
private var phoneGridContentView: some View {
|
||||||
let columns = [GridItem(.flexible(), spacing: EdgeInsets.edgePadding), GridItem(.flexible())]
|
let columns = [GridItem(.flexible(), spacing: EdgeInsets.edgePadding), GridItem(.flexible())]
|
||||||
|
@ -275,6 +299,8 @@ struct SelectUserView: View {
|
||||||
.scrollIfLargerThanContainer(padding: 100)
|
.scrollIfLargerThanContainer(padding: 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Grid Item View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func gridItemView(for item: UserGridItem) -> some View {
|
private func gridItemView(for item: UserGridItem) -> some View {
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -307,7 +333,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: list
|
// MARK: - List Content View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listContentView: some View {
|
private var listContentView: some View {
|
||||||
|
@ -320,6 +346,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - List Item View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func listItemView(for item: UserGridItem) -> some View {
|
private func listItemView(for item: UserGridItem) -> some View {
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -352,6 +380,8 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete Users Button
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var deleteUsersButton: some View {
|
private var deleteUsersButton: some View {
|
||||||
Button {
|
Button {
|
||||||
|
@ -360,7 +390,7 @@ struct SelectUserView: View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.red
|
Color.red
|
||||||
|
|
||||||
Text("Delete")
|
Text(L10n.delete)
|
||||||
.font(.body.weight(.semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundStyle(selectedUsers.isNotEmpty ? .primary : .secondary)
|
.foregroundStyle(selectedUsers.isNotEmpty ? .primary : .secondary)
|
||||||
|
|
||||||
|
@ -377,7 +407,7 @@ struct SelectUserView: View {
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: userView
|
// MARK: - User View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var userView: some View {
|
private var userView: some View {
|
||||||
|
@ -449,7 +479,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: emptyView
|
// MARK: - Empty View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var emptyView: some View {
|
private var emptyView: some View {
|
||||||
|
@ -466,7 +496,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: body
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WrappedView {
|
WrappedView {
|
||||||
|
@ -477,7 +507,7 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||||
.navigationTitle("Users")
|
.navigationTitle(L10n.users)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
|
@ -555,9 +585,7 @@ struct SelectUserView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
self.error = eventError
|
self.error = eventError
|
||||||
self.isPresentingError = true
|
|
||||||
case let .signedIn(user):
|
case let .signedIn(user):
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
|
@ -589,37 +617,28 @@ struct SelectUserView: View {
|
||||||
selectUserAllServersSplashscreen = serverSelection
|
selectUserAllServersSplashscreen = serverSelection
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(
|
||||||
Text("Delete User"),
|
Text(L10n.deleteUser),
|
||||||
isPresented: $isPresentingConfirmDeleteUsers,
|
isPresented: $isPresentingConfirmDeleteUsers,
|
||||||
presenting: selectedUsers
|
presenting: selectedUsers
|
||||||
) { selectedUsers in
|
) { selectedUsers in
|
||||||
Button("Delete", role: .destructive) {
|
Button(L10n.delete, role: .destructive) {
|
||||||
viewModel.send(.deleteUsers(Array(selectedUsers)))
|
viewModel.send(.deleteUsers(Array(selectedUsers)))
|
||||||
}
|
}
|
||||||
} message: { selectedUsers in
|
} message: { selectedUsers in
|
||||||
if selectedUsers.count == 1, let first = selectedUsers.first {
|
if selectedUsers.count == 1, let first = selectedUsers.first {
|
||||||
Text("Are you sure you want to delete \(first.username)?")
|
Text(L10n.deleteUserSingleConfirmation(first.username))
|
||||||
} else {
|
} else {
|
||||||
Text("Are you sure you want to delete \(selectedUsers.count) users?")
|
Text(L10n.deleteUserMultipleConfirmation(selectedUsers.count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(L10n.signIn, isPresented: $isPresentingLocalPin) {
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .destructive)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert("Sign in", isPresented: $isPresentingLocalPin) {
|
|
||||||
|
|
||||||
TextField("Pin", text: $pin)
|
TextField(L10n.pin, text: $pin)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
// bug in SwiftUI: having .disabled will dismiss
|
// bug in SwiftUI: having .disabled will dismiss
|
||||||
// alert but not call the closure (for length)
|
// alert but not call the closure (for length)
|
||||||
Button("Sign In") {
|
Button(L10n.signIn) {
|
||||||
guard let user = selectedUsers.first else {
|
guard let user = selectedUsers.first else {
|
||||||
assertionFailure("User not selected")
|
assertionFailure("User not selected")
|
||||||
return
|
return
|
||||||
|
@ -628,15 +647,16 @@ struct SelectUserView: View {
|
||||||
select(user: user, needsPin: false)
|
select(user: user, needsPin: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {}
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
if let user = selectedUsers.first, user.pinHint.isNotEmpty {
|
if let user = selectedUsers.first, user.pinHint.isNotEmpty {
|
||||||
Text(user.pinHint)
|
Text(user.pinHint)
|
||||||
} else {
|
} else {
|
||||||
let username = selectedUsers.first?.username ?? .emptyDash
|
let username = selectedUsers.first?.username ?? .emptyDash
|
||||||
|
|
||||||
Text("Enter pin for \(username)")
|
Text(L10n.enterPinForUser(username))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,27 +12,41 @@ import SwiftUI
|
||||||
|
|
||||||
struct QuickConnectAuthorizeView: View {
|
struct QuickConnectAuthorizeView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
@EnvironmentObject
|
// MARK: - Focus Fields
|
||||||
private var router: SettingsCoordinator.Router
|
|
||||||
|
|
||||||
@FocusState
|
@FocusState
|
||||||
private var isCodeFocused: Bool
|
private var isCodeFocused: Bool
|
||||||
|
|
||||||
@State
|
// MARK: - State & Environment Objects
|
||||||
private var code: String = ""
|
|
||||||
@State
|
@EnvironmentObject
|
||||||
private var error: Error? = nil
|
private var router: SettingsCoordinator.Router
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingSuccess: Bool = false
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel = QuickConnectAuthorizeViewModel()
|
private var viewModel = QuickConnectAuthorizeViewModel()
|
||||||
|
|
||||||
|
// MARK: - Quick Connect Variables
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var code: String = ""
|
||||||
|
|
||||||
|
// MARK: - Dialog State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingSuccess: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -41,7 +55,7 @@ struct QuickConnectAuthorizeView: View {
|
||||||
.disabled(viewModel.state == .authorizing)
|
.disabled(viewModel.state == .authorizing)
|
||||||
.focused($isCodeFocused)
|
.focused($isCodeFocused)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Enter the 6 digit code from your other device.")
|
Text(L10n.quickConnectCodeInstruction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.state == .authorizing {
|
if viewModel.state == .authorizing {
|
||||||
|
@ -81,7 +95,6 @@ struct QuickConnectAuthorizeView: View {
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.topBarTrailing {
|
.topBarTrailing {
|
||||||
|
@ -89,17 +102,6 @@ struct QuickConnectAuthorizeView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .destructive) {
|
|
||||||
isCodeFocused = true
|
|
||||||
}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
L10n.quickConnect,
|
L10n.quickConnect,
|
||||||
isPresented: $isPresentingSuccess
|
isPresented: $isPresentingSuccess
|
||||||
|
@ -110,5 +112,8 @@ struct QuickConnectAuthorizeView: View {
|
||||||
} message: {
|
} message: {
|
||||||
L10n.quickConnectSuccessMessage.text
|
L10n.quickConnectSuccessMessage.text
|
||||||
}
|
}
|
||||||
|
.errorMessage($error) {
|
||||||
|
isCodeFocused = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,20 +19,21 @@ import SwiftUI
|
||||||
|
|
||||||
struct UserLocalSecurityView: View {
|
struct UserLocalSecurityView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: SettingsCoordinator.Router
|
private var router: SettingsCoordinator.Router
|
||||||
|
|
||||||
@State
|
@StateObject
|
||||||
private var error: Error? = nil
|
private var viewModel = UserLocalSecurityViewModel()
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
// MARK: - Local Security Variables
|
||||||
@State
|
|
||||||
private var isPresentingOldPinPrompt: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingNewPinPrompt: Bool = false
|
|
||||||
@State
|
@State
|
||||||
private var listSize: CGSize = .zero
|
private var listSize: CGSize = .zero
|
||||||
@State
|
@State
|
||||||
|
@ -44,8 +45,19 @@ struct UserLocalSecurityView: View {
|
||||||
@State
|
@State
|
||||||
private var signInPolicy: UserAccessPolicy = .none
|
private var signInPolicy: UserAccessPolicy = .none
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Dialog States
|
||||||
private var viewModel = UserLocalSecurityViewModel()
|
|
||||||
|
@State
|
||||||
|
private var isPresentingOldPinPrompt: Bool = false
|
||||||
|
@State
|
||||||
|
private var isPresentingNewPinPrompt: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Check Old Policy
|
||||||
|
|
||||||
private func checkOldPolicy() {
|
private func checkOldPolicy() {
|
||||||
do {
|
do {
|
||||||
|
@ -57,6 +69,8 @@ struct UserLocalSecurityView: View {
|
||||||
checkNewPolicy()
|
checkNewPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Check New Policy
|
||||||
|
|
||||||
private func checkNewPolicy() {
|
private func checkNewPolicy() {
|
||||||
do {
|
do {
|
||||||
try viewModel.checkFor(newPolicy: signInPolicy)
|
try viewModel.checkFor(newPolicy: signInPolicy)
|
||||||
|
@ -67,6 +81,8 @@ struct UserLocalSecurityView: View {
|
||||||
viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint)
|
viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Perform Device Authentication
|
||||||
|
|
||||||
// error logging/presentation is handled within here, just
|
// error logging/presentation is handled within here, just
|
||||||
// use try+thrown error in local Task for early return
|
// use try+thrown error in local Task for early return
|
||||||
private func performDeviceAuthentication(reason: String) async throws {
|
private func performDeviceAuthentication(reason: String) async throws {
|
||||||
|
@ -78,14 +94,10 @@ struct UserLocalSecurityView: View {
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self
|
self
|
||||||
.error =
|
.error = JellyfinAPIError(L10n.unableToPerformDeviceAuthFaceID)
|
||||||
JellyfinAPIError(
|
|
||||||
"Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."
|
|
||||||
)
|
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -94,24 +106,23 @@ struct UserLocalSecurityView: View {
|
||||||
viewModel.logger.critical("\(error.localizedDescription)")
|
viewModel.logger.critical("\(error.localizedDescription)")
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.error = JellyfinAPIError("Unable to perform device authentication")
|
self.error = JellyfinAPIError(L10n.unableToPerformDeviceAuth)
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
CaseIterablePicker("Security", selection: $signInPolicy)
|
CaseIterablePicker(L10n.security, selection: $signInPolicy)
|
||||||
} footer: {
|
} footer: {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(
|
Text(L10n.additionalSecurityAccessDescription)
|
||||||
"Additional security access for users signed in to this device. This does not change any Jellyfin server user settings."
|
|
||||||
)
|
|
||||||
|
|
||||||
// frame necessary with bug within BulletedList
|
// frame necessary with bug within BulletedList
|
||||||
BulletedList {
|
BulletedList {
|
||||||
|
@ -120,7 +131,7 @@ struct UserLocalSecurityView: View {
|
||||||
Text(UserAccessPolicy.requireDeviceAuthentication.displayTitle)
|
Text(UserAccessPolicy.requireDeviceAuthentication.displayTitle)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
Text("Require device authentication when signing in to the user.")
|
Text(L10n.requireDeviceAuthDescription)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 15)
|
.padding(.bottom, 15)
|
||||||
|
|
||||||
|
@ -128,7 +139,7 @@ struct UserLocalSecurityView: View {
|
||||||
Text(UserAccessPolicy.requirePin.displayTitle)
|
Text(UserAccessPolicy.requirePin.displayTitle)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
Text("Require a local pin when signing in to the user. This pin is unrecoverable.")
|
Text(L10n.requirePinDescription)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 15)
|
.padding(.bottom, 15)
|
||||||
|
|
||||||
|
@ -136,7 +147,7 @@ struct UserLocalSecurityView: View {
|
||||||
Text(UserAccessPolicy.none.displayTitle)
|
Text(UserAccessPolicy.none.displayTitle)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
Text("Save the user to this device without any local authentication.")
|
Text(L10n.saveUserWithoutAuthDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: max(10, listSize.width - 50))
|
.frame(width: max(10, listSize.width - 50))
|
||||||
|
@ -145,16 +156,16 @@ struct UserLocalSecurityView: View {
|
||||||
|
|
||||||
if signInPolicy == .requirePin {
|
if signInPolicy == .requirePin {
|
||||||
Section {
|
Section {
|
||||||
TextField("Hint", text: $pinHint)
|
TextField(L10n.hint, text: $pinHint)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Hint")
|
Text(L10n.hint)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Set a hint when prompting for the pin.")
|
Text(L10n.setPinHintDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.linear, value: signInPolicy)
|
.animation(.linear, value: signInPolicy)
|
||||||
.navigationTitle("Security")
|
.navigationTitle(L10n.security)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
pinHint = viewModel.userSession.user.pinHint
|
pinHint = viewModel.userSession.user.pinHint
|
||||||
|
@ -164,13 +175,11 @@ struct UserLocalSecurityView: View {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .promptForOldDeviceAuth:
|
case .promptForOldDeviceAuth:
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try await performDeviceAuthentication(
|
try await performDeviceAuthentication(
|
||||||
reason: "User \(viewModel.userSession.user.username) requires device authentication"
|
reason: L10n.userRequiresDeviceAuthentication(viewModel.userSession.user.username)
|
||||||
)
|
)
|
||||||
|
|
||||||
checkNewPolicy()
|
checkNewPolicy()
|
||||||
|
@ -189,7 +198,7 @@ struct UserLocalSecurityView: View {
|
||||||
case .promptForNewDeviceAuth:
|
case .promptForNewDeviceAuth:
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try await performDeviceAuthentication(
|
try await performDeviceAuthentication(
|
||||||
reason: "User \(viewModel.userSession.user.username) requires device authentication"
|
reason: L10n.userRequiresDeviceAuthentication(viewModel.userSession.user.username)
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: "")
|
viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: "")
|
||||||
|
@ -211,9 +220,9 @@ struct UserLocalSecurityView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Group {
|
Group {
|
||||||
if signInPolicy == .requirePin, signInPolicy == viewModel.userSession.user.accessPolicy {
|
if signInPolicy == .requirePin, signInPolicy == viewModel.userSession.user.accessPolicy {
|
||||||
Text("Change Pin")
|
Text(L10n.changePin)
|
||||||
} else {
|
} else {
|
||||||
Text("Save")
|
Text(L10n.save)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(accentColor.overlayColor)
|
.foregroundStyle(accentColor.overlayColor)
|
||||||
|
@ -228,51 +237,43 @@ struct UserLocalSecurityView: View {
|
||||||
}
|
}
|
||||||
.trackingSize($listSize)
|
.trackingSize($listSize)
|
||||||
.alert(
|
.alert(
|
||||||
L10n.error.text,
|
L10n.enterPin,
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
|
||||||
"Enter Pin",
|
|
||||||
isPresented: $isPresentingOldPinPrompt,
|
isPresented: $isPresentingOldPinPrompt,
|
||||||
presenting: onPinCompletion
|
presenting: onPinCompletion
|
||||||
) { completion in
|
) { completion in
|
||||||
|
|
||||||
TextField("Pin", text: $pin)
|
TextField(L10n.pin, text: $pin)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
// bug in SwiftUI: having .disabled will dismiss
|
// bug in SwiftUI: having .disabled will dismiss
|
||||||
// alert but not call the closure (for length)
|
// alert but not call the closure (for length)
|
||||||
Button("Continue") {
|
Button(L10n.continue) {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(L10n.cancel, role: .cancel) {}
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
} message: { _ in
|
} message: { _ in
|
||||||
Text("Enter pin for \(viewModel.userSession.user.username)")
|
Text(L10n.enterPinForUser(viewModel.userSession.user.username))
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(
|
||||||
"Set Pin",
|
L10n.setPin,
|
||||||
isPresented: $isPresentingNewPinPrompt,
|
isPresented: $isPresentingNewPinPrompt,
|
||||||
presenting: onPinCompletion
|
presenting: onPinCompletion
|
||||||
) { completion in
|
) { completion in
|
||||||
|
|
||||||
TextField("Pin", text: $pin)
|
TextField(L10n.pin, text: $pin)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
// bug in SwiftUI: having .disabled will dismiss
|
// bug in SwiftUI: having .disabled will dismiss
|
||||||
// alert but not call the closure (for length)
|
// alert but not call the closure (for length)
|
||||||
Button("Set") {
|
Button(L10n.set) {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(L10n.cancel, role: .cancel) {}
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
} message: { _ in
|
} message: { _ in
|
||||||
Text("Create a pin to sign in to \(viewModel.userSession.user.username) on this device")
|
Text(L10n.createPinForUser(viewModel.userSession.user.username))
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,23 +14,32 @@ extension UserProfileImagePicker {
|
||||||
|
|
||||||
struct SquareImageCropView: View {
|
struct SquareImageCropView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: UserProfileImageCoordinator.Router
|
private var router: UserProfileImageCoordinator.Router
|
||||||
|
|
||||||
@State
|
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var proxy: _SquareImageCropView.Proxy = .init()
|
private var proxy: _SquareImageCropView.Proxy = .init()
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel = UserProfileImageViewModel()
|
private var viewModel = UserProfileImageViewModel()
|
||||||
|
|
||||||
|
// MARK: - Image Variable
|
||||||
|
|
||||||
let image: UIImage
|
let image: UIImage
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
_SquareImageCropView(initialImage: image, proxy: proxy) {
|
_SquareImageCropView(initialImage: image, proxy: proxy) {
|
||||||
viewModel.send(.upload($0))
|
viewModel.send(.upload($0))
|
||||||
|
@ -41,7 +50,7 @@ extension UserProfileImagePicker {
|
||||||
.topBarTrailing {
|
.topBarTrailing {
|
||||||
|
|
||||||
if viewModel.state == .initial {
|
if viewModel.state == .initial {
|
||||||
Button("Rotate", systemImage: "rotate.right") {
|
Button(L10n.rotate, systemImage: "rotate.right") {
|
||||||
proxy.rotate()
|
proxy.rotate()
|
||||||
}
|
}
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
|
@ -56,7 +65,7 @@ extension UserProfileImagePicker {
|
||||||
Button {
|
Button {
|
||||||
proxy.crop()
|
proxy.crop()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Save")
|
Text(L10n.save)
|
||||||
.foregroundStyle(accentColor.overlayColor)
|
.foregroundStyle(accentColor.overlayColor)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
|
@ -73,7 +82,7 @@ extension UserProfileImagePicker {
|
||||||
if viewModel.state == .uploading {
|
if viewModel.state == .uploading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
Button("Reset") {
|
Button(L10n.reset) {
|
||||||
proxy.reset()
|
proxy.reset()
|
||||||
}
|
}
|
||||||
.foregroundStyle(.yellow)
|
.foregroundStyle(.yellow)
|
||||||
|
@ -89,23 +98,16 @@ extension UserProfileImagePicker {
|
||||||
switch event {
|
switch event {
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case .uploaded:
|
case .uploaded:
|
||||||
router.dismissCoordinator()
|
router.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.errorMessage($error)
|
||||||
L10n.error.text,
|
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .destructive)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Square Image Crop View
|
||||||
|
|
||||||
struct _SquareImageCropView: UIViewControllerRepresentable {
|
struct _SquareImageCropView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
class Proxy: ObservableObject {
|
class Proxy: ObservableObject {
|
||||||
|
|
|
@ -19,26 +19,29 @@ import SwiftUI
|
||||||
|
|
||||||
struct UserSignInView: View {
|
struct UserSignInView: View {
|
||||||
|
|
||||||
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
@EnvironmentObject
|
// MARK: - Focus Fields
|
||||||
private var router: UserSignInCoordinator.Router
|
|
||||||
|
|
||||||
@FocusState
|
@FocusState
|
||||||
private var focusedTextField: Int?
|
private var focusedTextField: Int?
|
||||||
|
|
||||||
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: UserSignInCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel: UserSignInViewModel
|
||||||
|
|
||||||
|
// MARK: - User Signin Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var duplicateUser: UserState? = nil
|
private var duplicateUser: UserState? = nil
|
||||||
@State
|
@State
|
||||||
private var error: Error? = nil
|
|
||||||
@State
|
|
||||||
private var isPresentingDuplicateUser: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingError: Bool = false
|
|
||||||
@State
|
|
||||||
private var isPresentingLocalPin: Bool = false
|
|
||||||
@State
|
|
||||||
private var onPinCompletion: (() -> Void)? = nil
|
private var onPinCompletion: (() -> Void)? = nil
|
||||||
@State
|
@State
|
||||||
private var password: String = ""
|
private var password: String = ""
|
||||||
|
@ -51,13 +54,26 @@ struct UserSignInView: View {
|
||||||
@State
|
@State
|
||||||
private var username: String = ""
|
private var username: String = ""
|
||||||
|
|
||||||
@StateObject
|
// MARK: - Error State
|
||||||
private var viewModel: UserSignInViewModel
|
|
||||||
|
@State
|
||||||
|
private var isPresentingDuplicateUser: Bool = false
|
||||||
|
@State
|
||||||
|
private var isPresentingLocalPin: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Error State
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var error: Error? = nil
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
init(server: ServerState) {
|
init(server: ServerState) {
|
||||||
self._viewModel = StateObject(wrappedValue: UserSignInViewModel(server: server))
|
self._viewModel = StateObject(wrappedValue: UserSignInViewModel(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Handle Sign In
|
||||||
|
|
||||||
private func handleSignIn(_ event: UserSignInViewModel.Event) {
|
private func handleSignIn(_ event: UserSignInViewModel.Event) {
|
||||||
switch event {
|
switch event {
|
||||||
case let .duplicateUser(duplicateUser):
|
case let .duplicateUser(duplicateUser):
|
||||||
|
@ -67,9 +83,7 @@ struct UserSignInView: View {
|
||||||
isPresentingDuplicateUser = true
|
isPresentingDuplicateUser = true
|
||||||
case let .error(eventError):
|
case let .error(eventError):
|
||||||
UIDevice.feedback(.error)
|
UIDevice.feedback(.error)
|
||||||
|
|
||||||
error = eventError
|
error = eventError
|
||||||
isPresentingError = true
|
|
||||||
case let .signedIn(user):
|
case let .signedIn(user):
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
|
@ -79,16 +93,15 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: don't have multiple ways to handle device authentication vs required pin
|
// MARK: - Open Quick Connect
|
||||||
|
|
||||||
|
// TODO: don't have multiple ways to handle device authentication vs required pin
|
||||||
private func openQuickConnect(needsPin: Bool = true) {
|
private func openQuickConnect(needsPin: Bool = true) {
|
||||||
Task {
|
Task {
|
||||||
switch accessPolicy {
|
switch accessPolicy {
|
||||||
case .none: ()
|
case .none: ()
|
||||||
case .requireDeviceAuthentication:
|
case .requireDeviceAuthentication:
|
||||||
try await performDeviceAuthentication(
|
try await performDeviceAuthentication(reason: L10n.requireDeviceAuthForQuickConnectUser)
|
||||||
reason: "Require device authentication to sign in to the Quick Connect user on this device"
|
|
||||||
)
|
|
||||||
case .requirePin:
|
case .requirePin:
|
||||||
if needsPin {
|
if needsPin {
|
||||||
onPinCompletion = {
|
onPinCompletion = {
|
||||||
|
@ -103,12 +116,14 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sign In User Password
|
||||||
|
|
||||||
private func signInUserPassword(needsPin: Bool = true) {
|
private func signInUserPassword(needsPin: Bool = true) {
|
||||||
Task {
|
Task {
|
||||||
switch accessPolicy {
|
switch accessPolicy {
|
||||||
case .none: ()
|
case .none: ()
|
||||||
case .requireDeviceAuthentication:
|
case .requireDeviceAuthentication:
|
||||||
try await performDeviceAuthentication(reason: "Require device authentication to sign in to \(username) on this device")
|
try await performDeviceAuthentication(reason: L10n.requireDeviceAuthForUser(username))
|
||||||
case .requirePin:
|
case .requirePin:
|
||||||
if needsPin {
|
if needsPin {
|
||||||
onPinCompletion = {
|
onPinCompletion = {
|
||||||
|
@ -123,12 +138,14 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func signInUplicate(user: UserState, needsPin: Bool = true, replace: Bool) {
|
// MARK: - Sign In Duplicate User
|
||||||
|
|
||||||
|
private func signInDuplicate(user: UserState, needsPin: Bool = true, replace: Bool) {
|
||||||
Task {
|
Task {
|
||||||
switch user.accessPolicy {
|
switch user.accessPolicy {
|
||||||
case .none: ()
|
case .none: ()
|
||||||
case .requireDeviceAuthentication:
|
case .requireDeviceAuthentication:
|
||||||
try await performDeviceAuthentication(reason: "User \(user.username) requires device authentication")
|
try await performDeviceAuthentication(reason: L10n.userRequiresDeviceAuthentication(user.username))
|
||||||
case .requirePin:
|
case .requirePin:
|
||||||
onPinCompletion = {
|
onPinCompletion = {
|
||||||
viewModel.send(.signInDuplicate(user, replace: replace))
|
viewModel.send(.signInDuplicate(user, replace: replace))
|
||||||
|
@ -141,14 +158,18 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Perform Pin Authentication
|
||||||
|
|
||||||
private func performPinAuthentication() async throws {
|
private func performPinAuthentication() async throws {
|
||||||
isPresentingLocalPin = true
|
isPresentingLocalPin = true
|
||||||
|
|
||||||
guard pin.count > 4, pin.count < 30 else {
|
guard pin.count > 4, pin.count < 30 else {
|
||||||
throw JellyfinAPIError("Pin auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Perform Device Authentication
|
||||||
|
|
||||||
// error logging/presentation is handled within here, just
|
// error logging/presentation is handled within here, just
|
||||||
// use try+thrown error in local Task for early return
|
// use try+thrown error in local Task for early return
|
||||||
private func performDeviceAuthentication(reason: String) async throws {
|
private func performDeviceAuthentication(reason: String) async throws {
|
||||||
|
@ -161,13 +182,10 @@ struct UserSignInView: View {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self
|
self
|
||||||
.error =
|
.error =
|
||||||
JellyfinAPIError(
|
JellyfinAPIError(L10n.unableToPerformDeviceAuthFaceID)
|
||||||
"Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."
|
|
||||||
)
|
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -176,14 +194,15 @@ struct UserSignInView: View {
|
||||||
viewModel.logger.critical("\(error.localizedDescription)")
|
viewModel.logger.critical("\(error.localizedDescription)")
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.error = JellyfinAPIError("Unable to perform device authentication")
|
self.error = JellyfinAPIError(L10n.unableToPerformDeviceAuth)
|
||||||
self.isPresentingError = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw JellyfinAPIError("Device auth failed")
|
throw JellyfinAPIError(L10n.deviceAuthFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sign In Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var signInSection: some View {
|
private var signInSection: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -208,10 +227,10 @@ struct UserSignInView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
switch accessPolicy {
|
switch accessPolicy {
|
||||||
case .requireDeviceAuthentication:
|
case .requireDeviceAuthentication:
|
||||||
Label("This user will require device authentication.", systemImage: "exclamationmark.circle.fill")
|
Label(L10n.userDeviceAuthRequiredDescription, systemImage: "exclamationmark.circle.fill")
|
||||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
case .requirePin:
|
case .requirePin:
|
||||||
Label("This user will require a pin.", systemImage: "exclamationmark.circle.fill")
|
Label(L10n.userPinRequiredDescription, systemImage: "exclamationmark.circle.fill")
|
||||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
@ -251,13 +270,15 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let disclaimer = viewModel.serverDisclaimer {
|
if let disclaimer = viewModel.serverDisclaimer {
|
||||||
Section("Disclaimer") {
|
Section(L10n.disclaimer) {
|
||||||
Text(disclaimer)
|
Text(disclaimer)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Users Section
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var publicUsersSection: some View {
|
private var publicUsersSection: some View {
|
||||||
Section(L10n.publicUsers) {
|
Section(L10n.publicUsers) {
|
||||||
|
@ -281,6 +302,8 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
signInSection
|
signInSection
|
||||||
|
@ -324,7 +347,7 @@ struct UserSignInView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Security", systemImage: "gearshape.fill") {
|
Button(L10n.security, systemImage: "gearshape.fill") {
|
||||||
let parameters = UserSignInCoordinator.SecurityParameters(
|
let parameters = UserSignInCoordinator.SecurityParameters(
|
||||||
pinHint: $pinHint,
|
pinHint: $pinHint,
|
||||||
accessPolicy: $accessPolicy
|
accessPolicy: $accessPolicy
|
||||||
|
@ -333,51 +356,43 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(
|
||||||
Text("Duplicate User"),
|
Text(L10n.duplicateUser),
|
||||||
isPresented: $isPresentingDuplicateUser,
|
isPresented: $isPresentingDuplicateUser,
|
||||||
presenting: duplicateUser
|
presenting: duplicateUser
|
||||||
) { _ in
|
) { _ in
|
||||||
|
|
||||||
// TODO: uncomment when duplicate user fixed
|
// TODO: uncomment when duplicate user fixed
|
||||||
// Button(L10n.signIn) {
|
// Button(L10n.signIn) {
|
||||||
// signInUplicate(user: user, replace: false)
|
// signInDuplicate(user: user, replace: false)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Button("Replace") {
|
// Button("Replace") {
|
||||||
// signInUplicate(user: user, replace: true)
|
// signInDuplicate(user: user, replace: true)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
Button(L10n.dismiss, role: .cancel)
|
||||||
} message: { duplicateUser in
|
} message: { duplicateUser in
|
||||||
Text("\(duplicateUser.username) is already saved")
|
Text(L10n.duplicateUserSaved(duplicateUser.username))
|
||||||
}
|
}
|
||||||
.alert(
|
.alert(
|
||||||
L10n.error.text,
|
L10n.setPin,
|
||||||
isPresented: $isPresentingError,
|
|
||||||
presenting: error
|
|
||||||
) { _ in
|
|
||||||
Button(L10n.dismiss, role: .cancel)
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.alert(
|
|
||||||
"Set Pin",
|
|
||||||
isPresented: $isPresentingLocalPin,
|
isPresented: $isPresentingLocalPin,
|
||||||
presenting: onPinCompletion
|
presenting: onPinCompletion
|
||||||
) { completion in
|
) { completion in
|
||||||
|
|
||||||
TextField("Pin", text: $pin)
|
TextField(L10n.pin, text: $pin)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
// bug in SwiftUI: having .disabled will dismiss
|
// bug in SwiftUI: having .disabled will dismiss
|
||||||
// alert but not call the closure (for length)
|
// alert but not call the closure (for length)
|
||||||
Button("Sign In") {
|
Button(L10n.signIn) {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(L10n.cancel, role: .cancel) {}
|
Button(L10n.cancel, role: .cancel) {}
|
||||||
} message: { _ in
|
} message: { _ in
|
||||||
Text("Set pin for new user.")
|
Text(L10n.setPinForNewUser)
|
||||||
}
|
}
|
||||||
|
.errorMessage($error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2048,3 +2048,135 @@
|
||||||
// Translator - Enum
|
// Translator - Enum
|
||||||
// Represents a translator
|
// Represents a translator
|
||||||
"translator" = "Translator";
|
"translator" = "Translator";
|
||||||
|
|
||||||
|
// Loading User Failed - Error Message
|
||||||
|
// Displayed when loading user data fails
|
||||||
|
"loadingUserFailed" = "Loading user failed";
|
||||||
|
|
||||||
|
// Pin - Personal Identification Number
|
||||||
|
// Abbreviation to describe the login code for users
|
||||||
|
"pin" = "Pin";
|
||||||
|
|
||||||
|
// Are You Sure Delete Single User - Alert Message
|
||||||
|
// Message asking for confirmation when deleting a single user
|
||||||
|
"deleteUserSingleConfirmation" = "Are you sure you want to delete %@?";
|
||||||
|
|
||||||
|
// Are You Sure Delete Multiple Users - Alert Message
|
||||||
|
// Message asking for confirmation when deleting multiple users
|
||||||
|
"deleteUserMultipleConfirmation" = "Are you sure you want to delete %d users?";
|
||||||
|
|
||||||
|
// Enter Pin - Alert Message
|
||||||
|
// Message asking for a PIN to sign in for a specific user
|
||||||
|
"enterPinForUser" = "Enter PIN for %@";
|
||||||
|
|
||||||
|
// Layout - Label
|
||||||
|
// Label for selecting a display layout in the advanced menu
|
||||||
|
"layout" = "Layout";
|
||||||
|
|
||||||
|
// User Requires Device Authentication - Error Message
|
||||||
|
// Message indicating that a specific user requires device authentication
|
||||||
|
"userRequiresDeviceAuthentication" = "User %@ requires device authentication";
|
||||||
|
|
||||||
|
// Unable to Perform Device Authentication - Error Message
|
||||||
|
// Informs the user that device authentication is not possible and suggests enabling Face ID in the Settings app for Swiftfin
|
||||||
|
"unableToPerformDeviceAuthFaceID" = "Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin.";
|
||||||
|
|
||||||
|
// Device Authentication Failed - Error Message
|
||||||
|
// Indicates that device authentication has failed
|
||||||
|
"deviceAuthFailed" = "Device authentication failed";
|
||||||
|
|
||||||
|
// Unable to Perform Device Authentication - Error Message
|
||||||
|
// Indicates that device authentication cannot be performed
|
||||||
|
"unableToPerformDeviceAuth" = "Unable to perform device authentication";
|
||||||
|
|
||||||
|
// Rotate - Button
|
||||||
|
// Label for an action that rotates an element
|
||||||
|
"rotate" = "Rotate";
|
||||||
|
|
||||||
|
// Quick Connect Code - Instruction
|
||||||
|
// Prompts the user to enter a 6-digit Quick Connect code from another device
|
||||||
|
"quickConnectCodeInstruction" = "Enter the 6 digit code from your other device.";
|
||||||
|
|
||||||
|
// Security - Section Title
|
||||||
|
// Title for sections or settings related to security features
|
||||||
|
"security" = "Security";
|
||||||
|
|
||||||
|
// Additional Security Access - Description
|
||||||
|
// Explains additional security options for users signed in to the current device, without affecting Jellyfin server settings
|
||||||
|
"additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.";
|
||||||
|
|
||||||
|
// Hint - Label
|
||||||
|
// Label for a field or section providing additional guidance or information
|
||||||
|
"hint" = "Hint";
|
||||||
|
|
||||||
|
// Set - Button
|
||||||
|
// Button label for confirming or applying a setting
|
||||||
|
"set" = "Set";
|
||||||
|
|
||||||
|
// Create PIN - Instruction
|
||||||
|
// Prompts the user to create a PIN to sign in to a specific user account on the device
|
||||||
|
"createPinForUser" = "Create a pin to sign in to %@ on this device";
|
||||||
|
|
||||||
|
// Set PIN - Button
|
||||||
|
// Button label for setting a PIN
|
||||||
|
"setPin" = "Set Pin";
|
||||||
|
|
||||||
|
// Enter PIN - Instruction
|
||||||
|
// Prompts the user to enter their PIN
|
||||||
|
"enterPin" = "Enter Pin";
|
||||||
|
|
||||||
|
// Change PIN - Button
|
||||||
|
// Button label for changing an existing PIN
|
||||||
|
"changePin" = "Change Pin";
|
||||||
|
|
||||||
|
// PIN Hint - Description
|
||||||
|
// Explains the option to set a hint when prompting for the PIN
|
||||||
|
"setPinHintDescription" = "Set a hint when prompting for the pin.";
|
||||||
|
|
||||||
|
// Save User Without Local Authentication - Description
|
||||||
|
// Explains the option to save a user without requiring local authentication
|
||||||
|
"saveUserWithoutAuthDescription" = "Save the user to this device without any local authentication.";
|
||||||
|
|
||||||
|
// Require PIN - Description
|
||||||
|
// Explains the option to require a local PIN when signing in
|
||||||
|
"requirePinDescription" = "Require a local pin when signing in to the user. This pin is unrecoverable.";
|
||||||
|
|
||||||
|
// Require Device Authentication - Description
|
||||||
|
// Explains the option to require device authentication when signing in
|
||||||
|
"requireDeviceAuthDescription" = "Require device authentication when signing in to the user.";
|
||||||
|
|
||||||
|
// Set PIN for New User - Instruction
|
||||||
|
// Prompts the user to set a PIN for a new user account
|
||||||
|
"setPinForNewUser" = "Set pin for new user.";
|
||||||
|
|
||||||
|
// Duplicate User Saved - Error Message
|
||||||
|
// Indicates that the specified user is already saved on the device
|
||||||
|
"duplicateUserSaved" = "%@ is already saved";
|
||||||
|
|
||||||
|
// Duplicate User - Error Title
|
||||||
|
// Title for an error indicating a duplicate user
|
||||||
|
"duplicateUser" = "Duplicate User";
|
||||||
|
|
||||||
|
// Disclaimer - Section Title
|
||||||
|
// Title for a section providing important information or warnings
|
||||||
|
"disclaimer" = "Disclaimer";
|
||||||
|
|
||||||
|
// PIN Required - Description
|
||||||
|
// Indicates that the user will require a PIN for authentication
|
||||||
|
"userPinRequiredDescription" = "This user will require a pin.";
|
||||||
|
|
||||||
|
// Device Authentication Required - Description
|
||||||
|
// Indicates that the user will require device authentication
|
||||||
|
"userDeviceAuthRequiredDescription" = "This user will require device authentication.";
|
||||||
|
|
||||||
|
// Require Device Authentication for User - Description
|
||||||
|
// Explains that device authentication is required to sign in to a specific user on this device
|
||||||
|
"requireDeviceAuthForUser" = "Require device authentication to sign in to %@ on this device.";
|
||||||
|
|
||||||
|
// Require Device Authentication for Quick Connect User - Description
|
||||||
|
// Explains that device authentication is required to sign in to the Quick Connect user on this device
|
||||||
|
"requireDeviceAuthForQuickConnectUser" = "Require device authentication to sign in to the Quick Connect user on this device.";
|
||||||
|
|
||||||
|
// Server Already Connected - Error Message
|
||||||
|
// Indicates that the specified server is already connected
|
||||||
|
"serverAlreadyConnected" = "%@ is already connected.";
|
||||||
|
|
Loading…
Reference in New Issue