58 lines
8.6 KiB
C#
58 lines
8.6 KiB
C#
using UIKit;
|
|
using Voicecat.V1;
|
|
using Channel = Voicecat.V1.Channel;
|
|
|
|
namespace VoiceCat.iOS;
|
|
|
|
internal sealed class ChannelEditorController : UIViewController
|
|
{
|
|
private readonly AppModel model; private readonly Channel? existing;
|
|
private readonly UITextField name = UiHelpers.Field("Channel name"), topic = UiHelpers.Field("Topic (optional)"), maximum = UiHelpers.Field("Maximum users, 0 is unlimited"), order = UiHelpers.Field("Sort order"), password = UiHelpers.Field("Password; blank preserves existing", true), bitrate = UiHelpers.Field("Bitrate in bits per second"), loss = UiHelpers.Field("Expected packet loss percent"), complexity = UiHelpers.Field("Complexity 0 through 10");
|
|
private readonly UISegmentedControl type = new(["Permanent", "Temporary"]), mode = new(["Mono", "Stereo"]), application = new(["VoIP", "Audio", "Low delay"]);
|
|
private readonly UIButton parent = UIButton.FromType(UIButtonType.System), sampleRate = UIButton.FromType(UIButtonType.System), frame = UIButton.FromType(UIButtonType.System);
|
|
private readonly UIButton lossSpeed = UIButton.FromType(UIButtonType.System);
|
|
private readonly UISwitch automaticLoss = new(), fec = new(), dtx = new(), dred = new(); private uint parentId;
|
|
|
|
internal ChannelEditorController(AppModel model, Channel? existing) { this.model = model; this.existing = existing; Title = existing is null ? "New Channel" : "Edit Channel"; }
|
|
public override void ViewDidLoad()
|
|
{
|
|
base.ViewDidLoad(); View!.BackgroundColor = UIColor.SystemGroupedBackground; UIScrollView scroll = new() { TranslatesAutoresizingMaskIntoConstraints = false }; UIStackView stack = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
|
|
View.AddSubview(scroll); scroll.AddSubview(stack); NSLayoutConstraint.ActivateConstraints([scroll.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor), scroll.BottomAnchor.ConstraintEqualTo(View.BottomAnchor), scroll.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor), scroll.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor), stack.TopAnchor.ConstraintEqualTo(scroll.ContentLayoutGuide.TopAnchor, 16), stack.BottomAnchor.ConstraintEqualTo(scroll.ContentLayoutGuide.BottomAnchor, -24), stack.LeadingAnchor.ConstraintEqualTo(scroll.FrameLayoutGuide.LeadingAnchor, 20), stack.TrailingAnchor.ConstraintEqualTo(scroll.FrameLayoutGuide.TrailingAnchor, -20)]);
|
|
foreach (UIView row in new UIView[] { name, topic, PickerRow("Parent channel", parent), PickerRow("Channel type", type), maximum, order, password, PickerRow("Channel mode", mode), PickerRow("Sample rate", sampleRate), PickerRow("Frame duration", frame), PickerRow("Opus application", application), bitrate, SwitchRow("Automatic packet loss", automaticLoss), PickerRow("Adaptation speed", lossSpeed), loss, complexity, SwitchRow("Forward error correction", fec), SwitchRow("Discontinuous transmission", dtx), SwitchRow("Deep redundancy", dred) }) stack.AddArrangedSubview(row);
|
|
maximum.KeyboardType = order.KeyboardType = bitrate.KeyboardType = loss.KeyboardType = complexity.KeyboardType = UIKeyboardType.NumberPad;
|
|
parent.Menu = UIMenu.Create(model.Channels.Where(channel => channel.Id != existing?.Id).OrderBy(channel => channel.Name).Select(channel => UIAction.Create(channel.Name, null, null, _ => { parentId = channel.Id; parent.SetTitle(channel.Name, UIControlState.Normal); })).Prepend(UIAction.Create("Root", null, null, _ => { parentId = 0; parent.SetTitle("Root", UIControlState.Normal); })).ToArray()); parent.ShowsMenuAsPrimaryAction = true;
|
|
sampleRate.Menu = Choice(sampleRate, ["48000"]); sampleRate.ShowsMenuAsPrimaryAction = true; frame.Menu = Choice(frame, ["5", "10", "20", "40", "60"]); frame.ShowsMenuAsPrimaryAction = true;
|
|
lossSpeed.Menu = Choice(lossSpeed, ["Fast", "Balanced", "Stable"]); lossSpeed.ShowsMenuAsPrimaryAction = true;
|
|
automaticLoss.Enabled = model.SupportsAdaptivePacketLoss;
|
|
automaticLoss.ValueChanged += (_, _) => UpdateLossControls();
|
|
Load(); NavigationItem.RightBarButtonItem = new("Save", UIBarButtonItemStyle.Done, async (_, _) => await Save());
|
|
}
|
|
private void Load()
|
|
{
|
|
Channel channel = existing ?? new Channel { Audio = new AudioConfig { SampleRate = 48000, BitrateBps = 64000, FrameMs = 20, Fec = true, Complexity = 10, ExpectedPacketLoss = 5 } };
|
|
name.Text = channel.Name; topic.Text = channel.Topic; maximum.Text = channel.MaxUsers.ToString(); order.Text = channel.Order.ToString(); parentId = channel.ParentId;
|
|
parent.SetTitle(model.Channels.FirstOrDefault(value => value.Id == parentId)?.Name ?? "Root", UIControlState.Normal); type.SelectedSegment = channel.Type == ChannelType.ChannelTemporary ? 1 : 0; mode.SelectedSegment = channel.Audio?.Mode == ChannelMode.ModeStereo ? 1 : 0;
|
|
sampleRate.SetTitle((channel.Audio?.SampleRate ?? 48000).ToString(), UIControlState.Normal); frame.SetTitle((channel.Audio?.FrameMs ?? 20).ToString(), UIControlState.Normal); application.SelectedSegment = (nint)(channel.Audio?.Application ?? OpusApplication.OpusVoip);
|
|
bitrate.Text = (channel.Audio?.BitrateBps ?? 64000).ToString(); loss.Text = (channel.Audio?.ExpectedPacketLoss ?? 5).ToString(); complexity.Text = (channel.Audio?.Complexity ?? 10).ToString(); fec.On = channel.Audio?.Fec ?? true; dtx.On = channel.Audio?.Dtx ?? false; dred.On = channel.Audio?.Dred ?? false;
|
|
automaticLoss.On = channel.Audio?.PacketLossMode != PacketLossMode.PacketLossManual;
|
|
lossSpeed.SetTitle(channel.Audio?.PacketLossMode switch { PacketLossMode.PacketLossAutoFast => "Fast", PacketLossMode.PacketLossAutoStable => "Stable", _ => "Balanced" }, UIControlState.Normal);
|
|
UpdateLossControls();
|
|
}
|
|
private async Task Save()
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name.Text) || !uint.TryParse(maximum.Text, out uint max) || !int.TryParse(order.Text, out int sort) || !uint.TryParse(sampleRate.Title(UIControlState.Normal), out uint rate) || !uint.TryParse(frame.Title(UIControlState.Normal), out uint frameMs) || !uint.TryParse(bitrate.Text, out uint bits) || !uint.TryParse(loss.Text, out uint packetLoss) || !uint.TryParse(complexity.Text, out uint cpu) || packetLoss > 100 || cpu > 10) throw new ArgumentException("Enter valid channel and Opus settings.");
|
|
var channel = new Channel { Id = existing?.Id ?? 0, ParentId = parentId, Name = name.Text.Trim(), Topic = topic.Text?.Trim() ?? "", MaxUsers = max, Order = sort, Type = type.SelectedSegment == 1 ? ChannelType.ChannelTemporary : ChannelType.ChannelPermanent,
|
|
Audio = new AudioConfig { Codec = 0, Mode = mode.SelectedSegment == 1 ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = rate, FrameMs = frameMs, Application = (OpusApplication)(int)application.SelectedSegment, BitrateBps = bits, ExpectedPacketLoss = packetLoss, Complexity = cpu, Fec = fec.On, Dtx = dtx.On, Dred = dred.On,
|
|
PacketLossMode = automaticLoss.On ? lossSpeed.Title(UIControlState.Normal) switch { "Fast" => PacketLossMode.PacketLossAutoFast, "Stable" => PacketLossMode.PacketLossAutoStable, _ => PacketLossMode.PacketLossAutoBalanced } : PacketLossMode.PacketLossManual } };
|
|
GenericResult result = await model.RunAdminAsync(client => existing is null ? client.CreateChannelAsync(channel, password.Text ?? "") : client.EditChannelAsync(channel, password.Text ?? "")); if (!result.Ok) throw new InvalidOperationException(result.Message); NavigationController?.PopViewController(true);
|
|
}
|
|
catch (Exception exception) { UiHelpers.ShowError(this, exception); }
|
|
}
|
|
private void UpdateLossControls() { loss.Enabled = !automaticLoss.On; lossSpeed.Enabled = model.SupportsAdaptivePacketLoss && automaticLoss.On; }
|
|
private static UIView PickerRow(string label, UIView control) { var row = new UIStackView { Axis = UILayoutConstraintAxis.Horizontal, Distribution = UIStackViewDistribution.FillEqually, Spacing = 8 }; var text = new UILabel { Text = label }; control.AccessibilityLabel = label; row.AddArrangedSubview(text); row.AddArrangedSubview(control); return row; }
|
|
private static UIView SwitchRow(string label, UISwitch toggle) { var row = new UIStackView { Axis = UILayoutConstraintAxis.Horizontal, Distribution = UIStackViewDistribution.EqualSpacing }; row.AddArrangedSubview(new UILabel { Text = label }); toggle.AccessibilityLabel = label; row.AddArrangedSubview(toggle); return row; }
|
|
private static UIMenu Choice(UIButton button, string[] values) => UIMenu.Create(values.Select(value => UIAction.Create(value, null, null, _ => button.SetTitle(value, UIControlState.Normal))).ToArray());
|
|
}
|