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