Files
voice-cat/clients/apple/dotnet/VoiceCat.Mac/MainWindowController.cs
T

90 lines
5.9 KiB
C#
Raw Normal View History

2026-09-16 17:34:16 +02:00
using AppKit;
using CoreGraphics;
using Foundation;
using VoiceCat.Core;
using Voicecat.V1;
namespace VoiceCat.Mac;
internal sealed class MainWindowController : NSWindowController
{
private readonly VoiceCatClient client;
private readonly uint selfId;
private readonly NSPopUpButton channels = new(new CGRect(20, 515, 300, 28), false);
private readonly NSTextView users = new(new CGRect(0, 0, 190, 430)) { Editable = false, Selectable = true };
private readonly NSTextView chat = new(new CGRect(0, 0, 510, 390)) { Editable = false, Selectable = true };
private readonly NSTextField compose = new(new CGRect(230, 55, 400, 28)) { PlaceholderString = "Message to current channel" };
private readonly NSButton send = new(new CGRect(640, 53, 90, 32)) { Title = "Send" };
private readonly NSButton voice = new(new CGRect(630, 510, 100, 32)) { Title = "Join voice" };
private readonly NSTextField status = NSTextField.CreateLabel("Connected");
private readonly NSTimer timer;
private uint currentChannel = 1;
private bool joinedVoice;
internal MainWindowController(VoiceCatClient client, uint selfId, string nickname) : base(new NSWindow(new CGRect(0, 0, 760, 570),
NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable | NSWindowStyle.Miniaturizable, NSBackingStore.Buffered, false))
{
this.client = client; this.selfId = selfId;
Window!.Title = $"VoiceCat — {nickname}"; Window.Center(); Window.MinSize = new CGSize(680, 480);
var content = Window.ContentView!;
channels.AccessibilityLabel = "Channel"; channels.Activated += ChangeChannel; content.AddSubview(channels);
voice.AccessibilityLabel = "Join or leave voice"; voice.Activated += ToggleVoice; content.AddSubview(voice);
var userScroll = new NSScrollView(new CGRect(20, 90, 190, 410)) { HasVerticalScroller = true, DocumentView = users }; userScroll.AccessibilityLabel = "Users in channel"; content.AddSubview(userScroll);
var chatScroll = new NSScrollView(new CGRect(230, 90, 500, 410)) { HasVerticalScroller = true, DocumentView = chat }; chatScroll.AccessibilityLabel = "Channel messages"; content.AddSubview(chatScroll);
compose.AccessibilityLabel = "Message"; compose.Activated += Send; content.AddSubview(compose);
send.AccessibilityLabel = "Send message"; send.Activated += Send; content.AddSubview(send);
status.Frame = new CGRect(20, 22, 700, 22); status.AccessibilityLabel = "Connection status"; content.AddSubview(status);
timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.FromMilliseconds(50), _ => Pump());
RefreshState();
}
private void Pump()
{
while (client.TryReadEvent(out Envelope? envelope))
{
if (envelope!.TextMessage is { } text) Append($"[{DateTime.Now:t}] {Name(text.SenderId)}: {text.Body}");
if (envelope.ServerState is not null || envelope.ChannelEvent is not null || envelope.UserEvent is not null) RefreshState();
if (envelope.Disconnect is { } disconnected) { status.StringValue = "Disconnected: " + disconnected.Reason; voice.Enabled = send.Enabled = false; }
}
if (client.Audio.Failure is { } failure) status.StringValue = "Audio stopped: " + failure.Message;
}
private void RefreshState()
{
string? selected = channels.SelectedItem?.RepresentedObject?.ToString();
channels.RemoveAllItems();
foreach (var channel in client.Channels.OrderBy(c => c.Order).ThenBy(c => c.Name)) { channels.AddItem(channel.Name); channels.LastItem!.RepresentedObject = new NSString(channel.Id.ToString()); }
currentChannel = client.Users.FirstOrDefault(u => u.Id == selfId)?.ChannelId ?? currentChannel;
int selectedIndex = client.Channels.ToList().FindIndex(c => c.Id == currentChannel); if (selectedIndex >= 0) channels.SelectItem(selectedIndex);
users.Value = string.Join("\n", client.Users.Where(u => u.ChannelId == currentChannel).OrderBy(u => u.Nickname).Select(u => (u.Id == selfId ? "You — " : "") + u.Nickname));
status.StringValue = $"Connected · {client.Users.Count} users";
}
private async void ChangeChannel(object? sender, EventArgs args)
{
if (!uint.TryParse(channels.SelectedItem?.RepresentedObject?.ToString(), out uint id) || id == currentChannel) return;
try { var result = (await client.RequestAsync(new() { JoinChannel = new() { ChannelId = id } })).JoinChannelResult; if (!result.Ok) status.StringValue = result.Error; }
catch (Exception exception) { status.StringValue = exception.Message; }
}
private void Send(object? sender, EventArgs args)
{
string body = compose.StringValue.Trim(); if (body.Length == 0) return;
try { client.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = currentChannel, Body = body, ClientMsgId = Guid.NewGuid().ToString("N") } }); compose.StringValue = ""; }
catch (Exception exception) { status.StringValue = exception.Message; }
}
private async void ToggleVoice(object? sender, EventArgs args)
{
try
{
if (!joinedVoice) { var result = await client.SubscribeVoiceAsync(); if (!result.Ok) throw new InvalidOperationException(result.Error); joinedVoice = true; voice.Title = "Leave voice"; }
else { await client.RequestAsync(new() { UnsubscribeVoice = new() }); joinedVoice = false; voice.Title = "Join voice"; }
}
catch (Exception exception) { status.StringValue = exception.Message; }
}
private string Name(uint id) => client.Users.FirstOrDefault(u => u.Id == id)?.Nickname ?? $"User {id}";
private void Append(string line) { chat.Value = chat.Value.Length == 0 ? line : chat.Value + "\n" + line; chat.ScrollToEndOfDocument(this); }
protected override void Dispose(bool disposing)
{
if (disposing) { timer.Invalidate(); client.DisposeAsync().AsTask().GetAwaiter().GetResult(); }
base.Dispose(disposing);
}
}