diff options
| author | Christian Cleberg <[email protected]> | 2026-03-24 21:48:23 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-24 21:48:23 -0500 |
| commit | cf587aa0573ac4f34e1effe70108a9eca82093ac (patch) | |
| tree | 833e6f4961522e35b08af01b5f012983f8e34678 /Rune/ViewModels | |
| download | rune-1.0.0.tar.gz rune-1.0.0.tar.bz2 rune-1.0.0.zip | |
v1.0v1.0.0
Diffstat (limited to 'Rune/ViewModels')
| -rw-r--r-- | Rune/ViewModels/DomainViewModel.swift | 143 | ||||
| -rw-r--r-- | Rune/ViewModels/SettingsViewModel.swift | 102 | ||||
| -rw-r--r-- | Rune/ViewModels/TokenViewModel.swift | 91 |
3 files changed, 336 insertions, 0 deletions
diff --git a/Rune/ViewModels/DomainViewModel.swift b/Rune/ViewModels/DomainViewModel.swift new file mode 100644 index 0000000..efad5b9 --- /dev/null +++ b/Rune/ViewModels/DomainViewModel.swift @@ -0,0 +1,143 @@ +import Combine +import Foundation + +@MainActor +final class DomainViewModel: ObservableObject { + @Published private(set) var domains: [Domain] = [] + @Published private(set) var selectedDomain: Domain? + @Published private(set) var records: [DNSRecord] = [] + @Published private(set) var isLoadingDomains = false + @Published private(set) var isLoadingDetail = false + @Published private(set) var isLoadingRecords = false + @Published private(set) var isSaving = false + @Published var errorMessage: String? + + func reset() { + domains = [] + selectedDomain = nil + records = [] + isLoadingDomains = false + isLoadingDetail = false + isLoadingRecords = false + isSaving = false + errorMessage = nil + } + + func loadDomains(client: NjallaClient) async { + isLoadingDomains = true + defer { + isLoadingDomains = false + } + + do { + domains = try await client.listDomains().sorted { + $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending + } + errorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + errorMessage = error.localizedDescription + } + } + + func loadDomainDetail(named name: String, client: NjallaClient) async { + isLoadingDetail = true + defer { + isLoadingDetail = false + } + + do { + let domain = try await client.getDomain(named: name) + selectedDomain = domain + if let index = domains.firstIndex(where: { $0.name == name }) { + domains[index] = domain + } + errorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + errorMessage = error.localizedDescription + } + } + + func updateDomain(named name: String, request: DomainUpdateRequest, client: NjallaClient) async throws { + isSaving = true + defer { + isSaving = false + } + + let updated = try await client.editDomain(named: name, request: request) + selectedDomain = updated + if let index = domains.firstIndex(where: { $0.name == updated.name }) { + domains[index] = updated + } + errorMessage = nil + } + + func loadRecords(for domain: String, client: NjallaClient) async { + isLoadingRecords = true + defer { + isLoadingRecords = false + } + + do { + records = try await client.listRecords(for: domain).sorted { + ($0.name, $0.type, $0.id) < ($1.name, $1.type, $1.id) + } + errorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + errorMessage = error.localizedDescription + } + } + + func addRecord(for domain: String, draft: DNSRecordDraft, client: NjallaClient) async throws { + isSaving = true + defer { + isSaving = false + } + + _ = try await client.addRecord(for: domain, draft: draft) + try await reloadRecords(for: domain, client: client) + errorMessage = nil + } + + func editRecord(for domain: String, recordID: String, draft: DNSRecordDraft, client: NjallaClient) async throws { + isSaving = true + defer { + isSaving = false + } + + _ = try await client.editRecord(for: domain, id: recordID, draft: draft) + try await reloadRecords(for: domain, client: client) + errorMessage = nil + } + + func removeRecord(_ record: DNSRecord, client: NjallaClient) async throws { + isSaving = true + defer { + isSaving = false + } + + try await client.removeRecord(record) + try await reloadRecords(for: record.domain, client: client) + errorMessage = nil + } + + private func reloadRecords(for domain: String, client: NjallaClient) async throws { + records = try await client.listRecords(for: domain).sorted { + ($0.name, $0.type, $0.id) < ($1.name, $1.type, $1.id) + } + } +} diff --git a/Rune/ViewModels/SettingsViewModel.swift b/Rune/ViewModels/SettingsViewModel.swift new file mode 100644 index 0000000..6b773eb --- /dev/null +++ b/Rune/ViewModels/SettingsViewModel.swift @@ -0,0 +1,102 @@ +import Combine +import Foundation + +@MainActor +final class SettingsViewModel: ObservableObject { + @Published private(set) var client: NjallaClient? + @Published private(set) var balance: WalletBalance? + @Published private(set) var isAuthenticated = false + @Published private(set) var isBootstrapped = false + @Published private(set) var isLoadingBalance = false + @Published var errorMessage: String? + + private let keychainManager = KeychainManager() + + var requiresOnboarding: Bool { + isBootstrapped && !isAuthenticated + } + + func bootstrap() async { + guard !isBootstrapped else { return } + + defer { + isBootstrapped = true + } + + do { + guard let token = try keychainManager.readToken(), !token.isEmpty else { + isAuthenticated = false + client = nil + balance = nil + return + } + + let client = NjallaClient(token: token) + self.client = client + isAuthenticated = true + try await refreshBalance() + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + errorMessage = error.localizedDescription + client = nil + balance = nil + isAuthenticated = false + } + } + + func login(token: String) async throws { + let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) + let client = NjallaClient(token: trimmedToken) + let balance = try await client.getBalance() + try keychainManager.saveToken(trimmedToken) + + self.client = client + self.balance = balance + isAuthenticated = true + isBootstrapped = true + errorMessage = nil + } + + func refreshBalance() async throws { + guard let client else { return } + + isLoadingBalance = true + defer { + isLoadingBalance = false + } + + do { + balance = try await client.getBalance() + errorMessage = nil + } catch is CancellationError { + throw CancellationError() + } catch { + if (error as? URLError)?.code == .cancelled { + throw error + } + errorMessage = error.localizedDescription + throw error + } + } + + func logout() throws { + try keychainManager.deleteToken() + client = nil + balance = nil + isAuthenticated = false + errorMessage = nil + } + + func logoutIfCurrentTokenDeleted(_ key: String) throws -> Bool { + guard let storedToken = try keychainManager.readToken(), storedToken == key else { + return false + } + + try logout() + return true + } +} diff --git a/Rune/ViewModels/TokenViewModel.swift b/Rune/ViewModels/TokenViewModel.swift new file mode 100644 index 0000000..40495b4 --- /dev/null +++ b/Rune/ViewModels/TokenViewModel.swift @@ -0,0 +1,91 @@ +import Combine +import Foundation + +@MainActor +final class TokenViewModel: ObservableObject { + @Published private(set) var tokens: [APIToken] = [] + @Published private(set) var isLoading = false + @Published private(set) var isSaving = false + @Published var errorMessage: String? + + func reset() { + tokens = [] + isLoading = false + isSaving = false + errorMessage = nil + } + + func loadTokens(client: NjallaClient) async { + isLoading = true + defer { + isLoading = false + } + + do { + tokens = try await client.listTokens().sorted { + tokenLabel(for: $0).localizedCaseInsensitiveCompare(tokenLabel(for: $1)) == .orderedAscending + } + errorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + errorMessage = error.localizedDescription + } + } + + func addToken(request: TokenCreateRequest, client: NjallaClient) async -> Bool { + isSaving = true + defer { + isSaving = false + } + + do { + try await client.addToken(request: request) + tokens = try await client.listTokens() + errorMessage = nil + return true + } catch is CancellationError { + return false + } catch { + if (error as? URLError)?.code == .cancelled { + return false + } + errorMessage = error.localizedDescription + return false + } + } + + func removeToken(_ token: APIToken, client: NjallaClient) async -> Bool { + isSaving = true + defer { + isSaving = false + } + + do { + try await client.removeToken(key: token.key) + tokens.removeAll { $0.key == token.key } + errorMessage = nil + return true + } catch is CancellationError { + return false + } catch { + if (error as? URLError)?.code == .cancelled { + return false + } + errorMessage = error.localizedDescription + return false + } + } + + func tokenLabel(for token: APIToken) -> String { + let trimmedComment = token.comment?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedComment.isEmpty { + return trimmedComment + } + + return String(token.key.prefix(8)) + } +} |
