summaryrefslogtreecommitdiff
path: root/Rune/ViewModels
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-24 21:48:23 -0500
committerChristian Cleberg <[email protected]>2026-03-24 21:48:23 -0500
commitcf587aa0573ac4f34e1effe70108a9eca82093ac (patch)
tree833e6f4961522e35b08af01b5f012983f8e34678 /Rune/ViewModels
downloadrune-cf587aa0573ac4f34e1effe70108a9eca82093ac.tar.gz
rune-cf587aa0573ac4f34e1effe70108a9eca82093ac.tar.bz2
rune-cf587aa0573ac4f34e1effe70108a9eca82093ac.zip
v1.0v1.0.0
Diffstat (limited to 'Rune/ViewModels')
-rw-r--r--Rune/ViewModels/DomainViewModel.swift143
-rw-r--r--Rune/ViewModels/SettingsViewModel.swift102
-rw-r--r--Rune/ViewModels/TokenViewModel.swift91
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))
+ }
+}