import AppKit import VoiceCatCore final class ChannelEditSheet: NSViewController { var onComplete: ((ChannelEdit?) -> Void)? private let channels: [Channel] private var editing: ChannelEdit? private let nameField = NSTextField() private let topicField = NSTextField() private let parentPicker = NSPopUpButton() private let pwCheckbox = NSButton(checkboxWithTitle: "Password protected", target: nil, action: nil) private let pwField = NSSecureTextField() private let maxUsersField: NSTextField = { let f = NSTextField(); f.stringValue = "0"; return f }() private let sortOrderField: NSTextField = { let f = NSTextField(); f.stringValue = "0"; return f }() private let stereoCheckbox = NSButton(checkboxWithTitle: "Stereo", target: nil, action: nil) private let bitrateField: NSTextField = { let f = NSTextField(); f.stringValue = "64000"; return f }() private let fecCheckbox = NSButton(checkboxWithTitle: "FEC", target: nil, action: nil) private let dtxCheckbox = NSButton(checkboxWithTitle: "DTX", target: nil, action: nil) private let dredCheckbox = NSButton(checkboxWithTitle: "DRED", target: nil, action: nil) private let frameMsPicker = NSPopUpButton() private let applicationPicker = NSPopUpButton() private let sampleRateField: NSTextField = { let f = NSTextField(); f.stringValue = "48000"; return f }() private let packetLossField: NSTextField = { let f = NSTextField(); f.stringValue = "5"; return f }() private let complexityField: NSTextField = { let f = NSTextField(); f.stringValue = "10"; return f }() init(channels: [Channel], editing: ChannelEdit?) { self.channels = channels 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: 440, height: 360)) } override func viewDidLoad() { super.viewDidLoad() buildUI() populateIfEditing() } private func buildUI() { let titleText = editing == nil ? "Create Channel" : "Edit Channel" let titleLabel = NSTextField(labelWithString: titleText) titleLabel.font = .boldSystemFont(ofSize: 13) nameField.placeholderString = "Channel name" nameField.setAccessibilityLabel("Channel name") topicField.placeholderString = "Topic (optional)" topicField.setAccessibilityLabel("Channel topic") parentPicker.addItem(withTitle: "(root)") parentPicker.menu?.items.last?.representedObject = nil as UInt32? for ch in channels.sorted(by: { $0.name < $1.name }) { let item = NSMenuItem(title: ch.name, action: nil, keyEquivalent: "") item.representedObject = ch.id parentPicker.menu?.addItem(item) } parentPicker.setAccessibilityLabel("Parent channel") pwCheckbox.target = self; pwCheckbox.action = #selector(pwToggled) pwField.placeholderString = "password (leave blank to keep existing)" pwField.setAccessibilityLabel("Channel password") pwField.isEnabled = false maxUsersField.setAccessibilityLabel("Max users (0 = unlimited)") sortOrderField.setAccessibilityLabel("Sort order") for ms in ["10", "20", "40", "60"] { frameMsPicker.addItem(withTitle: "\(ms) ms") } frameMsPicker.selectItem(withTitle: "20 ms") frameMsPicker.setAccessibilityLabel("Opus frame duration") applicationPicker.addItem(withTitle: "VoIP") applicationPicker.addItem(withTitle: "Audio") applicationPicker.addItem(withTitle: "Low delay") applicationPicker.setAccessibilityLabel("Opus application profile") fecCheckbox.state = .on stereoCheckbox.setAccessibilityLabel("Stereo audio") bitrateField.setAccessibilityLabel("Bitrate in bits per second") sampleRateField.setAccessibilityLabel("Sample rate in Hz") packetLossField.setAccessibilityLabel("Expected packet loss percent (0 to 100)") complexityField.setAccessibilityLabel("Opus complexity (0 to 10)") fecCheckbox.setAccessibilityLabel("Forward error correction") dtxCheckbox.setAccessibilityLabel("Discontinuous transmission") dredCheckbox.setAccessibilityLabel("Deep redundancy (DRED)") let generalGrid = NSGridView(views: [ [NSTextField(labelWithString: "Name:"), nameField], [NSTextField(labelWithString: "Topic:"), topicField], [NSTextField(labelWithString: "Parent:"), parentPicker], [pwCheckbox, pwField], [NSTextField(labelWithString: "Max users:"), maxUsersField], [NSTextField(labelWithString: "Sort order:"), sortOrderField], ]) generalGrid.rowSpacing = 8; generalGrid.columnSpacing = 8 generalGrid.column(at: 0).xPlacement = .trailing let audioGrid = NSGridView(views: [ [NSTextField(labelWithString: "Bitrate:"), bitrateField], [NSTextField(labelWithString: "Sample rate:"), sampleRateField], [NSTextField(labelWithString: "Frame:"), frameMsPicker], [NSTextField(labelWithString: "Application:"), applicationPicker], [NSTextField(labelWithString: "Packet loss %:"), packetLossField], [NSTextField(labelWithString: "Complexity:"), complexityField], [stereoCheckbox, fecCheckbox], [dtxCheckbox, dredCheckbox], ]) audioGrid.rowSpacing = 8; audioGrid.columnSpacing = 8 audioGrid.column(at: 0).xPlacement = .trailing let tabs = NSTabView() let generalTab = NSTabViewItem(identifier: "general") generalTab.label = "General" generalTab.view = generalGrid let audioTab = NSTabViewItem(identifier: "audio") audioTab.label = "Audio" audioTab.view = audioGrid tabs.addTabViewItem(generalTab) tabs.addTabViewItem(audioTab) let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancelClicked)) cancelButton.bezelStyle = .rounded let saveButton = NSButton(title: editing == nil ? "Create" : "Save", target: self, action: #selector(saveClicked)) saveButton.bezelStyle = .rounded; saveButton.keyEquivalent = "\r" let buttonRow = NSStackView(views: [NSView(), cancelButton, saveButton]) buttonRow.orientation = .horizontal; buttonRow.spacing = 8 let stack = NSStackView(views: [titleLabel, tabs, buttonRow]) stack.orientation = .vertical stack.spacing = 12 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), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } private func populateIfEditing() { guard let e = editing else { return } nameField.stringValue = e.name topicField.stringValue = e.topic if e.parentId != 0 { for item in parentPicker.itemArray where (item.representedObject as? UInt32) == e.parentId { parentPicker.select(item); break } } pwCheckbox.state = e.passwordProtected ? .on : .off pwField.isEnabled = e.passwordProtected maxUsersField.stringValue = "\(e.maxUsers)" sortOrderField.stringValue = "\(e.sortOrder)" stereoCheckbox.state = e.audio.stereo ? .on : .off bitrateField.stringValue = "\(e.audio.bitrateBps)" sampleRateField.stringValue = "\(e.audio.sampleRate)" packetLossField.stringValue = "\(e.audio.expectedPacketLoss)" complexityField.stringValue = "\(e.audio.complexity)" fecCheckbox.state = e.audio.fec ? .on : .off dtxCheckbox.state = e.audio.dtx ? .on : .off dredCheckbox.state = e.audio.dred ? .on : .off applicationPicker.selectItem(at: Int(min(e.audio.application, 2))) let frameStr = "\(e.audio.frameMs) ms" if let item = frameMsPicker.item(withTitle: frameStr) { frameMsPicker.select(item) } } @objc private func pwToggled() { pwField.isEnabled = pwCheckbox.state == .on } @objc private func saveClicked() { let name = nameField.stringValue.trimmingCharacters(in: .whitespaces) guard !name.isEmpty else { nameField.becomeFirstResponder(); return } let parentId = parentPicker.selectedItem?.representedObject as? UInt32 ?? 0 let maxUsers = UInt32(maxUsersField.stringValue) ?? 0 let sortOrder = UInt32(sortOrderField.stringValue) ?? 0 let bitrate = UInt32(bitrateField.stringValue) ?? 64000 let sampleRate = UInt32(sampleRateField.stringValue) ?? 48000 let packetLoss = min(UInt32(packetLossField.stringValue) ?? 5, 100) let complexity = min(UInt32(complexityField.stringValue) ?? 10, 10) let frameMsStr = frameMsPicker.titleOfSelectedItem?.replacingOccurrences(of: " ms", with: "") ?? "20" let frameMs = UInt32(frameMsStr) ?? 20 let application = UInt32(max(applicationPicker.indexOfSelectedItem, 0)) let audio = AudioConfig( stereo: stereoCheckbox.state == .on, sampleRate: sampleRate, bitrateBps: bitrate, frameMs: frameMs, application: application, fec: fecCheckbox.state == .on, expectedPacketLoss: packetLoss, dtx: dtxCheckbox.state == .on, complexity: complexity, dred: dredCheckbox.state == .on ) let pwProtected = pwCheckbox.state == .on let pw: String? = pwProtected ? (pwField.stringValue.isEmpty ? nil : pwField.stringValue) : nil let result = ChannelEdit( id: editing?.id ?? 0, parentId: parentId, name: name, topic: topicField.stringValue, passwordProtected: pwProtected, password: pw, maxUsers: maxUsers, sortOrder: sortOrder, audio: audio ) dismiss(nil) onComplete?(result) } @objc private func cancelClicked() { dismiss(nil) onComplete?(nil) } }