mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: add audio playback and file download features with corresponding UI updates
This commit is contained in:
@@ -22,6 +22,7 @@ public sealed class AppOrchestrator : IDisposable
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly CommandHandler _commandHandler;
|
||||
private readonly NotificationSoundService _notificationSound;
|
||||
private readonly AudioPlaybackService _audioPlayback = new();
|
||||
|
||||
private EchoHubConnection? _connection;
|
||||
private ApiClient? _apiClient;
|
||||
@@ -80,6 +81,8 @@ public sealed class AppOrchestrator : IDisposable
|
||||
_mainWindow.OnSavedServersRequested += HandleSavedServersRequested;
|
||||
_mainWindow.OnCreateChannelRequested += HandleCreateChannelRequested;
|
||||
_mainWindow.OnDeleteChannelRequested += HandleDeleteChannelRequested;
|
||||
_mainWindow.OnAudioPlayRequested += HandleAudioPlayRequested;
|
||||
_mainWindow.OnFileDownloadRequested += HandleFileDownloadRequested;
|
||||
}
|
||||
|
||||
// ── Command Handler Wiring ─────────────────────────────────────────────
|
||||
@@ -798,6 +801,40 @@ public sealed class AppOrchestrator : IDisposable
|
||||
}, "Failed to delete channel");
|
||||
}
|
||||
|
||||
private void HandleAudioPlayRequested(string attachmentUrl, string fileName)
|
||||
{
|
||||
if (!IsAuthenticated) return;
|
||||
|
||||
RunAsync(async () =>
|
||||
{
|
||||
InvokeUI(() => _mainWindow.AddSystemMessage(_mainWindow.CurrentChannel, $"Playing {fileName}..."));
|
||||
var tempPath = await _apiClient!.DownloadFileToTempAsync(attachmentUrl, fileName);
|
||||
await _audioPlayback.PlayAsync(tempPath);
|
||||
}, "Failed to play audio");
|
||||
}
|
||||
|
||||
private void HandleFileDownloadRequested(string attachmentUrl, string fileName)
|
||||
{
|
||||
if (!IsAuthenticated) return;
|
||||
|
||||
RunAsync(async () =>
|
||||
{
|
||||
InvokeUI(() => _mainWindow.AddSystemMessage(_mainWindow.CurrentChannel, $"Downloading {fileName}..."));
|
||||
var tempPath = await _apiClient!.DownloadFileToTempAsync(attachmentUrl, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo(tempPath) { UseShellExecute = true };
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to open file with default app: {Path}", tempPath);
|
||||
InvokeUI(() => _mainWindow.AddSystemMessage(_mainWindow.CurrentChannel, $"Downloaded to: {tempPath}"));
|
||||
}
|
||||
}, "Failed to download file");
|
||||
}
|
||||
|
||||
// ── Connection Event Wiring ────────────────────────────────────────────
|
||||
|
||||
private void WireConnectionEvents(EchoHubConnection connection)
|
||||
|
||||
@@ -339,7 +339,7 @@ public class CommandHandler
|
||||
/nick <name> - Set display name
|
||||
/color <#hex> - Set nickname color
|
||||
/theme <name> - Switch theme
|
||||
/send <filepath or URL> [-s|-m|-l] - Send a file or image (size: small/medium/large)
|
||||
/send <filepath or URL> [-s|-m|-l] - Send file/image/audio (size flag for images)
|
||||
/avatar <URL or filepath> - Set your avatar
|
||||
/profile [username] - View a profile
|
||||
/servers - Open saved servers
|
||||
|
||||
@@ -196,6 +196,23 @@ public sealed class ApiClient : IDisposable
|
||||
return await response.Content.ReadFromJsonAsync<MessageDto>();
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileToTempAsync(string relativeUrl, string fileName)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var response = await AuthenticatedGetAsync(relativeUrl);
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "EchoHub");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempPath = Path.Combine(tempDir, $"{Guid.NewGuid():N}_{fileName}");
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
await using var file = File.Create(tempPath);
|
||||
await stream.CopyToAsync(file);
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
public async Task<ChannelDto?> CreateChannelAsync(string name, string? topic = null, bool isPublic = true)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using NetCoreAudio;
|
||||
using Serilog;
|
||||
|
||||
namespace EchoHub.Client.Services;
|
||||
|
||||
public class AudioPlaybackService
|
||||
{
|
||||
private readonly Player _player = new();
|
||||
|
||||
public bool IsPlaying => _player.Playing;
|
||||
|
||||
public async Task PlayAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_player.Playing)
|
||||
await _player.Stop();
|
||||
|
||||
await _player.Play(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to play audio file: {Path}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_player.Playing)
|
||||
await _player.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to stop audio playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Text.RegularExpressions;
|
||||
using EchoHub.Core.Models;
|
||||
using Terminal.Gui.Drawing;
|
||||
using Terminal.Gui.Text;
|
||||
using Terminal.Gui.Views;
|
||||
@@ -22,6 +23,9 @@ public partial class ChatLine
|
||||
public int TextLength { get; }
|
||||
public Guid? MessageId { get; set; }
|
||||
public bool IsMention { get; set; }
|
||||
public string? AttachmentUrl { get; set; }
|
||||
public string? AttachmentFileName { get; set; }
|
||||
public MessageType? Type { get; set; }
|
||||
|
||||
public ChatLine(string plainText)
|
||||
{
|
||||
@@ -217,6 +221,8 @@ public class ChatListSource : IListDataSource
|
||||
RaiseCollectionChanged();
|
||||
}
|
||||
|
||||
public ChatLine? GetLine(int index) => index >= 0 && index < _lines.Count ? _lines[index] : null;
|
||||
|
||||
public bool IsMarked(int item) => false;
|
||||
public void SetMark(int item, bool value) { }
|
||||
public IList ToList() => _lines.Select(l => l.ToString()).ToList();
|
||||
@@ -465,6 +471,8 @@ public static partial class ChatColors
|
||||
public static readonly Attribute EmbedTitleAttr = new(Color.White, Color.Black);
|
||||
public static readonly Attribute EmbedDescAttr = new(new Color(160, 160, 160), Color.Black);
|
||||
public static readonly Attribute EmbedUrlAttr = new(new Color(100, 100, 100), Color.Black);
|
||||
public static readonly Attribute AudioAttr = new(new Color(180, 100, 255), Color.Black);
|
||||
public static readonly Attribute FileAttr = new(new Color(100, 180, 255), Color.Black);
|
||||
|
||||
/// <summary>
|
||||
/// Split text around @mentions, giving each @word the MentionTextAttr accent color.
|
||||
|
||||
@@ -113,6 +113,16 @@ public sealed class MainWindow : Runnable
|
||||
/// </summary>
|
||||
public event Action? OnDeleteChannelRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName.
|
||||
/// </summary>
|
||||
public event Action<string, string>? OnAudioPlayRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the user activates (Enter/click) a file message. Parameters: attachmentUrl, fileName.
|
||||
/// </summary>
|
||||
public event Action<string, string>? OnFileDownloadRequested;
|
||||
|
||||
public MainWindow(IApplication app)
|
||||
{
|
||||
_app = app;
|
||||
@@ -175,6 +185,7 @@ public sealed class MainWindow : Runnable
|
||||
Height = Dim.Fill()
|
||||
};
|
||||
_messageList.Source = new ChatListSource();
|
||||
_messageList.Accepting += OnMessageListAccepting;
|
||||
_chatFrame.Add(_messageList);
|
||||
Add(_chatFrame);
|
||||
|
||||
@@ -360,6 +371,31 @@ public sealed class MainWindow : Runnable
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageListAccepting(object? sender, CommandEventArgs e)
|
||||
{
|
||||
if (_messageList.Source is not ChatListSource source)
|
||||
return;
|
||||
|
||||
var index = _messageList.SelectedItem;
|
||||
if (!index.HasValue || index.Value < 0 || index.Value >= source.Count)
|
||||
return;
|
||||
|
||||
var line = source.GetLine(index.Value);
|
||||
if (line?.AttachmentUrl is null || line.AttachmentFileName is null)
|
||||
return;
|
||||
|
||||
if (line.Type == MessageType.Audio)
|
||||
{
|
||||
OnAudioPlayRequested?.Invoke(line.AttachmentUrl, line.AttachmentFileName);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (line.Type == MessageType.File)
|
||||
{
|
||||
OnFileDownloadRequested?.Invoke(line.AttachmentUrl, line.AttachmentFileName);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInputKeyDown(object? sender, Key e)
|
||||
{
|
||||
if (e.KeyCode == TabKey.KeyCode)
|
||||
@@ -900,10 +936,24 @@ public sealed class MainWindow : Runnable
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.Audio:
|
||||
var audioName = message.AttachmentFileName ?? "unknown";
|
||||
var audioLine = BuildChatLineColored(time, senderName, senderColor,
|
||||
$" \u266a [Audio: {audioName}] (Enter to play)", ChatColors.AudioAttr);
|
||||
audioLine.AttachmentUrl = message.AttachmentUrl;
|
||||
audioLine.AttachmentFileName = audioName;
|
||||
audioLine.Type = MessageType.Audio;
|
||||
lines.Add(audioLine);
|
||||
break;
|
||||
|
||||
case MessageType.File:
|
||||
var fileName = message.AttachmentFileName ?? "unknown";
|
||||
var fileContent = !string.IsNullOrWhiteSpace(message.Content) ? $" {message.Content}" : "";
|
||||
lines.Add(BuildChatLine(time, senderName, senderColor, $" [File: {fileName}]{fileContent}"));
|
||||
var fileLine = BuildChatLineColored(time, senderName, senderColor,
|
||||
$" [File: {fileName}] (Enter to download)", ChatColors.FileAttr);
|
||||
fileLine.AttachmentUrl = message.AttachmentUrl;
|
||||
fileLine.AttachmentFileName = fileName;
|
||||
fileLine.Type = MessageType.File;
|
||||
lines.Add(fileLine);
|
||||
break;
|
||||
|
||||
case MessageType.Text:
|
||||
@@ -962,6 +1012,20 @@ public sealed class MainWindow : Runnable
|
||||
return new ChatLine(segments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a chat line with a colored suffix (used for audio/file indicators).
|
||||
/// </summary>
|
||||
private static ChatLine BuildChatLineColored(string time, string senderName, Attribute? senderColor, string suffix, Attribute suffixColor)
|
||||
{
|
||||
var segments = new List<ChatSegment>
|
||||
{
|
||||
new($"[{time}] ", ChatColors.TimestampAttr),
|
||||
new(senderName, senderColor),
|
||||
new(suffix, suffixColor)
|
||||
};
|
||||
return new ChatLine(segments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a chat line with @mention highlighting in the suffix text.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user