feat(macos): VoiceCatMac AppKit client + fix xcodebuild
The previous "shipped" claim was false — xcodebuild had never been run and the macOS app source was never committed. This commit adds the 17 Swift source files + xcodeproj and fixes three real defect classes so Debug and Release both build clean: 1. MainWindowController.swift compile errors: - NSAccessibility.post arg order (element:notification:userInfo:) - NSAccessibilityPriorityMedium -> NSAccessibilityPriorityLevel.medium - StreamSummary.streamId -> .id (Identifiable conformance) - drop redundant VoiceCatResult.description extension 2. Linker: add -lc++ to OTHER_LDFLAGS (libvoicecat-fat.a is C++20; pure-Swift app target has no .cpp sources so libc++ wasn't pulled in — swift test passed because Package.swift testTarget has linkerSettings: c++). 3. Release config: add ONLY_ACTIVE_ARCH=YES (XCFramework only has arm64). Verified: clean Debug + Release builds, otool -L shows libc++.1.dylib, nm shows _vc_client_create/_vc_version_string, app launches and runs.
This commit is contained in:
154
clients/apple/macOS/VoiceCatMac/Sheets/AddServerSheet.swift
Normal file
154
clients/apple/macOS/VoiceCatMac/Sheets/AddServerSheet.swift
Normal file
@@ -0,0 +1,154 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user