import SwiftUI import VoiceCatCore struct ChannelEditView: View { let channelId: UInt32? @Bindable var session: SessionState @Environment(\.dismiss) private var dismiss // General @State private var name = "" @State private var topic = "" @State private var parentId: UInt32 = 0 @State private var passwordProtected = false @State private var password = "" @State private var maxUsers = "0" @State private var sortOrder = "0" // Audio (Opus) — populated from the channel's current config when editing. @State private var stereo = false @State private var bitrate = "64000" @State private var sampleRate = "48000" @State private var frameMs: UInt32 = 20 @State private var application: UInt32 = 0 @State private var packetLoss = "5" @State private var complexity = 10 @State private var fec = true @State private var dtx = false @State private var dred = false private var isEditing: Bool { channelId != nil } var body: some View { NavigationStack { Form { Section("Channel Info") { TextField("Name", text: $name) .autocorrectionDisabled() .accessibilityLabel("Channel name") TextField("Topic (optional)", text: $topic) .accessibilityLabel("Channel topic, optional") Picker("Parent", selection: $parentId) { Text("(root)").tag(UInt32(0)) ForEach(parentOptions) { ch in Text(ch.name).tag(ch.id) } } .accessibilityLabel("Parent channel") Toggle("Password protected", isOn: $passwordProtected) if passwordProtected { SecureField("Password (blank keeps existing)", text: $password) .accessibilityLabel("Channel password") } TextField("Max users (0 = unlimited)", text: $maxUsers) .keyboardType(.numberPad) .accessibilityLabel("Maximum users, zero means unlimited") TextField("Sort order", text: $sortOrder) .keyboardType(.numberPad) .accessibilityLabel("Sort order") } Section("Audio (Opus)") { Toggle("Stereo", isOn: $stereo) TextField("Bitrate (bps)", text: $bitrate) .keyboardType(.numberPad) .accessibilityLabel("Bitrate in bits per second") TextField("Sample rate (Hz)", text: $sampleRate) .keyboardType(.numberPad) .accessibilityLabel("Sample rate in Hz") Picker("Frame", selection: $frameMs) { ForEach([UInt32(10), 20, 40, 60], id: \.self) { ms in Text("\(ms) ms").tag(ms) } } .accessibilityLabel("Opus frame duration") Picker("Application", selection: $application) { Text("VoIP").tag(UInt32(0)) Text("Audio").tag(UInt32(1)) Text("Low delay").tag(UInt32(2)) } .accessibilityLabel("Opus application profile") TextField("Expected packet loss %", text: $packetLoss) .keyboardType(.numberPad) .accessibilityLabel("Expected packet loss percent, 0 to 100") Stepper("Complexity: \(complexity)", value: $complexity, in: 0...10) .accessibilityLabel("Opus complexity, 0 to 10") Toggle("FEC (forward error correction)", isOn: $fec) Toggle("DTX (discontinuous transmission)", isOn: $dtx) Toggle("DRED (deep redundancy)", isOn: $dred) } } .navigationTitle(isEditing ? "Edit Channel" : "New Channel") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { save() dismiss() } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } } .onAppear(perform: loadIfEditing) } } /// Channels offered as a parent. Excludes the channel being edited so it can't parent itself. private var parentOptions: [Channel] { session.channels .filter { $0.id != channelId } .sorted { $0.name < $1.name } } private func loadIfEditing() { guard let id = channelId, let ch = session.channels.first(where: { $0.id == id }) else { return } name = ch.name topic = ch.topic parentId = ch.parentId passwordProtected = ch.passwordProtected maxUsers = "\(ch.maxUsers)" sortOrder = "\(ch.sortOrder)" stereo = ch.audio.stereo bitrate = "\(ch.audio.bitrateBps)" sampleRate = "\(ch.audio.sampleRate)" frameMs = ch.audio.frameMs application = ch.audio.application packetLoss = "\(ch.audio.expectedPacketLoss)" complexity = Int(ch.audio.complexity) fec = ch.audio.fec dtx = ch.audio.dtx dred = ch.audio.dred } private func save() { let trimmedName = name.trimmingCharacters(in: .whitespaces) guard !trimmedName.isEmpty else { return } let audio = AudioConfig( stereo: stereo, sampleRate: UInt32(sampleRate) ?? 48000, bitrateBps: UInt32(bitrate) ?? 64000, frameMs: frameMs, application: application, fec: fec, expectedPacketLoss: min(UInt32(packetLoss) ?? 5, 100), dtx: dtx, complexity: UInt32(complexity), dred: dred ) let pw: String? = passwordProtected ? (password.isEmpty ? nil : password) : nil let info = ChannelEdit( id: channelId ?? 0, parentId: parentId, name: trimmedName, topic: topic, passwordProtected: passwordProtected, password: pw, maxUsers: UInt32(maxUsers) ?? 0, sortOrder: UInt32(sortOrder) ?? 0, audio: audio ) if isEditing { session.editChannel(info) } else { session.createChannel(info) } } }