[iOS] Admin Dashboard - QuickConnect Other User (#1488)

* Allow other user authorization.

* Show user being logged in.

* Fix localizations & update screenshot

* Cleanup Locales

* mirror lable changes on tvOS

* cleanup

* fix strings

* adjust sizes

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-04-14 15:19:32 -06:00 committed by GitHub
parent 93033262d6
commit 8c4fde87f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 88 additions and 32 deletions

View File

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

View File

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

View File

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

View File

@ -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<Event, Never> = .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()

View File

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

View File

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

View File

@ -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.quickConnect) {
router.route(to: \.quickConnectAuthorize, viewModel.user)
}
ChevronButton(L10n.permissions) {
router.route(to: \.userPermissions, viewModel)
}
}

View File

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

View File

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

View File

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