155 lines
5.8 KiB
Swift
155 lines
5.8 KiB
Swift
|
|
import AppKit
|
||
|
|
|
||
|
|
final class AddServerSheet: NSViewController {
|
||
|
|
|
||
|
|
var onComplete: ((SavedServer?) -> Void)?
|
||
|
|
|
||
|
|
private var editing: SavedServer?
|
||
|
|
|
||
|
|
private let hostField = NSTextField()
|
||
|
|
private let portField: NSTextField = {
|
||
|
|
let f = NSTextField()
|
||
|
|
f.stringValue = "7878"
|
||
|
|
return f
|
||
|
|
}()
|
||
|
|
private let authPicker = NSPopUpButton()
|
||
|
|
private let usernameField = NSTextField()
|
||
|
|
private let passwordField = NSSecureTextField()
|
||
|
|
private let savePwCheckbox = NSButton(checkboxWithTitle: "Save password in Keychain", target: nil, action: nil)
|
||
|
|
|
||
|
|
init(editing: SavedServer?) {
|
||
|
|
self.editing = editing
|
||
|
|
super.init(nibName: nil, bundle: nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
required init?(coder: NSCoder) { fatalError() }
|
||
|
|
|
||
|
|
override func loadView() {
|
||
|
|
view = NSView(frame: NSRect(x: 0, y: 0, width: 380, height: 260))
|
||
|
|
}
|
||
|
|
|
||
|
|
override func viewDidLoad() {
|
||
|
|
super.viewDidLoad()
|
||
|
|
buildUI()
|
||
|
|
if let s = editing {
|
||
|
|
hostField.stringValue = s.host
|
||
|
|
portField.stringValue = "\(s.port)"
|
||
|
|
authPicker.selectItem(withTitle: s.authMode == .guest ? "Guest" : "Account")
|
||
|
|
usernameField.stringValue = s.savedUsername ?? ""
|
||
|
|
updateAuthVisibility()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func buildUI() {
|
||
|
|
let title = NSTextField(labelWithString: editing == nil ? "Add Server" : "Edit Server")
|
||
|
|
title.font = .boldSystemFont(ofSize: 14)
|
||
|
|
|
||
|
|
let hostLabel = NSTextField(labelWithString: "Host:")
|
||
|
|
let portLabel = NSTextField(labelWithString: "Port:")
|
||
|
|
let authLabel = NSTextField(labelWithString: "Auth:")
|
||
|
|
let userLabel = NSTextField(labelWithString: "Username:")
|
||
|
|
let pwLabel = NSTextField(labelWithString: "Password:")
|
||
|
|
|
||
|
|
hostField.placeholderString = "hostname or IP"
|
||
|
|
hostField.setAccessibilityLabel("Server hostname or IP address")
|
||
|
|
|
||
|
|
portField.setAccessibilityLabel("Server port")
|
||
|
|
|
||
|
|
authPicker.addItem(withTitle: "Guest")
|
||
|
|
authPicker.addItem(withTitle: "Account")
|
||
|
|
authPicker.target = self; authPicker.action = #selector(authChanged)
|
||
|
|
authPicker.setAccessibilityLabel("Authentication mode")
|
||
|
|
|
||
|
|
usernameField.placeholderString = "username"
|
||
|
|
usernameField.setAccessibilityLabel("Username")
|
||
|
|
|
||
|
|
passwordField.placeholderString = "leave blank to enter at connect"
|
||
|
|
passwordField.setAccessibilityLabel("Password (optional — enter at connect time if blank)")
|
||
|
|
|
||
|
|
savePwCheckbox.setAccessibilityLabel("Save password in system Keychain")
|
||
|
|
|
||
|
|
let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancelClicked))
|
||
|
|
cancelButton.bezelStyle = .rounded
|
||
|
|
let saveButton = NSButton(title: "Save", target: self, action: #selector(saveClicked))
|
||
|
|
saveButton.bezelStyle = .rounded
|
||
|
|
saveButton.keyEquivalent = "\r"
|
||
|
|
saveButton.setAccessibilityLabel("Save server")
|
||
|
|
|
||
|
|
let grid = NSGridView(views: [
|
||
|
|
[hostLabel, hostField],
|
||
|
|
[portLabel, portField],
|
||
|
|
[authLabel, authPicker],
|
||
|
|
[userLabel, usernameField],
|
||
|
|
[pwLabel, passwordField],
|
||
|
|
[NSView(), savePwCheckbox],
|
||
|
|
])
|
||
|
|
grid.rowSpacing = 8
|
||
|
|
grid.columnSpacing = 8
|
||
|
|
grid.column(at: 0).xPlacement = .trailing
|
||
|
|
|
||
|
|
let buttonRow = NSStackView(views: [NSView(), cancelButton, saveButton])
|
||
|
|
buttonRow.orientation = .horizontal; buttonRow.spacing = 8
|
||
|
|
|
||
|
|
let stack = NSStackView(views: [title, grid, buttonRow])
|
||
|
|
stack.orientation = .vertical
|
||
|
|
stack.spacing = 16
|
||
|
|
stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
|
||
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||
|
|
view.addSubview(stack)
|
||
|
|
NSLayoutConstraint.activate([
|
||
|
|
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
||
|
|
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||
|
|
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||
|
|
stack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||
|
|
])
|
||
|
|
|
||
|
|
updateAuthVisibility()
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func authChanged() { updateAuthVisibility() }
|
||
|
|
|
||
|
|
private func updateAuthVisibility() {
|
||
|
|
let isAccount = authPicker.titleOfSelectedItem == "Account"
|
||
|
|
usernameField.isEnabled = isAccount
|
||
|
|
passwordField.isEnabled = isAccount
|
||
|
|
savePwCheckbox.isEnabled = isAccount
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func saveClicked() {
|
||
|
|
let host = hostField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
|
|
guard !host.isEmpty else {
|
||
|
|
hostField.becomeFirstResponder(); return
|
||
|
|
}
|
||
|
|
let portStr = portField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
|
|
guard let port = UInt16(portStr), port > 0 else {
|
||
|
|
portField.becomeFirstResponder(); return
|
||
|
|
}
|
||
|
|
let isGuest = authPicker.titleOfSelectedItem == "Guest"
|
||
|
|
var server = editing ?? SavedServer(host: host, port: port,
|
||
|
|
authMode: isGuest ? .guest : .password)
|
||
|
|
server.host = host; server.port = port
|
||
|
|
server.authMode = isGuest ? .guest : .password
|
||
|
|
server.savedUsername = isGuest ? nil : usernameField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
|
|
|
||
|
|
if !isGuest && savePwCheckbox.state == .on {
|
||
|
|
let pw = passwordField.stringValue
|
||
|
|
if !pw.isEmpty {
|
||
|
|
let tag = server.keychainTag ?? "voicecat.server.\(server.id.uuidString)"
|
||
|
|
server.keychainTag = tag
|
||
|
|
ServerListStore.savePassword(pw, tag: tag)
|
||
|
|
}
|
||
|
|
} else if isGuest {
|
||
|
|
if let tag = server.keychainTag { ServerListStore.deletePassword(tag: tag) }
|
||
|
|
server.keychainTag = nil
|
||
|
|
}
|
||
|
|
|
||
|
|
dismiss(nil)
|
||
|
|
onComplete?(server)
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func cancelClicked() {
|
||
|
|
dismiss(nil)
|
||
|
|
onComplete?(nil)
|
||
|
|
}
|
||
|
|
}
|