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.Dialogs; 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 = 12 }; // ── 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 volDownButton = new Button { Text = "-", X = 10, Y = 7, Width = 1, Height = 1, NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None }; var volumeBar = new ProgressBar { X = 12, Y = 7, Width = 22, Height = 1, Fraction = currentVolume / 100f, ProgressBarStyle = ProgressBarStyle.Continuous }; var volUpButton = new Button { Text = "+", X = 35, Y = 7, Width = 1, Height = 1, NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None }; var volumePercentLabel = new Label { Text = $"{currentVolume}%", X = 37, Y = 7, Width = 5 }; // ── Playback controls ── var playButton = new Button { Text = "\u25b6 Play", X = 2, Y = 9, IsDefault = true, ShadowStyle = ShadowStyle.None }; var stopButton = new Button { Text = "\u25a0 Stop", X = Pos.Right(playButton) + 1, Y = 9, ShadowStyle = ShadowStyle.None }; var closeButton = new Button { Text = "Close", X = Pos.Right(stopButton) + 1, Y = 9, ShadowStyle = ShadowStyle.None }; // ── 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; var isBusy = 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; if (isBusy) return; isBusy = true; Task.Run(async () => { if (audioService.IsPaused) { await audioService.ResumeAsync(); app.Invoke(() => { UpdateStatus(); StartAnimation(); isBusy = false; }); } else if (audioService.IsPlaying) { await audioService.PauseAsync(); app.Invoke(() => { UpdateStatus(); StopAnimation(); isBusy = false; }); } else { await audioService.SetVolumeAsync(currentVolume); await audioService.PlayAsync(filePath); app.Invoke(() => { UpdateStatus(); StartAnimation(); isBusy = false; }); } }); }; stopButton.Accepting += (s, e) => { e.Handled = true; if (isBusy) return; isBusy = true; Task.Run(async () => { await audioService.StopAsync(); app.Invoke(() => { UpdateStatus(); StopAnimation(); isBusy = false; }); }); }; 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; if (isBusy) return; isBusy = true; var newVol = (byte)Math.Max(0, currentVolume - 10); Task.Run(async () => { await UpdateVolume(newVol); app.Invoke(() => { volumeBar.SetNeedsDraw(); volumePercentLabel.SetNeedsDraw(); isBusy = false; }); }); }; volUpButton.Accepting += (s, e) => { e.Handled = true; if (isBusy) return; isBusy = true; var newVol = (byte)Math.Min(100, currentVolume + 10); Task.Run(async () => { await UpdateVolume(newVol); app.Invoke(() => { volumeBar.SetNeedsDraw(); volumePercentLabel.SetNeedsDraw(); isBusy = false; }); }); }; // 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; } }