vOOice/VoiceInk/Services/CloudTranscription/CustomModelManager.swift
Beingpax 948033ac28 Migrate API key storage to Keychain with iCloud sync
Move API keys from UserDefaults to secure Keychain storage. Add KeychainService and APIKeyManager for centralized key management. Enable iCloud Keychain sync for cross-device sharing between macOS and iOS.
2026-01-05 22:28:34 +05:45

140 lines
4.8 KiB
Swift

import Foundation
import os
class CustomModelManager: ObservableObject {
static let shared = CustomModelManager()
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "CustomModelManager")
private let userDefaults = UserDefaults.standard
private let customModelsKey = "customCloudModels"
@Published var customModels: [CustomCloudModel] = []
private init() {
loadCustomModels()
}
// MARK: - CRUD Operations
func addCustomModel(_ model: CustomCloudModel) {
customModels.append(model)
saveCustomModels()
logger.info("Added custom model: \(model.displayName)")
}
func removeCustomModel(withId id: UUID) {
customModels.removeAll { $0.id == id }
saveCustomModels()
APIKeyManager.shared.deleteCustomModelAPIKey(forModelId: id)
logger.info("Removed custom model with ID: \(id)")
}
func updateCustomModel(_ updatedModel: CustomCloudModel) {
if let index = customModels.firstIndex(where: { $0.id == updatedModel.id }) {
customModels[index] = updatedModel
saveCustomModels()
logger.info("Updated custom model: \(updatedModel.displayName)")
}
}
// MARK: - Persistence
private func loadCustomModels() {
guard let data = userDefaults.data(forKey: customModelsKey) else {
logger.info("No custom models found in UserDefaults")
return
}
do {
customModels = try JSONDecoder().decode([CustomCloudModel].self, from: data)
} catch {
logger.error("Failed to decode custom models: \(error.localizedDescription)")
customModels = []
}
}
func saveCustomModels() {
do {
let data = try JSONEncoder().encode(customModels)
userDefaults.set(data, forKey: customModelsKey)
} catch {
logger.error("Failed to encode custom models: \(error.localizedDescription)")
}
}
// MARK: - Validation
func validateModel(name: String, displayName: String, apiEndpoint: String, apiKey: String, modelName: String) -> [String] {
var errors: [String] = []
if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Name cannot be empty")
}
if displayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Display name cannot be empty")
}
if apiEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("API endpoint cannot be empty")
} else if !isValidURL(apiEndpoint) {
errors.append("API endpoint must be a valid URL")
}
if apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("API key cannot be empty")
}
if modelName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Model name cannot be empty")
}
// Check for duplicate names
if customModels.contains(where: { $0.name == name }) {
errors.append("A model with this name already exists")
}
return errors
}
func validateModel(name: String, displayName: String, apiEndpoint: String, apiKey: String, modelName: String, excludingId: UUID? = nil) -> [String] {
var errors: [String] = []
if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Name cannot be empty")
}
if displayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Display name cannot be empty")
}
if apiEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("API endpoint cannot be empty")
} else if !isValidURL(apiEndpoint) {
errors.append("API endpoint must be a valid URL")
}
if apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("API key cannot be empty")
}
if modelName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
errors.append("Model name cannot be empty")
}
// Check for duplicate names, excluding the specified ID
if customModels.contains(where: { $0.name == name && $0.id != excludingId }) {
errors.append("A model with this name already exists")
}
return errors
}
private func isValidURL(_ string: String) -> Bool {
if let url = URL(string: string) {
return url.scheme != nil && url.host != nil
}
return false
}
}