From 6950055f24a407721e8c5db80dafaa15fa002348 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 27 Mar 2024 21:38:38 +0800 Subject: [PATCH] feature: supports preview and diff with image files --- .../Commands/GetImageFileAsBitmap.cs | 40 +++++++ src/SourceGit/Models/DiffResult.cs | 8 ++ src/SourceGit/Models/RevisionFile.cs | 9 +- src/SourceGit/ViewModels/CommitDetail.cs | 26 ++++- src/SourceGit/ViewModels/DiffContext.cs | 97 ++++++++-------- src/SourceGit/Views/DiffView.axaml | 30 +++++ src/SourceGit/Views/DiffView.axaml.cs | 104 ++++++++++++++++++ .../Views/NameHighlightedTextBlock.cs | 1 - src/SourceGit/Views/RevisionFiles.axaml | 4 + 9 files changed, 268 insertions(+), 51 deletions(-) create mode 100644 src/SourceGit/Commands/GetImageFileAsBitmap.cs diff --git a/src/SourceGit/Commands/GetImageFileAsBitmap.cs b/src/SourceGit/Commands/GetImageFileAsBitmap.cs new file mode 100644 index 00000000..e145d67f --- /dev/null +++ b/src/SourceGit/Commands/GetImageFileAsBitmap.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; +using System.IO; + +using Avalonia.Media.Imaging; + +namespace SourceGit.Commands +{ + public static class GetImageFileAsBitmap + { + public static Bitmap Run(string repo, string revision, string file) + { + var starter = new ProcessStartInfo(); + starter.WorkingDirectory = repo; + starter.FileName = Native.OS.GitInstallPath; + starter.Arguments = $"show {revision}:\"{file}\""; + starter.UseShellExecute = false; + starter.CreateNoWindow = true; + starter.WindowStyle = ProcessWindowStyle.Hidden; + starter.RedirectStandardOutput = true; + + try + { + var stream = new MemoryStream(); + var proc = new Process() { StartInfo = starter }; + proc.Start(); + proc.StandardOutput.BaseStream.CopyTo(stream); + proc.WaitForExit(); + proc.Close(); + + stream.Position = 0; + return new Bitmap(stream); + } + catch + { + return null; + } + } + } +} diff --git a/src/SourceGit/Models/DiffResult.cs b/src/SourceGit/Models/DiffResult.cs index 6d6017e7..ecfcc97f 100644 --- a/src/SourceGit/Models/DiffResult.cs +++ b/src/SourceGit/Models/DiffResult.cs @@ -2,6 +2,8 @@ using System.Text; using System.Text.RegularExpressions; +using Avalonia.Media.Imaging; + namespace SourceGit.Models { public enum TextDiffLineType @@ -547,6 +549,12 @@ namespace SourceGit.Models public long NewSize { get; set; } = 0; } + public class ImageDiff + { + public Bitmap Old { get; set; } = null; + public Bitmap New { get; set; } = null; + } + public class NoOrEOLChange { } diff --git a/src/SourceGit/Models/RevisionFile.cs b/src/SourceGit/Models/RevisionFile.cs index 71ba4fd0..27ef1658 100644 --- a/src/SourceGit/Models/RevisionFile.cs +++ b/src/SourceGit/Models/RevisionFile.cs @@ -1,10 +1,17 @@ -namespace SourceGit.Models +using Avalonia.Media.Imaging; + +namespace SourceGit.Models { public class RevisionBinaryFile { public long Size { get; set; } = 0; } + public class RevisionImageFile + { + public Bitmap Image { get; set; } = null; + } + public class RevisionTextFile { public string FileName { get; set; } diff --git a/src/SourceGit/ViewModels/CommitDetail.cs b/src/SourceGit/ViewModels/CommitDetail.cs index 55e0d43b..24b362ea 100644 --- a/src/SourceGit/ViewModels/CommitDetail.cs +++ b/src/SourceGit/ViewModels/CommitDetail.cs @@ -432,11 +432,24 @@ namespace SourceGit.ViewModels var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result(); if (isBinary) { - var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result(); - Dispatcher.UIThread.Invoke(() => + var ext = Path.GetExtension(file.Path); + if (IMG_EXTS.Contains(ext)) { - ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; - }); + var bitmap = Commands.GetImageFileAsBitmap.Run(_repo, _commit.SHA, file.Path); + Dispatcher.UIThread.Invoke(() => + { + ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap }; + }); + } + else + { + var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result(); + Dispatcher.UIThread.Invoke(() => + { + ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; + }); + } + return; } @@ -485,6 +498,11 @@ namespace SourceGit.ViewModels } } + private static readonly HashSet IMG_EXTS = new HashSet() + { + ".ico", ".bmp", ".jpg", ".png", ".jpeg" + }; + private string _repo = string.Empty; private int _activePageIndex = 0; private Models.Commit _commit = null; diff --git a/src/SourceGit/ViewModels/DiffContext.cs b/src/SourceGit/ViewModels/DiffContext.cs index ef3f63e3..6fb5d99b 100644 --- a/src/SourceGit/ViewModels/DiffContext.cs +++ b/src/SourceGit/ViewModels/DiffContext.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Avalonia; @@ -46,12 +47,6 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _isLoading, value); } - public bool IsNoChange - { - get => _isNoChange; - private set => SetProperty(ref _isNoChange, value); - } - public bool IsTextDiff { get => _isTextDiff; @@ -77,7 +72,6 @@ namespace SourceGit.ViewModels if (previous != null) { - _isNoChange = previous._isNoChange; _isTextDiff = previous._isTextDiff; _content = previous._content; } @@ -89,53 +83,62 @@ namespace SourceGit.ViewModels Task.Run(() => { var latest = new Commands.Diff(repo, option).Result(); - var binaryDiff = null as Models.BinaryDiff; + var rs = null as object; - if (latest.IsBinary) + if (latest.TextDiff != null) + { + latest.TextDiff.File = _option.Path; + rs = latest.TextDiff; + } + else if (latest.IsBinary) { - binaryDiff = new Models.BinaryDiff(); - var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath; - if (option.Revisions.Count == 2) + var ext = Path.GetExtension(oldPath); + + if (IMG_EXTS.Contains(ext)) { - binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, option.Revisions[0]).Result(); - binaryDiff.NewSize = new Commands.QueryFileSize(repo, _option.Path, option.Revisions[1]).Result(); + var imgDiff = new Models.ImageDiff(); + if (option.Revisions.Count == 2) + { + imgDiff.Old = Commands.GetImageFileAsBitmap.Run(repo, option.Revisions[0], oldPath); + imgDiff.New = Commands.GetImageFileAsBitmap.Run(repo, option.Revisions[1], oldPath); + } + else + { + imgDiff.Old = Commands.GetImageFileAsBitmap.Run(repo, "HEAD", oldPath); + imgDiff.New = File.Exists(_option.Path) ? new Avalonia.Media.Imaging.Bitmap(_option.Path) : null; + } + rs = imgDiff; } else { - binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, "HEAD").Result(); - binaryDiff.NewSize = new FileInfo(Path.Combine(repo, _option.Path)).Length; - } + var binaryDiff = new Models.BinaryDiff(); + if (option.Revisions.Count == 2) + { + binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, option.Revisions[0]).Result(); + binaryDiff.NewSize = new Commands.QueryFileSize(repo, _option.Path, option.Revisions[1]).Result(); + } + else + { + binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, "HEAD").Result(); + binaryDiff.NewSize = new FileInfo(Path.Combine(repo, _option.Path)).Length; + } + rs = binaryDiff; + } + } + else if (latest.IsLFS) + { + rs = latest.LFSDiff; + } + else + { + rs = new Models.NoOrEOLChange(); } Dispatcher.UIThread.Post(() => { - if (latest.IsBinary) - { - Content = binaryDiff; - IsTextDiff = false; - IsNoChange = false; - } - else if (latest.IsLFS) - { - Content = latest.LFSDiff; - IsTextDiff = false; - IsNoChange = false; - } - else if (latest.TextDiff != null) - { - latest.TextDiff.File = _option.Path; - Content = latest.TextDiff; - IsTextDiff = true; - IsNoChange = false; - } - else - { - Content = new Models.NoOrEOLChange(); - IsTextDiff = false; - IsNoChange = true; - } - + Content = rs; + IsTextDiff = latest.TextDiff != null; IsLoading = false; }); }); @@ -157,10 +160,14 @@ namespace SourceGit.ViewModels await Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, exec, args, _option)); } + private static readonly HashSet IMG_EXTS = new HashSet() + { + ".ico", ".bmp", ".jpg", ".png", ".jpeg" + }; + private readonly string _repo = string.Empty; private readonly Models.DiffOption _option = null; private bool _isLoading = true; - private bool _isNoChange = false; private bool _isTextDiff = false; private object _content = null; private Vector _syncScrollOffset = Vector.Zero; diff --git a/src/SourceGit/Views/DiffView.axaml b/src/SourceGit/Views/DiffView.axaml index e3933839..7e8508e0 100644 --- a/src/SourceGit/Views/DiffView.axaml +++ b/src/SourceGit/Views/DiffView.axaml @@ -109,6 +109,36 @@ + + + + + + + + + + 0,0,0,4 + 0 + 0 + 8 + 16 + 16 + + + + + diff --git a/src/SourceGit/Views/DiffView.axaml.cs b/src/SourceGit/Views/DiffView.axaml.cs index 16eefe0b..c2f36952 100644 --- a/src/SourceGit/Views/DiffView.axaml.cs +++ b/src/SourceGit/Views/DiffView.axaml.cs @@ -1,7 +1,111 @@ +using System; + +using Avalonia; using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; namespace SourceGit.Views { + public class ImageDiffView : Control + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 0.5); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage), null); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage), null); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageDiffView() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + var alpha = Alpha; + var x = Bounds.Width * Alpha; + + var left = OldImage; + if (left != null && alpha > 0) + { + var src = new Rect(0, 0, left.Size.Width * Alpha, left.Size.Height); + var dst = new Rect(0, 0, x, Bounds.Height); + context.DrawImage(left, src, dst); + } + + var right = NewImage; + if (right != null) + { + var src = new Rect(right.Size.Width * Alpha, 0, right.Size.Width - right.Size.Width * Alpha, right.Size.Height); + var dst = new Rect(x, 0, Bounds.Width - x, Bounds.Height); + context.DrawImage(right, src, dst); + } + + context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height)); + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left != null) + { + return GetDesiredSize(left.Size, availableSize); + } + else if (right != null) + { + return GetDesiredSize(right.Size, availableSize); + } + else + { + return availableSize; + } + } + + private Size GetDesiredSize(Size img, Size available) + { + if (img.Width <= available.Width) + { + if (img.Height <= available.Height) + { + return img; + } + else + { + return new Size(available.Height * img.Width / img.Height, available.Height); + } + } + else + { + var s = Math.Max(img.Width / available.Width, img.Height / available.Height); + return new Size(img.Width / s, img.Height / s); + } + } + } + public partial class DiffView : UserControl { public DiffView() diff --git a/src/SourceGit/Views/NameHighlightedTextBlock.cs b/src/SourceGit/Views/NameHighlightedTextBlock.cs index f54e3e75..7252c8b4 100644 --- a/src/SourceGit/Views/NameHighlightedTextBlock.cs +++ b/src/SourceGit/Views/NameHighlightedTextBlock.cs @@ -9,7 +9,6 @@ namespace SourceGit.Views { public class NameHighlightedTextBlock : Control { - public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); diff --git a/src/SourceGit/Views/RevisionFiles.axaml b/src/SourceGit/Views/RevisionFiles.axaml index a58bacc1..3aff7689 100644 --- a/src/SourceGit/Views/RevisionFiles.axaml +++ b/src/SourceGit/Views/RevisionFiles.axaml @@ -91,6 +91,10 @@ + + + +