Skip to content

Commit 3c95102

Browse files
committed
feat(Avalonia): add thumbnail generation
1 parent da722fb commit 3c95102

3 files changed

Lines changed: 162 additions & 3 deletions

File tree

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,59 @@
1-
using SnapX.Core.History;
1+
using System.ComponentModel;
2+
using System.Runtime.CompilerServices;
3+
using SnapX.Core;
4+
using SnapX.Core.History;
5+
using SnapX.Core.Media.Services;
26

37
namespace SnapX.Avalonia.Models;
48

5-
public record ListTaskTemplate(Type ModelType, HistoryItem task);
9+
public record ListTaskTemplate(Type ModelType, HistoryItem task) : INotifyPropertyChanged
10+
{
11+
private string? _uiDisplaySource;
12+
private bool _isLoading;
13+
14+
public string? UIDisplaySource
15+
{
16+
get
17+
{
18+
if (_uiDisplaySource == null && !_isLoading)
19+
{
20+
_ = LoadSourceAsync();
21+
return task.BestImageSource;
22+
}
23+
return _uiDisplaySource;
24+
}
25+
}
26+
27+
private async Task LoadSourceAsync()
28+
{
29+
_isLoading = true;
30+
string? originalSource = task.BestImageSource;
31+
32+
try
33+
{
34+
// DebugHelper.Logger?.Debug($"ListTaskTemplate: Processing source: {originalSource}");
35+
36+
var result = await ThumbnailService.GetCompatibleSourceAsync(originalSource);
37+
38+
if (result != _uiDisplaySource)
39+
{
40+
_uiDisplaySource = result;
41+
// DebugHelper.Logger?.Debug($"ListTaskTemplate: Successfully resolved source to: {result}");
42+
OnPropertyChanged(nameof(UIDisplaySource));
43+
}
44+
}
45+
catch (Exception ex)
46+
{
47+
// DebugHelper.Logger?.Debug($"ListTaskTemplate: Error resolving image source: {ex.Message}");
48+
_uiDisplaySource = originalSource;
49+
}
50+
finally
51+
{
52+
_isLoading = false;
53+
}
54+
}
55+
56+
public event PropertyChangedEventHandler? PropertyChanged;
57+
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
58+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
59+
}

SnapX.Avalonia/Views/HomePageView.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@
295295
Height="150"
296296
Stretch="Uniform"
297297
Width="200"
298-
asyncImageLoader:ImageLoader.Source="{Binding task.BestImageSource}"
298+
asyncImageLoader:ImageLoader.Source="{Binding UIDisplaySource}"
299299
x:Name="PreviewImage" />
300300
<SelectableTextBlock
301301
FontWeight="SemiBold"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Collections.Concurrent;
2+
using System.IO.Hashing;
3+
using System.Text;
4+
using NeoSolve.ImageSharp.AVIF;
5+
using SixLabors.ImageSharp;
6+
using SixLabors.ImageSharp.Formats.Webp;
7+
using SixLabors.ImageSharp.Processing;
8+
using SnapX.Core.Utils.Miscellaneous;
9+
using Xdg.Directories;
10+
11+
namespace SnapX.Core.Media.Services;
12+
13+
public static class ThumbnailService
14+
{
15+
static ThumbnailService()
16+
{
17+
var imageConfig = Configuration.Default;
18+
imageConfig.ImageFormatsManager.SetEncoder(AVIFFormat.Instance, AVIFEncoder.Instance);
19+
imageConfig.ImageFormatsManager.SetDecoder(AVIFFormat.Instance, AVIFDecoder.Instance);
20+
imageConfig.ImageFormatsManager.AddImageFormatDetector(new PatchedAVIFImageFormatDetector());
21+
}
22+
private static readonly ConcurrentDictionary<string, string> _pathCache = new();
23+
private static readonly string CacheFolder = Path.Combine(BaseDirectory.CacheHome, SnapX.AppName, "Thumbnails");
24+
private static readonly HttpClient _httpClient = HttpClientFactory.Get();
25+
private static readonly SemaphoreSlim _processingSemaphore = new(3, 3);
26+
public static async Task<string> GetCompatibleSourceAsync(string? source)
27+
{
28+
if (string.IsNullOrEmpty(source)) return string.Empty;
29+
var result = await Task.Factory.StartNew(async () =>
30+
{
31+
await _processingSemaphore.WaitAsync();
32+
try
33+
{
34+
return await GenerateWebpThumbnail(source);
35+
}
36+
finally
37+
{
38+
_processingSemaphore.Release();
39+
}
40+
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
41+
_pathCache.TryAdd(source, result);
42+
return result;
43+
44+
}
45+
46+
private static async Task<string> GenerateWebpThumbnail(string source)
47+
{
48+
if (_pathCache.TryGetValue(source, out var cachedPath))
49+
{
50+
return cachedPath;
51+
}
52+
var isUrl = Uri.TryCreate(source, UriKind.Absolute, out var uri)
53+
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
54+
Directory.CreateDirectory(CacheFolder);
55+
56+
var sourceBytes = Encoding.UTF8.GetBytes(source);
57+
var hashBytes = XxHash64.Hash(sourceBytes);
58+
var hashName = Convert.ToHexString(hashBytes).Replace("-", "") + ".webp";
59+
var cachePath = Path.Combine(CacheFolder, hashName);
60+
61+
if (File.Exists(cachePath)) return cachePath;
62+
63+
try
64+
{
65+
DebugHelper.Logger?.Debug($"ThumbnailService: Generating thumbnail for {(isUrl ? "URL" : "File")}: {source}");
66+
67+
await using var imageStream = await GetImageStreamAsync(source, isUrl);
68+
using var image = await Image.LoadAsync(imageStream);
69+
image.Mutate(x => x.Resize(new ResizeOptions
70+
{
71+
Size = new Size(200, 150),
72+
Mode = ResizeMode.Max
73+
}));
74+
// CPU time is precious
75+
await image.SaveAsWebpAsync(cachePath, new WebpEncoder()
76+
{
77+
Quality = 70,
78+
Method = WebpEncodingMethod.Fastest
79+
});
80+
81+
return cachePath;
82+
}
83+
catch (Exception ex)
84+
{
85+
DebugHelper.Logger?.Debug($"ThumbnailService: Error during conversion: {ex.Message}");
86+
return source;
87+
}
88+
}
89+
90+
private static async Task<Stream> GetImageStreamAsync(string source, bool isUrl)
91+
{
92+
if (!isUrl) return File.OpenRead(source);
93+
using var request = new HttpRequestMessage(HttpMethod.Get, source);
94+
95+
request.Headers.Accept.ParseAdd("image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
96+
97+
request.Headers.ExpectContinue = true;
98+
99+
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
100+
response.EnsureSuccessStatusCode();
101+
102+
return await response.Content.ReadAsStreamAsync();
103+
104+
}
105+
}

0 commit comments

Comments
 (0)