[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:
Joe 2024-10-24 20:07:49 -06:00 committed by GitHub
parent c46ee13dbc
commit 56fa03257e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 458 additions and 3 deletions

View File

@ -76,6 +76,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
var addServerTaskTrigger = makeAddServerTaskTrigger var addServerTaskTrigger = makeAddServerTaskTrigger
@Route(.push) @Route(.push)
var serverLogs = makeServerLogs var serverLogs = makeServerLogs
@Route(.push)
var apiKeys = makeAPIKeys
// <- End of AdminDashboard Items // <- End of AdminDashboard Items
#if DEBUG #if DEBUG
@ -230,6 +232,11 @@ final class SettingsCoordinator: NavigationCoordinatable {
ServerLogsView() ServerLogsView()
} }
@ViewBuilder
func makeAPIKeys() -> some View {
APIKeysView()
}
// <- End of AdminDashboard Items // <- End of AdminDashboard Items
#if DEBUG #if DEBUG

View File

@ -22,6 +22,8 @@ internal enum L10n {
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices") internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
/// Add /// Add
internal static let add = L10n.tr("Localizable", "add", fallback: "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 /// Select Server View - Add Server
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server") internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
/// Add trigger /// Add trigger
@ -46,10 +48,22 @@ internal enum L10n {
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers") internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
/// TranscodeReason - Anamorphic Video Not Supported /// TranscodeReason - Anamorphic Video Not Supported
internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is 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 /// Represents the Appearance setting label
internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance") internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance")
/// App Icon /// App Icon
internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "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 /// Apply
internal static let apply = L10n.tr("Localizable", "apply", fallback: "Apply") internal static let apply = L10n.tr("Localizable", "apply", fallback: "Apply")
/// Aspect Fill /// Aspect Fill
@ -218,6 +232,10 @@ internal enum L10n {
internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue") internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue")
/// Continue Watching /// Continue Watching
internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: "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 /// Current
internal static let current = L10n.tr("Localizable", "current", fallback: "Current") internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
/// Current Position /// Current Position
@ -248,14 +266,18 @@ internal enum L10n {
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard") internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
/// Description for the dashboard section /// Description for the dashboard section
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.") 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 /// Day of Week
internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week") internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week")
/// Time Interval Help Text - Days /// Time Interval Help Text - Days
internal static let days = L10n.tr("Localizable", "days", fallback: "Days") internal static let days = L10n.tr("Localizable", "days", fallback: "Days")
/// Default Scheme /// Default Scheme
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "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") 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 /// Delete Device
internal static let deleteDevice = L10n.tr("Localizable", "deleteDevice", fallback: "Delete Device") internal static let deleteDevice = L10n.tr("Localizable", "deleteDevice", fallback: "Delete Device")
/// Failed to Delete Device /// Failed to Delete Device
@ -544,8 +566,8 @@ internal enum L10n {
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title") internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// Video Player Settings View - Offset /// Video Player Settings View - Offset
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset") internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// Ok /// OK
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok") internal static let ok = L10n.tr("Localizable", "ok", fallback: "OK")
/// On application startup /// On application startup
internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup") internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup")
/// 1 user /// 1 user

View File

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

View File

@ -56,6 +56,8 @@
4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; }; 4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; };
4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
4E35CE6D2CBEDB7600DBD886 /* 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 */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.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 */; }; 4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; }; 4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; };
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.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 */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = "<group>"; };
@ -1988,6 +1995,7 @@
children = ( children = (
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */, 4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */, 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */, 4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
E1DE64902CC6F06C00E423B6 /* Components */, E1DE64902CC6F06C00E423B6 /* Components */,
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */, 4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
@ -2108,6 +2116,23 @@
path = Components; path = Components;
sourceTree = "<group>"; 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 */ = { 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2206,6 +2231,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */, 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */,
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */,
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */, E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */,
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */, 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */,
@ -4767,6 +4793,7 @@
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */, E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */, E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
4E36395B2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */, E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */,
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */, E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */,
@ -5064,6 +5091,7 @@
6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */, 6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */,
4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */, 4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */, E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
E168BD10289A4162001A6922 /* HomeView.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */, E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */, 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
@ -5267,6 +5295,7 @@
E1D8429329340B8300D1041A /* Utilities.swift in Sources */, E1D8429329340B8300D1041A /* Utilities.swift in Sources */,
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */, E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */,
E1D5C39628DF90C100CDBEFB /* Slider.swift in Sources */, E1D5C39628DF90C100CDBEFB /* Slider.swift in Sources */,
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */,
E187A60229AB28F0008387E6 /* RotateContentView.swift in Sources */, E187A60229AB28F0008387E6 /* RotateContentView.swift in Sources */,
BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */, BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
@ -5343,6 +5372,7 @@
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */, 4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, 4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */,
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */, DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */, 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */,

View File

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

View File

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

View File

@ -37,6 +37,11 @@ struct UserDashboardView: View {
Section(L10n.advanced) { Section(L10n.advanced) {
ChevronButton(L10n.apiKeys)
.onSelect {
router.route(to: \.apiKeys)
}
ChevronButton(L10n.logs) ChevronButton(L10n.logs)
.onSelect { .onSelect {
router.route(to: \.serverLogs) router.route(to: \.serverLogs)

View File

@ -739,6 +739,81 @@
/* Section Title for Column Configuration */ /* Section Title for Column Configuration */
"columns" = "Columns"; "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 // Devices - Section Header
// Title for the devices section in the Admin Dashboard // Title for the devices section in the Admin Dashboard
// Used as the header for the devices section // Used as the header for the devices section
@ -1023,3 +1098,8 @@
"success" = "Success"; "success" = "Success";
"triggerAlreadyExists" = "Trigger already exists"; "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";