diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index b182cf92..d34db0b1 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -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) -> 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) -> 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) -> 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 diff --git a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift index c3561271..91878612 100644 --- a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift @@ -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() diff --git a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift index ccb3ceca..f3eba6f1 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift @@ -43,7 +43,6 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl enum State: Hashable { case initial case content - case updating case error(JellyfinAPIError) } diff --git a/Swiftfin tvOS/Views/ConnectToServerView.swift b/Swiftfin tvOS/Views/ConnectToServerView.swift index bb9cb8f6..d9c1793e 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView.swift @@ -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) } } diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index e0899e7b..da61e0c8 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -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 = [] @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 { 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) } } diff --git a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift index 1ccd8470..790e9f63 100644 --- a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift @@ -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) } } diff --git a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift index e423d194..adc62b1e 100644 --- a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift +++ b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift @@ -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) { - focusedfield = .username - } - } message: { error in - Text(error.localizedDescription) + .errorMessage($error) { + focusedfield = .username } } } diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift index 2839bfd3..538bf2f5 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift @@ -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) } } diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift index 62e36d03..b420e7c0 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift @@ -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() } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift index f6926c65..8695886b 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift @@ -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 diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index af7a4376..2781c9f7 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -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 diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift index 1d647b64..e8b0000c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift @@ -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 diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift index 4dd6df05..e08d51a1 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift @@ -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 } diff --git a/Swiftfin/Views/ConnectToServerView.swift b/Swiftfin/Views/ConnectToServerView.swift index 3de3934d..23ce660f 100644 --- a/Swiftfin/Views/ConnectToServerView.swift +++ b/Swiftfin/Views/ConnectToServerView.swift @@ -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) } } diff --git a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift index 126ae8e4..9a5c0d29 100644 --- a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift +++ b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift @@ -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 + } } } diff --git a/Swiftfin/Views/SelectUserView/SelectUserView.swift b/Swiftfin/Views/SelectUserView/SelectUserView.swift index 00ee2a42..c36b2cc8 100644 --- a/Swiftfin/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin/Views/SelectUserView/SelectUserView.swift @@ -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 = [] @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 { 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) } } diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift index de793931..a7af01cc 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift @@ -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 + } } } diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift index db4f0c35..7c33f531 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -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) } } diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift index d66907a3..57726e1c 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift @@ -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 { diff --git a/Swiftfin/Views/UserSignInView/UserSignInView.swift b/Swiftfin/Views/UserSignInView/UserSignInView.swift index 4d63febc..7be62cad 100644 --- a/Swiftfin/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/UserSignInView.swift @@ -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) } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 25ebb800..b835412e 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -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.";