[iOS] Admin Dashboard - API Keys (#1284)
* API Keys * Switch Deletion Alert for a Confirmation Dialog * Migrate from a list to a Collection VGrid. * Convert back to List. Also, now using my events! So, there is a confirmation and a failure message for both delete & create API. * want vs wish * Merge Issue Fixes * Review Changes * Reset newAPIName after creating a new API * cleanup --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
c46ee13dbc
commit
56fa03257e
|
@ -76,6 +76,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
var addServerTaskTrigger = makeAddServerTaskTrigger
|
||||
@Route(.push)
|
||||
var serverLogs = makeServerLogs
|
||||
@Route(.push)
|
||||
var apiKeys = makeAPIKeys
|
||||
// <- End of AdminDashboard Items
|
||||
|
||||
#if DEBUG
|
||||
|
@ -230,6 +232,11 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
ServerLogsView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeAPIKeys() -> some View {
|
||||
APIKeysView()
|
||||
}
|
||||
|
||||
// <- End of AdminDashboard Items
|
||||
|
||||
#if DEBUG
|
||||
|
|
|
@ -22,6 +22,8 @@ internal enum L10n {
|
|||
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
|
||||
/// Add
|
||||
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")
|
||||
/// Select Server View - Add Server
|
||||
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
||||
/// Add trigger
|
||||
|
@ -46,10 +48,22 @@ internal enum L10n {
|
|||
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
|
||||
/// TranscodeReason - Anamorphic Video Not Supported
|
||||
internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is not supported")
|
||||
/// API Key Copied
|
||||
internal static let apiKeyCopied = L10n.tr("Localizable", "apiKeyCopied", fallback: "API Key Copied")
|
||||
/// Your API Key was copied to your clipboard!
|
||||
internal static let apiKeyCopiedMessage = L10n.tr("Localizable", "apiKeyCopiedMessage", fallback: "Your API Key was copied to your clipboard!")
|
||||
/// API Keys
|
||||
internal static let apiKeys = L10n.tr("Localizable", "apiKeys", fallback: "API Keys")
|
||||
/// External applications require an API key to communicate with your server.
|
||||
internal static let apiKeysDescription = L10n.tr("Localizable", "apiKeysDescription", fallback: "External applications require an API key to communicate with your server.")
|
||||
/// API Keys
|
||||
internal static let apiKeysTitle = L10n.tr("Localizable", "apiKeysTitle", fallback: "API Keys")
|
||||
/// Represents the Appearance setting label
|
||||
internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance")
|
||||
/// App Icon
|
||||
internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "App Icon")
|
||||
/// Application Name
|
||||
internal static let applicationName = L10n.tr("Localizable", "applicationName", fallback: "Application Name")
|
||||
/// Apply
|
||||
internal static let apply = L10n.tr("Localizable", "apply", fallback: "Apply")
|
||||
/// Aspect Fill
|
||||
|
@ -218,6 +232,10 @@ internal enum L10n {
|
|||
internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue")
|
||||
/// Continue Watching
|
||||
internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: "Continue Watching")
|
||||
/// Create API Key
|
||||
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.")
|
||||
/// Current
|
||||
internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
|
||||
/// Current Position
|
||||
|
@ -248,14 +266,18 @@ internal enum L10n {
|
|||
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
|
||||
/// Description for the dashboard section
|
||||
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
|
||||
/// Date Created
|
||||
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date Created")
|
||||
/// Day of Week
|
||||
internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week")
|
||||
/// Time Interval Help Text - Days
|
||||
internal static let days = L10n.tr("Localizable", "days", fallback: "Days")
|
||||
/// Default Scheme
|
||||
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
|
||||
/// Server Detail View - Delete
|
||||
/// Delete
|
||||
internal static let delete = L10n.tr("Localizable", "delete", fallback: "Delete")
|
||||
/// Are you sure you want to permanently delete this key?
|
||||
internal static let deleteAPIKeyMessage = L10n.tr("Localizable", "deleteAPIKeyMessage", fallback: "Are you sure you want to permanently delete this key?")
|
||||
/// Delete Device
|
||||
internal static let deleteDevice = L10n.tr("Localizable", "deleteDevice", fallback: "Delete Device")
|
||||
/// Failed to Delete Device
|
||||
|
@ -544,8 +566,8 @@ internal enum L10n {
|
|||
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
|
||||
/// Video Player Settings View - Offset
|
||||
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
|
||||
/// Ok
|
||||
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok")
|
||||
/// OK
|
||||
internal static let ok = L10n.tr("Localizable", "ok", fallback: "OK")
|
||||
/// On application startup
|
||||
internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup")
|
||||
/// 1 user
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import OrderedCollections
|
||||
|
||||
// TODO: for APIKey updating, could temp set new APIKeys
|
||||
|
||||
final class APIKeysViewModel: ViewModel, Stateful {
|
||||
|
||||
// MARK: Action
|
||||
|
||||
enum Action: Equatable {
|
||||
case getAPIKeys
|
||||
case createAPIKey(name: String)
|
||||
case deleteAPIKey(key: String)
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
|
||||
enum State: Hashable {
|
||||
case initial
|
||||
case error(JellyfinAPIError)
|
||||
case content
|
||||
}
|
||||
|
||||
// MARK: Published Variables
|
||||
|
||||
@Published
|
||||
final var apiKeys: [AuthenticationInfo] = []
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
|
||||
// MARK: Action Responses
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .getAPIKeys:
|
||||
Task {
|
||||
do {
|
||||
try await getAPIKeys()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
case let .createAPIKey(name):
|
||||
Task {
|
||||
do {
|
||||
try await createAPIKey(name: name)
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
case let .deleteAPIKey(key):
|
||||
Task {
|
||||
do {
|
||||
try await deleteAPIKey(key: key)
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
private func getAPIKeys() async throws {
|
||||
let request = Paths.getKeys
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
guard let items = response.value.items else { return }
|
||||
|
||||
await MainActor.run {
|
||||
self.apiKeys = items
|
||||
}
|
||||
}
|
||||
|
||||
private func createAPIKey(name: String) async throws {
|
||||
let request = Paths.createKey(app: name)
|
||||
try await userSession.client.send(request).value
|
||||
|
||||
try await getAPIKeys()
|
||||
}
|
||||
|
||||
private func deleteAPIKey(key: String) async throws {
|
||||
let request = Paths.revokeKey(key: key)
|
||||
try await userSession.client.send(request)
|
||||
|
||||
try await getAPIKeys()
|
||||
}
|
||||
}
|
|
@ -56,6 +56,8 @@
|
|||
4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; };
|
||||
4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
|
||||
4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
|
||||
4E36395B2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; };
|
||||
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; };
|
||||
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
|
||||
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
|
||||
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */; };
|
||||
|
@ -81,6 +83,8 @@
|
|||
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */; };
|
||||
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */; };
|
||||
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */; };
|
||||
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
|
||||
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
|
||||
4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; };
|
||||
|
@ -1090,6 +1094,7 @@
|
|||
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTicks.swift; sourceTree = "<group>"; };
|
||||
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = "<group>"; };
|
||||
4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = "<group>"; };
|
||||
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = "<group>"; };
|
||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDashboardView.swift; sourceTree = "<group>"; };
|
||||
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1110,6 +1115,8 @@
|
|||
4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = "<group>"; };
|
||||
4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = "<group>"; };
|
||||
4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
|
||||
4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysView.swift; sourceTree = "<group>"; };
|
||||
4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysRow.swift; sourceTree = "<group>"; };
|
||||
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
|
||||
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
|
||||
4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = "<group>"; };
|
||||
|
@ -1988,6 +1995,7 @@
|
|||
children = (
|
||||
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
|
||||
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
|
||||
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
|
||||
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
|
||||
E1DE64902CC6F06C00E423B6 /* Components */,
|
||||
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
|
||||
|
@ -2108,6 +2116,23 @@
|
|||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EA09DE22CC4E7BE00CB27E4 /* Components */,
|
||||
4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */,
|
||||
);
|
||||
path = APIKeyView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EA09DE22CC4E7BE00CB27E4 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2206,6 +2231,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */,
|
||||
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */,
|
||||
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */,
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */,
|
||||
|
@ -4767,6 +4793,7 @@
|
|||
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
|
||||
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
|
||||
4E36395B2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
|
||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */,
|
||||
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */,
|
||||
|
@ -5064,6 +5091,7 @@
|
|||
6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */,
|
||||
4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
|
||||
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
|
||||
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
|
||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
||||
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
||||
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
|
||||
|
@ -5267,6 +5295,7 @@
|
|||
E1D8429329340B8300D1041A /* Utilities.swift in Sources */,
|
||||
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */,
|
||||
E1D5C39628DF90C100CDBEFB /* Slider.swift in Sources */,
|
||||
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */,
|
||||
E187A60229AB28F0008387E6 /* RotateContentView.swift in Sources */,
|
||||
BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */,
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
|
@ -5343,6 +5372,7 @@
|
|||
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
|
||||
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
|
||||
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
|
||||
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */,
|
||||
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
|
||||
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
|
||||
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct APIKeysView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
@State
|
||||
private var showCopiedAlert = false
|
||||
@State
|
||||
private var showDeleteConfirmation = false
|
||||
@State
|
||||
private var showCreateAPIAlert = false
|
||||
@State
|
||||
private var newAPIName: String = ""
|
||||
@State
|
||||
private var deleteAPI: AuthenticationInfo?
|
||||
|
||||
@StateObject
|
||||
private var viewModel = APIKeysViewModel()
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
ErrorView(error: error)
|
||||
.onRetry {
|
||||
viewModel.send(.getAPIKeys)
|
||||
}
|
||||
case .initial:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.2), value: viewModel.state)
|
||||
.animation(.linear(duration: 0.1), value: viewModel.apiKeys)
|
||||
.navigationTitle(L10n.apiKeys)
|
||||
.onFirstAppear {
|
||||
viewModel.send(.getAPIKeys)
|
||||
}
|
||||
.topBarTrailing {
|
||||
if viewModel.apiKeys.isNotEmpty {
|
||||
Button(L10n.add) {
|
||||
showCreateAPIAlert = true
|
||||
UIDevice.impact(.light)
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
}
|
||||
}
|
||||
.alert(L10n.apiKeyCopied, isPresented: $showCopiedAlert) {
|
||||
Button(L10n.ok, role: .cancel) {}
|
||||
} message: {
|
||||
Text(L10n.apiKeyCopiedMessage)
|
||||
}
|
||||
.confirmationDialog(
|
||||
L10n.delete,
|
||||
isPresented: $showDeleteConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(L10n.delete, role: .destructive) {
|
||||
if let key = deleteAPI?.accessToken {
|
||||
viewModel.send(.deleteAPIKey(key: key))
|
||||
}
|
||||
}
|
||||
Button(L10n.cancel, role: .cancel) {}
|
||||
} message: {
|
||||
Text(L10n.deleteAPIKeyMessage)
|
||||
}
|
||||
.alert(L10n.createAPIKey, isPresented: $showCreateAPIAlert) {
|
||||
TextField(L10n.applicationName, text: $newAPIName)
|
||||
Button(L10n.cancel, role: .cancel) {}
|
||||
Button(L10n.save) {
|
||||
viewModel.send(.createAPIKey(name: newAPIName))
|
||||
newAPIName = ""
|
||||
}
|
||||
} message: {
|
||||
Text(L10n.createAPIKeyMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Key Content
|
||||
|
||||
private var contentView: some View {
|
||||
List {
|
||||
ListTitleSection(
|
||||
L10n.apiKeysTitle,
|
||||
description: L10n.apiKeysDescription
|
||||
)
|
||||
|
||||
if viewModel.apiKeys.isNotEmpty {
|
||||
ForEach(viewModel.apiKeys, id: \.accessToken) { apiKey in
|
||||
APIKeysRow(apiKey: apiKey) {
|
||||
UIPasteboard.general.string = apiKey.accessToken
|
||||
showCopiedAlert = true
|
||||
} onDelete: {
|
||||
deleteAPI = apiKey
|
||||
showDeleteConfirmation = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(L10n.addAPIKey) {
|
||||
showCreateAPIAlert = true
|
||||
}
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import Factory
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension APIKeysView {
|
||||
|
||||
struct APIKeysRow: View {
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
let apiKey: AuthenticationInfo
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
let onSelect: () -> Void
|
||||
let onDelete: () -> Void
|
||||
|
||||
// MARK: - Row Content
|
||||
|
||||
@ViewBuilder
|
||||
private var rowContent: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(apiKey.appName ?? L10n.unknown)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(apiKey.accessToken ?? L10n.unknown)
|
||||
.lineLimit(2)
|
||||
|
||||
TextPairView(
|
||||
L10n.dateCreated,
|
||||
value: {
|
||||
if let creationDate = apiKey.dateCreated {
|
||||
Text(creationDate, format: .dateTime)
|
||||
} else {
|
||||
Text(L10n.unknown)
|
||||
}
|
||||
}()
|
||||
)
|
||||
.monospacedDigit()
|
||||
}
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
rowContent
|
||||
}
|
||||
.foregroundStyle(.primary, .secondary)
|
||||
.swipeActions {
|
||||
Button(
|
||||
L10n.delete,
|
||||
systemImage: "trash",
|
||||
action: onDelete
|
||||
)
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,11 @@ struct UserDashboardView: View {
|
|||
|
||||
Section(L10n.advanced) {
|
||||
|
||||
ChevronButton(L10n.apiKeys)
|
||||
.onSelect {
|
||||
router.route(to: \.apiKeys)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.logs)
|
||||
.onSelect {
|
||||
router.route(to: \.serverLogs)
|
||||
|
|
|
@ -739,6 +739,81 @@
|
|||
/* Section Title for Column Configuration */
|
||||
"columns" = "Columns";
|
||||
|
||||
// Date Created - Label
|
||||
// Label for displaying the date an API key was created
|
||||
// Appears in the API key details
|
||||
"dateCreated" = "Date Created";
|
||||
|
||||
// API Keys - Title
|
||||
// Section Title for displaying API keys in the list
|
||||
// Displays the title of the API key section
|
||||
"apiKeysTitle" = "API Keys";
|
||||
|
||||
// API Keys - Description
|
||||
// Explains the usage of API keys in external applications
|
||||
// Displays below the title in the API key section
|
||||
"apiKeysDescription" = "External applications require an API key to communicate with your server.";
|
||||
|
||||
// Add - Button
|
||||
// Adds a new record
|
||||
// Appears in the toolbar
|
||||
"add" = "Add";
|
||||
|
||||
// API Key Copied - Alert
|
||||
// Informs the user that the API key was copied to the clipboard
|
||||
// Displays an alert after the user copies the key
|
||||
"apiKeyCopied" = "API Key Copied";
|
||||
|
||||
// API Key Copied - Alert Message
|
||||
// Informs the user that the key was copied successfully
|
||||
// Appears as a message in the alert
|
||||
"apiKeyCopiedMessage" = "Your API Key was copied to your clipboard!";
|
||||
|
||||
// OK - Button
|
||||
// Acknowledges an action
|
||||
// Used to dismiss the alert
|
||||
"ok" = "OK";
|
||||
|
||||
// Delete API Key - Confirmation Message
|
||||
// Warns the user that deletion is permanent
|
||||
// Displays a warning message before deletion
|
||||
"deleteAPIKeyMessage" = "Are you sure you want to permanently delete this key?";
|
||||
|
||||
// Cancel - Button
|
||||
// Cancels the current action
|
||||
// Appears in dialogs and alerts
|
||||
"cancel" = "Cancel";
|
||||
|
||||
// Delete - Button
|
||||
// Confirms the deletion of an API key
|
||||
// Appears in the delete confirmation dialog
|
||||
"delete" = "Delete";
|
||||
|
||||
// Create API Key - Alert
|
||||
// Prompts the user to enter an app name to create an API key
|
||||
// Appears when creating a new API key
|
||||
"createAPIKey" = "Create API Key";
|
||||
|
||||
// Create API Key - Message
|
||||
// Asks the user to enter the name of the application for the new API key
|
||||
// Displays in the create API key dialog
|
||||
"createAPIKeyMessage" = "Enter the application name for the new API key.";
|
||||
|
||||
// Application Name - Text Field
|
||||
// Placeholder text for entering the name of the application
|
||||
// Appears in the create API key dialog
|
||||
"applicationName" = "Application Name";
|
||||
|
||||
// Save - Button
|
||||
// Confirms the creation of the new API key
|
||||
// Appears in the create API key dialog
|
||||
"save" = "Save";
|
||||
|
||||
// API Keys - Screen Title
|
||||
// Title for the API keys management screen
|
||||
// Appears in the navigation bar
|
||||
"apiKeys" = "API Keys";
|
||||
|
||||
// Devices - Section Header
|
||||
// Title for the devices section in the Admin Dashboard
|
||||
// Used as the header for the devices section
|
||||
|
@ -1023,3 +1098,8 @@
|
|||
"success" = "Success";
|
||||
|
||||
"triggerAlreadyExists" = "Trigger already exists";
|
||||
|
||||
// Add API Key - Button
|
||||
// Creates an API Key if there are no keys available
|
||||
// Appears in place of the API Key list if there are no API Keys
|
||||
"addAPIKey" = "Add API key";
|
||||
|
|
Loading…
Reference in New Issue