mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
feature<Histories>: show avatars in histories
This commit is contained in:
parent
cb9274c1c6
commit
5d6a7ba4df
4 changed files with 204 additions and 74 deletions
185
src/Helpers/Avatar.cs
Normal file
185
src/Helpers/Avatar.cs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace SourceGit.Helpers {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avatar control
|
||||||
|
/// </summary>
|
||||||
|
public class Avatar : Image {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Colors used in avatar
|
||||||
|
/// </summary>
|
||||||
|
public static Brush[] Colors = new Brush[] {
|
||||||
|
Brushes.DarkBlue,
|
||||||
|
Brushes.DarkCyan,
|
||||||
|
Brushes.DarkGoldenrod,
|
||||||
|
Brushes.DarkGray,
|
||||||
|
Brushes.DarkGreen,
|
||||||
|
Brushes.DarkKhaki,
|
||||||
|
Brushes.DarkMagenta,
|
||||||
|
Brushes.DarkOliveGreen,
|
||||||
|
Brushes.DarkOrange,
|
||||||
|
Brushes.DarkOrchid,
|
||||||
|
Brushes.DarkRed,
|
||||||
|
Brushes.DarkSalmon,
|
||||||
|
Brushes.DarkSeaGreen,
|
||||||
|
Brushes.DarkSlateBlue,
|
||||||
|
Brushes.DarkSlateGray,
|
||||||
|
Brushes.DarkTurquoise,
|
||||||
|
Brushes.DarkViolet
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User property definition.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty UserProperty = DependencyProperty.Register(
|
||||||
|
"User",
|
||||||
|
typeof(Git.User),
|
||||||
|
typeof(Avatar),
|
||||||
|
new PropertyMetadata(null, OnUserChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User property
|
||||||
|
/// </summary>
|
||||||
|
public Git.User User {
|
||||||
|
get { return (Git.User)GetValue(UserProperty); }
|
||||||
|
set { SetValue(UserProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loading request
|
||||||
|
/// </summary>
|
||||||
|
private class Request {
|
||||||
|
public BitmapImage img = null;
|
||||||
|
public List<Avatar> targets = new List<Avatar>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to cache downloaded avatars
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string CACHE_PATH = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"SourceGit",
|
||||||
|
"avatars");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current requests.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, Request> requesting = new Dictionary<string, Request>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Render implementation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dc"></param>
|
||||||
|
protected override void OnRender(DrawingContext dc) {
|
||||||
|
base.OnRender(dc);
|
||||||
|
|
||||||
|
if (Source == null && User != null) {
|
||||||
|
var placeholder = User.Name.Length > 0 ? User.Name.Substring(0, 1) : "?";
|
||||||
|
var formatted = new FormattedText(
|
||||||
|
placeholder,
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
new Typeface(new FontFamily("Consolas"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||||
|
Width * 0.75,
|
||||||
|
Brushes.White,
|
||||||
|
VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||||
|
|
||||||
|
double offsetX = 0;
|
||||||
|
if (HorizontalAlignment == HorizontalAlignment.Right) {
|
||||||
|
offsetX = -Width * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chars = placeholder.ToCharArray();
|
||||||
|
var sum = 0;
|
||||||
|
foreach (var ch in chars) sum += Math.Abs(ch);
|
||||||
|
var brush = Colors[sum % Colors.Length];
|
||||||
|
|
||||||
|
dc.DrawRoundedRectangle(brush, null, new Rect(-Width * 0.5 + offsetX, -Height * 0.5, Width, Height), Width / 16, Height / 16);
|
||||||
|
dc.DrawText(formatted, new Point(formatted.Width * -0.5 + offsetX, formatted.Height * -0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset image.
|
||||||
|
/// </summary>
|
||||||
|
private void ReloadImage(Git.User oldUser) {
|
||||||
|
if (oldUser != null && requesting.ContainsKey(oldUser.Email)) {
|
||||||
|
if (requesting[oldUser.Email].targets.Count <= 1) {
|
||||||
|
requesting.Remove(oldUser.Email);
|
||||||
|
} else {
|
||||||
|
requesting[oldUser.Email].targets.Remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Source = null;
|
||||||
|
InvalidateVisual();
|
||||||
|
|
||||||
|
if (User == null) return;
|
||||||
|
|
||||||
|
var email = User.Email;
|
||||||
|
if (requesting.ContainsKey(email)) {
|
||||||
|
requesting[email].targets.Add(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(email.ToLower().Trim()));
|
||||||
|
string md5 = "";
|
||||||
|
for (int i = 0; i < hash.Length; i++) md5 += hash[i].ToString("x2");
|
||||||
|
md5 = md5.ToLower();
|
||||||
|
|
||||||
|
string filePath = Path.Combine(CACHE_PATH, md5);
|
||||||
|
if (File.Exists(filePath)) {
|
||||||
|
Source = new BitmapImage(new Uri(filePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requesting.Add(email, new Request());
|
||||||
|
requesting[email].targets.Add(this);
|
||||||
|
|
||||||
|
BitmapImage downloading = new BitmapImage(new Uri("https://www.gravatar.com/avatar/" + md5 + "?d=404"));
|
||||||
|
requesting[email].img = downloading;
|
||||||
|
downloading.DownloadCompleted += (o, e) => {
|
||||||
|
var owner = o as BitmapImage;
|
||||||
|
if (owner != null) {
|
||||||
|
if (!Directory.Exists(CACHE_PATH)) Directory.CreateDirectory(CACHE_PATH);
|
||||||
|
|
||||||
|
var encoder = new PngBitmapEncoder();
|
||||||
|
encoder.Frames.Add(BitmapFrame.Create(owner));
|
||||||
|
using (var fs = new FileStream(filePath, FileMode.Create)) {
|
||||||
|
encoder.Save(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requesting.ContainsKey(email)) {
|
||||||
|
BitmapImage exists = new BitmapImage(new Uri(filePath));
|
||||||
|
foreach (var one in requesting[email].targets) one.Source = exists;
|
||||||
|
requesting.Remove(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
downloading.DownloadFailed += (o, e) => {
|
||||||
|
requesting.Remove(email);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback on user property changed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="d"></param>
|
||||||
|
/// <param name="e"></param>
|
||||||
|
private static void OnUserChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||||
|
Avatar a = d as Avatar;
|
||||||
|
if (a != null) a.ReloadImage(e.OldValue as Git.User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,11 +62,7 @@
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" x:Name="authorAvatarMask" Width="64" Height="64" HorizontalAlignment="Right" Background="Gray">
|
<helpers:Avatar Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Width="64" Height="64" x:Name="authorAvatar" HorizontalAlignment="Right"/>
|
||||||
<Path Style="{StaticResource Style.Icon}" Fill="White" Width="56" Height="56" Data="{StaticResource Icon.User}" VerticalAlignment="Bottom"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Image Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Width="64" Height="64" x:Name="authorAvatar" HorizontalAlignment="Right"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Margin="15,0,0,0" Text="{StaticResource Text.CommitViewer.Info.Author}" Opacity=".6" Foreground="{StaticResource Brush.FG1}"/>
|
<TextBlock Grid.Row="0" Grid.Column="1" Margin="15,0,0,0" Text="{StaticResource Text.CommitViewer.Info.Author}" Opacity=".6" Foreground="{StaticResource Brush.FG1}"/>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="12,0,0,0">
|
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="12,0,0,0">
|
||||||
|
@ -89,11 +85,7 @@
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" x:Name="committerAvatarMask" Width="64" Height="64" HorizontalAlignment="Right" Background="Gray">
|
<helpers:Avatar Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Width="64" Height="64" x:Name="committerAvatar" HorizontalAlignment="Right"/>
|
||||||
<Path Style="{StaticResource Style.Icon}" Fill="White" Width="56" Height="56" Data="{StaticResource Icon.User}" VerticalAlignment="Bottom"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Image Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Width="64" Height="64" x:Name="committerAvatar" HorizontalAlignment="Right"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Margin="15,0,0,0" Text="{StaticResource Text.CommitViewer.Info.Committer}" Opacity=".6" Foreground="{StaticResource Brush.FG1}"/>
|
<TextBlock Grid.Row="0" Grid.Column="1" Margin="15,0,0,0" Text="{StaticResource Text.CommitViewer.Info.Committer}" Opacity=".6" Foreground="{StaticResource Brush.FG1}"/>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" Margin="12,0,0,0" Orientation="Horizontal">
|
<StackPanel Grid.Row="1" Grid.Column="1" Margin="12,0,0,0" Orientation="Horizontal">
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
@ -20,11 +19,6 @@ namespace SourceGit.UI {
|
||||||
/// Commit detail viewer
|
/// Commit detail viewer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CommitViewer : UserControl {
|
public partial class CommitViewer : UserControl {
|
||||||
private static readonly string AVATAR_PATH = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
||||||
"SourceGit",
|
|
||||||
"avatars");
|
|
||||||
|
|
||||||
private Git.Repository repo = null;
|
private Git.Repository repo = null;
|
||||||
private Git.Commit commit = null;
|
private Git.Commit commit = null;
|
||||||
private List<Git.Change> cachedChanges = new List<Git.Change>();
|
private List<Git.Change> cachedChanges = new List<Git.Change>();
|
||||||
|
@ -104,31 +98,17 @@ namespace SourceGit.UI {
|
||||||
authorName.Text = commit.Author.Name;
|
authorName.Text = commit.Author.Name;
|
||||||
authorEmail.Text = commit.Author.Email;
|
authorEmail.Text = commit.Author.Email;
|
||||||
authorTime.Text = commit.Author.Time;
|
authorTime.Text = commit.Author.Time;
|
||||||
|
authorAvatar.User = commit.Author;
|
||||||
|
|
||||||
if (commit.Committer.Email == commit.Author.Email) {
|
committerName.Text = commit.Committer.Name;
|
||||||
if (commit.Committer.Time == commit.Author.Time) {
|
committerEmail.Text = commit.Committer.Email;
|
||||||
committerPanel.Visibility = Visibility.Hidden;
|
committerTime.Text = commit.Committer.Time;
|
||||||
|
committerAvatar.User = commit.Committer;
|
||||||
|
|
||||||
SetAvatar(authorAvatar, authorAvatarMask, commit.Author.Email);
|
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
|
||||||
} else {
|
committerPanel.Visibility = Visibility.Hidden;
|
||||||
committerPanel.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
committerName.Text = commit.Committer.Name;
|
|
||||||
committerEmail.Text = commit.Committer.Email;
|
|
||||||
committerTime.Text = commit.Committer.Time;
|
|
||||||
|
|
||||||
SetAvatar(authorAvatar, authorAvatarMask, commit.Author.Email);
|
|
||||||
SetAvatar(committerAvatar, committerAvatarMask, commit.Committer.Email, false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
committerPanel.Visibility = Visibility.Visible;
|
committerPanel.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
committerName.Text = commit.Committer.Name;
|
|
||||||
committerEmail.Text = commit.Committer.Email;
|
|
||||||
committerTime.Text = commit.Committer.Time;
|
|
||||||
|
|
||||||
SetAvatar(authorAvatar, authorAvatarMask, commit.Author.Email);
|
|
||||||
SetAvatar(committerAvatar, committerAvatarMask, commit.Committer.Email);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.Decorators.Count == 0) {
|
if (commit.Decorators.Count == 0) {
|
||||||
|
@ -138,41 +118,6 @@ namespace SourceGit.UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetAvatar(Image img, Border mask, string email, bool save = true) {
|
|
||||||
byte[] hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(email.ToLower().Trim()));
|
|
||||||
string md5 = "";
|
|
||||||
for (int i = 0; i < hash.Length; i++) md5 += hash[i].ToString("x2");
|
|
||||||
md5 = md5.ToLower();
|
|
||||||
|
|
||||||
if (!Directory.Exists(AVATAR_PATH)) Directory.CreateDirectory(AVATAR_PATH);
|
|
||||||
|
|
||||||
mask.Visibility = Visibility.Visible;
|
|
||||||
var sha = commit.SHA;
|
|
||||||
|
|
||||||
string filePath = Path.Combine(AVATAR_PATH, md5);
|
|
||||||
if (File.Exists(filePath)) {
|
|
||||||
img.Source = new BitmapImage(new Uri(filePath));
|
|
||||||
mask.Visibility = Visibility.Hidden;
|
|
||||||
} else {
|
|
||||||
var bitmap = new BitmapImage(new Uri("https://www.gravatar.com/avatar/" + md5 + "?d=404"));
|
|
||||||
if (save) {
|
|
||||||
bitmap.DownloadCompleted += (o, e) => {
|
|
||||||
var owner = o as BitmapImage;
|
|
||||||
if (owner != null) {
|
|
||||||
var encoder = new PngBitmapEncoder();
|
|
||||||
encoder.Frames.Add(BitmapFrame.Create(owner));
|
|
||||||
using (var fs = new FileStream(filePath, FileMode.Create)) {
|
|
||||||
encoder.Save(fs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commit.SHA == sha) mask.Visibility = Visibility.Hidden;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
img.Source = bitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
|
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
|
||||||
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|
|
@ -136,7 +136,15 @@
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
<DataGridTextColumn Width="100" IsReadOnly="True" Binding="{Binding Committer.Name}" ElementStyle="{StaticResource Style.DataGridText}"/>
|
<DataGridTemplateColumn Width="32" IsReadOnly="True">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<helpers:Avatar Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" User="{Binding Committer}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTextColumn Width="84" IsReadOnly="True" Binding="{Binding Committer.Name}" ElementStyle="{StaticResource Style.DataGridText.NoPadding}"/>
|
||||||
<DataGridTextColumn Width="84" IsReadOnly="True" Binding="{Binding ShortSHA}" ElementStyle="{StaticResource Style.DataGridText}"/>
|
<DataGridTextColumn Width="84" IsReadOnly="True" Binding="{Binding ShortSHA}" ElementStyle="{StaticResource Style.DataGridText}"/>
|
||||||
<DataGridTextColumn Width="128" IsReadOnly="True" Binding="{Binding Committer.Time}" ElementStyle="{StaticResource Style.DataGridText.NoPadding}"/>
|
<DataGridTextColumn Width="128" IsReadOnly="True" Binding="{Binding Committer.Time}" ElementStyle="{StaticResource Style.DataGridText.NoPadding}"/>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
|
@ -154,7 +162,7 @@
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|
||||||
<!-- Commit Graph -->
|
<!-- Commit Graph -->
|
||||||
<Border x:Name="commitGraphContainer" Grid.Row="1" Margin="0,0,310,0" ClipToBounds="True" IsHitTestVisible="False">
|
<Border x:Name="commitGraphContainer" Grid.Row="1" Margin="0,0,342,0" ClipToBounds="True" IsHitTestVisible="False">
|
||||||
<helpers:CommitGraph
|
<helpers:CommitGraph
|
||||||
x:Name="commitGraph"
|
x:Name="commitGraph"
|
||||||
Width="{Binding ElementName=commitGraphContainer, Path=ActualWidth}"
|
Width="{Binding ElementName=commitGraphContainer, Path=ActualWidth}"
|
||||||
|
|
Loading…
Reference in a new issue