Files
2026-09-27 15:42:22 +02:00

163 lines
6.7 KiB
Swift

import SwiftUI
struct HostEditor: View {
let store: ProxyStore
let isNew: Bool
@State private var host: ProxyHost
@Environment(\.dismiss) private var dismiss
@State private var domains: String
@State private var port: String
@State private var saving = false
@State private var error: String?
init(store: ProxyStore, host: ProxyHost, isNew: Bool) {
self.store = store
self.isNew = isNew
_host = State(initialValue: host)
_domains = State(initialValue: host.domainNames.joined(separator: ", "))
_port = State(initialValue: String(host.forwardPort))
}
private var valid: Bool {
let names = domains.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
return !names.isEmpty && names.allSatisfy { !$0.isEmpty && !$0.contains(" ") && !$0.contains("/") }
&& !host.forwardHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& (1...65535).contains(Int(port) ?? 0)
}
var body: some View {
VStack(spacing: 0) {
HStack {
Text(isNew ? "New Proxy Host" : "Edit Proxy Host").font(.headline)
Spacer()
}
.padding(20)
Form {
Section {
TextField("Domain Names", text: $domains, prompt: Text("app.example.com"))
Picker("Forward Scheme", selection: $host.forwardScheme) {
Text("HTTP").tag("http")
Text("HTTPS").tag("https")
}
TextField("Forward Host", text: $host.forwardHost, prompt: Text("192.168.1.20"))
TextField("Forward Port", text: $port, prompt: Text("8080"))
} header: {
Text("Forwarding")
} footer: {
Text("Separate multiple domain names with commas.")
}
Section("Security") {
Picker("SSL Certificate", selection: $host.certificateId) {
Text("None").tag(0)
ForEach(store.certificates) { certificate in
Text(certificate.niceName).tag(certificate.id)
}
}
Toggle("Force HTTPS", isOn: $host.sslForced).disabled(host.certificateId == 0)
Toggle("Block Common Exploits", isOn: $host.blockExploits)
}
Section("Options") {
Toggle("HTTP/2 Support", isOn: $host.http2Support)
Toggle("WebSocket Support", isOn: $host.allowWebsocketUpgrade)
}
}
.formStyle(.grouped)
.disabled(saving)
if let error {
Label(error, systemImage: "exclamationmark.triangle")
.foregroundStyle(.red).padding(.horizontal, 20).padding(.bottom, 12)
}
Divider()
HStack {
if saving { ProgressView().controlSize(.small) }
if store.isDemo { Text("Demo workspace").font(.callout).foregroundStyle(.secondary) }
Spacer()
Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction).disabled(saving)
Button(isNew ? "Add" : "Save") { Task { await save() } }
.keyboardShortcut(.defaultAction)
.disabled(!valid || saving)
}
.padding(20)
}
.frame(width: 510, height: 610)
.interactiveDismissDisabled(saving)
}
private func save() async {
saving = true
error = nil
host.domainNames = domains.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
host.forwardHost = host.forwardHost.trimmingCharacters(in: .whitespacesAndNewlines)
host.forwardPort = Int(port) ?? 80
if host.certificateId == 0 { host.sslForced = false }
do {
try await store.save(host, isNew: isNew)
dismiss()
} catch { self.error = error.localizedDescription }
saving = false
}
}
struct ConnectionSheet: View {
let store: ProxyStore
@Environment(\.dismiss) private var dismiss
@State private var url = ""
@State private var email = ""
@State private var password = ""
@State private var connecting = false
@State private var error: String?
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Connect to Server").font(.headline)
Text("Enter the address and credentials for your Nginx Proxy Manager server.")
.foregroundStyle(.secondary)
Form {
TextField("Server URL:", text: $url, prompt: Text("https://proxy.example.com"))
TextField("Email:", text: $email, prompt: Text("admin@example.com"))
SecureField("Password:", text: $password)
}
.textFieldStyle(.roundedBorder)
.disabled(connecting || store.isBusy)
Text("Your login is saved in Keychain after connecting and used to reconnect when you open the app.")
.font(.callout).foregroundStyle(.secondary)
Button("Forget Saved Login") {
do {
try store.forgetCredentials()
password = ""
error = nil
} catch { self.error = error.localizedDescription }
}
.disabled(connecting || store.isBusy)
Text("Forgetting the saved login leaves the current session connected.")
.font(.caption).foregroundStyle(.secondary)
if let error {
Label(error, systemImage: "exclamationmark.triangle").foregroundStyle(.red)
}
HStack {
if connecting { ProgressView().controlSize(.small) }
Spacer()
Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction).disabled(connecting)
Button("Connect") { Task { await connect() } }
.keyboardShortcut(.defaultAction)
.disabled(connecting || store.isBusy || url.isEmpty || email.isEmpty || password.isEmpty)
}
}
.padding(24)
.frame(width: 460)
.onAppear { url = store.baseURL }
.interactiveDismissDisabled(connecting)
}
private func connect() async {
connecting = true
error = nil
do {
try await store.connect(url: url, email: email, password: password)
password = ""
dismiss()
} catch { self.error = error.localizedDescription }
connecting = false
}
}