102 lines
4.1 KiB
Swift
102 lines
4.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import Observation
|
|
import TermindApplication
|
|
import TermindPresentation
|
|
|
|
/// Plaintext lives only in an editor or a request. Previewing never writes the Keychain.
|
|
@MainActor @Observable
|
|
final class MobileSecretDraft {
|
|
enum Action: String, CaseIterable { case preserve, replace, clear }
|
|
enum EditingMode { case explicitAction, direct }
|
|
let editingMode: EditingMode
|
|
private var originalReference = ""
|
|
private var originalSecret: String?
|
|
private var readError: String?
|
|
|
|
init(editingMode: EditingMode = .explicitAction) {
|
|
self.editingMode = editingMode
|
|
}
|
|
|
|
func begin(in ai: AIConfigurationModel, reference: String) {
|
|
originalReference = reference
|
|
originalSecret = nil
|
|
readError = nil
|
|
if !reference.isEmpty {
|
|
originalSecret = ai.secret(for: reference)
|
|
readError = ai.secretStorageError
|
|
}
|
|
if editingMode == .direct { text = originalSecret ?? "" }
|
|
}
|
|
|
|
var action = Action.preserve
|
|
var text = "" {
|
|
didSet {
|
|
guard editingMode == .direct else { return }
|
|
action = !canEdit || text == (originalSecret ?? "") ? .preserve : text.isEmpty ? .clear : .replace
|
|
}
|
|
}
|
|
var canEdit: Bool {
|
|
editingMode != .direct || originalReference.isEmpty || (readError == nil && originalSecret != nil)
|
|
}
|
|
var isDirty: Bool { action != .preserve || (editingMode == .explicitAction && !text.isEmpty) }
|
|
var isValid: Bool { action != .replace || !text.isEmpty }
|
|
|
|
func effective(in ai: AIConfigurationModel, reference: String) throws -> String {
|
|
switch action {
|
|
case .clear: return ""
|
|
case .replace: return text
|
|
case .preserve:
|
|
guard !reference.isEmpty else { return "" }
|
|
let value = ai.secret(for: reference) ?? ""
|
|
if let error = ai.secretStorageError { throw MobileEditorError.message(error) }
|
|
return value
|
|
}
|
|
}
|
|
|
|
func providerEdit(reference: String, kind: ModelProviderCredentialKind) throws -> ModelProviderCredentialEdit? {
|
|
guard action != .preserve else { return nil }
|
|
if let readError { throw MobileEditorError.message(readError) }
|
|
guard reference == originalReference else { throw SettingsDraftMerge.Conflict.changed }
|
|
return .init(
|
|
kind: kind, reference: reference, previous: originalSecret, replacement: action == .clear ? nil : text)
|
|
}
|
|
|
|
func discardPlaintext() {
|
|
originalSecret = nil
|
|
text = ""
|
|
}
|
|
|
|
func commit(in ai: AIConfigurationModel, reference: String, kind: String = "key", save: (String) -> Bool) throws {
|
|
if action != .preserve {
|
|
if let readError { throw MobileEditorError.message(readError) }
|
|
guard reference == originalReference else { throw SettingsDraftMerge.Conflict.changed }
|
|
if !reference.isEmpty {
|
|
let current = ai.secret(for: reference)
|
|
if let error = ai.secretStorageError { throw MobileEditorError.message(error) }
|
|
guard current == originalSecret else { throw SettingsDraftMerge.Conflict.changed }
|
|
}
|
|
}
|
|
let ref = reference.isEmpty ? AIConfigurationModel.makeCredentialRef(kind) : reference
|
|
var previous: String?
|
|
if action != .preserve {
|
|
previous = ai.secret(for: ref)
|
|
if let error = ai.secretStorageError { throw MobileEditorError.message(error) }
|
|
ai.setSecret(action == .clear ? nil : text, for: ref)
|
|
if let error = ai.secretStorageError { throw MobileEditorError.message(error) }
|
|
}
|
|
let next = action == .preserve ? reference : action == .clear ? "" : ref
|
|
guard save(next) else {
|
|
var message = ai.storageError ?? String(localized: AppStrings.Common.unableToSave)
|
|
if action != .preserve {
|
|
ai.setSecret(previous, for: ref)
|
|
if let error = ai.secretStorageError { message += "\n" + error }
|
|
}
|
|
throw MobileEditorError.message(message)
|
|
}
|
|
text = ""
|
|
}
|
|
}
|