mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
Add dialogs for connection, channel creation, profile editing, and status management
- Implemented ConnectDialog for server connection and authentication. - Added CreateChannelDialog for creating new channels with name and topic. - Developed ProfileEditDialog for editing user profiles including display name, bio, and avatar. - Created ProfileViewDialog for viewing user profiles with options to edit or set status. - Introduced StatusDialog for setting user status and status message. - Added UpdateConfirmDialog for confirming updates and UpdateProgressDialog for showing update progress. - Removed obsolete hue_icon.ico file from the server.
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
using EchoHub.Client.Services;
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Drawing;
|
||||
using Terminal.Gui.ViewBase;
|
||||
using Terminal.Gui.Views;
|
||||
using Attribute = Terminal.Gui.Drawing.Attribute;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
public sealed class AudioPlayerDialog
|
||||
{
|
||||
// Block characters for wave animation (increasing height)
|
||||
private static readonly string[] WaveBlocks = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
||||
private const int WaveBarCount = 24;
|
||||
private const int AnimationIntervalMs = 150;
|
||||
|
||||
private static readonly Attribute WaveActiveAttr = new(new Color(180, 100, 255), Color.None);
|
||||
private static readonly Attribute WaveIdleAttr = new(new Color(80, 50, 120), Color.None);
|
||||
private static readonly Attribute FileNameAttr = new(new Color(180, 100, 255), Color.None);
|
||||
private static readonly Attribute StatusPlayingAttr = new(new Color(0, 200, 0), Color.None);
|
||||
private static readonly Attribute StatusPausedAttr = new(new Color(220, 180, 0), Color.None);
|
||||
private static readonly Attribute StatusStoppedAttr = new(new Color(160, 160, 160), Color.None);
|
||||
|
||||
public static void Show(IApplication app, AudioPlaybackService audioService, string filePath, string fileName)
|
||||
{
|
||||
var dialog = new Dialog { Title = "Audio Player", Width = 52, Height = 14 };
|
||||
|
||||
// ── File name ──
|
||||
var fileLabel = new Label
|
||||
{
|
||||
Text = $"\u266a {TruncateFileName(fileName, 44)}",
|
||||
X = 2,
|
||||
Y = 1,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
// ── Wave visualization ──
|
||||
var waveLabel = new Label
|
||||
{
|
||||
X = 2,
|
||||
Y = 3,
|
||||
Width = Dim.Fill(2),
|
||||
Height = 1
|
||||
};
|
||||
|
||||
// ── Status label ──
|
||||
var statusLabel = new Label
|
||||
{
|
||||
Text = "Stopped",
|
||||
X = 2,
|
||||
Y = 5,
|
||||
Width = 20
|
||||
};
|
||||
|
||||
// ── Volume controls ──
|
||||
var volumeHeaderLabel = new Label
|
||||
{
|
||||
Text = "Volume:",
|
||||
X = 2,
|
||||
Y = 7
|
||||
};
|
||||
|
||||
byte currentVolume = 50;
|
||||
var volumeBar = new ProgressBar
|
||||
{
|
||||
X = 14,
|
||||
Y = 7,
|
||||
Width = 20,
|
||||
Height = 1,
|
||||
Fraction = currentVolume / 100f,
|
||||
ProgressBarStyle = ProgressBarStyle.Continuous
|
||||
};
|
||||
|
||||
var volumePercentLabel = new Label
|
||||
{
|
||||
Text = $"{currentVolume}%",
|
||||
X = 35,
|
||||
Y = 7,
|
||||
Width = 5
|
||||
};
|
||||
|
||||
var volDownButton = new Button
|
||||
{
|
||||
Text = "-",
|
||||
X = 10,
|
||||
Y = 7,
|
||||
Width = 3
|
||||
};
|
||||
|
||||
var volUpButton = new Button
|
||||
{
|
||||
Text = "+",
|
||||
X = 41,
|
||||
Y = 7,
|
||||
Width = 3
|
||||
};
|
||||
|
||||
// ── Playback controls ──
|
||||
var playButton = new Button
|
||||
{
|
||||
Text = "\u25b6 Play",
|
||||
X = 2,
|
||||
Y = 10,
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
var stopButton = new Button
|
||||
{
|
||||
Text = "\u25a0 Stop",
|
||||
X = Pos.Right(playButton) + 2,
|
||||
Y = 10
|
||||
};
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Text = "Close",
|
||||
X = Pos.Right(stopButton) + 2,
|
||||
Y = 10
|
||||
};
|
||||
|
||||
// ── Animation state ──
|
||||
var animationOffset = 0;
|
||||
var random = new Random();
|
||||
// Pre-generate a repeating wave pattern
|
||||
var wavePattern = new int[WaveBarCount + 8];
|
||||
for (int i = 0; i < wavePattern.Length; i++)
|
||||
wavePattern[i] = random.Next(0, WaveBlocks.Length);
|
||||
|
||||
Timer? animationTimer = null;
|
||||
var isDisposed = false;
|
||||
|
||||
// ── Helper functions ──
|
||||
void UpdateWave(bool isActive)
|
||||
{
|
||||
if (isDisposed) return;
|
||||
|
||||
var bars = new string[WaveBarCount];
|
||||
for (int i = 0; i < WaveBarCount; i++)
|
||||
{
|
||||
if (isActive)
|
||||
{
|
||||
var idx = wavePattern[(i + animationOffset) % wavePattern.Length];
|
||||
bars[i] = WaveBlocks[idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
bars[i] = WaveBlocks[1]; // low idle bars
|
||||
}
|
||||
}
|
||||
waveLabel.Text = string.Join(" ", bars);
|
||||
}
|
||||
|
||||
void UpdateStatus()
|
||||
{
|
||||
if (isDisposed) return;
|
||||
|
||||
if (audioService.IsPlaying && !audioService.IsPaused)
|
||||
{
|
||||
statusLabel.Text = "Playing";
|
||||
playButton.Text = "\u23f8 Pause";
|
||||
}
|
||||
else if (audioService.IsPaused)
|
||||
{
|
||||
statusLabel.Text = "Paused";
|
||||
playButton.Text = "\u25b6 Resume";
|
||||
}
|
||||
else
|
||||
{
|
||||
statusLabel.Text = "Stopped";
|
||||
playButton.Text = "\u25b6 Play";
|
||||
}
|
||||
}
|
||||
|
||||
void StartAnimation()
|
||||
{
|
||||
animationTimer?.Dispose();
|
||||
animationTimer = new Timer(_ =>
|
||||
{
|
||||
if (isDisposed) return;
|
||||
animationOffset++;
|
||||
// Shuffle a few bars each tick for organic movement
|
||||
var idx = random.Next(0, wavePattern.Length);
|
||||
wavePattern[idx] = random.Next(0, WaveBlocks.Length);
|
||||
|
||||
app.Invoke(() =>
|
||||
{
|
||||
if (isDisposed) return;
|
||||
UpdateWave(true);
|
||||
});
|
||||
}, null, 0, AnimationIntervalMs);
|
||||
}
|
||||
|
||||
void StopAnimation()
|
||||
{
|
||||
animationTimer?.Dispose();
|
||||
animationTimer = null;
|
||||
if (!isDisposed)
|
||||
UpdateWave(false);
|
||||
}
|
||||
|
||||
async Task UpdateVolume(byte newVolume)
|
||||
{
|
||||
currentVolume = Math.Clamp(newVolume, (byte)0, (byte)100);
|
||||
await audioService.SetVolumeAsync(currentVolume);
|
||||
if (!isDisposed)
|
||||
{
|
||||
volumeBar.Fraction = currentVolume / 100f;
|
||||
volumePercentLabel.Text = $"{currentVolume}%";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Custom drawing for colored elements ──
|
||||
fileLabel.DrawingContent += (s, e) =>
|
||||
{
|
||||
var normalAttr = fileLabel.GetAttributeForRole(VisualRole.Normal);
|
||||
var resolvedAttr = FileNameAttr.Background == Color.None
|
||||
? FileNameAttr with { Background = normalAttr.Background }
|
||||
: FileNameAttr;
|
||||
fileLabel.SetAttribute(resolvedAttr);
|
||||
fileLabel.Move(0, 0);
|
||||
var text = fileLabel.Text ?? "";
|
||||
foreach (var g in Terminal.Gui.Drawing.GraphemeHelper.GetGraphemes(text))
|
||||
fileLabel.AddStr(g);
|
||||
// Fill remaining width
|
||||
var width = fileLabel.Viewport.Width;
|
||||
var textCols = Terminal.Gui.Text.StringExtensions.GetColumns(text);
|
||||
for (int i = textCols; i < width; i++)
|
||||
fileLabel.AddStr(" ");
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
waveLabel.DrawingContent += (s, e) =>
|
||||
{
|
||||
var normalAttr = waveLabel.GetAttributeForRole(VisualRole.Normal);
|
||||
var attr = (audioService.IsPlaying && !audioService.IsPaused) ? WaveActiveAttr : WaveIdleAttr;
|
||||
var resolvedAttr = attr.Background == Color.None
|
||||
? attr with { Background = normalAttr.Background }
|
||||
: attr;
|
||||
waveLabel.SetAttribute(resolvedAttr);
|
||||
waveLabel.Move(0, 0);
|
||||
var text = waveLabel.Text ?? "";
|
||||
foreach (var g in Terminal.Gui.Drawing.GraphemeHelper.GetGraphemes(text))
|
||||
waveLabel.AddStr(g);
|
||||
var width = waveLabel.Viewport.Width;
|
||||
var textCols = Terminal.Gui.Text.StringExtensions.GetColumns(text);
|
||||
for (int i = textCols; i < width; i++)
|
||||
waveLabel.AddStr(" ");
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
statusLabel.DrawingContent += (s, e) =>
|
||||
{
|
||||
var normalAttr = statusLabel.GetAttributeForRole(VisualRole.Normal);
|
||||
Attribute attr;
|
||||
if (audioService.IsPlaying && !audioService.IsPaused)
|
||||
attr = StatusPlayingAttr;
|
||||
else if (audioService.IsPaused)
|
||||
attr = StatusPausedAttr;
|
||||
else
|
||||
attr = StatusStoppedAttr;
|
||||
|
||||
var resolvedAttr = attr.Background == Color.None
|
||||
? attr with { Background = normalAttr.Background }
|
||||
: attr;
|
||||
statusLabel.SetAttribute(resolvedAttr);
|
||||
statusLabel.Move(0, 0);
|
||||
var text = statusLabel.Text ?? "";
|
||||
foreach (var g in Terminal.Gui.Drawing.GraphemeHelper.GetGraphemes(text))
|
||||
statusLabel.AddStr(g);
|
||||
var width = statusLabel.Viewport.Width;
|
||||
var textCols = Terminal.Gui.Text.StringExtensions.GetColumns(text);
|
||||
for (int i = textCols; i < width; i++)
|
||||
statusLabel.AddStr(" ");
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
// ── Event handlers ──
|
||||
playButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
if (audioService.IsPaused)
|
||||
{
|
||||
await audioService.ResumeAsync();
|
||||
app.Invoke(() =>
|
||||
{
|
||||
UpdateStatus();
|
||||
StartAnimation();
|
||||
});
|
||||
}
|
||||
else if (audioService.IsPlaying)
|
||||
{
|
||||
await audioService.PauseAsync();
|
||||
app.Invoke(() =>
|
||||
{
|
||||
UpdateStatus();
|
||||
StopAnimation();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await audioService.SetVolumeAsync(currentVolume);
|
||||
await audioService.PlayAsync(filePath);
|
||||
app.Invoke(() =>
|
||||
{
|
||||
UpdateStatus();
|
||||
StartAnimation();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
stopButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await audioService.StopAsync();
|
||||
app.Invoke(() =>
|
||||
{
|
||||
UpdateStatus();
|
||||
StopAnimation();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
closeButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
isDisposed = true;
|
||||
animationTimer?.Dispose();
|
||||
_ = audioService.StopAsync(); // fire-and-forget
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
volDownButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
var newVol = (byte)Math.Max(0, currentVolume - 10);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await UpdateVolume(newVol);
|
||||
app.Invoke(() =>
|
||||
{
|
||||
volumeBar.SetNeedsDraw();
|
||||
volumePercentLabel.SetNeedsDraw();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
volUpButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
var newVol = (byte)Math.Min(100, currentVolume + 10);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await UpdateVolume(newVol);
|
||||
app.Invoke(() =>
|
||||
{
|
||||
volumeBar.SetNeedsDraw();
|
||||
volumePercentLabel.SetNeedsDraw();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Handle playback finishing naturally
|
||||
EventHandler? finishedHandler = null;
|
||||
finishedHandler = (s, e) =>
|
||||
{
|
||||
app.Invoke(() =>
|
||||
{
|
||||
if (isDisposed) return;
|
||||
UpdateStatus();
|
||||
StopAnimation();
|
||||
});
|
||||
};
|
||||
audioService.PlaybackFinished += finishedHandler;
|
||||
|
||||
// ── Initial state ──
|
||||
UpdateWave(false);
|
||||
UpdateStatus();
|
||||
|
||||
dialog.Add(fileLabel, waveLabel, statusLabel,
|
||||
volumeHeaderLabel, volDownButton, volumeBar, volumePercentLabel, volUpButton,
|
||||
playButton, stopButton, closeButton);
|
||||
|
||||
playButton.SetFocus();
|
||||
app.Run(dialog);
|
||||
|
||||
// Cleanup
|
||||
isDisposed = true;
|
||||
animationTimer?.Dispose();
|
||||
audioService.PlaybackFinished -= finishedHandler;
|
||||
}
|
||||
|
||||
private static string TruncateFileName(string name, int maxLen)
|
||||
{
|
||||
if (name.Length <= maxLen)
|
||||
return name;
|
||||
|
||||
var ext = Path.GetExtension(name);
|
||||
var stem = Path.GetFileNameWithoutExtension(name);
|
||||
var available = maxLen - ext.Length - 3; // 3 for "..."
|
||||
if (available < 1)
|
||||
return name[..maxLen];
|
||||
|
||||
return stem[..available] + "..." + ext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using EchoHub.Client.Config;
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Result returned from the connect dialog.
|
||||
/// </summary>
|
||||
public record ConnectDialogResult(
|
||||
string ServerUrl, string Username, string Password,
|
||||
bool IsRegister, bool RememberMe, string? SavedRefreshToken);
|
||||
|
||||
/// <summary>
|
||||
/// A Terminal.Gui dialog for entering server connection and authentication details.
|
||||
/// Includes a saved servers selector when saved servers are available.
|
||||
/// </summary>
|
||||
public sealed class ConnectDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the connect dialog with an optional list of saved servers.
|
||||
/// Returns the result, or null if cancelled.
|
||||
/// </summary>
|
||||
public static ConnectDialogResult? Show(IApplication app, List<SavedServer>? savedServers = null)
|
||||
{
|
||||
ConnectDialogResult? result = null;
|
||||
savedServers ??= [];
|
||||
|
||||
var hasSavedServers = savedServers.Count > 0;
|
||||
var dialogHeight = hasSavedServers ? 22 : 18;
|
||||
|
||||
var dialog = new Dialog { Title = "Connect to Server", Width = 60, Height = dialogHeight };
|
||||
|
||||
int yOffset = 0;
|
||||
SavedServer? selectedSavedServer = null;
|
||||
|
||||
// -- Saved Servers section (if any) -----------------------------------
|
||||
ListView? savedServerList = null;
|
||||
if (hasSavedServers)
|
||||
{
|
||||
var savedLabel = new Label
|
||||
{
|
||||
Text = "Saved Servers:",
|
||||
X = 1,
|
||||
Y = 1
|
||||
};
|
||||
dialog.Add(savedLabel);
|
||||
|
||||
var serverDisplayNames = savedServers
|
||||
.Select(s =>
|
||||
{
|
||||
var session = !string.IsNullOrEmpty(s.RefreshToken) ? " [session]" : "";
|
||||
return $"{s.Name} ({s.Username ?? "?"}){session}";
|
||||
})
|
||||
.ToList();
|
||||
|
||||
savedServerList = new ListView
|
||||
{
|
||||
Source = new ListWrapper<string>(new ObservableCollection<string>(serverDisplayNames)),
|
||||
X = 1,
|
||||
Y = 2,
|
||||
Width = Dim.Fill(2),
|
||||
Height = 3
|
||||
};
|
||||
dialog.Add(savedServerList);
|
||||
|
||||
// Visual separator
|
||||
var separator = new Label
|
||||
{
|
||||
Text = new string('-', 56),
|
||||
X = 1,
|
||||
Y = 5
|
||||
};
|
||||
dialog.Add(separator);
|
||||
|
||||
yOffset = 5;
|
||||
}
|
||||
|
||||
// -- Manual entry fields ----------------------------------------------
|
||||
var urlLabel = new Label
|
||||
{
|
||||
Text = "Server URL:",
|
||||
X = 1,
|
||||
Y = yOffset + 1
|
||||
};
|
||||
var urlField = new TextField
|
||||
{
|
||||
Text = "http://localhost:5000",
|
||||
X = 15,
|
||||
Y = yOffset + 1,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
var userLabel = new Label
|
||||
{
|
||||
Text = "Username:",
|
||||
X = 1,
|
||||
Y = yOffset + 3
|
||||
};
|
||||
var userField = new TextField
|
||||
{
|
||||
Text = "",
|
||||
X = 15,
|
||||
Y = yOffset + 3,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
var passLabel = new Label
|
||||
{
|
||||
Text = "Password:",
|
||||
X = 1,
|
||||
Y = yOffset + 5
|
||||
};
|
||||
var passField = new TextField
|
||||
{
|
||||
Text = "",
|
||||
X = 15,
|
||||
Y = yOffset + 5,
|
||||
Width = Dim.Fill(2),
|
||||
Secret = true
|
||||
};
|
||||
|
||||
var tokenHintLabel = new Label
|
||||
{
|
||||
Text = "Session saved \u2014 password optional",
|
||||
X = 15,
|
||||
Y = yOffset + 6,
|
||||
Width = Dim.Fill(2),
|
||||
Visible = false
|
||||
};
|
||||
|
||||
var rememberMeCheckbox = new CheckBox
|
||||
{
|
||||
Text = "Remember me",
|
||||
X = 15,
|
||||
Y = yOffset + 7,
|
||||
Value = CheckState.UnChecked
|
||||
};
|
||||
|
||||
var displayLabel = new Label
|
||||
{
|
||||
Text = "Display Name:",
|
||||
X = 1,
|
||||
Y = yOffset + 9
|
||||
};
|
||||
var displayField = new TextField
|
||||
{
|
||||
Text = "",
|
||||
X = 15,
|
||||
Y = yOffset + 9,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
var loginButton = new Button
|
||||
{
|
||||
Text = "Login",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 20,
|
||||
Y = yOffset + 11
|
||||
};
|
||||
|
||||
var registerButton = new Button
|
||||
{
|
||||
Text = "Register",
|
||||
X = Pos.Center() - 5,
|
||||
Y = yOffset + 11
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 10,
|
||||
Y = yOffset + 11
|
||||
};
|
||||
|
||||
// Wire saved server selection to auto-fill fields
|
||||
if (savedServerList is not null && savedServers.Count > 0)
|
||||
{
|
||||
savedServerList.ValueChanged += (sender, e) =>
|
||||
{
|
||||
var index = e.NewValue;
|
||||
if (index.HasValue && index.Value >= 0 && index.Value < savedServers.Count)
|
||||
{
|
||||
selectedSavedServer = savedServers[index.Value];
|
||||
urlField.Text = selectedSavedServer.Url;
|
||||
userField.Text = selectedSavedServer.Username ?? "";
|
||||
rememberMeCheckbox.Value = selectedSavedServer.RememberMe
|
||||
? CheckState.Checked : CheckState.UnChecked;
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedSavedServer.RefreshToken))
|
||||
{
|
||||
passField.Text = "";
|
||||
tokenHintLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
tokenHintLabel.Visible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-fill with the first saved server
|
||||
selectedSavedServer = savedServers[0];
|
||||
urlField.Text = savedServers[0].Url;
|
||||
userField.Text = savedServers[0].Username ?? "";
|
||||
rememberMeCheckbox.Value = savedServers[0].RememberMe
|
||||
? CheckState.Checked : CheckState.UnChecked;
|
||||
if (!string.IsNullOrEmpty(savedServers[0].RefreshToken))
|
||||
tokenHintLabel.Visible = true;
|
||||
}
|
||||
|
||||
loginButton.Accepting += (s, e) =>
|
||||
{
|
||||
var url = urlField.Text?.Trim() ?? string.Empty;
|
||||
var user = userField.Text?.Trim() ?? string.Empty;
|
||||
var pass = passField.Text ?? string.Empty;
|
||||
var rememberMe = rememberMeCheckbox.Value == CheckState.Checked;
|
||||
|
||||
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user))
|
||||
{
|
||||
MessageBox.ErrorQuery(app, "Validation", "Server URL and username are required.", "OK");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if we can use a saved token
|
||||
string? savedRefreshToken = null;
|
||||
if (string.IsNullOrEmpty(pass)
|
||||
&& selectedSavedServer is not null
|
||||
&& !string.IsNullOrEmpty(selectedSavedServer.RefreshToken)
|
||||
&& string.Equals(selectedSavedServer.Url, url, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(selectedSavedServer.Username, user, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
savedRefreshToken = selectedSavedServer.RefreshToken;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(pass) && savedRefreshToken is null)
|
||||
{
|
||||
MessageBox.ErrorQuery(app, "Validation", "Password is required.", "OK");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
result = new ConnectDialogResult(url, user, pass, IsRegister: false, rememberMe, savedRefreshToken);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
registerButton.Accepting += (s, e) =>
|
||||
{
|
||||
var url = urlField.Text?.Trim() ?? string.Empty;
|
||||
var user = userField.Text?.Trim() ?? string.Empty;
|
||||
var pass = passField.Text ?? string.Empty;
|
||||
var rememberMe = rememberMeCheckbox.Value == CheckState.Checked;
|
||||
|
||||
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
|
||||
{
|
||||
MessageBox.ErrorQuery(app, "Validation", "Server URL, username, and password are required.", "OK");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
result = new ConnectDialogResult(url, user, pass, IsRegister: true, rememberMe, SavedRefreshToken: null);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
cancelButton.Accepting += (s, e) =>
|
||||
{
|
||||
result = null;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(urlLabel, urlField, userLabel, userField, passLabel, passField,
|
||||
tokenHintLabel, rememberMeCheckbox, displayLabel, displayField,
|
||||
loginButton, registerButton, cancelButton);
|
||||
|
||||
if (hasSavedServers && savedServerList is not null)
|
||||
savedServerList.SetFocus();
|
||||
else
|
||||
urlField.SetFocus();
|
||||
|
||||
app.Run(dialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
public record CreateChannelResult(string Name, string? Topic, bool IsPublic);
|
||||
|
||||
public sealed class CreateChannelDialog
|
||||
{
|
||||
public static CreateChannelResult? Show(IApplication app)
|
||||
{
|
||||
CreateChannelResult? result = null;
|
||||
|
||||
var dialog = new Dialog { Title = "Create Channel", Width = 50, Height = 14 };
|
||||
|
||||
var nameLabel = new Label { Text = "Name:", X = 1, Y = 1 };
|
||||
var nameField = new TextField { X = 10, Y = 1, Width = Dim.Fill(2) };
|
||||
|
||||
var topicLabel = new Label { Text = "Topic:", X = 1, Y = 3 };
|
||||
var topicField = new TextField { X = 10, Y = 3, Width = Dim.Fill(2) };
|
||||
|
||||
var publicCheckbox = new CheckBox
|
||||
{
|
||||
Text = "Public (visible to all users)",
|
||||
X = 1,
|
||||
Y = 5,
|
||||
Value = CheckState.Checked
|
||||
};
|
||||
|
||||
var hintLabel = new Label
|
||||
{
|
||||
Text = "Lowercase letters, digits, hyphens, underscores (2-100 chars)",
|
||||
X = 1,
|
||||
Y = 7,
|
||||
};
|
||||
|
||||
var createButton = new Button
|
||||
{
|
||||
Text = "Create",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 10,
|
||||
Y = 9
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 5,
|
||||
Y = 9
|
||||
};
|
||||
|
||||
createButton.Accepting += (s, e) =>
|
||||
{
|
||||
var name = nameField.Text?.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.ErrorQuery(app, "Error", "Channel name is required.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
var topic = topicField.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(topic))
|
||||
topic = null;
|
||||
|
||||
var isPublic = publicCheckbox.Value == CheckState.Checked;
|
||||
result = new CreateChannelResult(name, topic, isPublic);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
cancelButton.Accepting += (s, e) =>
|
||||
{
|
||||
result = null;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(nameLabel, nameField, topicLabel, topicField, publicCheckbox, hintLabel, createButton, cancelButton);
|
||||
|
||||
nameField.SetFocus();
|
||||
app.Run(dialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
using Terminal.Gui.Drawing;
|
||||
using Attribute = Terminal.Gui.Drawing.Attribute;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Result returned from the profile edit dialog.
|
||||
/// </summary>
|
||||
public record ProfileEditResult(string? DisplayName, string? Bio, string? NicknameColor, string? AvatarPath, bool? NotificationSoundEnabled, byte? NotificationVolume);
|
||||
|
||||
/// <summary>
|
||||
/// A Terminal.Gui dialog for editing the user's profile (display name, bio, nickname color).
|
||||
/// </summary>
|
||||
public sealed class ProfileEditDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the profile edit dialog and returns the result, or null if cancelled.
|
||||
/// </summary>
|
||||
public static ProfileEditResult? Show(IApplication app, string? currentDisplayName, string? currentBio, string? currentColor, bool notificationSoundEnabled = false, byte notificationVolume = 30)
|
||||
{
|
||||
ProfileEditResult? result = null;
|
||||
|
||||
var dialog = new Dialog { Title = "Edit Profile", Width = 60, Height = 26 };
|
||||
|
||||
// Display Name
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = "Display Name:",
|
||||
X = 1,
|
||||
Y = 1
|
||||
};
|
||||
var nameField = new TextField
|
||||
{
|
||||
Text = currentDisplayName ?? "",
|
||||
X = 17,
|
||||
Y = 1,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
// Bio
|
||||
var bioLabel = new Label
|
||||
{
|
||||
Text = "Bio:",
|
||||
X = 1,
|
||||
Y = 3
|
||||
};
|
||||
var bioField = new TextField
|
||||
{
|
||||
Text = currentBio ?? "",
|
||||
X = 17,
|
||||
Y = 3,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
// Nickname Color
|
||||
var colorLabel = new Label
|
||||
{
|
||||
Text = "Nickname Color:",
|
||||
X = 1,
|
||||
Y = 5
|
||||
};
|
||||
var colorField = new TextField
|
||||
{
|
||||
Text = currentColor ?? "",
|
||||
X = 17,
|
||||
Y = 5,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
var colorHintLabel = new Label
|
||||
{
|
||||
Text = "(hex e.g. #FF5733)",
|
||||
X = 17,
|
||||
Y = 6
|
||||
};
|
||||
colorHintLabel.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.DarkGray, Color.Blue)
|
||||
});
|
||||
|
||||
// Color Preview
|
||||
var previewLabel = new Label
|
||||
{
|
||||
Text = "Preview:",
|
||||
X = 1,
|
||||
Y = 8
|
||||
};
|
||||
var colorPreview = new Label
|
||||
{
|
||||
Text = "\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
X = 17,
|
||||
Y = 8
|
||||
};
|
||||
|
||||
UpdateColorPreview(colorPreview, colorField.Text);
|
||||
|
||||
colorField.TextChanged += (sender, e) =>
|
||||
{
|
||||
UpdateColorPreview(colorPreview, colorField.Text);
|
||||
};
|
||||
|
||||
// Avatar
|
||||
var avatarLabel = new Label
|
||||
{
|
||||
Text = "Avatar:",
|
||||
X = 1,
|
||||
Y = 10
|
||||
};
|
||||
var avatarField = new TextField
|
||||
{
|
||||
Text = "",
|
||||
X = 17,
|
||||
Y = 10,
|
||||
Width = Dim.Fill(12)
|
||||
};
|
||||
var browseButton = new Button
|
||||
{
|
||||
Text = "Browse",
|
||||
X = Pos.AnchorEnd(10),
|
||||
Y = 10
|
||||
};
|
||||
var avatarHintLabel = new Label
|
||||
{
|
||||
Text = "(file path or URL)",
|
||||
X = 17,
|
||||
Y = 11
|
||||
};
|
||||
avatarHintLabel.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.DarkGray, Color.Blue)
|
||||
});
|
||||
|
||||
browseButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
var openDialog = new OpenDialog
|
||||
{
|
||||
Title = "Select Avatar Image",
|
||||
OpenMode = OpenMode.File,
|
||||
};
|
||||
app.Run(openDialog);
|
||||
if (openDialog.FilePaths.Count > 0)
|
||||
{
|
||||
avatarField.Text = openDialog.FilePaths[0];
|
||||
}
|
||||
};
|
||||
|
||||
// Notification Sound
|
||||
var notifCheckbox = new CheckBox
|
||||
{
|
||||
Text = "Notification sound on @mention",
|
||||
X = 1,
|
||||
Y = 13,
|
||||
Value = notificationSoundEnabled ? CheckState.Checked : CheckState.UnChecked
|
||||
};
|
||||
|
||||
var volumeLabel = new Label
|
||||
{
|
||||
Text = "Volume:",
|
||||
X = 1,
|
||||
Y = 15
|
||||
};
|
||||
var volumeField = new TextField
|
||||
{
|
||||
Text = notificationVolume.ToString(),
|
||||
X = 17,
|
||||
Y = 15,
|
||||
Width = 6
|
||||
};
|
||||
var volumeHintLabel = new Label
|
||||
{
|
||||
Text = "(0-100)",
|
||||
X = 24,
|
||||
Y = 15
|
||||
};
|
||||
volumeHintLabel.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.DarkGray, Color.Blue)
|
||||
});
|
||||
|
||||
// Buttons
|
||||
var saveButton = new Button
|
||||
{
|
||||
Text = "Save",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 10,
|
||||
Y = 18
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 5,
|
||||
Y = 18
|
||||
};
|
||||
|
||||
saveButton.Accepting += (s, e) =>
|
||||
{
|
||||
var displayName = NullIfEmpty(nameField.Text?.Trim());
|
||||
var bio = NullIfEmpty(bioField.Text?.Trim());
|
||||
var nicknameColor = NullIfEmpty(colorField.Text?.Trim());
|
||||
var avatarPath = NullIfEmpty(avatarField.Text?.Trim());
|
||||
|
||||
byte? volume = byte.TryParse(volumeField.Text, out var v) ? Math.Min(v, (byte)100) : null;
|
||||
result = new ProfileEditResult(displayName, bio, nicknameColor, avatarPath, notifCheckbox.Value == CheckState.Checked, volume);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
cancelButton.Accepting += (s, e) =>
|
||||
{
|
||||
result = null;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(nameLabel, nameField, bioLabel, bioField, colorLabel, colorField,
|
||||
colorHintLabel, previewLabel, colorPreview,
|
||||
avatarLabel, avatarField, browseButton, avatarHintLabel,
|
||||
notifCheckbox, volumeLabel, volumeField, volumeHintLabel,
|
||||
saveButton, cancelButton);
|
||||
|
||||
nameField.SetFocus();
|
||||
app.Run(dialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a hex color string and update the preview label color.
|
||||
/// </summary>
|
||||
private static void UpdateColorPreview(Label preview, string? hexColor)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hexColor))
|
||||
{
|
||||
preview.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.White, Color.Blue)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var color = ParseHexToTrueColor(hexColor.Trim());
|
||||
preview.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(color, Color.Blue)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a hex color string to a Terminal.Gui TrueColor Color.
|
||||
/// V2 supports TrueColor via new Color(r, g, b).
|
||||
/// </summary>
|
||||
private static Color ParseHexToTrueColor(string hex)
|
||||
{
|
||||
if (hex.StartsWith('#'))
|
||||
hex = hex[1..];
|
||||
|
||||
if (hex.Length != 6 || !int.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out var rgb))
|
||||
return Color.White;
|
||||
|
||||
int r = (rgb >> 16) & 0xFF;
|
||||
int g = (rgb >> 8) & 0xFF;
|
||||
int b = rgb & 0xFF;
|
||||
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
|
||||
private static string? NullIfEmpty(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
using Terminal.Gui.Drawing;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using Attribute = Terminal.Gui.Drawing.Attribute;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Action selected by the user in their own profile dialog.
|
||||
/// </summary>
|
||||
public enum ProfileAction
|
||||
{
|
||||
Close,
|
||||
EditProfile,
|
||||
SetStatus
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dialog for viewing a user's server profile.
|
||||
/// Shows Edit Profile / Set Status buttons when viewing own profile.
|
||||
/// </summary>
|
||||
public sealed class ProfileViewDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Show a read-only profile view for another user.
|
||||
/// </summary>
|
||||
public static void Show(IApplication app, UserProfileDto? profile)
|
||||
{
|
||||
ShowInternal(app, profile, isOwnProfile: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the profile view for the current user with action buttons.
|
||||
/// Returns the action the user selected.
|
||||
/// </summary>
|
||||
public static ProfileAction ShowOwn(
|
||||
IApplication app,
|
||||
UserProfileDto? profile,
|
||||
UserStatus currentStatus,
|
||||
string? currentStatusMessage)
|
||||
{
|
||||
return ShowInternal(app, profile, isOwnProfile: true, currentStatus, currentStatusMessage);
|
||||
}
|
||||
|
||||
private static ProfileAction ShowInternal(
|
||||
IApplication app,
|
||||
UserProfileDto? profile,
|
||||
bool isOwnProfile,
|
||||
UserStatus? currentStatus = null,
|
||||
string? currentStatusMessage = null)
|
||||
{
|
||||
if (profile is null)
|
||||
{
|
||||
MessageBox.ErrorQuery(app, "Profile", "User not found.", "OK");
|
||||
return ProfileAction.Close;
|
||||
}
|
||||
|
||||
var action = ProfileAction.Close;
|
||||
|
||||
var dialog = new Dialog
|
||||
{
|
||||
Title = isOwnProfile ? "My Profile" : $"Profile \u2014 {profile.Username}",
|
||||
Width = 50,
|
||||
Height = 20
|
||||
};
|
||||
|
||||
int row = 0;
|
||||
|
||||
// Username
|
||||
var usernameLabel = new Label { Text = "Username:", X = 1, Y = row };
|
||||
var usernameValue = new Label { Text = profile.Username, X = 14, Y = row };
|
||||
usernameValue.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.BrightYellow, Color.Blue)
|
||||
});
|
||||
dialog.Add(usernameLabel, usernameValue);
|
||||
row++;
|
||||
|
||||
// Display Name
|
||||
var nameLabel = new Label { Text = "Name:", X = 1, Y = row };
|
||||
var nameValue = new Label { Text = profile.DisplayName ?? "-", X = 14, Y = row };
|
||||
dialog.Add(nameLabel, nameValue);
|
||||
row++;
|
||||
|
||||
// Status — use live status for own profile, stored status for others
|
||||
var displayStatus = isOwnProfile && currentStatus.HasValue ? currentStatus.Value : profile.Status;
|
||||
var displayStatusMsg = isOwnProfile ? currentStatusMessage : profile.StatusMessage;
|
||||
|
||||
var statusLabel = new Label { Text = "Status:", X = 1, Y = row };
|
||||
var statusText = FormatStatus(displayStatus);
|
||||
var statusValue = new Label { Text = statusText, X = 14, Y = row };
|
||||
statusValue.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(GetStatusColor(displayStatus), Color.Blue)
|
||||
});
|
||||
dialog.Add(statusLabel, statusValue);
|
||||
row++;
|
||||
|
||||
// Status Message
|
||||
if (!string.IsNullOrWhiteSpace(displayStatusMsg))
|
||||
{
|
||||
var msgLabel = new Label { Text = "Message:", X = 1, Y = row };
|
||||
var msgValue = new Label { Text = displayStatusMsg, X = 14, Y = row, Width = Dim.Fill(2) };
|
||||
dialog.Add(msgLabel, msgValue);
|
||||
row++;
|
||||
}
|
||||
|
||||
// Color
|
||||
var colorLabel = new Label { Text = "Color:", X = 1, Y = row };
|
||||
var colorValue = new Label { Text = profile.NicknameColor ?? "-", X = 14, Y = row };
|
||||
if (ColorHelper.ParseHexColor(profile.NicknameColor) is { } colorAttr)
|
||||
colorValue.SetScheme(new Scheme { Normal = colorAttr });
|
||||
dialog.Add(colorLabel, colorValue);
|
||||
row++;
|
||||
|
||||
// Bio
|
||||
row++;
|
||||
var bioLabel = new Label { Text = "Bio:", X = 1, Y = row };
|
||||
dialog.Add(bioLabel);
|
||||
row++;
|
||||
|
||||
var bioView = new TextView
|
||||
{
|
||||
X = 1,
|
||||
Y = row,
|
||||
Width = Dim.Fill(2),
|
||||
Height = 3,
|
||||
Text = profile.Bio ?? "-",
|
||||
ReadOnly = true,
|
||||
WordWrap = true
|
||||
};
|
||||
bioView.SetScheme(new Scheme
|
||||
{
|
||||
Normal = new Attribute(Color.White, Color.DarkGray),
|
||||
Focus = new Attribute(Color.White, Color.DarkGray)
|
||||
});
|
||||
dialog.Add(bioView);
|
||||
row += 3;
|
||||
|
||||
// ASCII Avatar — render with color tags
|
||||
if (!string.IsNullOrWhiteSpace(profile.AvatarAscii))
|
||||
{
|
||||
row++;
|
||||
var rawLines = profile.AvatarAscii.Split('\n');
|
||||
var avatarSource = new ChatListSource();
|
||||
foreach (var line in rawLines)
|
||||
{
|
||||
avatarSource.Add(ChatLine.HasColorTags(line)
|
||||
? ChatLine.FromColoredText(line)
|
||||
: new ChatLine(line));
|
||||
}
|
||||
|
||||
var avatarHeight = Math.Min(rawLines.Length + 2, 24);
|
||||
var avatarFrame = new FrameView
|
||||
{
|
||||
Title = "Avatar",
|
||||
X = 1,
|
||||
Y = row,
|
||||
Width = Dim.Fill(2),
|
||||
Height = avatarHeight
|
||||
};
|
||||
var avatarList = new ListView
|
||||
{
|
||||
X = 0,
|
||||
Y = 0,
|
||||
Width = Dim.Fill(),
|
||||
Height = Dim.Fill(),
|
||||
Source = avatarSource
|
||||
};
|
||||
avatarFrame.Add(avatarList);
|
||||
dialog.Add(avatarFrame);
|
||||
|
||||
// Grow dialog to fit avatar + widen for art
|
||||
var artWidth = rawLines.Max(l => ChatLine.HasColorTags(l)
|
||||
? ChatLine.FromColoredText(l).TextLength
|
||||
: l.Length);
|
||||
dialog.Width = Math.Max(50, artWidth + 6);
|
||||
dialog.Height = row + avatarHeight + 4;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
if (isOwnProfile)
|
||||
{
|
||||
var editButton = new Button
|
||||
{
|
||||
Text = "Edit Profile",
|
||||
X = Pos.Center() - 20,
|
||||
Y = Pos.AnchorEnd(2)
|
||||
};
|
||||
editButton.Accepting += (s, e) =>
|
||||
{
|
||||
action = ProfileAction.EditProfile;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
var statusButton = new Button
|
||||
{
|
||||
Text = "Set Status",
|
||||
X = Pos.Center() - 4,
|
||||
Y = Pos.AnchorEnd(2)
|
||||
};
|
||||
statusButton.Accepting += (s, e) =>
|
||||
{
|
||||
action = ProfileAction.SetStatus;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Text = "Close",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() + 13,
|
||||
Y = Pos.AnchorEnd(2)
|
||||
};
|
||||
closeButton.Accepting += (s, e) =>
|
||||
{
|
||||
action = ProfileAction.Close;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(editButton, statusButton, closeButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
var closeButton = new Button
|
||||
{
|
||||
Text = "Close",
|
||||
IsDefault = true,
|
||||
X = Pos.Center(),
|
||||
Y = Pos.AnchorEnd(2)
|
||||
};
|
||||
closeButton.Accepting += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
dialog.Add(closeButton);
|
||||
}
|
||||
|
||||
app.Run(dialog);
|
||||
return action;
|
||||
}
|
||||
|
||||
private static string FormatStatus(UserStatus status) => status switch
|
||||
{
|
||||
UserStatus.Online => "\u25cf Online",
|
||||
UserStatus.Away => "\u25cf Away",
|
||||
UserStatus.DoNotDisturb => "\u25cf Do Not Disturb",
|
||||
UserStatus.Invisible => "\u25cb Invisible",
|
||||
_ => "\u25cf Unknown"
|
||||
};
|
||||
|
||||
private static Color GetStatusColor(UserStatus status) => status switch
|
||||
{
|
||||
UserStatus.Online => Color.BrightGreen,
|
||||
UserStatus.Away => Color.BrightYellow,
|
||||
UserStatus.DoNotDisturb => Color.BrightRed,
|
||||
UserStatus.Invisible => Color.Gray,
|
||||
_ => Color.White
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
using EchoHub.Core.Models;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Result returned from the status dialog.
|
||||
/// </summary>
|
||||
public record StatusDialogResult(UserStatus Status, string? StatusMessage);
|
||||
|
||||
/// <summary>
|
||||
/// A Terminal.Gui dialog for setting the user's status and status message.
|
||||
/// </summary>
|
||||
public sealed class StatusDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the status dialog and returns the result, or null if cancelled.
|
||||
/// </summary>
|
||||
public static StatusDialogResult? Show(IApplication app, UserStatus currentStatus, string? currentMessage)
|
||||
{
|
||||
StatusDialogResult? result = null;
|
||||
|
||||
var dialog = new Dialog { Title = "Set Status", Width = 50, Height = 12 };
|
||||
|
||||
var statusLabel = new Label
|
||||
{
|
||||
Text = "Status:",
|
||||
X = 1,
|
||||
Y = 1
|
||||
};
|
||||
|
||||
var optionSelector = new OptionSelector<UserStatus>
|
||||
{
|
||||
X = 12,
|
||||
Y = 1
|
||||
};
|
||||
optionSelector.Value = currentStatus;
|
||||
|
||||
var messageLabel = new Label
|
||||
{
|
||||
Text = "Message:",
|
||||
X = 1,
|
||||
Y = 6
|
||||
};
|
||||
var messageField = new TextField
|
||||
{
|
||||
Text = currentMessage ?? "",
|
||||
X = 12,
|
||||
Y = 6,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
var saveButton = new Button
|
||||
{
|
||||
Text = "Save",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 10,
|
||||
Y = 8
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 5,
|
||||
Y = 8
|
||||
};
|
||||
|
||||
saveButton.Accepting += (s, e) =>
|
||||
{
|
||||
var status = optionSelector.Value ?? UserStatus.Online;
|
||||
var message = messageField.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
message = null;
|
||||
|
||||
result = new StatusDialogResult(status, message);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
cancelButton.Accepting += (s, e) =>
|
||||
{
|
||||
result = null;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(statusLabel, optionSelector, messageLabel, messageField, saveButton, cancelButton);
|
||||
|
||||
optionSelector.SetFocus();
|
||||
app.Run(dialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
public sealed class UpdateConfirmDialog
|
||||
{
|
||||
public static bool Show(IApplication app, string currentVersion, string newVersion)
|
||||
{
|
||||
var confirmed = false;
|
||||
|
||||
var dialog = new Dialog { Title = "Update Available", Width = 50, Height = 10 };
|
||||
|
||||
var messageLabel = new Label
|
||||
{
|
||||
Text = $"A new version of EchoHub is available.\n\n Current: {currentVersion}\n Latest: {newVersion}",
|
||||
X = 1,
|
||||
Y = 1,
|
||||
Width = Dim.Fill(2),
|
||||
Height = 4
|
||||
};
|
||||
|
||||
var updateButton = new Button
|
||||
{
|
||||
Text = "Update",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 10,
|
||||
Y = 6
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 5,
|
||||
Y = 6
|
||||
};
|
||||
|
||||
updateButton.Accepting += (s, e) =>
|
||||
{
|
||||
confirmed = true;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
cancelButton.Accepting += (s, e) =>
|
||||
{
|
||||
confirmed = false;
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(messageLabel, updateButton, cancelButton);
|
||||
app.Run(dialog);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
public sealed class UpdateProgressDialog
|
||||
{
|
||||
private readonly Dialog _dialog;
|
||||
private readonly ProgressBar _progressBar;
|
||||
private readonly Label _infoLabel;
|
||||
private readonly IApplication _app;
|
||||
|
||||
public UpdateProgressDialog(IApplication app, string newVersion)
|
||||
{
|
||||
_app = app;
|
||||
|
||||
_dialog = new Dialog { Title = $"Updating to {newVersion}", Width = 50, Height = 10 };
|
||||
|
||||
_infoLabel = new Label
|
||||
{
|
||||
Text = "Preparing update...",
|
||||
X = 1,
|
||||
Y = 1,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
_progressBar = new ProgressBar
|
||||
{
|
||||
X = 1,
|
||||
Y = 3,
|
||||
Width = Dim.Fill(2),
|
||||
Fraction = 0f
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center(),
|
||||
Y = 6
|
||||
};
|
||||
|
||||
_dialog.Add(_infoLabel, _progressBar);
|
||||
}
|
||||
|
||||
public void UpdateProgress(float fraction, string statusText)
|
||||
{
|
||||
_progressBar.Fraction = fraction;
|
||||
_infoLabel.Text = statusText;
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
_app.Run(_dialog);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_app.RequestStop();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user