[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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 */; };
|
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 */,
|
||||||
|
|
|
@ -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) {
|
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)
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue