From 24957b8c86e4b71684219b24fa1a53bb25deabfd Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 25 Aug 2023 18:15:26 +0800 Subject: [PATCH 0001/2758] style: redesign style for blame --- src/Commands/Blame.cs | 2 +- src/Views/Blame.xaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 5fc8332a..4d364be3 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -53,7 +53,7 @@ namespace SourceGit.Commands { var author = match.Groups[2].Value; var timestamp = int.Parse(match.Groups[3].Value); var content = match.Groups[4].Value; - var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd"); var blameLine = new Models.BlameLine() { LineNumber = $"{data.Lines.Count + 1}", diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml index 299cca9a..db12c6db 100644 --- a/src/Views/Blame.xaml +++ b/src/Views/Blame.xaml @@ -80,6 +80,8 @@ + + @@ -109,8 +111,6 @@ - - From 6e624797ae5a3d01223257a3be1207a70a19adf4 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 28 Aug 2023 10:17:10 +0800 Subject: [PATCH 0002/2758] optimize: only navigate to commit when user clicked the commit SHA --- src/Views/Blame.xaml | 6 +++++- src/Views/Blame.xaml.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml index db12c6db..f54a37a8 100644 --- a/src/Views/Blame.xaml +++ b/src/Views/Blame.xaml @@ -88,7 +88,11 @@ - + + + + + diff --git a/src/Views/Blame.xaml.cs b/src/Views/Blame.xaml.cs index 8d1340e9..2a8c1598 100644 --- a/src/Views/Blame.xaml.cs +++ b/src/Views/Blame.xaml.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; +using System.Windows.Navigation; namespace SourceGit.Views { /// @@ -156,12 +157,15 @@ namespace SourceGit.Views { var r = blame.SelectedItem as Record; if (r == null) return; - Models.Watcher.Get(repo).NavigateTo(r.Line.CommitSHA); - foreach (var one in Records) { one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA; } } + + private void GotoCommit(object sender, RequestNavigateEventArgs e) { + Models.Watcher.Get(repo).NavigateTo(e.Uri.OriginalString); + e.Handled = true; + } #endregion private string repo = null; From afe0220a46990827f5aab78a41a0c11abb94292e Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 28 Aug 2023 10:59:46 +0800 Subject: [PATCH 0003/2758] fix: bookmark changes will not be saved when there's no welcome page --- src/Models/Repository.cs | 12 +++++++++++- src/Models/Watcher.cs | 7 +++---- src/Views/Widgets/PageTabBar.xaml.cs | 3 ++- src/Views/Widgets/Welcome.xaml.cs | 12 +++--------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Models/Repository.cs b/src/Models/Repository.cs index 7774e54d..ef1d9a9d 100644 --- a/src/Models/Repository.cs +++ b/src/Models/Repository.cs @@ -27,11 +27,20 @@ namespace SourceGit.Models { public string Name { get; set; } = ""; public string Path { get; set; } = ""; public string GitDir { get; set; } = ""; - public int Bookmark { get; set; } = 0; public long LastOpenTime { get; set; } = 0; public List SubTrees { get; set; } = new List(); public List Filters { get; set; } = new List(); public List CommitMessages { get; set; } = new List(); + + public int Bookmark { + get { return bookmark; } + set { + if (value != bookmark) { + bookmark = value; + Watcher.NotifyBookmarkChanged(this); + } + } + } #endregion #region PROPERTIES_RUNTIME @@ -122,5 +131,6 @@ namespace SourceGit.Models { } private readonly object updateFilterLock = new object(); + private int bookmark = 0; } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index c9bb6787..c05dadbf 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -103,12 +103,11 @@ namespace SourceGit.Models { } /// - /// 设置仓库标签变化 + /// 通知仓库标签变化 /// /// - /// - public static void SetBookmark(string repo, int bookmark) { - BookmarkChanged?.Invoke(repo, bookmark); + public static void NotifyBookmarkChanged(Repository repo) { + BookmarkChanged?.Invoke(repo.Path, repo.Bookmark); } /// diff --git a/src/Views/Widgets/PageTabBar.xaml.cs b/src/Views/Widgets/PageTabBar.xaml.cs index 0df9afc0..3214d0bc 100644 --- a/src/Views/Widgets/PageTabBar.xaml.cs +++ b/src/Views/Widgets/PageTabBar.xaml.cs @@ -364,7 +364,8 @@ namespace SourceGit.Views.Widgets { var refIdx = i; mark.Click += (o, ev) => { - Models.Watcher.SetBookmark(tab.Id, refIdx); + var repo = Models.Preference.Instance.FindRepository(tab.Id); + if (repo != null) repo.Bookmark = refIdx; ev.Handled = true; }; bookmark.Items.Add(mark); diff --git a/src/Views/Widgets/Welcome.xaml.cs b/src/Views/Widgets/Welcome.xaml.cs index 88e9ea1d..f1fc72a1 100644 --- a/src/Views/Widgets/Welcome.xaml.cs +++ b/src/Views/Widgets/Welcome.xaml.cs @@ -18,15 +18,9 @@ namespace SourceGit.Views.Widgets { public Welcome() { InitializeComponent(); UpdateVisibles(); - Models.Theme.AddListener(this, UpdateVisibles); - Models.Watcher.BookmarkChanged += (repoPath, bookmark) => { - var repo = Models.Preference.Instance.FindRepository(repoPath); - if (repo != null) { - repo.Bookmark = bookmark; - UpdateVisibles(); - } - }; + Models.Theme.AddListener(this, UpdateVisibles); + Models.Watcher.BookmarkChanged += (_, __) => { UpdateVisibles(); }; } #region FUNC_EVENTS @@ -148,7 +142,7 @@ namespace SourceGit.Views.Widgets { var refIdx = i; mark.Click += (o, ev) => { - Models.Watcher.SetBookmark(repo.Path, refIdx); + repo.Bookmark = refIdx; ev.Handled = true; }; bookmark.Items.Add(mark); From 559829c05472fe88e0a67036e32f78bb7fee260e Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 28 Aug 2023 15:24:53 +0800 Subject: [PATCH 0004/2758] style: new blame style --- src/Views/Blame.xaml | 49 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml index f54a37a8..f2c46655 100644 --- a/src/Views/Blame.xaml +++ b/src/Views/Blame.xaml @@ -74,13 +74,54 @@ SelectionChanged="OnSelectionChanged"> - + + + + - + + + + + + + + + + + + @@ -94,7 +135,7 @@ - + @@ -109,7 +150,7 @@ - + From fcad60214488347a5962d65020cd0644a5cedaa8 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Aug 2023 15:58:37 +0800 Subject: [PATCH 0005/2758] optimize: remove use this version button from file histories. --- src/Views/FileHistories.xaml | 19 +------------------ src/Views/FileHistories.xaml.cs | 19 ------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/Views/FileHistories.xaml b/src/Views/FileHistories.xaml index 27aa7ac3..52f40a12 100644 --- a/src/Views/FileHistories.xaml +++ b/src/Views/FileHistories.xaml @@ -131,24 +131,7 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/About.axaml.cs b/src/Views/About.axaml.cs new file mode 100644 index 00000000..90d047ef --- /dev/null +++ b/src/Views/About.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using System.Reflection; + +namespace SourceGit.Views { + public partial class About : Window { + public string Version { + get; + private set; + } + + public About() { + var ver = Assembly.GetExecutingAssembly().GetName().Version; + Version = $"{ver.Major}.{ver.Minor}"; + DataContext = this; + InitializeComponent(); + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + Close(); + } + + private void OnVisitAvaloniaUI(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.avaloniaui.net/"); + e.Handled = true; + } + + private void OnVisitAvaloniaEdit(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.nuget.org/packages/OneWare.AvaloniaEdit"); + e.Handled = true; + } + + private void OnVisitJetBrainsMonoFont(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.jetbrains.com/lp/mono/"); + e.Handled = true; + } + } +} diff --git a/src/Views/About.xaml b/src/Views/About.xaml deleted file mode 100644 index 1b754a30..00000000 --- a/src/Views/About.xaml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/About.xaml.cs b/src/Views/About.xaml.cs deleted file mode 100644 index cce5e618..00000000 --- a/src/Views/About.xaml.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Windows; - -namespace SourceGit.Views { - - /// - /// 关于对话框 - /// - public partial class About : Controls.Window { - - public class Keymap { - public string Key { get; set; } - public string Desc { get; set; } - public Keymap(string k, string d) { Key = k; Desc = App.Text($"Hotkeys.{d}"); } - } - - public About() { - InitializeComponent(); - - var asm = Assembly.GetExecutingAssembly().GetName(); - version.Text = $"VERSION : v{asm.Version.Major}.{asm.Version.Minor}"; - - hotkeys.ItemsSource = new List() { - new Keymap("CTRL + T", "NewTab"), - new Keymap("CTRL + W", "CloseTab"), - new Keymap("CTRL + TAB", "NextTab"), - new Keymap("CTRL + [1-9]", "SwitchTo"), - new Keymap("CTRL + F", "Search"), - new Keymap("F5", "Refresh"), - new Keymap("SPACE", "ToggleStage"), - new Keymap("ESC", "CancelPopup"), - }; - } - - private void OnRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) { - var info = new ProcessStartInfo("cmd", $"/c start {e.Uri.AbsoluteUri}"); - info.CreateNoWindow = true; - Process.Start(info); - } - - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - } -} diff --git a/src/Views/AddRemote.axaml b/src/Views/AddRemote.axaml new file mode 100644 index 00000000..4b460d79 --- /dev/null +++ b/src/Views/AddRemote.axaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/AddRemote.axaml.cs b/src/Views/AddRemote.axaml.cs new file mode 100644 index 00000000..f3640e30 --- /dev/null +++ b/src/Views/AddRemote.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class AddRemote : UserControl { + public AddRemote() { + InitializeComponent(); + } + + private async void SelectSSHKey(object sender, RoutedEventArgs e) { + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtSSHKey.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/AddSubmodule.axaml b/src/Views/AddSubmodule.axaml new file mode 100644 index 00000000..f7c70bfd --- /dev/null +++ b/src/Views/AddSubmodule.axaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/src/Views/AddSubmodule.axaml.cs b/src/Views/AddSubmodule.axaml.cs new file mode 100644 index 00000000..358faaa7 --- /dev/null +++ b/src/Views/AddSubmodule.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class AddSubmodule : UserControl { + public AddSubmodule() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Apply.axaml b/src/Views/Apply.axaml new file mode 100644 index 00000000..979741c8 --- /dev/null +++ b/src/Views/Apply.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Apply.axaml.cs b/src/Views/Apply.axaml.cs new file mode 100644 index 00000000..3f61a4dd --- /dev/null +++ b/src/Views/Apply.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Apply : UserControl { + public Apply() { + InitializeComponent(); + } + + private async void SelectPatchFile(object sender, RoutedEventArgs e) { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) return; + + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [ new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }] }; + var selected = await topLevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtPatchFile.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/Archive.axaml b/src/Views/Archive.axaml new file mode 100644 index 00000000..be2d0ddc --- /dev/null +++ b/src/Views/Archive.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Archive.axaml.cs b/src/Views/Archive.axaml.cs new file mode 100644 index 00000000..63e73448 --- /dev/null +++ b/src/Views/Archive.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Archive : UserControl { + public Archive() { + InitializeComponent(); + } + + private async void SelectOutputFile(object sender, RoutedEventArgs e) { + var options = new FilePickerSaveOptions() { DefaultExtension = ".zip", FileTypeChoices = [ new FilePickerFileType("ZIP") { Patterns = [ "*.zip" ]}] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.SaveFilePickerAsync(options); + if (selected != null) { + txtSaveFile.Text = selected.Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/AssumeUnchanged.xaml b/src/Views/AssumeUnchanged.xaml deleted file mode 100644 index 01d5413f..00000000 --- a/src/Views/AssumeUnchanged.xaml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/AssumeUnchanged.xaml.cs b/src/Views/AssumeUnchanged.xaml.cs deleted file mode 100644 index 1336f0bb..00000000 --- a/src/Views/AssumeUnchanged.xaml.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; - -namespace SourceGit.Views { - /// - /// 管理不跟踪变更的文件 - /// - public partial class AssumeUnchanged : Controls.Window { - private string repo = null; - - public ObservableCollection Files { get; set; } - - public AssumeUnchanged(string repo) { - this.repo = repo; - this.Files = new ObservableCollection(); - - InitializeComponent(); - - Task.Run(() => { - var unchanged = new Commands.AssumeUnchanged(repo).View(); - Dispatcher.Invoke(() => { - if (unchanged.Count > 0) { - foreach (var file in unchanged) Files.Add(file); - - mask.Visibility = Visibility.Collapsed; - list.Visibility = Visibility.Visible; - list.ItemsSource = Files; - } else { - list.Visibility = Visibility.Collapsed; - mask.Visibility = Visibility.Visible; - } - }); - }); - } - - private void OnQuit(object sender, RoutedEventArgs e) { - Close(); - } - - private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = true; - } - - private void Remove(object sender, RoutedEventArgs e) { - var btn = sender as Button; - if (btn == null) return; - - var file = btn.DataContext as string; - if (file == null) return; - - new Commands.AssumeUnchanged(repo).Remove(file); - Files.Remove(file); - - if (Files.Count == 0) { - list.Visibility = Visibility.Collapsed; - mask.Visibility = Visibility.Visible; - } - - e.Handled = true; - } - } -} diff --git a/src/Views/AssumeUnchangedManager.axaml b/src/Views/AssumeUnchangedManager.axaml new file mode 100644 index 00000000..b7c59691 --- /dev/null +++ b/src/Views/AssumeUnchangedManager.axaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/AssumeUnchangedManager.axaml.cs b/src/Views/AssumeUnchangedManager.axaml.cs new file mode 100644 index 00000000..616db87d --- /dev/null +++ b/src/Views/AssumeUnchangedManager.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace SourceGit.Views { + public partial class AssumeUnchangedManager : Window { + public AssumeUnchangedManager() { + InitializeComponent(); + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + Close(); + } + } +} diff --git a/src/Views/Avatar.cs b/src/Views/Avatar.cs new file mode 100644 index 00000000..3feb04bb --- /dev/null +++ b/src/Views/Avatar.cs @@ -0,0 +1,122 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace SourceGit.Views { + public class Avatar : Control, Models.IAvatarHost { + private static readonly GradientStops[] FALLBACK_GRADIENTS = [ + new GradientStops() { new GradientStop(Colors.Orange, 0), new GradientStop(Color.FromRgb(255, 213, 134), 1) }, + new GradientStops() { new GradientStop(Colors.DodgerBlue, 0), new GradientStop(Colors.LightSkyBlue, 1) }, + new GradientStops() { new GradientStop(Colors.LimeGreen, 0), new GradientStop(Color.FromRgb(124, 241, 124), 1) }, + new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) }, + new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) }, + ]; + + public static readonly StyledProperty FallbackFontFamilyProperty = + AvaloniaProperty.Register(nameof(FallbackFontFamily)); + + public FontFamily FallbackFontFamily { + get => GetValue(FallbackFontFamilyProperty); + set => SetValue(FallbackFontFamilyProperty, value); + } + + public static readonly StyledProperty UserProperty = + AvaloniaProperty.Register(nameof(User)); + + public Models.User User { + get => GetValue(UserProperty); + set => SetValue(UserProperty, value); + } + + static Avatar() { + AffectsRender(FallbackFontFamilyProperty); + UserProperty.Changed.AddClassHandler(OnUserPropertyChanged); + } + + public Avatar() { + var refetch = new MenuItem() { Header = App.Text("RefetchAvatar") }; + refetch.Click += (o, e) => { + if (User != null) { + _image = Models.AvatarManager.Request(_emailMD5, true); + InvalidateVisual(); + } + }; + + ContextMenu = new ContextMenu(); + ContextMenu.Items.Add(refetch); + + Models.AvatarManager.Subscribe(this); + } + + public override void Render(DrawingContext context) { + if (User == null) return; + + float corner = (float)Math.Max(2, Bounds.Width / 16); + if (_image != null) { + var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); + context.PushClip(new RoundedRect(rect, corner)); + context.DrawImage(_image, rect); + } else { + Point textOrigin = new Point((Bounds.Width - _fallbackLabel.Width) * 0.5, (Bounds.Height - _fallbackLabel.Height) * 0.5); + context.DrawRectangle(_fallbackBrush, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner); + context.DrawText(_fallbackLabel, textOrigin); + } + } + + public void OnAvatarResourceReady(string md5, Bitmap bitmap) { + if (_emailMD5 == md5) { + _image = bitmap; + InvalidateVisual(); + } + } + + private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e) { + if (avatar.User == null) { + avatar._emailMD5 = null; + return; + } + + var placeholder = string.IsNullOrWhiteSpace(avatar.User.Name) ? "?" : avatar.User.Name.Substring(0, 1); + var chars = placeholder.ToCharArray(); + var sum = 0; + foreach (var c in chars) sum += Math.Abs(c); + + var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(avatar.User.Email.ToLower().Trim())); + var builder = new StringBuilder(); + foreach (var c in hash) builder.Append(c.ToString("x2")); + var md5 = builder.ToString(); + if (avatar._emailMD5 != md5) { + avatar._emailMD5 = md5; + avatar._image = Models.AvatarManager.Request(md5, false); + } + + avatar._fallbackBrush = new LinearGradientBrush { + GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length], + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }; + + var typeface = avatar.FallbackFontFamily == null ? Typeface.Default : new Typeface(avatar.FallbackFontFamily); + + avatar._fallbackLabel = new FormattedText( + placeholder, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + avatar.Width * 0.65, + Brushes.White); + + avatar.InvalidateVisual(); + } + + private FormattedText _fallbackLabel = null; + private LinearGradientBrush _fallbackBrush = null; + private string _emailMD5 = null; + private Bitmap _image = null; + } +} diff --git a/src/Views/Blame.axaml b/src/Views/Blame.axaml new file mode 100644 index 00000000..d3d054da --- /dev/null +++ b/src/Views/Blame.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs new file mode 100644 index 00000000..5492cba7 --- /dev/null +++ b/src/Views/Blame.axaml.cs @@ -0,0 +1,261 @@ +using Avalonia; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Styling; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; +using AvaloniaEdit.TextMate; +using AvaloniaEdit.Utils; +using System; +using System.Globalization; +using System.IO; +using TextMateSharp.Grammars; + +namespace SourceGit.Views { + public class BlameTextEditor : TextEditor { + public class CommitInfoMargin : AbstractMargin { + public CommitInfoMargin(BlameTextEditor editor) { + _editor = editor; + ClipToBounds = true; + } + + public override void Render(DrawingContext context) { + if (_editor.BlameData == null) return; + + var view = TextView; + if (view != null && view.VisualLinesValid) { + var typeface = view.CreateTypeface(); + var underlinePen = new Pen(Brushes.DarkOrange, 1); + + foreach (var line in view.VisualLines) { + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber > _editor.BlameData.LineInfos.Count) break; + + var info = _editor.BlameData.LineInfos[lineNumber - 1]; + if (!info.IsFirstInGroup) continue; + + var x = 0.0; + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + + var shaLink = new FormattedText( + info.CommitSHA, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.DarkOrange); + context.DrawText(shaLink, new Point(x, y)); + context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2)); + x += shaLink.Width + 8; + + var time = new FormattedText( + info.Time, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + _editor.Foreground); + context.DrawText(time, new Point(x, y)); + x += time.Width + 8; + + var author = new FormattedText( + info.Author, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + _editor.Foreground); + context.DrawText(author, new Point(x, y)); + } + } + } + + protected override Size MeasureOverride(Size availableSize) { + return new Size(250, 0); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + + var view = TextView; + if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid) { + var pos = e.GetPosition(this); + var typeface = view.CreateTypeface(); + + foreach (var line in view.VisualLines) { + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber >= _editor.BlameData.LineInfos.Count) break; + + var info = _editor.BlameData.LineInfos[lineNumber - 1]; + if (!info.IsFirstInGroup) continue; + + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + var shaLink = new FormattedText( + info.CommitSHA, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.DarkOrange); + + var rect = new Rect(0, y, shaLink.Width, shaLink.Height); + if (rect.Contains(pos)) { + _editor.OnCommitSHAClicked(info.CommitSHA); + e.Handled = true; + break; + } + } + } + } + + private BlameTextEditor _editor = null; + } + + public class VerticalSeperatorMargin : AbstractMargin { + public VerticalSeperatorMargin(BlameTextEditor editor) { + _editor = editor; + } + + public override void Render(DrawingContext context) { + var pen = new Pen(_editor.BorderBrush, 1); + context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); + } + + protected override Size MeasureOverride(Size availableSize) { + return new Size(1, 0); + } + + private BlameTextEditor _editor = null; + } + + public static readonly StyledProperty BlameDataProperty = + AvaloniaProperty.Register(nameof(BlameData)); + + public Models.BlameData BlameData { + get => GetValue(BlameDataProperty); + set => SetValue(BlameDataProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public BlameTextEditor() : base(new TextArea(), new TextDocument()) { + IsReadOnly = true; + ShowLineNumbers = false; + WordWrap = false; + } + + public void OnCommitSHAClicked(string sha) { + if (DataContext is ViewModels.Blame blame) { + blame.NavigateToCommit(sha); + } + } + + protected override void OnLoaded(RoutedEventArgs e) { + base.OnLoaded(e); + + TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); + TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); + TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) }); + TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); + TextArea.TextView.ContextRequested += OnTextViewContextRequested; + TextArea.TextView.Margin = new Thickness(4, 0); + + if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) { + _registryOptions = new RegistryOptions(ThemeName.DarkPlus); + } else { + _registryOptions = new RegistryOptions(ThemeName.LightPlus); + } + + _textMate = this.InstallTextMate(_registryOptions); + + if (BlameData != null) { + _textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File))); + } + } + + protected override void OnUnloaded(RoutedEventArgs e) { + base.OnUnloaded(e); + + TextArea.LeftMargins.Clear(); + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + + _registryOptions = null; + _textMate.Dispose(); + _textMate = null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); + + if (change.Property == BlameDataProperty) { + if (BlameData != null) { + Text = BlameData.Content; + if (_textMate != null) _textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File))); + } else { + Text = string.Empty; + } + } else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null && _textMate != null) { + if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) { + _textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.DarkPlus)); + } else { + _textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.LightPlus)); + } + } + } + + private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) { + var selected = SelectedText; + if (string.IsNullOrEmpty(selected)) return; + + var icon = new Avalonia.Controls.Shapes.Path(); + icon.Width = 10; + icon.Height = 10; + icon.Stretch = Stretch.Uniform; + icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = icon; + copy.Click += (o, ev) => { + App.CopyText(selected); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(copy); + menu.Open(TextArea.TextView); + e.Handled = true; + } + + private RegistryOptions _registryOptions = null; + private TextMate.Installation _textMate = null; + } + + public partial class Blame : Window { + public Blame() { + if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + Owner = desktop.MainWindow; + } + + InitializeComponent(); + } + + protected override void OnClosed(EventArgs e) { + base.OnClosed(e); + GC.Collect(); + } + + private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) { + if (DataContext is ViewModels.Blame blame) { + var txt = sender as TextBlock; + blame.NavigateToCommit(txt.Text); + } + e.Handled = true; + } + } +} diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml deleted file mode 100644 index f2c46655..00000000 --- a/src/Views/Blame.xaml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/Blame.xaml.cs b/src/Views/Blame.xaml.cs deleted file mode 100644 index 2a8c1598..00000000 --- a/src/Views/Blame.xaml.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Navigation; - -namespace SourceGit.Views { - /// - /// 逐行追溯 - /// - public partial class Blame : Controls.Window { - /// - /// DataGrid数据源结构 - /// - public class Record : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; - - /// - /// 原始Blame行数据 - /// - public Models.BlameLine Line { get; set; } - - /// - /// 是否是第一行 - /// - public bool IsFirstLine { get; set; } = false; - - /// - /// 前一行与本行的提交不同 - /// - public bool IsFirstLineInGroup { get; set; } = false; - - /// - /// 是否当前选中,会影响背景色 - /// - private bool isSelected = false; - public bool IsSelected { - get { return isSelected; } - set { - if (isSelected != value) { - isSelected = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsSelected")); - } - } - } - } - - /// - /// Blame数据 - /// - public ObservableCollection Records { get; set; } - - public Blame(string repo, string file, string revision) { - InitializeComponent(); - - this.repo = repo; - Records = new ObservableCollection(); - txtFile.Text = $"{file}@{revision.Substring(0, 8)}"; - - Task.Run(() => { - var lfs = new Commands.LFS(repo).IsFiltered(file); - if (lfs) { - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - notSupport.Visibility = Visibility.Visible; - }); - return; - } - - var rs = new Commands.Blame(repo, file, revision).Result(); - if (rs.IsBinary) { - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - notSupport.Visibility = Visibility.Visible; - }); - } else { - string lastSHA = null; - foreach (var line in rs.Lines) { - var r = new Record(); - r.Line = line; - r.IsSelected = false; - - if (line.CommitSHA != lastSHA) { - lastSHA = line.CommitSHA; - r.IsFirstLineInGroup = true; - } else { - r.IsFirstLineInGroup = false; - } - - Records.Add(r); - } - - if (Records.Count > 0) Records[0].IsFirstLine = true; - - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - blame.ItemsSource = Records; - }); - } - }); - } - - #region WINDOW_COMMANDS - private void Minimize(object sender, RoutedEventArgs e) { - SystemCommands.MinimizeWindow(this); - } - - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - #endregion - - #region EVENTS - private T GetVisualChild(DependencyObject parent) where T : Visual { - T child = null; - - int count = VisualTreeHelper.GetChildrenCount(parent); - for (int i = 0; i < count; i++) { - Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); - child = v as T; - - if (child == null) { - child = GetVisualChild(v); - } - - if (child != null) { - break; - } - } - - return child; - } - - private void OnViewerSizeChanged(object sender, SizeChangedEventArgs e) { - var total = blame.ActualWidth; - var offset = blame.NonFrozenColumnsViewportHorizontalOffset; - var minWidth = total - offset; - - var scroller = GetVisualChild(blame); - if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; - - blame.Columns[2].MinWidth = minWidth; - blame.Columns[2].Width = DataGridLength.SizeToCells; - blame.UpdateLayout(); - } - - private void OnViewerRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = true; - } - - private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { - var r = blame.SelectedItem as Record; - if (r == null) return; - - foreach (var one in Records) { - one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA; - } - } - - private void GotoCommit(object sender, RequestNavigateEventArgs e) { - Models.Watcher.Get(repo).NavigateTo(e.Uri.OriginalString); - e.Handled = true; - } - #endregion - - private string repo = null; - } -} diff --git a/src/Views/CaptionButtons.axaml b/src/Views/CaptionButtons.axaml new file mode 100644 index 00000000..581f7be1 --- /dev/null +++ b/src/Views/CaptionButtons.axaml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/Views/CaptionButtons.axaml.cs b/src/Views/CaptionButtons.axaml.cs new file mode 100644 index 00000000..09f70dad --- /dev/null +++ b/src/Views/CaptionButtons.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views { + public partial class CaptionButtons : UserControl { + public CaptionButtons() { + InitializeComponent(); + } + + private void MinimizeWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = WindowState.Minimized; + } + } + + private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.Close(); + } + } + } +} + diff --git a/src/Views/CaptionButtonsMacOS.axaml b/src/Views/CaptionButtonsMacOS.axaml new file mode 100644 index 00000000..fa3f1f34 --- /dev/null +++ b/src/Views/CaptionButtonsMacOS.axaml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/src/Views/CaptionButtonsMacOS.axaml.cs b/src/Views/CaptionButtonsMacOS.axaml.cs new file mode 100644 index 00000000..64b93d21 --- /dev/null +++ b/src/Views/CaptionButtonsMacOS.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views { + public partial class CaptionButtonsMacOS : UserControl { + public CaptionButtonsMacOS() { + InitializeComponent(); + } + + private void MinimizeWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = WindowState.Minimized; + } + } + + private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.Close(); + } + } + } +} + diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs new file mode 100644 index 00000000..8d0001e6 --- /dev/null +++ b/src/Views/ChangeStatusIcon.cs @@ -0,0 +1,112 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using System; +using System.Globalization; + +namespace SourceGit.Views { + public class ChangeStatusIcon : Control { + private static readonly IBrush[] BACKGROUNDS = [ + Brushes.Transparent, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + ]; + + private static readonly string[] INDICATOR = ["?", "±", "+", "−", "➜", "❏", "U", "★"]; + + public static readonly StyledProperty IsWorkingCopyChangeProperty = + AvaloniaProperty.Register(nameof(IsWorkingCopyChange)); + + public bool IsWorkingCopyChange { + get => GetValue(IsWorkingCopyChangeProperty); + set => SetValue(IsWorkingCopyChangeProperty, value); + } + + public static readonly StyledProperty ChangeProperty = + AvaloniaProperty.Register(nameof(Change)); + + public Models.Change Change { + get => GetValue(ChangeProperty); + set => SetValue(ChangeProperty, value); + } + + public static readonly StyledProperty IconFontFamilyProperty = + AvaloniaProperty.Register(nameof(IconFontFamily)); + + public FontFamily IconFontFamily { + get => GetValue(IconFontFamilyProperty); + set => SetValue(IconFontFamilyProperty, value); + } + + static ChangeStatusIcon() { + AffectsRender(IsWorkingCopyChangeProperty, ChangeProperty, IconFontFamilyProperty); + } + + public override void Render(DrawingContext context) { + if (Change == null || Bounds.Width <= 0) return; + + var typeface = IconFontFamily == null ? Typeface.Default : new Typeface(IconFontFamily); + + IBrush background = null; + string indicator; + if (IsWorkingCopyChange) { + if (Change.IsConflit) { + background = Brushes.OrangeRed; + indicator = "!"; + } else { + background = BACKGROUNDS[(int)Change.WorkTree]; + indicator = INDICATOR[(int)Change.WorkTree]; + } + } else { + background = BACKGROUNDS[(int)Change.Index]; + indicator = INDICATOR[(int)Change.Index]; + } + + var txt = new FormattedText( + indicator, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + Bounds.Width * 0.8, + Brushes.White); + + float corner = (float)Math.Max(2, Bounds.Width / 16); + Point textOrigin = new Point((Bounds.Width - txt.Width) * 0.5, (Bounds.Height - txt.Height) * 0.5); + context.DrawRectangle(background, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner); + context.DrawText(txt, textOrigin); + } + } +} diff --git a/src/Views/ChangeViewModeSwitcher.axaml b/src/Views/ChangeViewModeSwitcher.axaml new file mode 100644 index 00000000..32d4f1e9 --- /dev/null +++ b/src/Views/ChangeViewModeSwitcher.axaml @@ -0,0 +1,35 @@ + + + diff --git a/src/Views/ChangeViewModeSwitcher.axaml.cs b/src/Views/ChangeViewModeSwitcher.axaml.cs new file mode 100644 index 00000000..59a041e9 --- /dev/null +++ b/src/Views/ChangeViewModeSwitcher.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class ChangeViewModeSwitcher : UserControl { + public static readonly StyledProperty ViewModeProperty = + AvaloniaProperty.Register(nameof(ViewMode)); + + public Models.ChangeViewMode ViewMode { + get => GetValue(ViewModeProperty); + set => SetValue(ViewModeProperty, value); + } + + public ChangeViewModeSwitcher() { + DataContext = this; + InitializeComponent(); + } + + public void SwitchMode(object param) { + ViewMode = (Models.ChangeViewMode)param; + } + } +} diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml new file mode 100644 index 00000000..f921326b --- /dev/null +++ b/src/Views/Checkout.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/Views/Checkout.axaml.cs b/src/Views/Checkout.axaml.cs new file mode 100644 index 00000000..d2d9edca --- /dev/null +++ b/src/Views/Checkout.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class Checkout : UserControl { + public Checkout() { + InitializeComponent(); + } + } +} diff --git a/src/Views/CherryPick.axaml b/src/Views/CherryPick.axaml new file mode 100644 index 00000000..a4b654fd --- /dev/null +++ b/src/Views/CherryPick.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/src/Views/CherryPick.axaml.cs b/src/Views/CherryPick.axaml.cs new file mode 100644 index 00000000..306d4702 --- /dev/null +++ b/src/Views/CherryPick.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class CherryPick : UserControl { + public CherryPick() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Cleanup.axaml b/src/Views/Cleanup.axaml new file mode 100644 index 00000000..7e5d8ff9 --- /dev/null +++ b/src/Views/Cleanup.axaml @@ -0,0 +1,19 @@ + + + + + + diff --git a/src/Views/Cleanup.axaml.cs b/src/Views/Cleanup.axaml.cs new file mode 100644 index 00000000..a098d9a4 --- /dev/null +++ b/src/Views/Cleanup.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class Cleanup : UserControl { + public Cleanup() { + InitializeComponent(); + } + } +} diff --git a/src/Views/ClearStashes.axaml b/src/Views/ClearStashes.axaml new file mode 100644 index 00000000..666bf273 --- /dev/null +++ b/src/Views/ClearStashes.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/src/Views/ClearStashes.axaml.cs b/src/Views/ClearStashes.axaml.cs new file mode 100644 index 00000000..d95e18c1 --- /dev/null +++ b/src/Views/ClearStashes.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class ClearStashes : UserControl { + public ClearStashes() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Clone.axaml b/src/Views/Clone.axaml new file mode 100644 index 00000000..ca3a4d3f --- /dev/null +++ b/src/Views/Clone.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Clone.axaml.cs b/src/Views/Clone.axaml.cs new file mode 100644 index 00000000..cd52e0bf --- /dev/null +++ b/src/Views/Clone.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Clone : UserControl { + public Clone() { + InitializeComponent(); + } + + private async void SelectParentFolder(object sender, RoutedEventArgs e) { + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) { + txtParentFolder.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + + private async void SelectSSHKey(object sender, RoutedEventArgs e) { + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtSSHKey.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/Clone.xaml b/src/Views/Clone.xaml deleted file mode 100644 index 07536e82..00000000 --- a/src/Views/Clone.xaml +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs new file mode 100644 index 00000000..db65922d --- /dev/null +++ b/src/Views/CommitChanges.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class CommitChanges : UserControl { + public CommitChanges() { + InitializeComponent(); + } + + private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { + if (sender is DataGrid datagrid && datagrid.IsVisible && datagrid.SelectedItem != null) { + datagrid.ScrollIntoView(datagrid.SelectedItem, null); + } + e.Handled = true; + } + + private void OnDataGridContextRequested(object sender, ContextRequestedEventArgs e) { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null) { + var detail = DataContext as ViewModels.CommitDetail; + var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); + menu.Open(datagrid); + } + + e.Handled = true; + } + + private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) { + if (sender is TreeView view && view.SelectedItem != null) { + var detail = DataContext as ViewModels.CommitDetail; + var node = view.SelectedItem as ViewModels.FileTreeNode; + if (node != null && !node.IsFolder) { + var menu = detail.CreateChangeContextMenu(node.Backend as Models.Change); + menu.Open(view); + } + } + + e.Handled = true; + } + } +} diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml new file mode 100644 index 00000000..47d9fde7 --- /dev/null +++ b/src/Views/CommitDetail.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs new file mode 100644 index 00000000..326c612d --- /dev/null +++ b/src/Views/CommitDetail.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views { + public partial class CommitDetail : UserControl { + public CommitDetail() { + InitializeComponent(); + } + + private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) { + if (DataContext is ViewModels.CommitDetail detail) { + var datagrid = sender as DataGrid; + detail.ActivePageIndex = 1; + detail.SelectedChange = datagrid.SelectedItem as Models.Change; + } + e.Handled = true; + } + + private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) { + if (DataContext is ViewModels.CommitDetail detail) { + var datagrid = sender as DataGrid; + var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); + menu.Open(datagrid); + } + e.Handled = true; + } + } +} diff --git a/src/Views/ConfirmDialog.xaml b/src/Views/ConfirmDialog.xaml deleted file mode 100644 index 867fedb1..00000000 --- a/src/Views/ConfirmDialog.xaml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -