diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index 7dd1ee7c..30cc3757 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -71,6 +71,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { var userLiveTVAccess = makeUserLiveTVAccess @Route(.modal) var userPermissions = makeUserPermissions + @Route(.push) + var quickConnectAuthorize = makeQuickConnectAuthorize @Route(.modal) var userParentalRatings = makeUserParentalRatings @Route(.modal) @@ -234,6 +236,11 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { } } + @ViewBuilder + func makeQuickConnectAuthorize(user: UserDto) -> some View { + QuickConnectAuthorizeView(user: user) + } + // MARK: - Views: API Keys @ViewBuilder diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 57167b74..10750aa1 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -112,8 +112,8 @@ final class SettingsCoordinator: NavigationCoordinatable { } @ViewBuilder - func makeQuickConnectAuthorize() -> some View { - QuickConnectAuthorizeView() + func makeQuickConnectAuthorize(user: UserDto) -> some View { + QuickConnectAuthorizeView(user: user) } func makeResetUserPassword(userID: String) -> NavigationViewCoordinator { diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 7797a308..cc5dcc43 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -94,14 +94,8 @@ internal enum L10n { internal static let allLanguages = L10n.tr("Localizable", "allLanguages", fallback: "All languages") /// All Media internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media") - /// Allow collection management - internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management") /// Allowed internal static let allowed = L10n.tr("Localizable", "allowed", fallback: "Allowed") - /// Allow media item deletion - internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion") - /// Allow media item editing - internal static let allowItemEditing = L10n.tr("Localizable", "allowItemEditing", fallback: "Allow media item editing") /// All Servers internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers") /// View and manage all registered users on the server, including their permissions and activity status. @@ -440,6 +434,8 @@ internal enum L10n { internal static let deleteItemConfirmation = L10n.tr("Localizable", "deleteItemConfirmation", fallback: "Are you sure you want to delete this item?") /// Are you sure you want to delete this item? This action cannot be undone. internal static let deleteItemConfirmationMessage = L10n.tr("Localizable", "deleteItemConfirmationMessage", fallback: "Are you sure you want to delete this item? This action cannot be undone.") + /// Delete media + internal static let deleteMedia = L10n.tr("Localizable", "deleteMedia", fallback: "Delete media") /// Delete Schedule internal static let deleteSchedule = L10n.tr("Localizable", "deleteSchedule", fallback: "Delete Schedule") /// Are you sure you wish to delete this schedule? @@ -544,6 +540,10 @@ internal enum L10n { internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD") /// Edit internal static let edit = L10n.tr("Localizable", "edit", fallback: "Edit") + /// Edit Collections + internal static let editCollections = L10n.tr("Localizable", "editCollections", fallback: "Edit Collections") + /// Edit media + internal static let editMedia = L10n.tr("Localizable", "editMedia", fallback: "Edit media") /// Editor internal static let editor = L10n.tr("Localizable", "editor", fallback: "Editor") /// Edit Server @@ -1056,6 +1056,8 @@ internal enum L10n { internal static let quickConnectStep3 = L10n.tr("Localizable", "quickConnectStep3", fallback: "Enter the following code:") /// Authorizing Quick Connect successful. Please continue on your other device. internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: "Authorizing Quick Connect successful. Please continue on your other device.") + /// This user will be authenticated to the other device. + internal static let quickConnectUserDisclaimer = L10n.tr("Localizable", "quickConnectUserDisclaimer", fallback: "This user will be authenticated to the other device.") /// Random internal static let random = L10n.tr("Localizable", "random", fallback: "Random") /// Random image diff --git a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift index c3a6f42e..efc14d44 100644 --- a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift +++ b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift @@ -22,7 +22,7 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful { // MARK: Action enum Action: Equatable { - case authorize(String) + case authorize(code: String) case cancel } @@ -45,6 +45,12 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful { private var authorizeTask: AnyCancellable? private var eventSubject: PassthroughSubject = .init() + let user: UserDto + + init(user: UserDto) { + self.user = user + } + func respond(to action: Action) -> State { switch action { case let .authorize(code): @@ -53,7 +59,7 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful { try? await Task.sleep(nanoseconds: 10_000_000_000) do { - try await authorize(code: code) + try await authorize(code: code, userID: user.id) await MainActor.run { self.eventSubject.send(.authorized) @@ -76,8 +82,8 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful { } } - private func authorize(code: String) async throws { - let request = Paths.authorize(code: code) + private func authorize(code: String, userID: String? = nil) async throws { + let request = Paths.authorizeQuickConnect(code: code, userID: userID) let response = try await userSession.client.send(request) let decoder = JSONDecoder() diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index f2937091..1498c4c5 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -43,15 +43,15 @@ extension CustomizeViewsSettings { /// Enable Refreshing Items from All Visible LIbraries if userSession?.user.permissions.items.canEditMetadata ?? false { - Toggle(L10n.allowItemEditing, isOn: $enableItemEditing) + Toggle(L10n.editMedia, isOn: $enableItemEditing) } /// Enable Deleting Items from Approved Libraries if userSession?.user.permissions.items.canDelete ?? false { - Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion) + Toggle(L10n.deleteMedia, isOn: $enableItemDeletion) } /// Enable Refreshing & Deleting Collections if userSession?.user.permissions.items.canManageCollections ?? false { - Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement) + Toggle(L10n.editCollections, isOn: $enableCollectionManagement) } } } diff --git a/Swiftfin/Components/ListRowButton.swift b/Swiftfin/Components/ListRowButton.swift index dc17bb2d..544742ea 100644 --- a/Swiftfin/Components/ListRowButton.swift +++ b/Swiftfin/Components/ListRowButton.swift @@ -44,7 +44,7 @@ private struct ListRowButtonStyle: ButtonStyle { } private func secondaryStyle(configuration: Configuration) -> some ShapeStyle { - if configuration.role == .destructive { + if configuration.role == .destructive || configuration.role == .cancel { return AnyShapeStyle(Color.red.opacity(0.2)) } else { return isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray) diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift index 7072b256..9eca6f33 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift @@ -81,13 +81,16 @@ struct ServerUserDetailsView: View { } } } + ChevronButton(L10n.permissions) { + router.route(to: \.userPermissions, viewModel) + } if let userId = viewModel.user.id { ChevronButton(L10n.password) { router.route(to: \.resetUserPassword, userId) } - } - ChevronButton(L10n.permissions) { - router.route(to: \.userPermissions, viewModel) + ChevronButton(L10n.quickConnect) { + router.route(to: \.quickConnectAuthorize, viewModel.user) + } } } diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index dc960895..96df806f 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -46,19 +46,19 @@ extension CustomizeViewsSettings { /// Enable Editing Items from All Visible LIbraries if userSession?.user.permissions.items.canEditMetadata ?? false { - Toggle(L10n.allowItemEditing, isOn: $enableItemEditing) + Toggle(L10n.editMedia, isOn: $enableItemEditing) } /// Enable Deleting Items from Approved Libraries if userSession?.user.permissions.items.canDelete ?? false { - Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion) + Toggle(L10n.deleteMedia, isOn: $enableItemDeletion) } /// Enable Downloading All Items /* if userSession?.user.permissions.items.canDownload ?? false { - Toggle(L10n.allowItemDownloading, isOn: $enableItemDownloads) + Toggle(L10n.itemDownloading, isOn: $enableItemDownloads) } */ /// Enable Deleting or Editing Collections if userSession?.user.permissions.items.canManageCollections ?? false { - Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement) + Toggle(L10n.editCollections, isOn: $enableCollectionManagement) } /// Manage Item Lyrics /* if userSession?.user.permissions.items.canManageLyrics ?? false { diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift index b15a5e0a..c82a7783 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift @@ -8,10 +8,16 @@ import Defaults import Foundation +import JellyfinAPI import SwiftUI struct QuickConnectAuthorizeView: View { + // MARK: - Dismiss Environment + + @Environment(\.dismiss) + private var dismiss + // MARK: - Defaults @Default(.accentColor) @@ -24,11 +30,8 @@ struct QuickConnectAuthorizeView: View { // MARK: - State & Environment Objects - @EnvironmentObject - private var router: SettingsCoordinator.Router - @StateObject - private var viewModel = QuickConnectAuthorizeViewModel() + private var viewModel: QuickConnectAuthorizeViewModel // MARK: - Quick Connect Variables @@ -45,10 +48,46 @@ struct QuickConnectAuthorizeView: View { @State private var error: Error? = nil + // MARK: - Initialize + + init(user: UserDto) { + self._viewModel = StateObject(wrappedValue: QuickConnectAuthorizeViewModel(user: user)) + } + + // MARK: Display the User Being Authenticated + + @ViewBuilder + private var loginUserRow: some View { + HStack { + UserProfileImage( + userID: viewModel.user.id, + source: viewModel.user.profileImageSource( + client: viewModel.userSession.client, + maxWidth: 120 + ) + ) + .frame(width: 50, height: 50) + + Text(viewModel.user.name ?? L10n.unknown) + .fontWeight(.semibold) + .foregroundStyle(.primary) + + Spacer() + } + } + // MARK: - Body var body: some View { Form { + Section { + loginUserRow + } header: { + Text(L10n.user) + } footer: { + Text(L10n.quickConnectUserDisclaimer) + } + Section { TextField(L10n.quickConnectCode, text: $code) .keyboardType(.numberPad) @@ -59,14 +98,13 @@ struct QuickConnectAuthorizeView: View { } if viewModel.state == .authorizing { - ListRowButton(L10n.cancel) { + ListRowButton(L10n.cancel, role: .cancel) { viewModel.send(.cancel) isCodeFocused = true } - .foregroundStyle(.red, .red.opacity(0.2)) } else { ListRowButton(L10n.authorize) { - viewModel.send(.authorize(code)) + viewModel.send(.authorize(code: code)) } .disabled(code.count != 6 || viewModel.state == .authorizing) .foregroundStyle( @@ -107,7 +145,7 @@ struct QuickConnectAuthorizeView: View { isPresented: $isPresentingSuccess ) { Button(L10n.dismiss, role: .cancel) { - router.pop() + dismiss() } } message: { L10n.quickConnectSuccessMessage.text diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift index 245c57df..16f30ff1 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -45,7 +45,7 @@ struct UserProfileSettingsView: View { Section { ChevronButton(L10n.quickConnect) { - router.route(to: \.quickConnect) + router.route(to: \.quickConnect, viewModel.userSession.user.data) } ChevronButton(L10n.password) { diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index cae61aec..598a4483 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ