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:
123
clients/apple/macOS/VoiceCatMac/Sheets/PerUserTuningSheet.swift
Normal file
123
clients/apple/macOS/VoiceCatMac/Sheets/PerUserTuningSheet.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import AppKit
|
||||
import VoiceCatCore
|
||||
|
||||
final class PerUserTuningSheet: NSViewController {
|
||||
|
||||
private let client: VoiceCatClient
|
||||
private let userId: UInt32
|
||||
private let nickname: String
|
||||
|
||||
private var streams: [StreamSummary] = []
|
||||
private var rows: [StreamRow] = []
|
||||
|
||||
private struct StreamRow {
|
||||
let streamId: UInt32
|
||||
let label: String
|
||||
let gainSlider: NSSlider
|
||||
let muteCheckbox: NSButton
|
||||
let nrCheckbox: NSButton
|
||||
}
|
||||
|
||||
init(client: VoiceCatClient, userId: UInt32, nickname: String) {
|
||||
self.client = client
|
||||
self.userId = userId
|
||||
self.nickname = nickname
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func loadView() {
|
||||
view = NSView(frame: NSRect(x: 0, y: 0, width: 400, height: 200))
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
streams = client.listUserStreams(userId)
|
||||
buildUI()
|
||||
}
|
||||
|
||||
private func buildUI() {
|
||||
let titleLabel = NSTextField(labelWithString: "Volume & Noise Settings — \(nickname)")
|
||||
titleLabel.font = .boldSystemFont(ofSize: 13)
|
||||
|
||||
var rowViews: [NSView] = [titleLabel]
|
||||
|
||||
for stream in streams {
|
||||
let (_, state) = client.getRemoteStream(userId: userId, streamId: stream.id)
|
||||
let gain = state?.gain ?? 1.0
|
||||
let muted = state?.muted ?? false
|
||||
let nr = state?.noiseReduction ?? false
|
||||
|
||||
let gainSlider = NSSlider(value: Double(gain * 100), minValue: 0, maxValue: 200, target: self, action: #selector(sliderChanged))
|
||||
gainSlider.tag = Int(stream.id)
|
||||
gainSlider.numberOfTickMarks = 0
|
||||
gainSlider.setAccessibilityLabel("Volume for \(stream.label): \(Int(gain * 100)) percent")
|
||||
|
||||
let muteCheckbox = NSButton(checkboxWithTitle: "Mute", target: self, action: #selector(muteChanged))
|
||||
muteCheckbox.state = muted ? .on : .off
|
||||
muteCheckbox.tag = Int(stream.id)
|
||||
muteCheckbox.setAccessibilityLabel("Mute \(stream.label)")
|
||||
|
||||
let nrCheckbox = NSButton(checkboxWithTitle: "Noise reduction", target: self, action: #selector(nrChanged))
|
||||
nrCheckbox.state = nr ? .on : .off
|
||||
nrCheckbox.tag = Int(stream.id)
|
||||
nrCheckbox.setAccessibilityLabel("Noise reduction for \(stream.label)")
|
||||
|
||||
let streamLabel = NSTextField(labelWithString: "\(stream.label):")
|
||||
let gainLabel = NSTextField(labelWithString: "Volume:")
|
||||
let row = NSStackView(views: [streamLabel, gainLabel, gainSlider, muteCheckbox, nrCheckbox])
|
||||
row.orientation = .horizontal; row.spacing = 8
|
||||
rowViews.append(row)
|
||||
|
||||
rows.append(StreamRow(streamId: stream.id, label: stream.label,
|
||||
gainSlider: gainSlider, muteCheckbox: muteCheckbox,
|
||||
nrCheckbox: nrCheckbox))
|
||||
}
|
||||
|
||||
if streams.isEmpty {
|
||||
rowViews.append(NSTextField(labelWithString: "This user has no active streams."))
|
||||
}
|
||||
|
||||
let doneButton = NSButton(title: "Done", target: self, action: #selector(doneClicked))
|
||||
doneButton.bezelStyle = .rounded; doneButton.keyEquivalent = "\r"
|
||||
doneButton.setAccessibilityLabel("Close settings")
|
||||
rowViews.append(NSStackView(views: [NSView(), doneButton]))
|
||||
|
||||
let stack = NSStackView(views: rowViews)
|
||||
stack.orientation = .vertical
|
||||
stack.spacing = 10
|
||||
stack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
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),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func sliderChanged(_ sender: NSSlider) {
|
||||
apply(streamId: UInt32(sender.tag))
|
||||
}
|
||||
|
||||
@objc private func muteChanged(_ sender: NSButton) {
|
||||
apply(streamId: UInt32(sender.tag))
|
||||
}
|
||||
|
||||
@objc private func nrChanged(_ sender: NSButton) {
|
||||
apply(streamId: UInt32(sender.tag))
|
||||
}
|
||||
|
||||
private func apply(streamId: UInt32) {
|
||||
guard let row = rows.first(where: { $0.streamId == streamId }) else { return }
|
||||
let gain = Float(row.gainSlider.doubleValue) / 100.0
|
||||
let muted = row.muteCheckbox.state == .on
|
||||
let nr = row.nrCheckbox.state == .on
|
||||
client.setRemoteStream(userId: userId, streamId: streamId,
|
||||
gain: gain, muted: muted, noiseReduction: nr)
|
||||
row.gainSlider.setAccessibilityLabel("Volume for \(row.label): \(Int(row.gainSlider.doubleValue)) percent")
|
||||
}
|
||||
|
||||
@objc private func doneClicked() { dismiss(nil) }
|
||||
}
|
||||
Reference in New Issue
Block a user