[iOS & tvOS] Error Cleanup (#1357)

* Error Cleanup

* Localize everything!

* cleanup

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2024-12-10 23:23:05 -07:00 committed by GitHub
parent a6c1908b87
commit 8f05169097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 738 additions and 471 deletions

View File

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

View File

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

View File

@ -43,7 +43,6 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl
enum State: Hashable {
case initial
case content
case updating
case error(JellyfinAPIError)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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