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 frameMsPicker = NSPopUpButton() 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 ["20", "40", "60"] { frameMsPicker.addItem(withTitle: "\(ms) ms") } frameMsPicker.setAccessibilityLabel("Opus frame duration") fecCheckbox.state = .on stereoCheckbox.setAccessibilityLabel("Stereo audio") bitrateField.setAccessibilityLabel("Bitrate in bits per second") fecCheckbox.setAccessibilityLabel("Forward error correction") dtxCheckbox.setAccessibilityLabel("Discontinuous transmission") 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: "Frame:"), frameMsPicker], [stereoCheckbox, fecCheckbox], [dtxCheckbox, NSView()], ]) 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)" fecCheckbox.state = e.audio.fec ? .on : .off dtxCheckbox.state = e.audio.dtx ? .on : .off 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 frameMsStr = frameMsPicker.titleOfSelectedItem?.replacingOccurrences(of: " ms", with: "") ?? "20" let frameMs = UInt32(frameMsStr) ?? 20 let audio = AudioConfig( stereo: stereoCheckbox.state == .on, bitrateBps: bitrate, frameMs: frameMs, fec: fecCheckbox.state == .on, dtx: dtxCheckbox.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) } }