2026-09-19 00:39:04 +02:00
using AppKit ;
using CoreGraphics ;
using VoiceCat.Core ;
using Voicecat.V1 ;
using Channel = Voicecat . V1 . Channel ;
namespace VoiceCat.Mac ;
internal sealed class AdministrationWindowController : NSWindowController
{
private readonly VoiceCatClient client ;
private readonly uint selfId ;
private readonly NSPopUpButton users = new ( new CGRect ( 25 , 335 , 310 , 28 ), false );
private readonly NSPopUpButton channels = new ( new CGRect ( 25 , 175 , 310 , 28 ), false );
private readonly NSTextField status = NSTextField . CreateLabel ( "" );
internal AdministrationWindowController ( VoiceCatClient client , uint selfId ) : base ( new NSWindow ( new CGRect ( 0 , 0 , 560 , 420 ),
NSWindowStyle . Titled | NSWindowStyle . Closable , NSBackingStore . Buffered , false ))
{
this . client = client ; this . selfId = selfId ; Window !. Title = "VoiceCat Administration" ; Window . Center ();
NSView view = Window . ContentView !;
AddLabel ( view , "User moderation" , 25 , 375 ); view . AddSubview ( users );
AddButton ( view , "Move…" , 350 , 333 , Move ); AddButton ( view , "Kick…" , 440 , 333 , Kick );
AddButton ( view , "Ban…" , 350 , 293 , Ban ); AddButton ( view , "Permissions…" , 440 , 293 , Permissions );
AddButton ( view , "Server mute" , 350 , 253 , ServerMute ); AddButton ( view , "Server deafen" , 440 , 253 , ServerDeafen );
AddButton ( view , "Audio tuning…" , 25 , 253 , Tune );
AddLabel ( view , "Channel administration" , 25 , 215 ); view . AddSubview ( channels );
AddButton ( view , "Create…" , 350 , 173 , CreateChannel ); AddButton ( view , "Edit…" , 440 , 173 , EditChannel );
AddButton ( view , "Delete" , 350 , 133 , DeleteChannel ); AddButton ( view , "Accounts…" , 440 , 133 , Accounts );
var refresh = new NSButton ( new CGRect ( 25 , 75 , 100 , 32 )) { Title = "Refresh" }; refresh . Activated += ( _ , _ ) => Refresh (); view . AddSubview ( refresh );
status . Frame = new CGRect ( 25 , 25 , 510 , 35 ); status . AccessibilityLabel = "Administration result" ; view . AddSubview ( status );
(( INSAccessibility ) users ). AccessibilityLabel = "User to administer" ; (( INSAccessibility ) channels ). AccessibilityLabel = "Channel to administer" ;
Refresh (); ApplyPermissions ();
}
private void Refresh ()
{
uint selectedUser = Selected ( users ), selectedChannel = Selected ( channels );
users . RemoveAllItems (); foreach ( User user in client . Users . Where ( user => user . Id != selfId ). OrderBy ( user => user . Nickname )) Add ( users , user . Nickname , user . Id );
channels . RemoveAllItems (); foreach ( Channel channel in client . Channels . OrderBy ( channel => channel . Order ). ThenBy ( channel => channel . Name )) Add ( channels , channel . Name , channel . Id );
Select ( users , selectedUser ); Select ( channels , selectedChannel );
}
private void ApplyPermissions ()
{
Permissions permissions = client . Permissions ;
status . StringValue = permissions . IsAdmin ? "Connected as administrator" : "Only actions allowed by your server permissions will succeed." ;
}
private async void Move ( object? sender , EventArgs args )
{
uint userId = Selected ( users ); if ( userId == 0 ) return ;
string [] names = client . Channels . Select ( channel => channel . Name ). ToArray (); int index = PromptChoice ( "Move user" , "Destination channel" , names ); if ( index < 0 ) return ;
Show ( await client . MoveUserAsync ( userId , client . Channels [ index ]. Id ));
}
private async void Kick ( object? sender , EventArgs args ) { uint id = Selected ( users ); string? reason = PromptText ( "Kick user" , "Reason" , "Kicked by moderator" ); if ( id != 0 && reason is not null ) Show ( await client . KickUserAsync ( id , reason )); }
private async void Ban ( object? sender , EventArgs args )
{
uint id = Selected ( users ); if ( id == 0 ) return ;
string? reason = PromptText ( "Ban user" , "Reason (append @1h, @1d, or @permanent)" , "Banned by moderator @1d" ); if ( reason is null ) return ;
ulong expiry = 0 ; string lowered = reason . ToLowerInvariant ();
if ( lowered . EndsWith ( "@1h" )) expiry = checked (( ulong ) DateTimeOffset . UtcNow . AddHours ( 1 ). ToUnixTimeMilliseconds ());
else if ( lowered . EndsWith ( "@1d" )) expiry = checked (( ulong ) DateTimeOffset . UtcNow . AddDays ( 1 ). ToUnixTimeMilliseconds ());
Show ( await client . BanUserAsync ( id , reason . Replace ( "@1h" , "" , StringComparison . OrdinalIgnoreCase ). Replace ( "@1d" , "" , StringComparison . OrdinalIgnoreCase ). Replace ( "@permanent" , "" , StringComparison . OrdinalIgnoreCase ). Trim (), expiry ));
}
private async void Permissions ( object? sender , EventArgs args )
{
uint id = Selected ( users ); if ( id == 0 ) return ;
Permissions ? permissions = PermissionEditor . Run (); if ( permissions is not null ) Show ( await client . SetPermissionsAsync ( id , permissions ));
}
private async void ServerMute ( object? sender , EventArgs args ) { User ? user = User (); if ( user is not null ) Show ( await client . SetServerMuteAsync ( user . Id , ! user . ServerMuted , user . ServerDeafened )); }
private async void ServerDeafen ( object? sender , EventArgs args ) { User ? user = User (); if ( user is not null ) Show ( await client . SetServerMuteAsync ( user . Id , user . ServerMuted , ! user . ServerDeafened )); }
private void Tune ( object? sender , EventArgs args ) { User ? user = User (); if ( user is not null ) PerUserTuning . Run ( client , user ); }
2026-09-22 16:54:49 +02:00
private async void CreateChannel ( object? sender , EventArgs args ) { ChannelEdit ? edit = ChannelEditor . Run ( null , client . Channels , client . SupportsAdaptivePacketLoss ); if ( edit is not null ) { Show ( await client . CreateChannelAsync ( edit . Channel , edit . Password )); Refresh (); } }
private async void EditChannel ( object? sender , EventArgs args ) { Channel ? channel = Channel (); if ( channel is null ) return ; ChannelEdit ? edit = ChannelEditor . Run ( channel , client . Channels , client . SupportsAdaptivePacketLoss ); if ( edit is not null ) { Show ( await client . EditChannelAsync ( edit . Channel , edit . Password )); Refresh (); } }
2026-09-19 00:39:04 +02:00
private async void DeleteChannel ( object? sender , EventArgs args )
{
Channel ? channel = Channel (); if ( channel is null || channel . Id == 1 ) return ;
if (! Confirm ( "Delete channel?" , channel . Name )) return ; Show ( await client . DeleteChannelAsync ( channel . Id )); Refresh ();
}
private async void Accounts ( object? sender , EventArgs args ) => await AccountEditor . RunAsync ( client );
private User ? User () => client . Users . FirstOrDefault ( user => user . Id == Selected ( users ));
private Channel ? Channel () => client . Channels . FirstOrDefault ( channel => channel . Id == Selected ( channels ));
private void Show ( GenericResult result ) { status . StringValue = result . Ok ? result . Message . Length == 0 ? "Operation completed." : result . Message : result . Message ; Refresh (); }
private static void Add ( NSPopUpButton picker , string title , uint id ) { picker . AddItem ( title ); picker . LastItem !. RepresentedObject = new Foundation . NSString ( id . ToString ()); }
private static uint Selected ( NSPopUpButton picker ) => uint . TryParse ( picker . SelectedItem ?. RepresentedObject ?. ToString (), out uint id ) ? id : 0 ;
private static void Select ( NSPopUpButton picker , uint id ) { for ( nint i = 0 ; i < picker . ItemCount ; i ++) if ( picker . ItemAtIndex ( i )?. RepresentedObject ?. ToString () == id . ToString ()) { picker . SelectItem ( i ); break ; } }
private static void AddLabel ( NSView view , string value , double x , double y ) { NSTextField label = NSTextField . CreateLabel ( value ); label . Frame = new CGRect ( x , y , 300 , 22 ); label . Font = NSFont . BoldSystemFontOfSize ( 13 )!; view . AddSubview ( label ); }
private static void AddButton ( NSView view , string title , double x , double y , EventHandler handler ) { var button = new NSButton ( new CGRect ( x , y , 95 , 32 )) { Title = title }; button . Activated += handler ; view . AddSubview ( button ); }
internal static string? PromptText ( string title , string prompt , string value = "" , bool secure = false )
{
NSTextField field = secure ? new NSSecureTextField ( new CGRect ( 0 , 0 , 340 , 26 )) : new NSTextField ( new CGRect ( 0 , 0 , 340 , 26 )); field . StringValue = value ;
var alert = new NSAlert { MessageText = title , InformativeText = prompt , AccessoryView = field }; alert . AddButton ( "OK" ); alert . AddButton ( "Cancel" );
return alert . RunModal () == 1000 ? field . StringValue : null ;
}
internal static int PromptChoice ( string title , string prompt , string [] values )
{
var picker = new NSPopUpButton ( new CGRect ( 0 , 0 , 340 , 28 ), false ); picker . AddItems ( values );
var alert = new NSAlert { MessageText = title , InformativeText = prompt , AccessoryView = picker }; alert . AddButton ( "OK" ); alert . AddButton ( "Cancel" );
return alert . RunModal () == 1000 ? checked (( int ) picker . IndexOfSelectedItem ) : - 1 ;
}
internal static bool Confirm ( string title , string detail ) { var alert = new NSAlert { MessageText = title , InformativeText = detail , AlertStyle = NSAlertStyle . Warning }; alert . AddButton ( "Continue" ); alert . AddButton ( "Cancel" ); return alert . RunModal () == 1000 ; }
}
internal sealed record ChannelEdit ( Channel Channel , string Password );
internal static class PermissionEditor
{
internal static Permissions ? Run ()
{
string [] labels = [ "Create temporary channels" , "Kick users" , "Ban users" , "Move users" , "Manage accounts" , "Full administrator" ];
var view = new NSView ( new CGRect ( 0 , 0 , 330 , 160 )); var checks = new List < NSButton >();
for ( int i = 0 ; i < labels . Length ; i ++) { NSButton check = NSButton . CreateCheckbox ( labels [ i ], () => { }); check . Frame = new CGRect ( 0 , 135 - i * 25 , 320 , 22 ); view . AddSubview ( check ); checks . Add ( check ); }
var alert = new NSAlert { MessageText = "Set permissions" , AccessoryView = view }; alert . AddButton ( "Save" ); alert . AddButton ( "Cancel" ); if ( alert . RunModal () != 1000 ) return null ;
return new () { CanCreateTempChannel = On ( 0 ), CanKick = On ( 1 ), CanBan = On ( 2 ), CanMoveUsers = On ( 3 ), CanAdminAccounts = On ( 4 ), IsAdmin = On ( 5 ) };
bool On ( int i ) => checks [ i ]. State == NSCellStateValue . On ;
}
}
internal static class PerUserTuning
{
internal static void Run ( VoiceCatClient client , User user )
{
if ( user . Streams . Count == 0 ) { AdministrationWindowController . PromptText ( "Audio tuning" , "This user has no active streams." ); return ; }
var stream = new NSPopUpButton ( new CGRect ( 0 , 95 , 340 , 28 ), false ); stream . AddItems ( user . Streams . Select ( value => $"{value.Label} ({value.Kind})" ). ToArray ());
var gain = new NSSlider ( new CGRect ( 0 , 60 , 340 , 24 )) { MinValue = 0 , MaxValue = 400 , DoubleValue = 100 };
var muted = NSButton . CreateCheckbox ( "Mute this stream" , () => { }); muted . Frame = new CGRect ( 0 , 30 , 160 , 24 );
var noise = NSButton . CreateCheckbox ( "Receive noise reduction" , () => { }); noise . Frame = new CGRect ( 170 , 30 , 170 , 24 );
var view = new NSView ( new CGRect ( 0 , 0 , 340 , 130 )); view . AddSubview ( stream ); view . AddSubview ( gain ); view . AddSubview ( muted ); view . AddSubview ( noise );
void LoadSelected ()
{
if ( stream . IndexOfSelectedItem < 0 ) return ;
StreamInfo selected = user . Streams [ checked (( int ) stream . IndexOfSelectedItem )];
( float Gain , bool Muted , bool NoiseReduction )? state = client . Audio . GetRemotePlayback ( user . Id , selected . StreamId );
gain . DoubleValue = ( state ?. Gain ?? 1 ) * 100 ; muted . State = state ?. Muted == true ? NSCellStateValue . On : NSCellStateValue . Off ;
noise . State = state ?. NoiseReduction == true ? NSCellStateValue . On : NSCellStateValue . Off ;
}
stream . Activated += ( _ , _ ) => LoadSelected (); LoadSelected ();
var alert = new NSAlert { MessageText = $"Audio — {user.Nickname}" , InformativeText = "Stream, volume, mute, and receive noise reduction" , AccessoryView = view };
alert . AddButton ( "Apply" ); alert . AddButton ( "Cancel" ); if ( alert . RunModal () != 1000 ) return ;
StreamInfo selected = user . Streams [ checked (( int ) stream . IndexOfSelectedItem )]; client . Audio . SetRemotePlayback ( user . Id , selected . StreamId , ( float ) gain . DoubleValue / 100 ,
muted . State == NSCellStateValue . On , noise . State == NSCellStateValue . On );
}
}
internal static class AccountEditor
{
internal static async Task RunAsync ( VoiceCatClient client )
{
IReadOnlyList < AccountEntry > accounts = await client . ListAccountsAsync ();
string [] actions = [ "Create account" , "Reset password" , "Delete account" , "Close" ];
int action = AdministrationWindowController . PromptChoice ( "Server accounts" , string . Join ( "\n" , accounts . Select ( a => $"{a.Username}{(a.IsAdmin ? " ( admin ) " : "")}" )), actions );
if ( action is < 0 or 3 ) return ;
if ( action == 0 )
{
string? name = AdministrationWindowController . PromptText ( "Create account" , "Username" ); if ( string . IsNullOrWhiteSpace ( name )) return ;
string? password = AdministrationWindowController . PromptText ( "Create account" , "Password" , secure : true ); if (! string . IsNullOrEmpty ( password )) await client . CreateAccountAsync ( name . Trim (), password );
return ;
}
int selected = AdministrationWindowController . PromptChoice ( action == 1 ? "Reset password" : "Delete account" , "Account" , accounts . Select ( a => a . Username ). ToArray ()); if ( selected < 0 ) return ;
if ( action == 1 ) { string? password = AdministrationWindowController . PromptText ( "Reset password" , "New password" , secure : true ); if (! string . IsNullOrEmpty ( password )) await client . ResetPasswordAsync ( accounts [ selected ]. Username , password ); }
else if ( AdministrationWindowController . Confirm ( "Delete account?" , accounts [ selected ]. Username )) await client . DeleteAccountAsync ( accounts [ selected ]. Username );
}
}