2026-09-19 00:39:04 +02:00
using AppKit ;
using CoreGraphics ;
using Voicecat.V1 ;
using Channel = Voicecat . V1 . Channel ;
namespace VoiceCat.Mac ;
internal static class ChannelEditor
{
2026-09-22 16:54:49 +02:00
internal static ChannelEdit ? Run ( Channel ? existing , IReadOnlyList < Channel > channels , bool supportsAdaptivePacketLoss )
2026-09-19 00:39:04 +02:00
{
var view = new NSView ( new CGRect ( 0 , 0 , 520 , 500 ));
var name = Field ( existing ?. Name ?? "" , 360 ); var topic = Field ( existing ?. Topic ?? "" , 330 );
var parent = Picker ( channels . Select ( channel => channel . Name ). Prepend ( "No parent" ). ToArray (), 300 );
int parentIndex = existing is null ? 0 : channels . ToList (). FindIndex ( channel => channel . Id == existing . ParentId ) + 1 ; parent . SelectItem ( Math . Max ( 0 , parentIndex ));
var type = Picker ([ "Permanent" , "Temporary" ], 270 ); type . SelectItem ( existing ?. Type == ChannelType . ChannelTemporary ? 1 : 0 );
var maximum = Field (( existing ?. MaxUsers ?? 0 ). ToString (), 240 ); var order = Field (( existing ?. Order ?? 0 ). ToString (), 210 );
var password = new NSSecureTextField ( new CGRect ( 155 , 180 , 345 , 24 )) { PlaceholderString = existing ?. PasswordProtected == true ? "Leave blank to preserve" : "Optional" };
var mode = Picker ([ "Mono" , "Stereo" ], 150 ); mode . SelectItem ( existing ?. Audio ?. Mode == ChannelMode . ModeStereo ? 1 : 0 );
var rate = Picker ([ "8000" , "12000" , "16000" , "24000" , "48000" ], 120 ); Select ( rate , ( existing ?. Audio ?. SampleRate ?? 48000 ). ToString ());
var frame = Picker ([ "5" , "10" , "20" , "40" , "60" ], 90 ); Select ( frame , ( existing ?. Audio ?. FrameMs ?? 20 ). ToString ());
var application = Picker ([ "VoIP" , "Audio" , "Low delay" ], 60 ); application . SelectItem (( int )( existing ?. Audio ?. Application ?? OpusApplication . OpusVoip ));
var bitrate = Field (( existing ?. Audio ?. BitrateBps ?? 64000 ). ToString (), 30 );
Add ( view , "Name" , name , 360 ); Add ( view , "Topic" , topic , 330 ); Add ( view , "Parent" , parent , 300 ); Add ( view , "Type" , type , 270 );
Add ( view , "Maximum users (0=none)" , maximum , 240 ); Add ( view , "Sort order" , order , 210 ); Add ( view , "Password" , password , 180 );
Add ( view , "Mode" , mode , 150 ); Add ( view , "Sample rate" , rate , 120 ); Add ( view , "Frame ms" , frame , 90 ); Add ( view , "Application" , application , 60 ); Add ( view , "Bitrate" , bitrate , 30 );
2026-09-22 16:54:49 +02:00
var advanced = new NSView ( new CGRect ( 0 , 0 , 520 , 175 ));
var automaticLoss = Check ( "Automatic packet loss" , existing ?. Audio ?. PacketLossMode != PacketLossMode . PacketLossManual , 155 , 140 );
automaticLoss . Frame = new CGRect ( 155 , 140 , 200 , 24 );
automaticLoss . Enabled = supportsAdaptivePacketLoss ;
var lossSpeed = Picker ([ "Fast" , "Balanced" , "Stable" ], 105 );
lossSpeed . SelectItem ( existing ?. Audio ?. PacketLossMode switch { PacketLossMode . PacketLossAutoFast => 0 , PacketLossMode . PacketLossAutoStable => 2 , _ => 1 });
var loss = Field (( existing ?. Audio ?. ExpectedPacketLoss ?? 5 ). ToString (), 70 ); loss . Frame = new CGRect ( 155 , 70 , 100 , 24 );
var complexity = Field (( existing ?. Audio ?. Complexity ?? 10 ). ToString (), 40 ); complexity . Frame = new CGRect ( 155 , 40 , 100 , 24 );
var fec = Check ( "In-band FEC" , existing ?. Audio ?. Fec ?? true , 0 , 10 ); var dtx = Check ( "DTX" , existing ?. Audio ?. Dtx ?? true , 130 , 10 );
var dred = Check ( "Deep redundancy" , existing ?. Audio ?. Dred ?? false , 230 , 10 );
void UpdateLossControls () { loss . Enabled = ! On ( automaticLoss ); lossSpeed . Enabled = supportsAdaptivePacketLoss && On ( automaticLoss ); }
automaticLoss . Activated += ( _ , _ ) => UpdateLossControls (); UpdateLossControls ();
advanced . AddSubview ( automaticLoss ); Add ( advanced , "Adaptation speed" , lossSpeed , 105 ); Add ( advanced , "Packet loss %" , loss , 70 ); Add ( advanced , "Complexity 0– 10" , complexity , 40 );
advanced . AddSubview ( fec ); advanced . AddSubview ( dtx ); advanced . AddSubview ( dred );
var container = new NSView ( new CGRect ( 0 , 0 , 520 , 690 )); view . Frame = new CGRect ( 0 , 180 , 520 , 500 ); container . AddSubview ( view ); container . AddSubview ( advanced );
2026-09-19 00:39:04 +02:00
var alert = new NSAlert { MessageText = existing is null ? "Create channel" : "Edit channel" , AccessoryView = container };
alert . AddButton ( existing is null ? "Create" : "Save" ); alert . AddButton ( "Cancel" ); if ( alert . RunModal () != 1000 ) return null ;
if ( string . IsNullOrWhiteSpace ( name . StringValue ) || ! uint . TryParse ( maximum . StringValue , out uint max ) || ! int . TryParse ( order . StringValue , out int sort ) ||
! uint . TryParse ( rate . TitleOfSelectedItem , out uint sampleRate ) || ! uint . TryParse ( frame . TitleOfSelectedItem , out uint frameMs ) ||
! uint . TryParse ( bitrate . StringValue , out uint bitrateValue ) || ! uint . TryParse ( loss . StringValue , out uint packetLoss ) ||
! uint . TryParse ( complexity . StringValue , out uint complexityValue ) || packetLoss > 100 || complexityValue > 10 ) return null ;
uint parentId = parent . IndexOfSelectedItem <= 0 ? 0 : channels [ checked (( int ) parent . IndexOfSelectedItem - 1 )]. Id ;
return new ( new Channel
{
Id = existing ?. Id ?? 0 , ParentId = parentId , Name = name . StringValue . Trim (), Topic = topic . StringValue . Trim (), MaxUsers = max , Order = sort ,
Type = type . IndexOfSelectedItem == 1 ? ChannelType . ChannelTemporary : ChannelType . ChannelPermanent ,
Audio = new AudioConfig { Codec = 0 , Mode = mode . IndexOfSelectedItem == 1 ? ChannelMode . ModeStereo : ChannelMode . ModeMono ,
SampleRate = sampleRate , FrameMs = frameMs , Application = ( OpusApplication )( int ) application . IndexOfSelectedItem , BitrateBps = bitrateValue ,
2026-09-22 16:54:49 +02:00
Fec = On ( fec ), Dtx = On ( dtx ), Dred = On ( dred ), ExpectedPacketLoss = packetLoss , Complexity = complexityValue ,
PacketLossMode = On ( automaticLoss ) ? lossSpeed . IndexOfSelectedItem switch
{ 0 => PacketLossMode . PacketLossAutoFast , 2 => PacketLossMode . PacketLossAutoStable , _ => PacketLossMode . PacketLossAutoBalanced } : PacketLossMode . PacketLossManual }
2026-09-19 00:39:04 +02:00
}, password . StringValue );
}
private static NSTextField Field ( string value , double y ) => new ( new CGRect ( 155 , y , 345 , 24 )) { StringValue = value };
private static NSPopUpButton Picker ( string [] values , double y ) { var picker = new NSPopUpButton ( new CGRect ( 155 , y , 345 , 26 ), false ); picker . AddItems ( values ); return picker ; }
private static NSButton Check ( string title , bool value , double x , double y ) { NSButton check = NSButton . CreateCheckbox ( title , () => { }); check . Frame = new CGRect ( x , y , 125 , 24 ); check . State = value ? NSCellStateValue . On : NSCellStateValue . Off ; return check ; }
private static bool On ( NSButton value ) => value . State == NSCellStateValue . On ;
private static void Add ( NSView view , string label , NSView control , double y ) { var text = NSTextField . CreateLabel ( label ); text . Frame = new CGRect ( 0 , y + 2 , 145 , 20 ); view . AddSubview ( text ); view . AddSubview ( control ); (( INSAccessibility ) control ). AccessibilityLabel = label ; }
private static void Select ( NSPopUpButton picker , string value ) { for ( nint i = 0 ; i < picker . ItemCount ; i ++) if ( picker . ItemTitle ( i ) == value ) { picker . SelectItem ( i ); return ; } }
}