[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:
parent
93033262d6
commit
8c4fde87f1
|
@ -71,6 +71,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
var userLiveTVAccess = makeUserLiveTVAccess
|
var userLiveTVAccess = makeUserLiveTVAccess
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userPermissions = makeUserPermissions
|
var userPermissions = makeUserPermissions
|
||||||
|
@Route(.push)
|
||||||
|
var quickConnectAuthorize = makeQuickConnectAuthorize
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var userParentalRatings = makeUserParentalRatings
|
var userParentalRatings = makeUserParentalRatings
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
|
@ -234,6 +236,11 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeQuickConnectAuthorize(user: UserDto) -> some View {
|
||||||
|
QuickConnectAuthorizeView(user: user)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Views: API Keys
|
// MARK: - Views: API Keys
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -112,8 +112,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeQuickConnectAuthorize() -> some View {
|
func makeQuickConnectAuthorize(user: UserDto) -> some View {
|
||||||
QuickConnectAuthorizeView()
|
QuickConnectAuthorizeView(user: user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||||
|
|
|
@ -94,14 +94,8 @@ internal enum L10n {
|
||||||
internal static let allLanguages = L10n.tr("Localizable", "allLanguages", fallback: "All languages")
|
internal static let allLanguages = L10n.tr("Localizable", "allLanguages", fallback: "All languages")
|
||||||
/// All Media
|
/// All Media
|
||||||
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "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
|
/// Allowed
|
||||||
internal static let allowed = L10n.tr("Localizable", "allowed", fallback: "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
|
/// All Servers
|
||||||
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "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.
|
/// 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?")
|
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.
|
/// 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.")
|
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
|
/// Delete Schedule
|
||||||
internal static let deleteSchedule = L10n.tr("Localizable", "deleteSchedule", fallback: "Delete Schedule")
|
internal static let deleteSchedule = L10n.tr("Localizable", "deleteSchedule", fallback: "Delete Schedule")
|
||||||
/// Are you sure you wish to delete this 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")
|
internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD")
|
||||||
/// Edit
|
/// Edit
|
||||||
internal static let edit = L10n.tr("Localizable", "edit", fallback: "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
|
/// Editor
|
||||||
internal static let editor = L10n.tr("Localizable", "editor", fallback: "Editor")
|
internal static let editor = L10n.tr("Localizable", "editor", fallback: "Editor")
|
||||||
/// Edit Server
|
/// Edit Server
|
||||||
|
@ -1056,6 +1056,8 @@ internal enum L10n {
|
||||||
internal static let quickConnectStep3 = L10n.tr("Localizable", "quickConnectStep3", fallback: "Enter the following code:")
|
internal static let quickConnectStep3 = L10n.tr("Localizable", "quickConnectStep3", fallback: "Enter the following code:")
|
||||||
/// Authorizing Quick Connect successful. Please continue on your other device.
|
/// 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.")
|
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
|
/// Random
|
||||||
internal static let random = L10n.tr("Localizable", "random", fallback: "Random")
|
internal static let random = L10n.tr("Localizable", "random", fallback: "Random")
|
||||||
/// Random image
|
/// Random image
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful {
|
||||||
// MARK: Action
|
// MARK: Action
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
case authorize(String)
|
case authorize(code: String)
|
||||||
case cancel
|
case cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful {
|
||||||
private var authorizeTask: AnyCancellable?
|
private var authorizeTask: AnyCancellable?
|
||||||
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
||||||
|
|
||||||
|
let user: UserDto
|
||||||
|
|
||||||
|
init(user: UserDto) {
|
||||||
|
self.user = user
|
||||||
|
}
|
||||||
|
|
||||||
func respond(to action: Action) -> State {
|
func respond(to action: Action) -> State {
|
||||||
switch action {
|
switch action {
|
||||||
case let .authorize(code):
|
case let .authorize(code):
|
||||||
|
@ -53,7 +59,7 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful {
|
||||||
try? await Task.sleep(nanoseconds: 10_000_000_000)
|
try? await Task.sleep(nanoseconds: 10_000_000_000)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await authorize(code: code)
|
try await authorize(code: code, userID: user.id)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.eventSubject.send(.authorized)
|
self.eventSubject.send(.authorized)
|
||||||
|
@ -76,8 +82,8 @@ final class QuickConnectAuthorizeViewModel: ViewModel, Eventful, Stateful {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func authorize(code: String) async throws {
|
private func authorize(code: String, userID: String? = nil) async throws {
|
||||||
let request = Paths.authorize(code: code)
|
let request = Paths.authorizeQuickConnect(code: code, userID: userID)
|
||||||
let response = try await userSession.client.send(request)
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
|
|
|
@ -43,15 +43,15 @@ extension CustomizeViewsSettings {
|
||||||
|
|
||||||
/// Enable Refreshing Items from All Visible LIbraries
|
/// Enable Refreshing Items from All Visible LIbraries
|
||||||
if userSession?.user.permissions.items.canEditMetadata ?? false {
|
if userSession?.user.permissions.items.canEditMetadata ?? false {
|
||||||
Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
|
Toggle(L10n.editMedia, isOn: $enableItemEditing)
|
||||||
}
|
}
|
||||||
/// Enable Deleting Items from Approved Libraries
|
/// Enable Deleting Items from Approved Libraries
|
||||||
if userSession?.user.permissions.items.canDelete ?? false {
|
if userSession?.user.permissions.items.canDelete ?? false {
|
||||||
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion)
|
Toggle(L10n.deleteMedia, isOn: $enableItemDeletion)
|
||||||
}
|
}
|
||||||
/// Enable Refreshing & Deleting Collections
|
/// Enable Refreshing & Deleting Collections
|
||||||
if userSession?.user.permissions.items.canManageCollections ?? false {
|
if userSession?.user.permissions.items.canManageCollections ?? false {
|
||||||
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
|
Toggle(L10n.editCollections, isOn: $enableCollectionManagement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ private struct ListRowButtonStyle: ButtonStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func secondaryStyle(configuration: Configuration) -> some ShapeStyle {
|
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))
|
return AnyShapeStyle(Color.red.opacity(0.2))
|
||||||
} else {
|
} else {
|
||||||
return isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray)
|
return isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray)
|
||||||
|
|
|
@ -81,13 +81,16 @@ struct ServerUserDetailsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ChevronButton(L10n.permissions) {
|
||||||
|
router.route(to: \.userPermissions, viewModel)
|
||||||
|
}
|
||||||
if let userId = viewModel.user.id {
|
if let userId = viewModel.user.id {
|
||||||
ChevronButton(L10n.password) {
|
ChevronButton(L10n.password) {
|
||||||
router.route(to: \.resetUserPassword, userId)
|
router.route(to: \.resetUserPassword, userId)
|
||||||
}
|
}
|
||||||
|
ChevronButton(L10n.quickConnect) {
|
||||||
|
router.route(to: \.quickConnectAuthorize, viewModel.user)
|
||||||
}
|
}
|
||||||
ChevronButton(L10n.permissions) {
|
|
||||||
router.route(to: \.userPermissions, viewModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,19 +46,19 @@ extension CustomizeViewsSettings {
|
||||||
|
|
||||||
/// Enable Editing Items from All Visible LIbraries
|
/// Enable Editing Items from All Visible LIbraries
|
||||||
if userSession?.user.permissions.items.canEditMetadata ?? false {
|
if userSession?.user.permissions.items.canEditMetadata ?? false {
|
||||||
Toggle(L10n.allowItemEditing, isOn: $enableItemEditing)
|
Toggle(L10n.editMedia, isOn: $enableItemEditing)
|
||||||
}
|
}
|
||||||
/// Enable Deleting Items from Approved Libraries
|
/// Enable Deleting Items from Approved Libraries
|
||||||
if userSession?.user.permissions.items.canDelete ?? false {
|
if userSession?.user.permissions.items.canDelete ?? false {
|
||||||
Toggle(L10n.allowItemDeletion, isOn: $enableItemDeletion)
|
Toggle(L10n.deleteMedia, isOn: $enableItemDeletion)
|
||||||
}
|
}
|
||||||
/// Enable Downloading All Items
|
/// Enable Downloading All Items
|
||||||
/* if userSession?.user.permissions.items.canDownload ?? false {
|
/* if userSession?.user.permissions.items.canDownload ?? false {
|
||||||
Toggle(L10n.allowItemDownloading, isOn: $enableItemDownloads)
|
Toggle(L10n.itemDownloading, isOn: $enableItemDownloads)
|
||||||
} */
|
} */
|
||||||
/// Enable Deleting or Editing Collections
|
/// Enable Deleting or Editing Collections
|
||||||
if userSession?.user.permissions.items.canManageCollections ?? false {
|
if userSession?.user.permissions.items.canManageCollections ?? false {
|
||||||
Toggle(L10n.allowCollectionManagement, isOn: $enableCollectionManagement)
|
Toggle(L10n.editCollections, isOn: $enableCollectionManagement)
|
||||||
}
|
}
|
||||||
/// Manage Item Lyrics
|
/// Manage Item Lyrics
|
||||||
/* if userSession?.user.permissions.items.canManageLyrics ?? false {
|
/* if userSession?.user.permissions.items.canManageLyrics ?? false {
|
||||||
|
|
|
@ -8,10 +8,16 @@
|
||||||
|
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct QuickConnectAuthorizeView: View {
|
struct QuickConnectAuthorizeView: View {
|
||||||
|
|
||||||
|
// MARK: - Dismiss Environment
|
||||||
|
|
||||||
|
@Environment(\.dismiss)
|
||||||
|
private var dismiss
|
||||||
|
|
||||||
// MARK: - Defaults
|
// MARK: - Defaults
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
|
@ -24,11 +30,8 @@ struct QuickConnectAuthorizeView: View {
|
||||||
|
|
||||||
// MARK: - State & Environment Objects
|
// MARK: - State & Environment Objects
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var router: SettingsCoordinator.Router
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel = QuickConnectAuthorizeViewModel()
|
private var viewModel: QuickConnectAuthorizeViewModel
|
||||||
|
|
||||||
// MARK: - Quick Connect Variables
|
// MARK: - Quick Connect Variables
|
||||||
|
|
||||||
|
@ -45,10 +48,46 @@ struct QuickConnectAuthorizeView: View {
|
||||||
@State
|
@State
|
||||||
private var error: Error? = nil
|
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
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
Section {
|
||||||
|
loginUserRow
|
||||||
|
} header: {
|
||||||
|
Text(L10n.user)
|
||||||
|
} footer: {
|
||||||
|
Text(L10n.quickConnectUserDisclaimer)
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.quickConnectCode, text: $code)
|
TextField(L10n.quickConnectCode, text: $code)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
@ -59,14 +98,13 @@ struct QuickConnectAuthorizeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.state == .authorizing {
|
if viewModel.state == .authorizing {
|
||||||
ListRowButton(L10n.cancel) {
|
ListRowButton(L10n.cancel, role: .cancel) {
|
||||||
viewModel.send(.cancel)
|
viewModel.send(.cancel)
|
||||||
isCodeFocused = true
|
isCodeFocused = true
|
||||||
}
|
}
|
||||||
.foregroundStyle(.red, .red.opacity(0.2))
|
|
||||||
} else {
|
} else {
|
||||||
ListRowButton(L10n.authorize) {
|
ListRowButton(L10n.authorize) {
|
||||||
viewModel.send(.authorize(code))
|
viewModel.send(.authorize(code: code))
|
||||||
}
|
}
|
||||||
.disabled(code.count != 6 || viewModel.state == .authorizing)
|
.disabled(code.count != 6 || viewModel.state == .authorizing)
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
|
@ -107,7 +145,7 @@ struct QuickConnectAuthorizeView: View {
|
||||||
isPresented: $isPresentingSuccess
|
isPresented: $isPresentingSuccess
|
||||||
) {
|
) {
|
||||||
Button(L10n.dismiss, role: .cancel) {
|
Button(L10n.dismiss, role: .cancel) {
|
||||||
router.pop()
|
dismiss()
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
L10n.quickConnectSuccessMessage.text
|
L10n.quickConnectSuccessMessage.text
|
||||||
|
|
|
@ -45,7 +45,7 @@ struct UserProfileSettingsView: View {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ChevronButton(L10n.quickConnect) {
|
ChevronButton(L10n.quickConnect) {
|
||||||
router.route(to: \.quickConnect)
|
router.route(to: \.quickConnect, viewModel.userSession.user.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChevronButton(L10n.password) {
|
ChevronButton(L10n.password) {
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue