feature: supports get avatar from avatars.githubusercontent.com

* move all images to `src/Resources/Images` folder
This commit is contained in:
leo 2024-08-02 18:06:45 +08:00
parent 1729a64788
commit cd9196bb84
No known key found for this signature in database
42 changed files with 71 additions and 49 deletions

View file

@ -1,21 +1,26 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
namespace SourceGit.Models namespace SourceGit.Models
{ {
public interface IAvatarHost public interface IAvatarHost
{ {
void OnAvatarResourceChanged(string md5); void OnAvatarResourceChanged(string email);
} }
public static class AvatarManager public static partial class AvatarManager
{ {
public static string SelectedServer public static string SelectedServer
{ {
@ -33,29 +38,35 @@ namespace SourceGit.Models
{ {
while (true) while (true)
{ {
var md5 = null as string; var email = null as string;
lock (_synclock) lock (_synclock)
{ {
foreach (var one in _requesting) foreach (var one in _requesting)
{ {
md5 = one; email = one;
break; break;
} }
} }
if (md5 == null) if (email == null)
{ {
Thread.Sleep(100); Thread.Sleep(100);
continue; continue;
} }
var md5 = GetEmailHash(email);
var matchGithubUser = REG_GITHUB_USER_EMAIL().Match(email);
var url = matchGithubUser.Success ?
$"https://avatars.githubusercontent.com/{matchGithubUser.Groups[2].Value}" :
$"{SelectedServer}{md5}?d=404";
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, md5);
var img = null as Bitmap; var img = null as Bitmap;
try try
{ {
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) }; var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) };
var task = client.GetAsync($"{SelectedServer}{md5}?d=404"); var task = client.GetAsync(url);
task.Wait(); task.Wait();
var rsp = task.Result; var rsp = task.Result;
@ -82,13 +93,13 @@ namespace SourceGit.Models
lock (_synclock) lock (_synclock)
{ {
_requesting.Remove(md5); _requesting.Remove(email);
} }
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(() =>
{ {
_resources[md5] = img; _resources[email] = img;
NotifyResourceChanged(md5); NotifyResourceChanged(email);
}); });
} }
}); });
@ -104,25 +115,36 @@ namespace SourceGit.Models
_avatars.Remove(host); _avatars.Remove(host);
} }
public static Bitmap Request(string md5, bool forceRefetch = false) public static Bitmap Request(string email, bool forceRefetch)
{ {
if (email.Equals("noreply@github.com", StringComparison.Ordinal))
{
if (_githubEmailAvatar == null)
{
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute));
_githubEmailAvatar = new Bitmap(icon);
}
return _githubEmailAvatar;
}
if (forceRefetch) if (forceRefetch)
{ {
if (_resources.ContainsKey(md5)) if (_resources.ContainsKey(email))
_resources.Remove(md5); _resources.Remove(email);
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, GetEmailHash(email));
if (File.Exists(localFile)) if (File.Exists(localFile))
File.Delete(localFile); File.Delete(localFile);
NotifyResourceChanged(md5); NotifyResourceChanged(email);
} }
else else
{ {
if (_resources.TryGetValue(md5, out var value)) if (_resources.TryGetValue(email, out var value))
return value; return value;
var localFile = Path.Combine(_storePath, md5); var localFile = Path.Combine(_storePath, GetEmailHash(email));
if (File.Exists(localFile)) if (File.Exists(localFile))
{ {
try try
@ -130,7 +152,7 @@ namespace SourceGit.Models
using (var stream = File.OpenRead(localFile)) using (var stream = File.OpenRead(localFile))
{ {
var img = Bitmap.DecodeToWidth(stream, 128); var img = Bitmap.DecodeToWidth(stream, 128);
_resources.Add(md5, img); _resources.Add(email, img);
return img; return img;
} }
} }
@ -143,18 +165,28 @@ namespace SourceGit.Models
lock (_synclock) lock (_synclock)
{ {
if (!_requesting.Contains(md5)) if (!_requesting.Contains(email))
_requesting.Add(md5); _requesting.Add(email);
} }
return null; return null;
} }
private static void NotifyResourceChanged(string md5) private static string GetEmailHash(string email)
{
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
var builder = new StringBuilder();
foreach (var c in hash)
builder.Append(c.ToString("x2"));
return builder.ToString();
}
private static void NotifyResourceChanged(string email)
{ {
foreach (var avatar in _avatars) foreach (var avatar in _avatars)
{ {
avatar.OnAvatarResourceChanged(md5); avatar.OnAvatarResourceChanged(email);
} }
} }
@ -163,5 +195,9 @@ namespace SourceGit.Models
private static readonly List<IAvatarHost> _avatars = new List<IAvatarHost>(); private static readonly List<IAvatarHost> _avatars = new List<IAvatarHost>();
private static readonly Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>(); private static readonly Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
private static readonly HashSet<string> _requesting = new HashSet<string>(); private static readonly HashSet<string> _requesting = new HashSet<string>();
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")]
private static partial Regex REG_GITHUB_USER_EMAIL();
private static Bitmap _githubEmailAvatar = null;
} }
} }

View file

@ -20,7 +20,7 @@ namespace SourceGit.Models
{ {
get get
{ {
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{Icon}.png", UriKind.RelativeOrAbsolute)); var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/ExternalToolIcons/{Icon}.png", UriKind.RelativeOrAbsolute));
return new Bitmap(icon); return new Bitmap(icon);
} }
} }

View file

@ -25,7 +25,7 @@ namespace SourceGit.Models
try try
{ {
var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{icon}.png", var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/ExternalToolIcons/{icon}.png",
UriKind.RelativeOrAbsolute)); UriKind.RelativeOrAbsolute));
IconImage = new Bitmap(asset); IconImage = new Bitmap(asset);
} }

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -29,10 +29,11 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="App.ico" /> <AvaloniaResource Include="App.ico" />
<AvaloniaResource Include="Resources/ExternalToolIcons/*" />
<AvaloniaResource Include="Resources/ExternalToolIcons/JetBrains/*" />
<AvaloniaResource Include="Resources/Fonts/*" /> <AvaloniaResource Include="Resources/Fonts/*" />
<AvaloniaResource Include="Resources/ShellIcons/*" /> <AvaloniaResource Include="Resources/Images/*" />
<AvaloniaResource Include="Resources/Images/ExternalToolIcons/*" />
<AvaloniaResource Include="Resources/Images/ExternalToolIcons/JetBrains/*" />
<AvaloniaResource Include="Resources/Images/ShellIcons/*" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,7 +1,5 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -42,7 +40,7 @@ namespace SourceGit.Views
{ {
if (User != null) if (User != null)
{ {
Models.AvatarManager.Request(_emailMD5, true); Models.AvatarManager.Request(User.Email, true);
InvalidateVisual(); InvalidateVisual();
} }
}; };
@ -59,7 +57,7 @@ namespace SourceGit.Views
return; return;
var corner = (float)Math.Max(2, Bounds.Width / 16); var corner = (float)Math.Max(2, Bounds.Width / 16);
var img = Models.AvatarManager.Request(_emailMD5); var img = Models.AvatarManager.Request(User.Email, false);
if (img != null) if (img != null)
{ {
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
@ -74,9 +72,9 @@ namespace SourceGit.Views
} }
} }
public void OnAvatarResourceChanged(string md5) public void OnAvatarResourceChanged(string email)
{ {
if (_emailMD5 == md5) if (User.Email.Equals(email, StringComparison.Ordinal))
{ {
InvalidateVisual(); InvalidateVisual();
} }
@ -97,10 +95,7 @@ namespace SourceGit.Views
private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e) private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e)
{ {
if (avatar.User == null) if (avatar.User == null)
{
avatar._emailMD5 = null;
return; return;
}
var placeholder = string.IsNullOrWhiteSpace(avatar.User.Name) ? "?" : avatar.User.Name.Substring(0, 1); var placeholder = string.IsNullOrWhiteSpace(avatar.User.Name) ? "?" : avatar.User.Name.Substring(0, 1);
var chars = placeholder.ToCharArray(); var chars = placeholder.ToCharArray();
@ -108,15 +103,6 @@ namespace SourceGit.Views
foreach (var c in chars) foreach (var c in chars)
sum += Math.Abs(c); sum += Math.Abs(c);
var lowered = avatar.User.Email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
var builder = new StringBuilder();
foreach (var c in hash)
builder.Append(c.ToString("x2"));
var md5 = builder.ToString();
if (avatar._emailMD5 == null || avatar._emailMD5 != md5)
avatar._emailMD5 = md5;
avatar._fallbackBrush = new LinearGradientBrush avatar._fallbackBrush = new LinearGradientBrush
{ {
GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length], GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length],
@ -139,6 +125,5 @@ namespace SourceGit.Views
private FormattedText _fallbackLabel = null; private FormattedText _fallbackLabel = null;
private LinearGradientBrush _fallbackBrush = null; private LinearGradientBrush _fallbackBrush = null;
private string _emailMD5 = null;
} }
} }

View file

@ -294,22 +294,22 @@
IsVisible="{OnPlatform False, Windows=True}"> IsVisible="{OnPlatform False, Windows=True}">
<ComboBox.Items> <ComboBox.Items>
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="/Resources/ShellIcons/git-bash.png" RenderOptions.BitmapInterpolationMode="HighQuality"/> <Image Grid.Column="0" Width="16" Height="16" Source="/Resources/Images/ShellIcons/git-bash.png" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="Git Bash" Margin="6,0,0,0"/> <TextBlock Grid.Column="1" Text="Git Bash" Margin="6,0,0,0"/>
</Grid> </Grid>
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="/Resources/ShellIcons/pwsh.png" RenderOptions.BitmapInterpolationMode="HighQuality"/> <Image Grid.Column="0" Width="16" Height="16" Source="/Resources/Images/ShellIcons/pwsh.png" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="PowerShell" Margin="6,0,0,0"/> <TextBlock Grid.Column="1" Text="PowerShell" Margin="6,0,0,0"/>
</Grid> </Grid>
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="/Resources/ShellIcons/cmd.png" RenderOptions.BitmapInterpolationMode="HighQuality"/> <Image Grid.Column="0" Width="16" Height="16" Source="/Resources/Images/ShellIcons/cmd.png" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="Command Prompt" Margin="6,0,0,0"/> <TextBlock Grid.Column="1" Text="Command Prompt" Margin="6,0,0,0"/>
</Grid> </Grid>
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="/Resources/ShellIcons/wt.png" RenderOptions.BitmapInterpolationMode="HighQuality"/> <Image Grid.Column="0" Width="16" Height="16" Source="/Resources/Images/ShellIcons/wt.png" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="Default Shell in Windows Terminal" Margin="6,0,0,0"/> <TextBlock Grid.Column="1" Text="Default Shell in Windows Terminal" Margin="6,0,0,0"/>
</Grid> </Grid>
</ComboBox.Items> </ComboBox.Items>