Polish Apple network config sheets
This commit is contained in:
parent
2f69987742
commit
014bca073f
1 changed files with 261 additions and 13 deletions
|
|
@ -2,6 +2,11 @@ import AuthenticationServices
|
||||||
import BurrowConfiguration
|
import BurrowConfiguration
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
#if canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
public struct BurrowView: View {
|
public struct BurrowView: View {
|
||||||
@State private var networkViewModel: NetworkViewModel
|
@State private var networkViewModel: NetworkViewModel
|
||||||
|
|
@ -338,15 +343,10 @@ private struct ConfigurationSheetView: View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Text(sheet.kind.subtitle)
|
sheetSummaryCard
|
||||||
.font(.callout)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
if let availabilityNote = sheet.kind.availabilityNote {
|
|
||||||
Text(availabilityNote)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.listRowInsets(.init(top: 4, leading: 0, bottom: 4, trailing: 0))
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
|
||||||
Section("Identity") {
|
Section("Identity") {
|
||||||
TextField("Title", text: $draft.title)
|
TextField("Title", text: $draft.title)
|
||||||
|
|
@ -364,7 +364,10 @@ private struct ConfigurationSheetView: View {
|
||||||
Section("WireGuard Configuration") {
|
Section("WireGuard Configuration") {
|
||||||
TextEditor(text: $draft.wireGuardConfig)
|
TextEditor(text: $draft.wireGuardConfig)
|
||||||
.font(.body.monospaced())
|
.font(.body.monospaced())
|
||||||
.frame(minHeight: 220)
|
.frame(minHeight: wireGuardEditorHeight)
|
||||||
|
.contextMenu {
|
||||||
|
wireGuardContextActions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .tor:
|
case .tor:
|
||||||
Section("Tor Preferences") {
|
Section("Tor Preferences") {
|
||||||
|
|
@ -385,12 +388,35 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(sheet.kind.title)
|
.navigationTitle(sheet.kind.title)
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
sheetMenuActions
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis.circle")
|
||||||
|
}
|
||||||
|
.accessibilityLabel("More")
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Menu {
|
||||||
|
sheetMenuActions
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis.circle")
|
||||||
|
}
|
||||||
|
.accessibilityLabel("More")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if !showsBottomActionButton {
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
Button(confirmationTitle) {
|
Button(confirmationTitle) {
|
||||||
submit()
|
submit()
|
||||||
|
|
@ -399,9 +425,15 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 520, minHeight: 620)
|
.frame(minWidth: 520, minHeight: 620)
|
||||||
#endif
|
#endif
|
||||||
|
.safeAreaInset(edge: .bottom) {
|
||||||
|
if showsBottomActionButton {
|
||||||
|
bottomActionBar
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
runAutomationIfNeeded()
|
runAutomationIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +452,7 @@ private struct ConfigurationSheetView: View {
|
||||||
Text(provider.title).tag(provider)
|
Text(provider.title).tag(provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
Text(draft.tailnetProvider.subtitle)
|
Text(draft.tailnetProvider.subtitle)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -448,6 +481,7 @@ private struct ConfigurationSheetView: View {
|
||||||
Text(mode.title).tag(mode)
|
Text(mode.title).tag(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
if draft.authMode != .none {
|
if draft.authMode != .none {
|
||||||
SecureField(
|
SecureField(
|
||||||
draft.authMode == .password ? "Password" : "Preauth Key",
|
draft.authMode == .password ? "Password" : "Preauth Key",
|
||||||
|
|
@ -492,6 +526,164 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var sheetSummaryCard: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: sheet.iconName)
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.foregroundStyle(sheetAccentColor)
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(sheetAccentColor.opacity(0.14))
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(summaryTitle)
|
||||||
|
.font(.headline)
|
||||||
|
Text(sheet.kind.subtitle)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let availabilityNote = sheet.kind.availabilityNote {
|
||||||
|
Text(availabilityNote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 18)
|
||||||
|
.fill(.thinMaterial)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var bottomActionBar: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Divider()
|
||||||
|
.overlay(.white.opacity(0.3))
|
||||||
|
Button(confirmationTitle) {
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
.buttonStyle(.floating(color: sheetAccentColor, cornerRadius: 18))
|
||||||
|
.disabled(isSubmitting || submissionDisabled)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
}
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var sheetMenuActions: some View {
|
||||||
|
Button("Use Suggested Identity") {
|
||||||
|
applySuggestedIdentity()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sheet {
|
||||||
|
case .wireGuard:
|
||||||
|
Button("Paste Configuration") {
|
||||||
|
pasteWireGuardConfiguration()
|
||||||
|
}
|
||||||
|
.disabled(clipboardString?.isEmpty ?? true)
|
||||||
|
|
||||||
|
Button("Clear Configuration", role: .destructive) {
|
||||||
|
draft.wireGuardConfig = ""
|
||||||
|
}
|
||||||
|
.disabled(draft.wireGuardConfig.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
|
||||||
|
case .tor:
|
||||||
|
Menu("Presets") {
|
||||||
|
Button("Recommended Tor Defaults") {
|
||||||
|
applyTorDefaults()
|
||||||
|
}
|
||||||
|
Button("Restore Suggested Identity") {
|
||||||
|
applySuggestedIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .tailnet:
|
||||||
|
Menu("Provider") {
|
||||||
|
ForEach(TailnetProvider.allCases) { provider in
|
||||||
|
Button(provider.title) {
|
||||||
|
applyTailnetProvider(provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !draft.tailnetProvider.usesWebLogin {
|
||||||
|
Menu("Authentication") {
|
||||||
|
ForEach([AccountAuthMode.none, .password, .preauthKey]) { mode in
|
||||||
|
Button(mode.title) {
|
||||||
|
draft.authMode = mode
|
||||||
|
if mode == .none {
|
||||||
|
draft.secret = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Restore Provider Defaults") {
|
||||||
|
applyTailnetDefaults(for: draft.tailnetProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var wireGuardContextActions: some View {
|
||||||
|
Button("Paste Configuration") {
|
||||||
|
pasteWireGuardConfiguration()
|
||||||
|
}
|
||||||
|
.disabled(clipboardString?.isEmpty ?? true)
|
||||||
|
|
||||||
|
Button("Clear", role: .destructive) {
|
||||||
|
draft.wireGuardConfig = ""
|
||||||
|
}
|
||||||
|
.disabled(draft.wireGuardConfig.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sheetAccentColor: Color {
|
||||||
|
switch sheet {
|
||||||
|
case .wireGuard:
|
||||||
|
.blue
|
||||||
|
case .tor, .tailnet:
|
||||||
|
sheet.kind.accentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var summaryTitle: String {
|
||||||
|
switch sheet {
|
||||||
|
case .wireGuard:
|
||||||
|
"Import WireGuard"
|
||||||
|
case .tor:
|
||||||
|
"Configure Tor"
|
||||||
|
case .tailnet:
|
||||||
|
"Connect Tailnet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsBottomActionButton: Bool {
|
||||||
|
#if os(iOS)
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var wireGuardEditorHeight: CGFloat {
|
||||||
|
#if os(iOS)
|
||||||
|
180
|
||||||
|
#else
|
||||||
|
220
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private var confirmationTitle: String {
|
private var confirmationTitle: String {
|
||||||
switch sheet {
|
switch sheet {
|
||||||
case .wireGuard:
|
case .wireGuard:
|
||||||
|
|
@ -775,6 +967,62 @@ private struct ConfigurationSheetView: View {
|
||||||
try accountStore.upsert(record, secret: secret)
|
try accountStore.upsert(record, secret: secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applySuggestedIdentity() {
|
||||||
|
let defaults = AccountDraft(sheet: sheet)
|
||||||
|
if draft.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
draft.title = defaults.title
|
||||||
|
}
|
||||||
|
draft.accountName = defaults.accountName
|
||||||
|
draft.identityName = defaults.identityName
|
||||||
|
if sheet == .tailnet {
|
||||||
|
draft.hostname = defaults.hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTorDefaults() {
|
||||||
|
let defaults = AccountDraft(sheet: .tor)
|
||||||
|
draft.title = defaults.title
|
||||||
|
draft.accountName = defaults.accountName
|
||||||
|
draft.identityName = defaults.identityName
|
||||||
|
draft.torAddresses = defaults.torAddresses
|
||||||
|
draft.torDNS = defaults.torDNS
|
||||||
|
draft.torMTU = defaults.torMTU
|
||||||
|
draft.torListen = defaults.torListen
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTailnetProvider(_ provider: TailnetProvider) {
|
||||||
|
draft.tailnetProvider = provider
|
||||||
|
applyTailnetDefaults(for: provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTailnetDefaults(for provider: TailnetProvider) {
|
||||||
|
draft.authority = provider.defaultAuthority ?? ""
|
||||||
|
if provider.usesWebLogin {
|
||||||
|
draft.authMode = .web
|
||||||
|
draft.username = ""
|
||||||
|
draft.secret = ""
|
||||||
|
} else {
|
||||||
|
if draft.authMode == .web {
|
||||||
|
draft.authMode = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pasteWireGuardConfiguration() {
|
||||||
|
guard let clipboardString else { return }
|
||||||
|
draft.wireGuardConfig = clipboardString
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clipboardString: String? {
|
||||||
|
#if canImport(UIKit)
|
||||||
|
UIPasteboard.general.string
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
NSPasteboard.general.string(forType: .string)
|
||||||
|
#else
|
||||||
|
nil
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private func normalized(_ value: String, fallback: String) -> String {
|
private func normalized(_ value: String, fallback: String) -> String {
|
||||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return trimmed.isEmpty ? fallback : trimmed
|
return trimmed.isEmpty ? fallback : trimmed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue