krz/rune
an ios client for njalla
clone: git clone https://gitbay.org/krz/rune.git
v1.2.0: Rune/Views/Domains/DomainEditView.swift · raw
1import SwiftUI
2
3struct DomainEditView: View {
4 let domain: Domain
5 @ObservedObject var viewModel: DomainViewModel
6 let client: NjallaClient
7
8 @Environment(\.dismiss) private var dismiss
9
10 @State private var autorenew: Bool
11 @State private var autorenewDirty = false
12 @State private var mailforwarding: Bool
13 @State private var mailforwardingDirty = false
14 @State private var dnssec: Bool
15 @State private var dnssecDirty = false
16 @State private var lock: Bool
17 @State private var lockDirty = false
18 @State private var nameserversText: String
19 @State private var nameserversDirty = false
20 @State private var localErrorMessage: String?
21
22 init(domain: Domain, viewModel: DomainViewModel, client: NjallaClient) {
23 self.domain = domain
24 self.viewModel = viewModel
25 self.client = client
26 _autorenew = State(initialValue: domain.autorenew ?? false)
27 _mailforwarding = State(initialValue: domain.mailforwarding ?? false)
28 _dnssec = State(initialValue: domain.dnssec ?? false)
29 _lock = State(initialValue: domain.lock ?? false)
30 _nameserversText = State(initialValue: (domain.nameservers ?? []).joined(separator: "\n"))
31 }
32
33 var body: some View {
34 Form {
35 Section("Settings") {
36 Toggle("Autorenew", isOn: dirtyBinding(for: $autorenew, dirty: $autorenewDirty, original: originalAutorenew))
37 Toggle("Mail Forwarding", isOn: dirtyBinding(for: $mailforwarding, dirty: $mailforwardingDirty, original: originalMailForwarding))
38 Toggle("DNSSEC", isOn: dirtyBinding(for: $dnssec, dirty: $dnssecDirty, original: originalDNSSEC))
39 Toggle("Registrar Lock", isOn: dirtyBinding(for: $lock, dirty: $lockDirty, original: originalLock))
40 }
41
42 Section {
43 TextEditor(text: nameserversBinding)
44 .frame(minHeight: 120)
45 } header: {
46 Text("Nameservers")
47 } footer: {
48 Text("Enter one nameserver per line. Leave blank to use Njalla defaults.")
49 }
50
51 Section {
52 Button("Save") {
53 Task {
54 await save()
55 }
56 }
57 .disabled(viewModel.isSaving || !request.hasChanges)
58 }
59 }
60 .navigationTitle("Edit Domain")
61 .navigationBarTitleDisplayMode(.inline)
62 .overlay {
63 if viewModel.isSaving {
64 ProgressView()
65 .controlSize(.large)
66 }
67 }
68 .interactiveDismissDisabled(viewModel.isSaving)
69 .alert("Request Failed", isPresented: localErrorBinding) {
70 Button("OK", role: .cancel) {}
71 } message: {
72 Text(localErrorMessage ?? "")
73 }
74 }
75
76 private func save() async {
77 guard !viewModel.isSaving else {
78 return
79 }
80
81 let request = request
82
83 guard request.hasChanges else {
84 dismiss()
85 return
86 }
87
88 do {
89 try await viewModel.updateDomain(named: domain.name, request: request, client: client)
90 dismiss()
91 } catch is CancellationError {
92 return
93 } catch {
94 if (error as? URLError)?.code == .cancelled {
95 return
96 }
97 localErrorMessage = error.userFacingMessage
98 }
99 }
100
101 private var localErrorBinding: Binding<Bool> {
102 Binding(
103 get: { localErrorMessage != nil },
104 set: { newValue in
105 if !newValue {
106 localErrorMessage = nil
107 }
108 }
109 )
110 }
111
112 private var originalAutorenew: Bool {
113 domain.autorenew ?? false
114 }
115
116 private var originalMailForwarding: Bool {
117 domain.mailforwarding ?? false
118 }
119
120 private var originalDNSSEC: Bool {
121 domain.dnssec ?? false
122 }
123
124 private var originalLock: Bool {
125 domain.lock ?? false
126 }
127
128 private var originalNameservers: [String] {
129 normalizedNameservers(from: (domain.nameservers ?? []).joined(separator: "\n"))
130 }
131
132 private var request: DomainUpdateRequest {
133 DomainUpdateRequest(
134 autorenew: autorenewDirty ? autorenew : nil,
135 mailforwarding: mailforwardingDirty ? mailforwarding : nil,
136 dnssec: dnssecDirty ? dnssec : nil,
137 lock: lockDirty ? lock : nil,
138 nameservers: nameserversDirty ? normalizedNameservers(from: nameserversText) : nil
139 )
140 }
141
142 private var nameserversBinding: Binding<String> {
143 Binding(
144 get: { nameserversText },
145 set: { newValue in
146 nameserversText = newValue
147 nameserversDirty = normalizedNameservers(from: newValue) != originalNameservers
148 }
149 )
150 }
151
152 private func dirtyBinding(
153 for value: Binding<Bool>,
154 dirty: Binding<Bool>,
155 original: Bool
156 ) -> Binding<Bool> {
157 Binding(
158 get: { value.wrappedValue },
159 set: { newValue in
160 value.wrappedValue = newValue
161 dirty.wrappedValue = newValue != original
162 }
163 )
164 }
165
166 private func normalizedNameservers(from text: String) -> [String] {
167 text
168 .split(whereSeparator: \.isNewline)
169 .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
170 .filter { !$0.isEmpty }
171 }
172}