mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-04 00:46:11 +02:00
Add download button to image viewer and enable URL highlighting in messages
This commit is contained in:
@@ -14,16 +14,18 @@
|
|||||||
Background="{DynamicResource UiTheme03}">
|
Background="{DynamicResource UiTheme03}">
|
||||||
<Border Padding="8">
|
<Border Padding="8">
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
<Image x:Name="ImageView"
|
<Image x:Name="ImageView"
|
||||||
Stretch="Uniform"
|
Stretch="Uniform"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch" />
|
VerticalAlignment="Stretch" />
|
||||||
<Button Grid.Row="1"
|
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0" Spacing="8">
|
||||||
Content="Close"
|
<Button x:Name="DownloadButton"
|
||||||
HorizontalAlignment="Right"
|
Content="Download"
|
||||||
Width="80"
|
Click="OnDownloadClick" />
|
||||||
Margin="0 8 0 0"
|
<Button Content="Close"
|
||||||
Click="OnCloseClick" />
|
Width="80"
|
||||||
|
Click="OnCloseClick" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
namespace Decho.Views;
|
namespace Decho.Views;
|
||||||
|
|
||||||
public sealed partial class ImageViewerWindow : Window
|
public sealed partial class ImageViewerWindow : Window
|
||||||
{
|
{
|
||||||
|
private readonly Func<Task<string?>>? _downloadAsync;
|
||||||
|
|
||||||
public ImageViewerWindow()
|
public ImageViewerWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageViewerWindow(Bitmap bitmap, string fileName)
|
public ImageViewerWindow(Bitmap bitmap, string fileName, Func<Task<string?>>? downloadAsync = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Title = fileName;
|
Title = fileName;
|
||||||
ImageView.Source = bitmap;
|
ImageView.Source = bitmap;
|
||||||
|
_downloadAsync = downloadAsync;
|
||||||
|
DownloadButton.IsVisible = downloadAsync is not null;
|
||||||
|
|
||||||
Opened += (_, _) =>
|
Opened += (_, _) =>
|
||||||
{
|
{
|
||||||
@@ -27,6 +32,33 @@ public sealed partial class ImageViewerWindow : Window
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnDownloadClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_downloadAsync is null) return;
|
||||||
|
|
||||||
|
string? tempPath = await _downloadAsync();
|
||||||
|
if (tempPath is null) return;
|
||||||
|
|
||||||
|
IStorageFile? file = await this.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
SuggestedFileName = Title,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file?.TryGetLocalPath() is string savePath)
|
||||||
|
{
|
||||||
|
File.Copy(tempPath, savePath, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore if temp file cannot be deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCloseClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
private void OnCloseClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using EchoHub.Core.Models;
|
|||||||
|
|
||||||
using Romzetron.Avalonia;
|
using Romzetron.Avalonia;
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Decho.Views;
|
namespace Decho.Views;
|
||||||
@@ -22,6 +23,7 @@ public partial class MessageItemView : UserControl
|
|||||||
{
|
{
|
||||||
private static readonly Regex MentionRegex = new(@"@(\w+)", RegexOptions.Compiled);
|
private static readonly Regex MentionRegex = new(@"@(\w+)", RegexOptions.Compiled);
|
||||||
private static readonly Regex ChannelRegex = new(@"(?<!\w)#(\w+)", RegexOptions.Compiled);
|
private static readonly Regex ChannelRegex = new(@"(?<!\w)#(\w+)", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex UrlRegex = new(@"https?://[^\s]+", RegexOptions.Compiled);
|
||||||
private CancellationTokenSource? _loadCts;
|
private CancellationTokenSource? _loadCts;
|
||||||
private string? _loadedMessageId;
|
private string? _loadedMessageId;
|
||||||
|
|
||||||
@@ -31,6 +33,18 @@ public partial class MessageItemView : UserControl
|
|||||||
DataContextChanged += OnDataContextChanged;
|
DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OpenUrl(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDataContextChanged(object? sender, EventArgs args)
|
private void OnDataContextChanged(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
if (DataContext is MessageViewModel newMsg && newMsg.Model.Id == _loadedMessageId)
|
if (DataContext is MessageViewModel newMsg && newMsg.Model.Id == _loadedMessageId)
|
||||||
@@ -49,6 +63,53 @@ public partial class MessageItemView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Window? GetParentWindow() => TopLevel.GetTopLevel(this) as Window;
|
||||||
|
|
||||||
|
private async Task<Bitmap?> GetOrDownloadImageAsync(MessageViewModel msg, AttachmentDto att)
|
||||||
|
{
|
||||||
|
if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
||||||
|
if (mainVm is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
|
||||||
|
msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
|
||||||
|
|
||||||
|
if (bytes is null || bytes.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using MemoryStream stream = new MemoryStream(bytes);
|
||||||
|
Bitmap bitmap = new Bitmap(stream);
|
||||||
|
msg.ImageCache[att.Url] = bitmap;
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenProfileAsync(string username, string serverUrl)
|
||||||
|
{
|
||||||
|
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
||||||
|
Window? parent = GetParentWindow();
|
||||||
|
if (mainVm is null || parent is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
|
||||||
|
if (profile is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new ProfileWindow(profile).ShowDialog(parent);
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildMessageInlines(string content)
|
private void BuildMessageInlines(string content)
|
||||||
{
|
{
|
||||||
TextBlock? tb = MessageContent;
|
TextBlock? tb = MessageContent;
|
||||||
@@ -61,7 +122,8 @@ public partial class MessageItemView : UserControl
|
|||||||
|
|
||||||
IEnumerable<Match> mentionMatches = MentionRegex.Matches(content).Cast<Match>();
|
IEnumerable<Match> mentionMatches = MentionRegex.Matches(content).Cast<Match>();
|
||||||
IEnumerable<Match> channelMatches = ChannelRegex.Matches(content).Cast<Match>();
|
IEnumerable<Match> channelMatches = ChannelRegex.Matches(content).Cast<Match>();
|
||||||
List<Match> allMatches = mentionMatches.Concat(channelMatches)
|
IEnumerable<Match> urlMatches = UrlRegex.Matches(content).Cast<Match>();
|
||||||
|
List<Match> allMatches = mentionMatches.Concat(channelMatches).Concat(urlMatches)
|
||||||
.OrderBy(m => m.Index)
|
.OrderBy(m => m.Index)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -92,15 +154,23 @@ public partial class MessageItemView : UserControl
|
|||||||
string username = match.Groups[1].Value;
|
string username = match.Groups[1].Value;
|
||||||
label.Margin = new Thickness(0, 0, 0, -2);
|
label.Margin = new Thickness(0, 0, 0, -2);
|
||||||
label.Foreground = ColorPalette.Yellow05;
|
label.Foreground = ColorPalette.Yellow05;
|
||||||
label.PointerPressed += (_, args) => OnMentionPointerPressed(username, args);
|
string serverUrl = ResolveServerUrl();
|
||||||
|
label.PointerPressed += (_, args) => _ = OpenProfileAsync(username, serverUrl);
|
||||||
}
|
}
|
||||||
else
|
else if (match.Value[0] == '#')
|
||||||
{
|
{
|
||||||
string channelName = match.Groups[1].Value;
|
string channelName = match.Groups[1].Value;
|
||||||
label.Margin = new Thickness(0, 0, 0, -2);
|
label.Margin = new Thickness(0, 0, 0, -2);
|
||||||
label.Foreground = ColorPalette.Blue05;
|
label.Foreground = ColorPalette.Blue05;
|
||||||
label.PointerPressed += (_, args) => OnChannelPointerPressed(channelName, args);
|
label.PointerPressed += (_, args) => OnChannelPointerPressed(channelName, args);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string url = match.Value;
|
||||||
|
label.Foreground = ColorPalette.Blue05;
|
||||||
|
label.TextDecorations = TextDecorations.Underline;
|
||||||
|
label.PointerPressed += (_, args) => OpenUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
container.Child = label;
|
container.Child = label;
|
||||||
tb.Inlines?.Add(container);
|
tb.Inlines?.Add(container);
|
||||||
@@ -114,32 +184,17 @@ public partial class MessageItemView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnMentionPointerPressed(string username, PointerPressedEventArgs e)
|
private string ResolveServerUrl()
|
||||||
{
|
{
|
||||||
if (DataContext is not MessageViewModel msg)
|
if (DataContext is MessageViewModel msg)
|
||||||
{
|
{
|
||||||
return;
|
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
||||||
}
|
if (mainVm is not null)
|
||||||
|
{
|
||||||
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
return msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
|
||||||
if (mainVm is null)
|
}
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string serverUrl = msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
|
|
||||||
UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
|
|
||||||
if (profile is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileWindow dialog = new ProfileWindow(profile);
|
|
||||||
|
|
||||||
if (TopLevel.GetTopLevel(this) is Window parent)
|
|
||||||
{
|
|
||||||
await dialog.ShowDialog(parent);
|
|
||||||
}
|
}
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChannelPointerPressed(string channelName, PointerPressedEventArgs e)
|
private void OnChannelPointerPressed(string channelName, PointerPressedEventArgs e)
|
||||||
@@ -194,40 +249,19 @@ public partial class MessageItemView : UserControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CancellationTokenSource? cts = _loadCts;
|
if (_loadCts is null)
|
||||||
if (cts is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
|
|
||||||
{
|
|
||||||
image.Source = cached;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
Bitmap? bitmap = await GetOrDownloadImageAsync(msg, att);
|
||||||
if (mainVm is null)
|
_loadCts.Token.ThrowIfCancellationRequested();
|
||||||
|
if (bitmap is not null)
|
||||||
{
|
{
|
||||||
return;
|
image.Source = bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
|
|
||||||
msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
|
|
||||||
|
|
||||||
cts.Token.ThrowIfCancellationRequested();
|
|
||||||
if (bytes is null || bytes.Length == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using MemoryStream stream = new MemoryStream(bytes);
|
|
||||||
Bitmap bitmap = new Bitmap(stream);
|
|
||||||
cts.Token.ThrowIfCancellationRequested();
|
|
||||||
msg.ImageCache[att.Url] = bitmap;
|
|
||||||
image.Source = bitmap;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -237,32 +271,13 @@ public partial class MessageItemView : UserControl
|
|||||||
|
|
||||||
private async void OnAuthorNamePointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
private async void OnAuthorNamePointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
MessageViewModel? msg = this.GetDataContext<MessageViewModel>();
|
if (DataContext is not MessageViewModel msg)
|
||||||
if (msg is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
string serverUrl = ResolveServerUrl();
|
||||||
if (mainVm is null)
|
await OpenProfileAsync(msg.Model.Author.Id, serverUrl);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string serverUrl = msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
|
|
||||||
string username = msg.Model.Author.Id;
|
|
||||||
UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
|
|
||||||
if (profile is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileWindow dialog = new ProfileWindow(profile);
|
|
||||||
|
|
||||||
if (TopLevel.GetTopLevel(this) is Window parent)
|
|
||||||
{
|
|
||||||
await dialog.ShowDialog(parent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnAttachmentDownloadClicked(object? sender, RoutedEventArgs e)
|
private async void OnAttachmentDownloadClicked(object? sender, RoutedEventArgs e)
|
||||||
@@ -344,38 +359,32 @@ public partial class MessageItemView : UserControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
|
Bitmap? bitmap = await GetOrDownloadImageAsync(msg, att);
|
||||||
|
if (bitmap is null)
|
||||||
{
|
{
|
||||||
ImageViewerWindow viewer = new ImageViewerWindow(cached, att.FileName);
|
return;
|
||||||
if (TopLevel.GetTopLevel(this) is Window parent)
|
}
|
||||||
|
|
||||||
|
Window? parent = GetParentWindow();
|
||||||
|
if (parent is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string serverUrl = msg.ServerUrl ?? "";
|
||||||
|
string channelName = msg.Model.ChannelName;
|
||||||
|
Func<Task<string?>> downloadAsync = async () =>
|
||||||
|
{
|
||||||
|
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
||||||
|
if (mainVm is null)
|
||||||
{
|
{
|
||||||
await viewer.ShowDialog(parent);
|
return null;
|
||||||
}
|
}
|
||||||
return;
|
return await mainVm.ConnectionService.DownloadAttachmentAsync(
|
||||||
}
|
serverUrl, channelName, att.Url, att.FileName);
|
||||||
|
};
|
||||||
|
|
||||||
MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
|
ImageViewerWindow viewer = new ImageViewerWindow(bitmap, att.FileName, downloadAsync);
|
||||||
if (mainVm is null)
|
await viewer.ShowDialog(parent);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
|
|
||||||
msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
|
|
||||||
|
|
||||||
if (bytes is null || bytes.Length == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using MemoryStream stream = new MemoryStream(bytes);
|
|
||||||
Bitmap bitmap = new Bitmap(stream);
|
|
||||||
msg.ImageCache[att.Url] = bitmap;
|
|
||||||
|
|
||||||
ImageViewerWindow viewerWindow = new ImageViewerWindow(bitmap, att.FileName);
|
|
||||||
if (TopLevel.GetTopLevel(this) is Window parentWindow)
|
|
||||||
{
|
|
||||||
await viewerWindow.ShowDialog(parentWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user