118 lines
5.1 KiB
Swift
118 lines
5.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
public struct KeyExportForm: View {
|
|
@Bindable private var model: KeyExportModel
|
|
@State private var advanced = false
|
|
|
|
public init(model: KeyExportModel) { self.model = model }
|
|
|
|
public var body: some View {
|
|
Form {
|
|
if let key = model.key {
|
|
Section {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "key.fill").font(.title2).foregroundStyle(.tint)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(key.label).font(.headline)
|
|
Text(
|
|
key.algorithm == .unknown
|
|
? AppStrings.Keychain.unknownKeyType
|
|
: AppStrings.Keychain.keyType(key.algorithm.displayName)
|
|
).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if model.phase == .succeeded {
|
|
Section {
|
|
Label(AppStrings.KeyExport.completed.swiftUIKey, systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
}
|
|
} else if case .attachmentFailed(let message) = model.phase {
|
|
Section {
|
|
Label(AppStrings.KeyExport.attachmentFailed.swiftUIKey, systemImage: "exclamationmark.triangle")
|
|
Text(message).font(.caption).foregroundStyle(.secondary).textSelection(.enabled)
|
|
Button(AppStrings.KeyExport.retryAttachment.swiftUIKey) { model.retry() }
|
|
}
|
|
} else {
|
|
if case .failed(let message) = model.phase {
|
|
Section {
|
|
Text(message).font(.caption).foregroundStyle(.red).textSelection(.enabled)
|
|
}
|
|
}
|
|
Section {
|
|
Picker(AppStrings.Vault.host.swiftUIKey, selection: $model.hostID) {
|
|
Text(AppStrings.KeyExport.selectHost).tag(UUID?.none)
|
|
ForEach(model.hosts) { host in Text(host.name).tag(Optional(host.id)) }
|
|
}
|
|
if model.hosts.isEmpty { Text(AppStrings.KeyExport.noHosts).foregroundStyle(.secondary) }
|
|
LabeledContent(AppStrings.KeyExport.location.swiftUIKey) {
|
|
TextField(
|
|
AppStrings.KeyExport.location.swiftUIKey, text: $model.options.directory.singleLineText
|
|
).labelsHidden().multilineTextAlignment(.trailing).singleLinePaste()
|
|
}
|
|
LabeledContent(AppStrings.KeyExport.filename.swiftUIKey) {
|
|
TextField(
|
|
AppStrings.KeyExport.filename.swiftUIKey, text: $model.options.filename.singleLineText
|
|
)
|
|
.labelsHidden().multilineTextAlignment(.trailing).singleLinePaste()
|
|
}
|
|
}
|
|
if model.needsExportPassphrase {
|
|
Section {
|
|
RevealableSecretField(AppStrings.Keychain.passphrase, text: $model.exportPassphrase)
|
|
.multilineTextAlignment(.leading)
|
|
Toggle(AppStrings.Keychain.savePassphrase.swiftUIKey, isOn: $model.saveExportPassphrase)
|
|
if let error = model.passphraseError { Text(error).font(.caption).foregroundStyle(.red) }
|
|
} footer: {
|
|
Text(AppStrings.KeyExport.passphraseHelp)
|
|
}
|
|
}
|
|
Section {
|
|
Text(AppStrings.KeyExport.scriptHelp).font(.caption).foregroundStyle(.secondary)
|
|
DisclosureGroup(AppStrings.KeyExport.advanced.swiftUIKey, isExpanded: $advanced) {
|
|
scriptEditor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.disabled(model.isBusy)
|
|
#if os(iOS)
|
|
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
|
#endif
|
|
}
|
|
|
|
private var scriptFont: Font {
|
|
#if os(macOS)
|
|
.caption.monospaced()
|
|
#else
|
|
.body.monospaced()
|
|
#endif
|
|
}
|
|
|
|
private var scriptEditor: some View {
|
|
TextEditor(text: $model.options.script)
|
|
.autohidingScrollers()
|
|
.font(scriptFont)
|
|
.multilineTextAlignment(.leading)
|
|
.frame(minHeight: 240)
|
|
.scrollContentBackground(.hidden)
|
|
.overlay(alignment: .topLeading) {
|
|
if model.options.script.isEmpty {
|
|
Text(AppStrings.KeyExport.script)
|
|
.font(scriptFont)
|
|
.foregroundStyle(.tertiary)
|
|
.padding(.leading, 5)
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
.accessibilityLabel(AppStrings.KeyExport.script.swiftUIKey)
|
|
}
|
|
}
|