Bring managed iOS client to feature parity
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
using ReplayKit;
|
||||
using UIKit;
|
||||
using VoiceCat.Audio;
|
||||
using Voicecat.V1;
|
||||
using Channel = Voicecat.V1.Channel;
|
||||
|
||||
@@ -9,64 +9,104 @@ internal sealed class MainTabController : UITabBarController
|
||||
{
|
||||
internal MainTabController(AppModel model)
|
||||
{
|
||||
UIViewController channels = new UINavigationController(new ChannelsController(model));
|
||||
channels.TabBarItem = new("Channels", UIImage.GetSystemImage("list.bullet.indent"), 0);
|
||||
UIViewController chat = new UINavigationController(new ChatController(model));
|
||||
chat.TabBarItem = new("Chat", UIImage.GetSystemImage("message"), 1);
|
||||
UIViewController settings = new UINavigationController(new SettingsController(model));
|
||||
settings.TabBarItem = new("Settings", UIImage.GetSystemImage("gear"), 2);
|
||||
UIViewController channels = Wrap(new ChannelsController(model), "Channels", "list.bullet.indent", 0);
|
||||
UIViewController chat = Wrap(new ChatController(model), "Chat", "message", 1);
|
||||
UIViewController settings = Wrap(new SettingsController(model), "Settings", "gear", 2);
|
||||
ViewControllers = [channels, chat, settings];
|
||||
}
|
||||
|
||||
private static UIViewController Wrap(UIViewController content, string title, string image, nint tag)
|
||||
{
|
||||
var root = new UIViewController(); UIView rootView = root.View!; rootView.BackgroundColor = UIColor.SystemBackground;
|
||||
var navigation = new UINavigationController(content); UIView navigationView = navigation.View!; navigationView.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
root.AddChildViewController(navigation); rootView.AddSubview(navigationView); navigation.DidMoveToParentViewController(root);
|
||||
var voice = new VoiceControlsView(AppModel.Shared) { TranslatesAutoresizingMaskIntoConstraints = false }; rootView.AddSubview(voice);
|
||||
NSLayoutConstraint.ActivateConstraints([navigationView.TopAnchor.ConstraintEqualTo(rootView.TopAnchor), navigationView.LeadingAnchor.ConstraintEqualTo(rootView.LeadingAnchor),
|
||||
navigationView.TrailingAnchor.ConstraintEqualTo(rootView.TrailingAnchor), navigationView.BottomAnchor.ConstraintEqualTo(voice.TopAnchor),
|
||||
voice.LeadingAnchor.ConstraintEqualTo(rootView.LeadingAnchor), voice.TrailingAnchor.ConstraintEqualTo(rootView.TrailingAnchor),
|
||||
voice.BottomAnchor.ConstraintEqualTo(rootView.SafeAreaLayoutGuide.BottomAnchor), voice.HeightAnchor.ConstraintEqualTo(58)]);
|
||||
root.TabBarItem = new(title, UIImage.GetSystemImage(image), tag); return root;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class VoiceControlsView : UIView
|
||||
{
|
||||
private readonly AppModel model; private readonly UIButton join = UIButton.FromType(UIButtonType.System);
|
||||
private readonly UIButton ptt = UIButton.FromType(UIButtonType.System); private readonly UIButton mute = UIButton.FromType(UIButtonType.System);
|
||||
private readonly UIButton deafen = UIButton.FromType(UIButtonType.System); private readonly UIProgressView level = new(UIProgressViewStyle.Default);
|
||||
internal VoiceControlsView(AppModel model) { this.model = model; model.Changed += Refresh; Build(); }
|
||||
private void Build()
|
||||
{
|
||||
BackgroundColor = UIColor.SecondarySystemBackground; join.TouchUpInside += async (_, _) => await Run(model.ToggleVoiceAsync);
|
||||
ptt.TouchDown += (_, _) => model.SetPushToTalk(true); ptt.TouchUpInside += (_, _) => model.SetPushToTalk(false);
|
||||
ptt.TouchUpOutside += (_, _) => model.SetPushToTalk(false); ptt.TouchCancel += (_, _) => model.SetPushToTalk(false);
|
||||
mute.TouchUpInside += (_, _) => model.SetSelfAudio(!(model.Client?.Audio.MicMuted ?? false), model.Client?.Audio.Deafened ?? false);
|
||||
deafen.TouchUpInside += (_, _) => model.SetSelfAudio(model.Client?.Audio.MicMuted ?? false, !(model.Client?.Audio.Deafened ?? false));
|
||||
UIStackView stack = new([join, ptt, level, mute, deafen]) { Axis = UILayoutConstraintAxis.Horizontal, Alignment = UIStackViewAlignment.Center,
|
||||
Distribution = UIStackViewDistribution.Fill, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
|
||||
AddSubview(stack); level.WidthAnchor.ConstraintEqualTo(65).Active = true;
|
||||
NSLayoutConstraint.ActivateConstraints([stack.LeadingAnchor.ConstraintEqualTo(LeadingAnchor, 12), stack.TrailingAnchor.ConstraintEqualTo(TrailingAnchor, -12), stack.CenterYAnchor.ConstraintEqualTo(CenterYAnchor)]); Refresh();
|
||||
}
|
||||
private void Refresh()
|
||||
{
|
||||
join.SetTitle(model.VoiceJoined ? "Leave Voice" : "Join Voice", UIControlState.Normal); join.AccessibilityLabel = join.Title(UIControlState.Normal); join.Enabled = model.CurrentChannelId != 0;
|
||||
ptt.SetTitle("Hold to Talk", UIControlState.Normal); ptt.AccessibilityLabel = "Push to talk, hold to transmit"; ptt.Hidden = model.Settings.InputMode != AudioInputMode.PushToTalk; ptt.Enabled = model.VoiceJoined;
|
||||
bool muted = model.Client?.Audio.MicMuted == true, deafened = model.Client?.Audio.Deafened == true;
|
||||
mute.SetImage(UIImage.GetSystemImage(muted ? "mic.slash.fill" : "mic.fill"), UIControlState.Normal); mute.AccessibilityLabel = muted ? "Unmute microphone" : "Mute microphone"; mute.Enabled = model.VoiceJoined;
|
||||
deafen.SetImage(UIImage.GetSystemImage(deafened ? "headphones.slash" : "headphones"), UIControlState.Normal); deafen.AccessibilityLabel = deafened ? "Undeafen" : "Deafen"; deafen.Enabled = model.VoiceJoined;
|
||||
level.Progress = Math.Clamp(model.MicrophoneLevel * 10, 0, 1); level.AccessibilityLabel = "Microphone level"; level.AccessibilityValue = $"{level.Progress:P0}";
|
||||
}
|
||||
private async Task Run(Func<Task> operation) { try { await operation(); } catch (Exception exception) { if (Window?.RootViewController is { } owner) UiHelpers.ShowError(owner, exception); } }
|
||||
}
|
||||
|
||||
internal sealed class ChannelsController : UITableViewController
|
||||
{
|
||||
private readonly AppModel model;
|
||||
internal ChannelsController(AppModel model) { this.model = model; Title = "Channels"; model.Changed += () => TableView.ReloadData(); }
|
||||
public override void ViewDidLoad() { base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "channel"); NavigationItem.RightBarButtonItem = new("Users", UIBarButtonItemStyle.Plain, (_, _) => NavigationController?.PushViewController(new UsersController(model), true)); }
|
||||
public override nint RowsInSection(UITableView tableView, nint section) => model.Channels.Count;
|
||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||
private readonly AppModel model; private IReadOnlyList<(Channel Channel, int Depth)> Visible => Flatten();
|
||||
internal ChannelsController(AppModel model) { this.model = model; Title = "Channels"; model.Changed += Reload; }
|
||||
public override void ViewDidLoad()
|
||||
{
|
||||
Channel channel = model.Channels.OrderBy(c => c.Order).ThenBy(c => c.Name).ElementAt(indexPath.Row);
|
||||
UITableViewCell cell = tableView.DequeueReusableCell("channel", indexPath); int count = model.Users.Count(u => u.ChannelId == channel.Id);
|
||||
var content = cell.DefaultContentConfiguration; content.Text = channel.Name; content.SecondaryText = $"{count} users" + (channel.PasswordProtected ? " • protected" : ""); content.Image = UIImage.GetSystemImage(channel.Id == model.CurrentChannelId ? "checkmark.circle.fill" : "bubble.left"); cell.ContentConfiguration = content; cell.AccessibilityLabel = $"{channel.Name}, {count} users"; return cell;
|
||||
base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "channel");
|
||||
NavigationItem.RightBarButtonItems = [new("Users", UIBarButtonItemStyle.Plain, (_, _) => NavigationController?.PushViewController(new UsersController(model), true)),
|
||||
new(UIBarButtonSystemItem.Add, (_, _) => NavigationController?.PushViewController(new ChannelEditorController(model, null), true))]; Reload();
|
||||
}
|
||||
public override async void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
Channel channel = model.Channels.OrderBy(c => c.Order).ThenBy(c => c.Name).ElementAt(indexPath.Row); tableView.DeselectRow(indexPath, true);
|
||||
if (channel.PasswordProtected)
|
||||
{
|
||||
UIAlertController prompt = UIAlertController.Create("Channel Password", channel.Name, UIAlertControllerStyle.Alert); prompt.AddTextField(f => { f.SecureTextEntry = true; f.Placeholder = "Password"; });
|
||||
prompt.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); prompt.AddAction(UIAlertAction.Create("Join", UIAlertActionStyle.Default, async _ => { try { await model.JoinChannelAsync(channel.Id, prompt.TextFields![0].Text ?? ""); } catch (Exception e) { UiHelpers.ShowError(this, e); } })); PresentViewController(prompt, true, null); return;
|
||||
}
|
||||
try { await model.JoinChannelAsync(channel.Id); } catch (Exception e) { UiHelpers.ShowError(this, e); }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UsersController : UITableViewController
|
||||
{
|
||||
private readonly AppModel model;
|
||||
private IReadOnlyList<User> Visible => model.Users.Where(u => u.ChannelId == model.CurrentChannelId).ToArray();
|
||||
internal UsersController(AppModel model) { this.model = model; Title = "Users"; model.Changed += () => TableView.ReloadData(); }
|
||||
public override void ViewDidLoad() { base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "user"); }
|
||||
private void Reload() { TableView.ReloadData(); NavigationItem.RightBarButtonItems![1].Enabled = model.Client?.Permissions is { } p && (p.IsAdmin || p.CanCreateTempChannel); }
|
||||
public override nint RowsInSection(UITableView tableView, nint section) => Visible.Count;
|
||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
User user = Visible[indexPath.Row]; UITableViewCell cell = tableView.DequeueReusableCell("user", indexPath);
|
||||
var content = cell.DefaultContentConfiguration; content.Text = user.Nickname + (user.Id == model.SelfUserId ? " (you)" : ""); content.SecondaryText = user.ServerMuted ? "server muted" : user.SelfMicMuted ? "muted" : user.IsGuest ? "guest" : "account"; content.Image = UIImage.GetSystemImage(user.ServerMuted || user.SelfMicMuted ? "mic.slash.fill" : "mic.fill"); cell.ContentConfiguration = content; cell.AccessibilityLabel = $"{content.Text}, {content.SecondaryText}"; return cell;
|
||||
(Channel channel, int depth) = Visible[indexPath.Row]; UITableViewCell cell = tableView.DequeueReusableCell("channel", indexPath);
|
||||
int count = model.Users.Count(user => user.ChannelId == channel.Id); var content = cell.DefaultContentConfiguration;
|
||||
content.Text = new string(' ', depth * 3) + channel.Name; content.SecondaryText = string.IsNullOrEmpty(channel.Topic) ? $"{count} users" : $"{channel.Topic} • {count} users";
|
||||
content.Image = UIImage.GetSystemImage(channel.Id == model.CurrentChannelId ? "checkmark.circle.fill" : channel.PasswordProtected ? "lock.fill" : "bubble.left"); cell.ContentConfiguration = content;
|
||||
cell.AccessibilityLabel = $"{channel.Name}{(channel.Id == model.CurrentChannelId ? ", current" : "")}{(channel.PasswordProtected ? ", password protected" : "")}, {count} users"; return cell;
|
||||
}
|
||||
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
public override async void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
User user = Visible[indexPath.Row]; tableView.DeselectRow(indexPath, true); if (user.Id == model.SelfUserId) return;
|
||||
UIAlertController menu = UIAlertController.Create(user.Nickname, null, UIAlertControllerStyle.ActionSheet);
|
||||
menu.AddAction(UIAlertAction.Create("Private message", UIAlertActionStyle.Default, _ => PromptPrivate(user)));
|
||||
if (model.Client?.Permissions.CanKick == true || model.Client?.Permissions.IsAdmin == true) menu.AddAction(UIAlertAction.Create("Kick", UIAlertActionStyle.Destructive, async _ => await Run(() => model.Client!.KickUserAsync(user.Id))));
|
||||
if (model.Client?.Permissions.CanBan == true || model.Client?.Permissions.IsAdmin == true) menu.AddAction(UIAlertAction.Create("Ban", UIAlertActionStyle.Destructive, async _ => await Run(() => model.Client!.BanUserAsync(user.Id))));
|
||||
if (model.Client?.Permissions.IsAdmin == true) menu.AddAction(UIAlertAction.Create(user.ServerMuted ? "Server unmute" : "Server mute", UIAlertActionStyle.Default, async _ => await Run(() => model.Client!.SetServerMuteAsync(user.Id, !user.ServerMuted, user.ServerDeafened))));
|
||||
menu.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); menu.PopoverPresentationController!.SourceView = tableView.CellAt(indexPath); PresentViewController(menu, true, null);
|
||||
Channel channel = Visible[indexPath.Row].Channel; tableView.DeselectRow(indexPath, true);
|
||||
if (channel.PasswordProtected) { PromptPassword(channel); return; }
|
||||
try { await model.JoinChannelAsync(channel.Id); } catch (Exception exception) { UiHelpers.ShowError(this, exception); }
|
||||
}
|
||||
public override UISwipeActionsConfiguration? GetTrailingSwipeActionsConfiguration(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
if (model.Client?.Permissions.IsAdmin != true) return null; Channel channel = Visible[indexPath.Row].Channel;
|
||||
UIContextualAction edit = UIContextualAction.FromContextualActionStyle(UIContextualActionStyle.Normal, "Edit", (_, _, done) => { NavigationController?.PushViewController(new ChannelEditorController(model, channel), true); done(true); });
|
||||
UIContextualAction delete = UIContextualAction.FromContextualActionStyle(UIContextualActionStyle.Destructive, "Delete", (_, _, done) => { ConfirmDelete(channel); done(true); });
|
||||
delete.BackgroundColor = UIColor.SystemRed; return UISwipeActionsConfiguration.FromActions(channel.Id == 1 ? [edit] : [delete, edit]);
|
||||
}
|
||||
private void PromptPassword(Channel channel)
|
||||
{
|
||||
UIAlertController prompt = UIAlertController.Create("Channel Password", channel.Name, UIAlertControllerStyle.Alert); prompt.AddTextField(field => { field.SecureTextEntry = true; field.AccessibilityLabel = "Channel password"; });
|
||||
prompt.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); prompt.AddAction(UIAlertAction.Create("Join", UIAlertActionStyle.Default, async _ => { try { await model.JoinChannelAsync(channel.Id, prompt.TextFields?[0].Text ?? ""); } catch (Exception exception) { UiHelpers.ShowError(this, exception); } })); PresentViewController(prompt, true, null);
|
||||
}
|
||||
private void ConfirmDelete(Channel channel)
|
||||
{
|
||||
UIAlertController alert = UIAlertController.Create("Delete channel?", channel.Name, UIAlertControllerStyle.Alert); alert.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null));
|
||||
alert.AddAction(UIAlertAction.Create("Delete", UIAlertActionStyle.Destructive, async _ => { try { GenericResult result = await model.RunAdminAsync(client => client.DeleteChannelAsync(channel.Id)); if (!result.Ok) throw new InvalidOperationException(result.Message); } catch (Exception exception) { UiHelpers.ShowError(this, exception); } })); PresentViewController(alert, true, null);
|
||||
}
|
||||
private IReadOnlyList<(Channel, int)> Flatten()
|
||||
{
|
||||
var result = new List<(Channel, int)>(); void Add(uint parent, int depth) { foreach (Channel value in model.Channels.Where(channel => channel.ParentId == parent).OrderBy(channel => channel.Order).ThenBy(channel => channel.Name)) { result.Add((value, depth)); Add(value.Id, depth + 1); } }
|
||||
Add(0, 0); return result;
|
||||
}
|
||||
private void PromptPrivate(User user) { UIAlertController p = UIAlertController.Create($"Message {user.Nickname}", null, UIAlertControllerStyle.Alert); p.AddTextField(f => f.Placeholder = "Message"); p.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); p.AddAction(UIAlertAction.Create("Send", UIAlertActionStyle.Default, _ => model.SendText(p.TextFields![0].Text ?? "", user.Id))); PresentViewController(p, true, null); }
|
||||
private async Task Run(Func<Task<GenericResult>> command) { try { GenericResult result = await command(); if (!result.Ok) throw new InvalidOperationException(result.Message); } catch (Exception e) { UiHelpers.ShowError(this, e); } }
|
||||
}
|
||||
|
||||
internal sealed class ChatController : UIViewController
|
||||
@@ -75,54 +115,15 @@ internal sealed class ChatController : UIViewController
|
||||
internal ChatController(AppModel model) { this.model = model; Title = "Chat"; model.Changed += Refresh; }
|
||||
public override void ViewDidLoad()
|
||||
{
|
||||
base.ViewDidLoad(); View!.BackgroundColor = UIColor.SystemBackground; log.Editable = false; log.Font = UIFont.PreferredBody; log.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
base.ViewDidLoad(); View!.BackgroundColor = UIColor.SystemBackground; log.Editable = false; log.Font = UIFont.PreferredBody; log.AccessibilityLabel = "Chat and activity timeline"; log.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
UIButton send = UIButton.FromType(UIButtonType.System); send.SetTitle("Send", UIControlState.Normal); send.AccessibilityLabel = "Send message"; send.TranslatesAutoresizingMaskIntoConstraints = false; send.TouchUpInside += (_, _) => { model.SendText(compose.Text ?? ""); compose.Text = ""; };
|
||||
compose.TranslatesAutoresizingMaskIntoConstraints = false; View.AddSubviews(log, compose, send);
|
||||
NSLayoutConstraint.ActivateConstraints([log.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor), log.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor, 12), log.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor, -12), compose.TopAnchor.ConstraintEqualTo(log.BottomAnchor, 8), compose.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor, 12), compose.BottomAnchor.ConstraintEqualTo(View.KeyboardLayoutGuide.TopAnchor, -8), send.LeadingAnchor.ConstraintEqualTo(compose.TrailingAnchor, 8), send.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor, -12), send.CenterYAnchor.ConstraintEqualTo(compose.CenterYAnchor), compose.WidthAnchor.ConstraintGreaterThanOrEqualTo(120)]); Refresh();
|
||||
}
|
||||
private void Refresh() { log.Text = string.Join("\n", model.Messages.Select(m => $"[{m.Timestamp:t}] {(m.Private ? "[private] " : "")}{m.Sender}: {m.Text}")); if (log.Text.Length > 0) log.ScrollRangeToVisible(new(log.Text.Length - 1, 1)); }
|
||||
}
|
||||
|
||||
internal sealed class SettingsController : UITableViewController
|
||||
{
|
||||
private readonly AppModel model; private readonly string[] rows = ["Join Voice", "Audio Preset", "Speaker Output", "Mute Microphone", "Deafen", "Share Screen Audio", "Accounts", "Disconnect"];
|
||||
internal SettingsController(AppModel model) : base(UITableViewStyle.InsetGrouped) { this.model = model; Title = "Settings"; model.Changed += () => TableView.ReloadData(); }
|
||||
public override void ViewDidLoad() { base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "setting"); }
|
||||
public override nint RowsInSection(UITableView tableView, nint section) => rows.Length;
|
||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||
private void Refresh()
|
||||
{
|
||||
UITableViewCell cell = tableView.DequeueReusableCell("setting", indexPath); string title = rows[indexPath.Row];
|
||||
if (indexPath.Row == 0) title = model.VoiceJoined ? "Leave Voice" : "Join Voice";
|
||||
var content = cell.DefaultContentConfiguration; content.Text = title; content.SecondaryText = indexPath.Row == 1 ? IosAudioRouter.Shared.Preset.ToString() : null; cell.ContentConfiguration = content; cell.Accessory = indexPath.Row is 1 or 6 ? UITableViewCellAccessory.DisclosureIndicator : UITableViewCellAccessory.None; return cell;
|
||||
IEnumerable<(DateTime Time, string Text)> chat = model.Messages.Select(message => (message.Timestamp, $"{(message.Private ? "[private] " : "")}{message.Sender}: {message.Text}"));
|
||||
IEnumerable<(DateTime Time, string Text)> activity = model.Activity.Select(value => (value.Timestamp, $"• {value.Text}"));
|
||||
log.Text = string.Join("\n", chat.Concat(activity).OrderBy(value => value.Time).Select(value => $"[{value.Time:t}] {value.Text}")); if (log.Text.Length > 0) log.ScrollRangeToVisible(new(log.Text.Length - 1, 1));
|
||||
}
|
||||
public override async void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
tableView.DeselectRow(indexPath, true); try
|
||||
{
|
||||
switch (indexPath.Row)
|
||||
{
|
||||
case 0: await model.ToggleVoiceAsync(); break;
|
||||
case 1: ShowPresets(); break;
|
||||
case 2: IosAudioRouter.Shared.ForceSpeaker = !IosAudioRouter.Shared.ForceSpeaker; IosAudioEngine.Shared.Reconfigure(); break;
|
||||
case 3: if (model.Client is { } c) c.SetSelfAudioState(!c.Audio.MicMuted, c.Audio.Deafened); break;
|
||||
case 4: if (model.Client is { } d) d.SetSelfAudioState(d.Audio.MicMuted, !d.Audio.Deafened); break;
|
||||
case 5: ShowBroadcastPicker(); break;
|
||||
case 6: NavigationController?.PushViewController(new AccountsController(model), true); break;
|
||||
case 7: await model.DisconnectAsync(); break;
|
||||
}
|
||||
}
|
||||
catch (Exception e) { UiHelpers.ShowError(this, e); }
|
||||
}
|
||||
private void ShowPresets() { UIAlertController a = UIAlertController.Create("Audio Preset", null, UIAlertControllerStyle.ActionSheet); foreach (IosAudioPreset p in Enum.GetValues<IosAudioPreset>()) a.AddAction(UIAlertAction.Create(p.ToString(), UIAlertActionStyle.Default, _ => { IosAudioRouter.Shared.SelectPreset(p); TableView.ReloadData(); })); a.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); a.PopoverPresentationController!.SourceView = View; PresentViewController(a, true, null); }
|
||||
private void ShowBroadcastPicker() { var picker = new RPSystemBroadcastPickerView(new CoreGraphics.CGRect(0, 0, 60, 60)) { PreferredExtension = IosConstants.BroadcastExtension, ShowsMicrophoneButton = false }; UIAlertController a = UIAlertController.Create("Screen Audio", "Tap the broadcast button, then choose Start Broadcast.", UIAlertControllerStyle.Alert); a.View.AddSubview(picker); picker.Center = new(a.View.Bounds.GetMidX(), 110); a.AddAction(UIAlertAction.Create("Done", UIAlertActionStyle.Cancel, null)); PresentViewController(a, true, null); }
|
||||
}
|
||||
|
||||
internal sealed class AccountsController : UITableViewController
|
||||
{
|
||||
private readonly AppModel model; private IReadOnlyList<AccountEntry> accounts = [];
|
||||
internal AccountsController(AppModel model) { this.model = model; Title = "Accounts"; }
|
||||
public override async void ViewDidLoad() { base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "account"); NavigationItem.RightBarButtonItem = new(UIBarButtonSystemItem.Add, (_, _) => PromptCreate()); try { accounts = await model.Client!.ListAccountsAsync(); TableView.ReloadData(); } catch (Exception e) { UiHelpers.ShowError(this, e); } }
|
||||
public override nint RowsInSection(UITableView tableView, nint section) => accounts.Count;
|
||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) { UITableViewCell c = tableView.DequeueReusableCell("account", indexPath); var x = c.DefaultContentConfiguration; x.Text = accounts[indexPath.Row].Username; x.SecondaryText = accounts[indexPath.Row].IsAdmin ? "Administrator" : "Account"; c.ContentConfiguration = x; return c; }
|
||||
private void PromptCreate() { UIAlertController p = UIAlertController.Create("Create Account", null, UIAlertControllerStyle.Alert); p.AddTextField(f => f.Placeholder = "Username"); p.AddTextField(f => { f.Placeholder = "Password"; f.SecureTextEntry = true; }); p.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, null)); p.AddAction(UIAlertAction.Create("Create", UIAlertActionStyle.Default, async _ => { try { GenericResult result = await model.Client!.CreateAccountAsync(p.TextFields![0].Text ?? "", p.TextFields[1].Text ?? ""); if (!result.Ok) throw new InvalidOperationException(result.Message); accounts = await model.Client.ListAccountsAsync(); TableView.ReloadData(); } catch (Exception e) { UiHelpers.ShowError(this, e); } })); PresentViewController(p, true, null); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user