krz/rune

an ios client for njalla

clone: git clone https://gitbay.org/krz/rune.git

main: 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}