M5: Windows client moderation UI; add C ABI getters for account list, user mute/deafen, channel topic
This commit is contained in:
200
clients/windows/VoiceCat.App/Forms/AccountsDialog.cs
Normal file
200
clients/windows/VoiceCat.App/Forms/AccountsDialog.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Admin account management: list, create, reset password, and delete server accounts.
|
||||
/// </summary>
|
||||
public sealed class AccountsDialog : Form
|
||||
{
|
||||
private readonly VoiceCatClient _client;
|
||||
private readonly ListView _lvAccounts;
|
||||
private readonly Button _btnRefresh;
|
||||
private readonly Button _btnAdd;
|
||||
private readonly Button _btnResetPassword;
|
||||
private readonly Button _btnDelete;
|
||||
|
||||
public AccountsDialog(VoiceCatClient client)
|
||||
{
|
||||
_client = client;
|
||||
|
||||
Text = "Server accounts";
|
||||
FormBorderStyle = FormBorderStyle.Sizable;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(560, 360);
|
||||
MinimumSize = new Size(420, 260);
|
||||
|
||||
_lvAccounts = new ListView
|
||||
{
|
||||
View = View.Details,
|
||||
FullRowSelect = true,
|
||||
GridLines = true,
|
||||
Dock = DockStyle.Fill,
|
||||
TabIndex = 0,
|
||||
};
|
||||
_lvAccounts.Columns.Add("Username", 160);
|
||||
_lvAccounts.Columns.Add("Admin", 50);
|
||||
_lvAccounts.Columns.Add("Created", 130);
|
||||
_lvAccounts.Columns.Add("Last login", 130);
|
||||
_lvAccounts.SelectedIndexChanged += (_, _) => UpdateButtons();
|
||||
|
||||
var pnlButtons = new Panel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
Height = 44,
|
||||
};
|
||||
|
||||
_btnRefresh = new Button
|
||||
{
|
||||
Text = "&Refresh",
|
||||
Location = new Point(12, 9),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 1,
|
||||
};
|
||||
_btnRefresh.Click += (_, _) => RefreshList();
|
||||
|
||||
_btnAdd = new Button
|
||||
{
|
||||
Text = "&Add...",
|
||||
Location = new Point(100, 9),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 2,
|
||||
};
|
||||
_btnAdd.Click += (_, _) => AddAccount();
|
||||
|
||||
_btnResetPassword = new Button
|
||||
{
|
||||
Text = "&Reset password...",
|
||||
Location = new Point(188, 9),
|
||||
Size = new Size(120, 27),
|
||||
TabIndex = 3,
|
||||
Enabled = false,
|
||||
};
|
||||
_btnResetPassword.Click += (_, _) => ResetPassword();
|
||||
|
||||
_btnDelete = new Button
|
||||
{
|
||||
Text = "&Delete",
|
||||
Location = new Point(320, 9),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 4,
|
||||
Enabled = false,
|
||||
};
|
||||
_btnDelete.Click += (_, _) => DeleteAccount();
|
||||
|
||||
pnlButtons.Controls.AddRange([_btnRefresh, _btnAdd, _btnResetPassword, _btnDelete]);
|
||||
|
||||
Controls.Add(_lvAccounts);
|
||||
Controls.Add(pnlButtons);
|
||||
|
||||
Load += (_, _) => RefreshList();
|
||||
}
|
||||
|
||||
private void UpdateButtons()
|
||||
{
|
||||
bool selected = _lvAccounts.SelectedItems.Count > 0;
|
||||
_btnResetPassword.Enabled = selected;
|
||||
_btnDelete.Enabled = selected;
|
||||
}
|
||||
|
||||
private void RefreshList()
|
||||
{
|
||||
_client.RequestAccountList();
|
||||
// The event will come back asynchronously; poll briefly for the list.
|
||||
var deadline = DateTime.UtcNow.AddSeconds(2);
|
||||
List<AccountInfo>? accounts = null;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
_client.PumpEvents();
|
||||
accounts = _client.ListAccounts();
|
||||
if (accounts.Count > 0) break;
|
||||
Thread.Sleep(30);
|
||||
}
|
||||
accounts ??= _client.ListAccounts();
|
||||
|
||||
_lvAccounts.BeginUpdate();
|
||||
_lvAccounts.Items.Clear();
|
||||
foreach (var a in accounts.OrderBy(a => a.Username))
|
||||
{
|
||||
var item = new ListViewItem(a.Username);
|
||||
item.SubItems.Add(a.IsAdmin ? "Yes" : "No");
|
||||
item.SubItems.Add(FormatDate(a.CreatedAtUnixMs));
|
||||
item.SubItems.Add(FormatDate(a.LastLoginUnixMs));
|
||||
_lvAccounts.Items.Add(item);
|
||||
}
|
||||
_lvAccounts.EndUpdate();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private static string FormatDate(ulong ms)
|
||||
{
|
||||
if (ms == 0) return "—";
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds((long)ms).LocalDateTime.ToString("g");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "—";
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAccount()
|
||||
{
|
||||
using var userDlg = new InputDialog("Add account", "&Username:");
|
||||
if (userDlg.ShowDialog(this) != DialogResult.OK || string.IsNullOrWhiteSpace(userDlg.TextValue))
|
||||
return;
|
||||
|
||||
using var pwDlg = new PasswordPromptDialog($"Password for {userDlg.TextValue}:");
|
||||
if (pwDlg.ShowDialog(this) != DialogResult.OK || string.IsNullOrEmpty(pwDlg.Password))
|
||||
return;
|
||||
|
||||
var r = _client.CreateAccount(userDlg.TextValue, pwDlg.Password);
|
||||
if (r != VcResult.Ok)
|
||||
{
|
||||
MessageBox.Show(this, $"Create account failed: {r}", "VoiceCat",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
RefreshList();
|
||||
}
|
||||
|
||||
private void ResetPassword()
|
||||
{
|
||||
if (_lvAccounts.SelectedItems.Count == 0) return;
|
||||
string username = _lvAccounts.SelectedItems[0].Text;
|
||||
|
||||
using var dlg = new PasswordPromptDialog($"New password for {username}:");
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || string.IsNullOrEmpty(dlg.Password))
|
||||
return;
|
||||
|
||||
var r = _client.ResetPassword(username, dlg.Password);
|
||||
if (r != VcResult.Ok)
|
||||
{
|
||||
MessageBox.Show(this, $"Reset password failed: {r}", "VoiceCat",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
RefreshList();
|
||||
}
|
||||
|
||||
private void DeleteAccount()
|
||||
{
|
||||
if (_lvAccounts.SelectedItems.Count == 0) return;
|
||||
string username = _lvAccounts.SelectedItems[0].Text;
|
||||
|
||||
var confirm = MessageBox.Show(this, $"Delete account '{username}'?",
|
||||
"VoiceCat", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (confirm != DialogResult.Yes) return;
|
||||
|
||||
var r = _client.DeleteAccount(username);
|
||||
if (r != VcResult.Ok)
|
||||
{
|
||||
MessageBox.Show(this, $"Delete account failed: {r}", "VoiceCat",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
RefreshList();
|
||||
}
|
||||
}
|
||||
94
clients/windows/VoiceCat.App/Forms/BanUserDialog.cs
Normal file
94
clients/windows/VoiceCat.App/Forms/BanUserDialog.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Prompt for a ban reason and duration.
|
||||
/// </summary>
|
||||
public sealed class BanUserDialog : Form
|
||||
{
|
||||
private readonly TextBox _txtReason;
|
||||
private readonly ComboBox _cboDuration;
|
||||
|
||||
public string Reason => _txtReason.Text.Trim();
|
||||
public ulong ExpiresUnixMs { get; private set; }
|
||||
|
||||
public BanUserDialog(string nickname)
|
||||
{
|
||||
Text = $"Ban {nickname}";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(384, 160);
|
||||
|
||||
var lblReason = new Label
|
||||
{
|
||||
Text = "&Reason:",
|
||||
Location = new Point(12, 12),
|
||||
AutoSize = true,
|
||||
};
|
||||
_txtReason = new TextBox
|
||||
{
|
||||
Location = new Point(12, 34),
|
||||
Size = new Size(360, 23),
|
||||
TabIndex = 1,
|
||||
};
|
||||
|
||||
var lblDuration = new Label
|
||||
{
|
||||
Text = "&Duration:",
|
||||
Location = new Point(12, 66),
|
||||
AutoSize = true,
|
||||
};
|
||||
_cboDuration = new ComboBox
|
||||
{
|
||||
Location = new Point(12, 88),
|
||||
Size = new Size(200, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
TabIndex = 2,
|
||||
};
|
||||
_cboDuration.Items.AddRange(new object[]
|
||||
{
|
||||
new DurationItem("1 hour", TimeSpan.FromHours(1)),
|
||||
new DurationItem("1 day", TimeSpan.FromDays(1)),
|
||||
new DurationItem("1 week", TimeSpan.FromDays(7)),
|
||||
new DurationItem("Permanent", TimeSpan.Zero),
|
||||
});
|
||||
_cboDuration.SelectedIndex = 1;
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&Ban",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(216, 126),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 3,
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(297, 126),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 4,
|
||||
};
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
Controls.AddRange([lblReason, _txtReason, lblDuration, _cboDuration, btnOk, btnCancel]);
|
||||
|
||||
btnOk.Click += (_, _) =>
|
||||
{
|
||||
var selected = _cboDuration.SelectedItem as DurationItem;
|
||||
ExpiresUnixMs = selected?.Duration == TimeSpan.Zero
|
||||
? 0
|
||||
: (ulong)DateTimeOffset.UtcNow.Add(selected!.Duration).ToUnixTimeMilliseconds();
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class DurationItem(string label, TimeSpan duration)
|
||||
{
|
||||
public TimeSpan Duration { get; } = duration;
|
||||
public override string ToString() => label;
|
||||
}
|
||||
}
|
||||
384
clients/windows/VoiceCat.App/Forms/ChannelEditDialog.cs
Normal file
384
clients/windows/VoiceCat.App/Forms/ChannelEditDialog.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Create or edit a channel, including its full per-channel Opus audio config.
|
||||
/// Returns DialogResult.OK with Result set.
|
||||
/// </summary>
|
||||
public sealed class ChannelEditDialog : Form
|
||||
{
|
||||
private readonly bool _isCreate;
|
||||
private readonly uint _editingId;
|
||||
private readonly List<ChannelInfo> _channels;
|
||||
|
||||
private TextBox _txtName = null!;
|
||||
private TextBox _txtTopic = null!;
|
||||
private ComboBox _cboParent = null!;
|
||||
private NumericUpDown _numMaxUsers = null!;
|
||||
private NumericUpDown _numSortOrder = null!;
|
||||
private CheckBox _chkPassword = null!;
|
||||
private TextBox _txtPassword = null!;
|
||||
|
||||
private ComboBox _cboMode = null!;
|
||||
private NumericUpDown _numSampleRate = null!;
|
||||
private NumericUpDown _numBitrate = null!;
|
||||
private NumericUpDown _numFrameMs = null!;
|
||||
private ComboBox _cboApplication = null!;
|
||||
private CheckBox _chkFec = null!;
|
||||
private NumericUpDown _numExpectedLoss = null!;
|
||||
private CheckBox _chkDtx = null!;
|
||||
private NumericUpDown _numComplexity = null!;
|
||||
|
||||
public ChannelEditInfo? Result { get; private set; }
|
||||
|
||||
public ChannelEditDialog(IEnumerable<ChannelInfo> channels, ChannelEditInfo? existing = null)
|
||||
{
|
||||
_isCreate = existing is null;
|
||||
_editingId = existing?.Id ?? 0;
|
||||
_channels = channels.Where(c => c.Id != _editingId).ToList();
|
||||
|
||||
Text = _isCreate ? "Create channel" : "Edit channel";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(520, 520);
|
||||
|
||||
var tabs = new TabControl
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
TabIndex = 0,
|
||||
};
|
||||
|
||||
var pageGeneral = new TabPage("General");
|
||||
BuildGeneralPage(pageGeneral, existing);
|
||||
var pageAudio = new TabPage("Audio config");
|
||||
BuildAudioPage(pageAudio, existing?.Audio);
|
||||
|
||||
tabs.TabPages.Add(pageGeneral);
|
||||
tabs.TabPages.Add(pageAudio);
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(350, 486),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 100,
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(431, 486),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 101,
|
||||
};
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
|
||||
Controls.Add(tabs);
|
||||
Controls.Add(btnOk);
|
||||
Controls.Add(btnCancel);
|
||||
|
||||
btnOk.Click += BtnOk_Click;
|
||||
}
|
||||
|
||||
private void BuildGeneralPage(TabPage page, ChannelEditInfo? existing)
|
||||
{
|
||||
int y = 16;
|
||||
int labelWidth = 110;
|
||||
int inputX = 128;
|
||||
|
||||
AddLabel(page, "&Name:", 12, y, labelWidth);
|
||||
_txtName = new TextBox
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(360, 23),
|
||||
Text = existing?.Name ?? "",
|
||||
TabIndex = 1,
|
||||
};
|
||||
page.Controls.Add(_txtName);
|
||||
y += 36;
|
||||
|
||||
AddLabel(page, "&Topic:", 12, y, labelWidth);
|
||||
_txtTopic = new TextBox
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(360, 23),
|
||||
Text = existing?.Topic ?? "",
|
||||
TabIndex = 2,
|
||||
};
|
||||
page.Controls.Add(_txtTopic);
|
||||
y += 36;
|
||||
|
||||
AddLabel(page, "&Parent channel:", 12, y, labelWidth);
|
||||
_cboParent = new ComboBox
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(360, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
TabIndex = 3,
|
||||
};
|
||||
_cboParent.Items.Add(new ChannelItem("(root)", 0));
|
||||
foreach (var ch in _channels.OrderBy(c => c.Name))
|
||||
_cboParent.Items.Add(new ChannelItem(ch.Name, ch.Id));
|
||||
SelectParent(existing?.ParentId ?? 0);
|
||||
page.Controls.Add(_cboParent);
|
||||
y += 36;
|
||||
|
||||
AddLabel(page, "&Max users:", 12, y, labelWidth);
|
||||
_numMaxUsers = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = 10000,
|
||||
Value = existing?.MaxUsers ?? 0,
|
||||
TabIndex = 4,
|
||||
};
|
||||
page.Controls.Add(_numMaxUsers);
|
||||
AddLabel(page, "(0 = unlimited)", inputX + 128, y, 100);
|
||||
y += 36;
|
||||
|
||||
AddLabel(page, "&Sort order:", 12, y, labelWidth);
|
||||
_numSortOrder = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = uint.MaxValue,
|
||||
Value = existing?.SortOrder ?? 0,
|
||||
TabIndex = 5,
|
||||
};
|
||||
page.Controls.Add(_numSortOrder);
|
||||
y += 36;
|
||||
|
||||
_chkPassword = new CheckBox
|
||||
{
|
||||
Text = "&Password protected",
|
||||
Location = new Point(inputX, y),
|
||||
AutoSize = true,
|
||||
Checked = existing?.PasswordProtected ?? false,
|
||||
TabIndex = 6,
|
||||
};
|
||||
page.Controls.Add(_chkPassword);
|
||||
y += 28;
|
||||
|
||||
_txtPassword = new TextBox
|
||||
{
|
||||
Location = new Point(inputX, y),
|
||||
Size = new Size(360, 23),
|
||||
UseSystemPasswordChar = true,
|
||||
Enabled = _chkPassword.Checked,
|
||||
Text = existing?.Password ?? "",
|
||||
TabIndex = 7,
|
||||
};
|
||||
page.Controls.Add(_txtPassword);
|
||||
_chkPassword.CheckedChanged += (_, _) => _txtPassword.Enabled = _chkPassword.Checked;
|
||||
}
|
||||
|
||||
private void BuildAudioPage(TabPage page, AudioConfigInfo? audio)
|
||||
{
|
||||
audio ??= new AudioConfigInfo(0, false, 48000, 0, 20, 0, true, 0, false, 10);
|
||||
|
||||
int y = 16;
|
||||
int labelWidth = 150;
|
||||
int inputX = 168;
|
||||
|
||||
AddLabel(page, "Codec (0 = Opus):", 12, y, labelWidth);
|
||||
var numCodec = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = 0,
|
||||
Value = audio.Codec,
|
||||
Enabled = false,
|
||||
};
|
||||
page.Controls.Add(numCodec);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "&Mode:", 12, y, labelWidth);
|
||||
_cboMode = new ComboBox
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(160, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
TabIndex = 10,
|
||||
};
|
||||
_cboMode.Items.AddRange(["Mono", "Stereo"]);
|
||||
_cboMode.SelectedIndex = audio.Stereo ? 1 : 0;
|
||||
page.Controls.Add(_cboMode);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "Sample &rate (Hz):", 12, y, labelWidth);
|
||||
_numSampleRate = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 8000,
|
||||
Maximum = 96000,
|
||||
Value = audio.SampleRate,
|
||||
TabIndex = 11,
|
||||
};
|
||||
page.Controls.Add(_numSampleRate);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "&Bitrate (bps, 0 = default):", 12, y, labelWidth);
|
||||
_numBitrate = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = 512000,
|
||||
Increment = 1000,
|
||||
Value = audio.BitrateBps,
|
||||
TabIndex = 12,
|
||||
};
|
||||
page.Controls.Add(_numBitrate);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "Frame &ms:", 12, y, labelWidth);
|
||||
_numFrameMs = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 5,
|
||||
Maximum = 120,
|
||||
Value = audio.FrameMs,
|
||||
TabIndex = 13,
|
||||
};
|
||||
page.Controls.Add(_numFrameMs);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "&Application:", 12, y, labelWidth);
|
||||
_cboApplication = new ComboBox
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(160, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
TabIndex = 14,
|
||||
};
|
||||
_cboApplication.Items.AddRange(["VoIP", "Audio", "Low delay"]);
|
||||
_cboApplication.SelectedIndex = (int)Math.Min(2, audio.Application);
|
||||
page.Controls.Add(_cboApplication);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "Expected packet loss (%):", 12, y, labelWidth);
|
||||
_numExpectedLoss = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = 100,
|
||||
Value = audio.ExpectedPacketLoss,
|
||||
TabIndex = 15,
|
||||
};
|
||||
page.Controls.Add(_numExpectedLoss);
|
||||
y += 34;
|
||||
|
||||
AddLabel(page, "Com&plexity (0–10):", 12, y, labelWidth);
|
||||
_numComplexity = new NumericUpDown
|
||||
{
|
||||
Location = new Point(inputX, y - 2),
|
||||
Size = new Size(120, 23),
|
||||
Minimum = 0,
|
||||
Maximum = 10,
|
||||
Value = audio.Complexity,
|
||||
TabIndex = 16,
|
||||
};
|
||||
page.Controls.Add(_numComplexity);
|
||||
y += 34;
|
||||
|
||||
_chkFec = new CheckBox
|
||||
{
|
||||
Text = "&FEC",
|
||||
Location = new Point(inputX, y),
|
||||
AutoSize = true,
|
||||
Checked = audio.Fec,
|
||||
TabIndex = 17,
|
||||
};
|
||||
page.Controls.Add(_chkFec);
|
||||
y += 28;
|
||||
|
||||
_chkDtx = new CheckBox
|
||||
{
|
||||
Text = "&DTX",
|
||||
Location = new Point(inputX, y),
|
||||
AutoSize = true,
|
||||
Checked = audio.Dtx,
|
||||
TabIndex = 18,
|
||||
};
|
||||
page.Controls.Add(_chkDtx);
|
||||
}
|
||||
|
||||
private static void AddLabel(Control parent, string text, int x, int y, int width)
|
||||
{
|
||||
parent.Controls.Add(new Label
|
||||
{
|
||||
Text = text,
|
||||
Location = new Point(x, y),
|
||||
Size = new Size(width, 17),
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
|
||||
});
|
||||
}
|
||||
|
||||
private void SelectParent(uint parentId)
|
||||
{
|
||||
for (int i = 0; i < _cboParent.Items.Count; i++)
|
||||
{
|
||||
if (_cboParent.Items[i] is ChannelItem item && item.Id == parentId)
|
||||
{
|
||||
_cboParent.SelectedIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_cboParent.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void BtnOk_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string name = _txtName.Text.Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
MessageBox.Show(this, "Channel name is required.", "VoiceCat",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
uint parentId = (_cboParent.SelectedItem as ChannelItem)?.Id ?? 0;
|
||||
var audio = new AudioConfigInfo(
|
||||
Codec: 0,
|
||||
Stereo: _cboMode.SelectedIndex == 1,
|
||||
SampleRate: (uint)_numSampleRate.Value,
|
||||
BitrateBps: (uint)_numBitrate.Value,
|
||||
FrameMs: (uint)_numFrameMs.Value,
|
||||
Application: (uint)_cboApplication.SelectedIndex,
|
||||
Fec: _chkFec.Checked,
|
||||
ExpectedPacketLoss: (uint)_numExpectedLoss.Value,
|
||||
Dtx: _chkDtx.Checked,
|
||||
Complexity: (uint)_numComplexity.Value);
|
||||
|
||||
Result = new ChannelEditInfo(
|
||||
Id: _editingId,
|
||||
ParentId: parentId,
|
||||
Name: name,
|
||||
Topic: _txtTopic.Text.Trim(),
|
||||
PasswordProtected: _chkPassword.Checked,
|
||||
Password: _chkPassword.Checked ? _txtPassword.Text : null,
|
||||
MaxUsers: (uint)_numMaxUsers.Value,
|
||||
SortOrder: (uint)_numSortOrder.Value,
|
||||
Audio: audio);
|
||||
}
|
||||
|
||||
private sealed class ChannelItem(string name, uint id)
|
||||
{
|
||||
public uint Id { get; } = id;
|
||||
public override string ToString() => name;
|
||||
}
|
||||
}
|
||||
60
clients/windows/VoiceCat.App/Forms/InputDialog.cs
Normal file
60
clients/windows/VoiceCat.App/Forms/InputDialog.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// A small reusable modal that prompts for a single line of text (e.g., kick/ban reason).
|
||||
/// Returns DialogResult.OK with Text set, or DialogResult.Cancel.
|
||||
/// </summary>
|
||||
public sealed class InputDialog : Form
|
||||
{
|
||||
private readonly TextBox _txtInput;
|
||||
|
||||
public string TextValue => _txtInput.Text.Trim();
|
||||
|
||||
public InputDialog(string caption, string label, string defaultText = "")
|
||||
{
|
||||
var lbl = new Label
|
||||
{
|
||||
Text = label,
|
||||
AutoSize = true,
|
||||
Location = new Point(12, 12),
|
||||
TabIndex = 0,
|
||||
};
|
||||
|
||||
_txtInput = new TextBox
|
||||
{
|
||||
Text = defaultText,
|
||||
Location = new Point(12, 36),
|
||||
Size = new Size(360, 23),
|
||||
TabIndex = 1,
|
||||
};
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(216, 72),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 2,
|
||||
};
|
||||
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(297, 72),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 3,
|
||||
};
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(384, 112);
|
||||
Controls.AddRange([lbl, _txtInput, btnOk, btnCancel]);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = caption;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ partial class MainForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null!;
|
||||
|
||||
// Menu bar
|
||||
private MenuStrip menuStrip = null!;
|
||||
|
||||
// Status bar
|
||||
private Label lblStatus = null!;
|
||||
|
||||
@@ -349,6 +352,13 @@ partial class MainForm
|
||||
pnlVoice.Controls.Add(flpVoiceBottom); // Fill — added first
|
||||
pnlVoice.Controls.Add(flpVoiceTop); // Top — added last
|
||||
|
||||
// ── Menu strip ────────────────────────────────────────────────────────
|
||||
menuStrip = new MenuStrip();
|
||||
menuStrip.AccessibleName = "Main menu";
|
||||
menuStrip.Dock = DockStyle.Top;
|
||||
menuStrip.TabIndex = 0;
|
||||
MainMenuStrip = menuStrip;
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────────
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(960, 680);
|
||||
@@ -358,6 +368,7 @@ partial class MainForm
|
||||
Controls.Add(splitMain); // DockStyle.Fill
|
||||
Controls.Add(pnlVoice); // DockStyle.Bottom
|
||||
Controls.Add(lblStatus); // DockStyle.Top
|
||||
Controls.Add(menuStrip); // DockStyle.Top (placed at the very top)
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "VoiceCat";
|
||||
}
|
||||
|
||||
@@ -19,10 +19,13 @@ public partial class MainForm : Form
|
||||
private List<ChannelInfo> _channels = [];
|
||||
private readonly Dictionary<uint, UserInfo> _users = [];
|
||||
private readonly HashSet<uint> _talkingUsers = [];
|
||||
private PermissionsInfo _ownPermissions = new(false, false, false, false, false, false);
|
||||
|
||||
// Voice state
|
||||
private uint _micStreamId; // 0 = not started
|
||||
private Keys _pttKey = Keys.F8;
|
||||
private bool _serverMuted;
|
||||
private bool _serverDeafened;
|
||||
|
||||
public MainForm(VoiceCatClient client, uint selfUserId, string nickname)
|
||||
{
|
||||
@@ -36,19 +39,18 @@ public partial class MainForm : Form
|
||||
_client.EventReceived += OnEvent;
|
||||
_client.LevelChanged += OnLevelChanged;
|
||||
_pumpTimer.Tick += (_, _) => _client.PumpEvents();
|
||||
|
||||
// M5: load own permissions and build permission-aware menus.
|
||||
_ownPermissions = SafeGetPermissions();
|
||||
BuildMenus();
|
||||
BuildChannelContextMenu();
|
||||
BuildUserContextMenu();
|
||||
|
||||
_pumpTimer.Start();
|
||||
|
||||
// Channel tree
|
||||
tvChannels.DoubleClick += TvChannels_DoubleClick;
|
||||
tvChannels.KeyDown += TvChannels_KeyDown;
|
||||
|
||||
// User list — double-click or Enter for per-user tuning, right-click for context menu
|
||||
var ctxUsers = new ContextMenuStrip();
|
||||
var miTune = new ToolStripMenuItem("&Adjust volume and noise settings...");
|
||||
miTune.Click += (_, _) => OpenUserTuning();
|
||||
ctxUsers.Opening += (_, _) => miTune.Enabled = lstUsers.SelectedItem is UserListItem;
|
||||
ctxUsers.Items.Add(miTune);
|
||||
lstUsers.ContextMenuStrip = ctxUsers;
|
||||
lstUsers.DoubleClick += (_, _) => OpenUserTuning();
|
||||
lstUsers.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) OpenUserTuning(); };
|
||||
|
||||
@@ -89,7 +91,11 @@ public partial class MainForm : Form
|
||||
foreach (var u in users)
|
||||
{
|
||||
_users[u.Id] = u;
|
||||
if (u.Id == _selfUserId) _currentChannelId = u.ChannelId;
|
||||
if (u.Id == _selfUserId)
|
||||
{
|
||||
_currentChannelId = u.ChannelId;
|
||||
UpdateSelfServerMuteState(u.ServerMuted, u.ServerDeafened);
|
||||
}
|
||||
}
|
||||
RefreshChannelTree();
|
||||
RefreshUserList();
|
||||
@@ -106,6 +112,105 @@ public partial class MainForm : Form
|
||||
LoadInputDevices();
|
||||
}
|
||||
|
||||
// ── Menu / context menu builders (M5) ─────────────────────────────────────
|
||||
|
||||
private PermissionsInfo SafeGetPermissions()
|
||||
{
|
||||
try { return _client.GetPermissions(); }
|
||||
catch { return new PermissionsInfo(false, false, false, false, false, false); }
|
||||
}
|
||||
|
||||
private void BuildMenus()
|
||||
{
|
||||
if (!_ownPermissions.CanAdminAccounts) return;
|
||||
|
||||
var adminMenu = new ToolStripMenuItem("&Admin");
|
||||
var miAccounts = new ToolStripMenuItem("&Server accounts...");
|
||||
miAccounts.Click += (_, _) =>
|
||||
{
|
||||
using var dlg = new AccountsDialog(_client);
|
||||
dlg.ShowDialog(this);
|
||||
};
|
||||
adminMenu.DropDownItems.Add(miAccounts);
|
||||
menuStrip.Items.Add(adminMenu);
|
||||
}
|
||||
|
||||
private void BuildChannelContextMenu()
|
||||
{
|
||||
var ctx = new ContextMenuStrip();
|
||||
ctx.Opening += (_, _) =>
|
||||
{
|
||||
ctx.Items.Clear();
|
||||
bool hasSelection = tvChannels.SelectedNode?.Tag is uint;
|
||||
bool canCreate = _ownPermissions.CanCreateTempChannel || _ownPermissions.IsAdmin;
|
||||
bool isAdmin = _ownPermissions.IsAdmin;
|
||||
|
||||
if (hasSelection)
|
||||
{
|
||||
ctx.Items.Add("&Join", null, (_, _) => ChannelTreeJoinSelected());
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
}
|
||||
|
||||
if (canCreate)
|
||||
{
|
||||
ctx.Items.Add("&Create channel...", null, (_, _) => CreateChannel());
|
||||
}
|
||||
|
||||
if (hasSelection && isAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Edit channel...", null, (_, _) => EditSelectedChannel());
|
||||
ctx.Items.Add("&Delete channel...", null, (_, _) => DeleteSelectedChannel());
|
||||
}
|
||||
};
|
||||
tvChannels.ContextMenuStrip = ctx;
|
||||
}
|
||||
|
||||
private void BuildUserContextMenu()
|
||||
{
|
||||
var ctx = new ContextMenuStrip();
|
||||
ctx.Opening += (_, _) =>
|
||||
{
|
||||
ctx.Items.Clear();
|
||||
if (lstUsers.SelectedItem is not UserListItem item) return;
|
||||
if (!_users.TryGetValue(item.UserId, out var user)) return;
|
||||
bool isSelf = user.Id == _selfUserId;
|
||||
|
||||
var miTune = new ToolStripMenuItem("&Adjust volume and noise settings...");
|
||||
miTune.Click += (_, _) => OpenUserTuning();
|
||||
ctx.Items.Add(miTune);
|
||||
|
||||
if (!isSelf)
|
||||
{
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
|
||||
if (_ownPermissions.CanMoveUsers || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Move to channel...", null, (_, _) => MoveSelectedUser());
|
||||
}
|
||||
if (_ownPermissions.CanKick || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Kick...", null, (_, _) => KickSelectedUser());
|
||||
}
|
||||
if (_ownPermissions.CanBan || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Ban...", null, (_, _) => BanSelectedUser());
|
||||
}
|
||||
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
|
||||
if (_ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add(user.ServerMuted ? "Server &unmute" : "Server &mute",
|
||||
null, (_, _) => ToggleServerMuteSelected());
|
||||
ctx.Items.Add(user.ServerDeafened ? "Server un&deafen" : "Server &deafen",
|
||||
null, (_, _) => ToggleServerDeafenSelected());
|
||||
ctx.Items.Add("&Set permissions...", null, (_, _) => SetPermissionsSelected());
|
||||
}
|
||||
}
|
||||
};
|
||||
lstUsers.ContextMenuStrip = ctx;
|
||||
}
|
||||
|
||||
// ── Event dispatch ────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEvent(VoiceCatEvent ev)
|
||||
@@ -127,6 +232,12 @@ public partial class MainForm : Form
|
||||
case VcEventType.JoinResult:
|
||||
HandleJoinResult(ev);
|
||||
break;
|
||||
case VcEventType.GenericResult:
|
||||
HandleGenericResult(ev);
|
||||
break;
|
||||
case VcEventType.AccountList:
|
||||
AddActivity("Account list updated");
|
||||
break;
|
||||
case VcEventType.TextMessage:
|
||||
HandleTextMessage(ev);
|
||||
break;
|
||||
@@ -166,7 +277,8 @@ public partial class MainForm : Form
|
||||
|
||||
private void HandleUserJoined(VoiceCatEvent ev)
|
||||
{
|
||||
var user = new UserInfo(ev.UserId, ev.Text ?? $"User#{ev.UserId}", false, ev.ChannelId);
|
||||
var user = new UserInfo(ev.UserId, ev.Text ?? $"User#{ev.UserId}", false, ev.ChannelId,
|
||||
false, false, false, false);
|
||||
_users[ev.UserId] = user;
|
||||
RefreshUserList();
|
||||
RebuildScopeCombo();
|
||||
@@ -192,7 +304,11 @@ public partial class MainForm : Form
|
||||
foreach (var u in users)
|
||||
{
|
||||
_users[u.Id] = u;
|
||||
if (u.Id == _selfUserId) _currentChannelId = u.ChannelId;
|
||||
if (u.Id == _selfUserId)
|
||||
{
|
||||
_currentChannelId = u.ChannelId;
|
||||
UpdateSelfServerMuteState(u.ServerMuted, u.ServerDeafened);
|
||||
}
|
||||
}
|
||||
RefreshChannelTree();
|
||||
RefreshUserList();
|
||||
@@ -221,6 +337,13 @@ public partial class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGenericResult(VoiceCatEvent ev)
|
||||
{
|
||||
string prefix = ev.Result == VcResult.Ok ? "Success" : "Failed";
|
||||
string detail = !string.IsNullOrEmpty(ev.Text) ? $": {ev.Text}" : "";
|
||||
AddActivity($"{prefix}{detail} ({ev.Result})");
|
||||
}
|
||||
|
||||
private void HandleTextMessage(VoiceCatEvent ev)
|
||||
{
|
||||
string time = ev.TimestampUnixMs > 0
|
||||
@@ -341,6 +464,8 @@ public partial class MainForm : Form
|
||||
string label = user.Nickname;
|
||||
if (user.Id == _selfUserId) label += " (you)";
|
||||
if (_talkingUsers.Contains(user.Id)) label += " (talking)";
|
||||
if (user.SelfMicMuted || user.ServerMuted) label += " (muted)";
|
||||
if (user.SelfDeafened || user.ServerDeafened) label += " (deafened)";
|
||||
lstUsers.Items.Add(new UserListItem(user.Id, label));
|
||||
}
|
||||
lstUsers.EndUpdate();
|
||||
@@ -376,15 +501,19 @@ public partial class MainForm : Form
|
||||
|
||||
private void UpdateStatusLabel()
|
||||
{
|
||||
string suffix = "";
|
||||
if (_serverMuted) suffix += " [server muted]";
|
||||
if (_serverDeafened) suffix += " [server deafened]";
|
||||
|
||||
if (_currentChannelId == 0)
|
||||
{
|
||||
lblStatus.Text = $"Connected as {_nickname} — not in a channel.";
|
||||
lblStatus.Text = $"Connected as {_nickname}{suffix} — not in a channel.";
|
||||
return;
|
||||
}
|
||||
string chanName = _channels.FirstOrDefault(c => c.Id == _currentChannelId)?.Name
|
||||
?? $"Channel #{_currentChannelId}";
|
||||
int count = _users.Values.Count(u => u.ChannelId == _currentChannelId);
|
||||
lblStatus.Text = $"Connected as {_nickname} — {chanName} ({count} user{(count == 1 ? "" : "s")})";
|
||||
lblStatus.Text = $"Connected as {_nickname}{suffix} — {chanName} ({count} user{(count == 1 ? "" : "s")})";
|
||||
}
|
||||
|
||||
// ── Device management ─────────────────────────────────────────────────────
|
||||
@@ -591,6 +720,130 @@ public partial class MainForm : Form
|
||||
_client.JoinChannel(channelId, password);
|
||||
}
|
||||
|
||||
private void ChannelTreeJoinSelected()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is uint channelId)
|
||||
JoinChannelRequest(channelId);
|
||||
}
|
||||
|
||||
// ── M5: Moderation helpers ────────────────────────────────────────────────
|
||||
|
||||
private void UpdateSelfServerMuteState(bool muted, bool deafened)
|
||||
{
|
||||
bool wasMuted = _serverMuted;
|
||||
bool wasDeafened = _serverDeafened;
|
||||
_serverMuted = muted;
|
||||
_serverDeafened = deafened;
|
||||
|
||||
if (muted && !wasMuted) AddActivity("You have been server-muted");
|
||||
if (deafened && !wasDeafened) AddActivity("You have been server-deafened");
|
||||
if (!muted && wasMuted) AddActivity("Server mute cleared");
|
||||
if (!deafened && wasDeafened) AddActivity("Server deafen cleared");
|
||||
|
||||
UpdateStatusLabel();
|
||||
}
|
||||
|
||||
private void CreateChannel()
|
||||
{
|
||||
using var dlg = new ChannelEditDialog(_channels);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.CreateChannel(dlg.Result);
|
||||
}
|
||||
|
||||
private void EditSelectedChannel()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is not uint channelId) return;
|
||||
var channel = _channels.FirstOrDefault(c => c.Id == channelId);
|
||||
if (channel is null) return;
|
||||
|
||||
var editInfo = new ChannelEditInfo(
|
||||
channel.Id,
|
||||
channel.ParentId,
|
||||
channel.Name,
|
||||
channel.Topic,
|
||||
channel.PasswordProtected,
|
||||
null,
|
||||
channel.MaxUsers,
|
||||
0,
|
||||
new AudioConfigInfo(0, false, 48000, 0, 20, 0, true, 0, false, 10));
|
||||
|
||||
using var dlg = new ChannelEditDialog(_channels, editInfo);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.EditChannel(dlg.Result);
|
||||
}
|
||||
|
||||
private void DeleteSelectedChannel()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is not uint channelId) return;
|
||||
var channel = _channels.FirstOrDefault(c => c.Id == channelId);
|
||||
if (channel is null) return;
|
||||
|
||||
var confirm = MessageBox.Show(this, $"Delete channel \"{channel.Name}\"?",
|
||||
"VoiceCat", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (confirm != DialogResult.Yes) return;
|
||||
|
||||
_client.DeleteChannel(channelId);
|
||||
}
|
||||
|
||||
private UserInfo? SelectedUser()
|
||||
{
|
||||
if (lstUsers.SelectedItem is not UserListItem item) return null;
|
||||
_users.TryGetValue(item.UserId, out var user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void MoveSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new MoveUserDialog(_channels, user.ChannelId);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
_client.MoveUser(user.Id, dlg.SelectedChannelId);
|
||||
}
|
||||
|
||||
private void KickSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new InputDialog("Kick user", "&Reason:", "Kicked by admin");
|
||||
string? reason = dlg.ShowDialog(this) == DialogResult.OK ? dlg.TextValue : null;
|
||||
_client.KickUser(user.Id, reason);
|
||||
}
|
||||
|
||||
private void BanSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new BanUserDialog(user.Nickname);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
_client.BanUser(user.Id, dlg.Reason, dlg.ExpiresUnixMs);
|
||||
}
|
||||
|
||||
private void ToggleServerMuteSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
_client.SetServerMute(user.Id, !user.ServerMuted, user.ServerDeafened);
|
||||
}
|
||||
|
||||
private void ToggleServerDeafenSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
_client.SetServerMute(user.Id, user.ServerMuted, !user.ServerDeafened);
|
||||
}
|
||||
|
||||
private void SetPermissionsSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
// The C ABI does not expose a user's current permissions, so start unchecked.
|
||||
var current = new PermissionsInfo(false, false, false, false, false, false);
|
||||
using var dlg = new PermissionsDialog(user.Nickname, current);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.SetPermission(user.Id, dlg.Result);
|
||||
}
|
||||
|
||||
// ── Per-user tuning ───────────────────────────────────────────────────────
|
||||
|
||||
private void OpenUserTuning()
|
||||
|
||||
79
clients/windows/VoiceCat.App/Forms/MoveUserDialog.cs
Normal file
79
clients/windows/VoiceCat.App/Forms/MoveUserDialog.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Prompt the moderator to pick a destination channel for a user move.
|
||||
/// </summary>
|
||||
public sealed class MoveUserDialog : Form
|
||||
{
|
||||
private readonly ComboBox _cboChannel;
|
||||
|
||||
public uint SelectedChannelId { get; private set; }
|
||||
|
||||
public MoveUserDialog(IEnumerable<ChannelInfo> channels, uint currentChannelId)
|
||||
{
|
||||
Text = "Move user to channel";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(384, 120);
|
||||
|
||||
var lbl = new Label
|
||||
{
|
||||
Text = "&Destination channel:",
|
||||
Location = new Point(12, 12),
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
_cboChannel = new ComboBox
|
||||
{
|
||||
Location = new Point(12, 36),
|
||||
Size = new Size(360, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
TabIndex = 1,
|
||||
};
|
||||
|
||||
foreach (var ch in channels.OrderBy(c => c.Name))
|
||||
{
|
||||
_cboChannel.Items.Add(new ChannelItem(ch.Name, ch.Id));
|
||||
if (ch.Id == currentChannelId) _cboChannel.SelectedIndex = _cboChannel.Items.Count - 1;
|
||||
}
|
||||
if (_cboChannel.SelectedIndex < 0 && _cboChannel.Items.Count > 0)
|
||||
_cboChannel.SelectedIndex = 0;
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(216, 72),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 2,
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(297, 72),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 3,
|
||||
};
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
Controls.AddRange([lbl, _cboChannel, btnOk, btnCancel]);
|
||||
|
||||
btnOk.Click += (_, _) =>
|
||||
{
|
||||
SelectedChannelId = (_cboChannel.SelectedItem as ChannelItem)?.Id ?? 0;
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class ChannelItem(string name, uint id)
|
||||
{
|
||||
public uint Id { get; } = id;
|
||||
public override string ToString() => name;
|
||||
}
|
||||
}
|
||||
110
clients/windows/VoiceCat.App/Forms/PermissionsDialog.cs
Normal file
110
clients/windows/VoiceCat.App/Forms/PermissionsDialog.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Edit a user's server permissions.
|
||||
/// </summary>
|
||||
public sealed class PermissionsDialog : Form
|
||||
{
|
||||
private readonly CheckBox _chkCreateTemp;
|
||||
private readonly CheckBox _chkKick;
|
||||
private readonly CheckBox _chkBan;
|
||||
private readonly CheckBox _chkMove;
|
||||
private readonly CheckBox _chkAdminAccounts;
|
||||
private readonly CheckBox _chkIsAdmin;
|
||||
|
||||
public PermissionsInfo? Result { get; private set; }
|
||||
|
||||
public PermissionsDialog(string nickname, PermissionsInfo current)
|
||||
{
|
||||
Text = $"Permissions — {nickname}";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(320, 260);
|
||||
|
||||
var lbl = new Label
|
||||
{
|
||||
Text = $"Set permissions for {nickname}:",
|
||||
Location = new Point(12, 12),
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
_chkCreateTemp = new CheckBox
|
||||
{
|
||||
Text = "Can create &temporary channels",
|
||||
Location = new Point(12, 38),
|
||||
AutoSize = true,
|
||||
Checked = current.CanCreateTempChannel,
|
||||
};
|
||||
_chkKick = new CheckBox
|
||||
{
|
||||
Text = "Can &kick users",
|
||||
Location = new Point(12, 64),
|
||||
AutoSize = true,
|
||||
Checked = current.CanKick,
|
||||
};
|
||||
_chkBan = new CheckBox
|
||||
{
|
||||
Text = "Can &ban users",
|
||||
Location = new Point(12, 90),
|
||||
AutoSize = true,
|
||||
Checked = current.CanBan,
|
||||
};
|
||||
_chkMove = new CheckBox
|
||||
{
|
||||
Text = "Can &move users",
|
||||
Location = new Point(12, 116),
|
||||
AutoSize = true,
|
||||
Checked = current.CanMoveUsers,
|
||||
};
|
||||
_chkAdminAccounts = new CheckBox
|
||||
{
|
||||
Text = "Can manage &accounts",
|
||||
Location = new Point(12, 142),
|
||||
AutoSize = true,
|
||||
Checked = current.CanAdminAccounts,
|
||||
};
|
||||
_chkIsAdmin = new CheckBox
|
||||
{
|
||||
Text = "Is &admin",
|
||||
Location = new Point(12, 168),
|
||||
AutoSize = true,
|
||||
Checked = current.IsAdmin,
|
||||
};
|
||||
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(152, 210),
|
||||
Size = new Size(75, 27),
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(233, 210),
|
||||
Size = new Size(75, 27),
|
||||
};
|
||||
|
||||
AcceptButton = btnOk;
|
||||
CancelButton = btnCancel;
|
||||
Controls.AddRange([lbl, _chkCreateTemp, _chkKick, _chkBan, _chkMove,
|
||||
_chkAdminAccounts, _chkIsAdmin, btnOk, btnCancel]);
|
||||
|
||||
btnOk.Click += (_, _) =>
|
||||
{
|
||||
Result = new PermissionsInfo(
|
||||
CanCreateTempChannel: _chkCreateTemp.Checked,
|
||||
CanKick: _chkKick.Checked,
|
||||
CanBan: _chkBan.Checked,
|
||||
CanMoveUsers: _chkMove.Checked,
|
||||
CanAdminAccounts: _chkAdminAccounts.Checked,
|
||||
IsAdmin: _chkIsAdmin.Checked);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user