diff --git a/README.md b/README.md index 63bbe45f..22e909bf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Opensource Git GUI client. * Supports Windows/macOS/Linux * Opensource/Free * Fast -* English/简体中文 +* English/简体中文/繁體中文 * Built-in light/dark themes * Visual commit graph * Supports SSH access with each remote @@ -75,6 +75,7 @@ This app supports open repository in external tools listed in the table below. > * You can set the given environment variable for special tool if it can NOT be found by this app automatically. > * Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. +> * On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. ## Screenshots diff --git a/VERSION b/VERSION index d9316e8b..b14fccec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.15 \ No newline at end of file +8.16 \ No newline at end of file diff --git a/build/resources/app/App.plist b/build/resources/app/App.plist index 01d93ad7..3ccf7335 100644 --- a/build/resources/app/App.plist +++ b/build/resources/app/App.plist @@ -12,6 +12,11 @@ SOURCE_GIT_VERSION.0 LSMinimumSystemVersion 10.12 + LSEnvironment + + PATH + /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + CFBundleExecutable SourceGit CFBundleInfoDictionaryVersion diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 7269b252..af2e9913 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -1,10 +1,12 @@ -using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace SourceGit { [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ViewModels.Preference))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml b/src/App.axaml index fff2ca0d..d73ea45d 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -13,6 +13,7 @@ + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index df491dd0..69917120 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -132,32 +132,48 @@ namespace SourceGit var app = Current as App; var targetLocale = app.Resources[localeKey] as ResourceDictionary; if (targetLocale == null || targetLocale == app._activeLocale) - { return; - } if (app._activeLocale != null) - { app.Resources.MergedDictionaries.Remove(app._activeLocale); - } app.Resources.MergedDictionaries.Add(targetLocale); app._activeLocale = targetLocale; } - public static void SetTheme(string theme) + public static void SetTheme(string theme, string colorsFile) { + var app = Current as App; + if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) - { - Current.RequestedThemeVariant = ThemeVariant.Light; - } + app.RequestedThemeVariant = ThemeVariant.Light; else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) - { - Current.RequestedThemeVariant = ThemeVariant.Dark; - } + app.RequestedThemeVariant = ThemeVariant.Dark; else + app.RequestedThemeVariant = ThemeVariant.Default; + + if (app._colorOverrides != null) { - Current.RequestedThemeVariant = ThemeVariant.Default; + app.Resources.MergedDictionaries.Remove(app._colorOverrides); + app._colorOverrides = null; + } + + if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile)) + { + try + { + var resDic = new ResourceDictionary(); + + var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.DictionaryStringString); + foreach (var kv in schema) + resDic[kv.Key] = Color.Parse(kv.Value); + + app.Resources.MergedDictionaries.Add(resDic); + app._colorOverrides = resDic; + } + catch + { + } } } @@ -166,9 +182,7 @@ namespace SourceGit if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { if (desktop.MainWindow.Clipboard is { } clipbord) - { await clipbord.SetTextAsync(data); - } } } @@ -256,7 +270,7 @@ namespace SourceGit var pref = ViewModels.Preference.Instance; SetLocale(pref.Locale); - SetTheme(pref.Theme); + SetTheme(pref.Theme, pref.ColorOverrides); } public override void OnFrameworkInitializationCompleted() @@ -300,6 +314,7 @@ namespace SourceGit } private ResourceDictionary _activeLocale = null; + private ResourceDictionary _colorOverrides = null; private Models.INotificationReceiver _notificationReceiver = null; } } diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index ed5282f7..9200db4b 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -52,12 +52,12 @@ namespace SourceGit.Commands var refName = parts[0]; if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return; - + if (refName.StartsWith(PREFIX_DETACHED, StringComparison.Ordinal)) { branch.IsHead = true; } - + if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { branch.Name = refName.Substring(PREFIX_LOCAL.Length); diff --git a/src/Commands/QueryCommitFullMessage.cs b/src/Commands/QueryCommitFullMessage.cs new file mode 100644 index 00000000..f4be9bc5 --- /dev/null +++ b/src/Commands/QueryCommitFullMessage.cs @@ -0,0 +1,19 @@ +namespace SourceGit.Commands +{ + public class QueryCommitFullMessage : Command + { + public QueryCommitFullMessage(string repo, string sha) + { + WorkingDirectory = repo; + Context = repo; + Args = $"show --no-show-signature --pretty=format:%B -s {sha}"; + } + + public string Result() + { + var rs = ReadToEnd(); + if (rs.IsSuccess) return rs.StdOut.TrimEnd(); + return string.Empty; + } + } +} diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 6788884c..075d6467 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -5,133 +5,101 @@ namespace SourceGit.Commands { public class QueryCommits : Command { - private const string GPGSIG_START = "gpgsig -----BEGIN "; - private const string GPGSIG_END = " -----END "; - - private readonly List commits = new List(); - private Models.Commit current = null; - private bool isSkipingGpgsig = false; - private bool isHeadFounded = false; - private readonly bool findFirstMerged = true; - public QueryCommits(string repo, string limits, bool needFindHead = true) { WorkingDirectory = repo; Context = repo; - Args = "log --date-order --decorate=full --pretty=raw " + limits; - findFirstMerged = needFindHead; + Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits; + _findFirstMerged = needFindHead; } public List Result() { - Exec(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return _commits; - if (current != null) + var nextPartIdx = 0; + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + while (end > 0) { - current.Message = current.Message.Trim(); - commits.Add(current); + var line = rs.StdOut.Substring(start, end - start); + switch (nextPartIdx) + { + case 0: + _current = new Models.Commit() { SHA = line }; + _commits.Add(_current); + break; + case 1: + ParseParent(line); + break; + case 2: + ParseDecorators(line); + break; + case 3: + _current.Author = Models.User.FindOrAdd(line); + break; + case 4: + _current.AuthorTime = ulong.Parse(line); + break; + case 5: + _current.Committer = Models.User.FindOrAdd(line); + break; + case 6: + _current.CommitterTime = ulong.Parse(line); + break; + case 7: + _current.Subject = line; + nextPartIdx = -1; + break; + default: + break; + } + + nextPartIdx++; + + start = end + 1; + end = rs.StdOut.IndexOf('\n', start); } - if (findFirstMerged && !isHeadFounded && commits.Count > 0) - { + if (_findFirstMerged && !_isHeadFounded && _commits.Count > 0) MarkFirstMerged(); - } - return commits; + return _commits; } - protected override void OnReadline(string line) + private void ParseParent(string data) { - if (isSkipingGpgsig) - { - if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) - isSkipingGpgsig = false; + if (data.Length < 8) return; - } - else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) + + var idx = data.IndexOf(' ', StringComparison.Ordinal); + if (idx == -1) { - isSkipingGpgsig = true; + _current.Parents.Add(data); return; } - if (line.StartsWith("commit ", StringComparison.Ordinal)) - { - if (current != null) - { - current.Message = current.Message.Trim(); - commits.Add(current); - } - - current = new Models.Commit(); - line = line.Substring(7); - - var decoratorStart = line.IndexOf('(', StringComparison.Ordinal); - if (decoratorStart < 0) - { - current.SHA = line.Trim(); - } - else - { - current.SHA = line.Substring(0, decoratorStart).Trim(); - current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1)); - if (!isHeadFounded) - isHeadFounded = current.IsMerged; - } - - return; - } - - if (current == null) - return; - - if (line.StartsWith("tree ", StringComparison.Ordinal)) - { - return; - } - else if (line.StartsWith("parent ", StringComparison.Ordinal)) - { - current.Parents.Add(line.Substring("parent ".Length)); - } - else if (line.StartsWith("author ", StringComparison.Ordinal)) - { - Models.User user = Models.User.Invalid; - ulong time = 0; - Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); - current.Author = user; - current.AuthorTime = time; - } - else if (line.StartsWith("committer ", StringComparison.Ordinal)) - { - Models.User user = Models.User.Invalid; - ulong time = 0; - Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); - current.Committer = user; - current.CommitterTime = time; - } - else if (string.IsNullOrEmpty(current.Subject)) - { - current.Subject = line.Trim(); - } - else - { - current.Message += (line.Trim() + "\n"); - } + _current.Parents.Add(data.Substring(0, idx)); + _current.Parents.Add(data.Substring(idx + 1)); } - private bool ParseDecorators(List decorators, string data) + private void ParseDecorators(string data) { - bool isHeadOfCurrent = false; + if (data.Length < 3) + return; - var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var sub in subs) { var d = sub.Trim(); if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { - decorators.Add(new Models.Decorator() + _current.Decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.Tag, - Name = d.Substring(15).Trim(), + Name = d.Substring(15), }); } else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) @@ -140,58 +108,55 @@ namespace SourceGit.Commands } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { - isHeadOfCurrent = true; - decorators.Add(new Models.Decorator() + _current.IsMerged = true; + _current.Decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.CurrentBranchHead, - Name = d.Substring(19).Trim(), + Name = d.Substring(19), }); } else if (d.Equals("HEAD")) { - isHeadOfCurrent = true; - decorators.Add(new Models.Decorator() + _current.IsMerged = true; + _current.Decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.CurrentCommitHead, - Name = d.Trim(), + Name = d, }); } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { - decorators.Add(new Models.Decorator() + _current.Decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.LocalBranchHead, - Name = d.Substring(11).Trim(), + Name = d.Substring(11), }); } else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) { - decorators.Add(new Models.Decorator() + _current.Decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.RemoteBranchHead, - Name = d.Substring(13).Trim(), + Name = d.Substring(13), }); } } - decorators.Sort((l, r) => + _current.Decorators.Sort((l, r) => { if (l.Type != r.Type) - { return (int)l.Type - (int)r.Type; - } else - { - return l.Name.CompareTo(r.Name); - } + return string.Compare(l.Name, r.Name, StringComparison.Ordinal); }); - return isHeadOfCurrent; + if (_current.IsMerged && !_isHeadFounded) + _isHeadFounded = true; } private void MarkFirstMerged() { - Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\""; + Args = $"log --since=\"{_commits[_commits.Count - 1].CommitterTimeStr}\" --format=\"%H\""; var rs = ReadToEnd(); var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -202,7 +167,7 @@ namespace SourceGit.Commands foreach (var sha in shas) set.Add(sha); - foreach (var c in commits) + foreach (var c in _commits) { if (set.Contains(c.SHA)) { @@ -211,5 +176,10 @@ namespace SourceGit.Commands } } } + + private List _commits = new List(); + private Models.Commit _current = null; + private bool _findFirstMerged = false; + private bool _isHeadFounded = false; } } diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs index 7a3db057..bcad9129 100644 --- a/src/Commands/QueryRevisionObjects.cs +++ b/src/Commands/QueryRevisionObjects.cs @@ -5,22 +5,23 @@ namespace SourceGit.Commands { public partial class QueryRevisionObjects : Command { - [GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")] private static partial Regex REG_FORMAT(); - private readonly List objects = new List(); - public QueryRevisionObjects(string repo, string sha) + public QueryRevisionObjects(string repo, string sha, string parentFolder) { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree -r {sha}"; + Args = $"ls-tree {sha}"; + + if (!string.IsNullOrEmpty(parentFolder)) + Args += $" -- \"{parentFolder}\""; } public List Result() { Exec(); - return objects; + return _objects; } protected override void OnReadline(string line) @@ -50,7 +51,9 @@ namespace SourceGit.Commands break; } - objects.Add(obj); + _objects.Add(obj); } + + private List _objects = new List(); } } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index 5c0fd760..4a553817 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -1,100 +1,50 @@ using System; using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { public class QuerySingleCommit : Command { - private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; - private const string GPGSIG_END = " -----END PGP SIGNATURE-----"; - - public QuerySingleCommit(string repo, string sha) { + public QuerySingleCommit(string repo, string sha) + { WorkingDirectory = repo; Context = repo; - Args = $"show --pretty=raw --decorate=full -s {sha}"; + Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}"; } public Models.Commit Result() { - var succ = Exec(); - if (!succ) - return null; - - _commit.Message.Trim(); - return _commit; - } - - protected override void OnReadline(string line) - { - if (isSkipingGpgsig) + var rs = ReadToEnd(); + if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) { - if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) - isSkipingGpgsig = false; - return; - } - else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) - { - isSkipingGpgsig = true; - return; + var commit = new Models.Commit(); + var lines = rs.StdOut.Split('\n'); + if (lines.Length < 8) + return null; + + commit.SHA = lines[0]; + if (!string.IsNullOrEmpty(lines[1])) + commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries)); + if (!string.IsNullOrEmpty(lines[2])) + commit.IsMerged = ParseDecorators(commit.Decorators, lines[2]); + commit.Author = Models.User.FindOrAdd(lines[3]); + commit.AuthorTime = ulong.Parse(lines[4]); + commit.Committer = Models.User.FindOrAdd(lines[5]); + commit.CommitterTime = ulong.Parse(lines[6]); + commit.Subject = lines[7]; + + return commit; } - if (line.StartsWith("commit ", StringComparison.Ordinal)) - { - line = line.Substring(7); - - var decoratorStart = line.IndexOf('(', StringComparison.Ordinal); - if (decoratorStart < 0) - { - _commit.SHA = line.Trim(); - } - else - { - _commit.SHA = line.Substring(0, decoratorStart).Trim(); - ParseDecorators(_commit.Decorators, line.Substring(decoratorStart + 1)); - } - - return; - } - - if (line.StartsWith("tree ", StringComparison.Ordinal)) - { - return; - } - else if (line.StartsWith("parent ", StringComparison.Ordinal)) - { - _commit.Parents.Add(line.Substring("parent ".Length)); - } - else if (line.StartsWith("author ", StringComparison.Ordinal)) - { - Models.User user = Models.User.Invalid; - ulong time = 0; - Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); - _commit.Author = user; - _commit.AuthorTime = time; - } - else if (line.StartsWith("committer ", StringComparison.Ordinal)) - { - Models.User user = Models.User.Invalid; - ulong time = 0; - Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); - _commit.Committer = user; - _commit.CommitterTime = time; - } - else if (string.IsNullOrEmpty(_commit.Subject)) - { - _commit.Subject = line.Trim(); - } - else - { - _commit.Message += (line.Trim() + "\n"); - } + return null; } private bool ParseDecorators(List decorators, string data) { bool isHeadOfCurrent = false; - var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var sub in subs) { var d = sub.Trim(); @@ -103,7 +53,7 @@ namespace SourceGit.Commands decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.Tag, - Name = d.Substring(15).Trim(), + Name = d.Substring(15), }); } else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) @@ -116,7 +66,7 @@ namespace SourceGit.Commands decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.CurrentBranchHead, - Name = d.Substring(19).Trim(), + Name = d.Substring(19), }); } else if (d.Equals("HEAD")) @@ -125,7 +75,7 @@ namespace SourceGit.Commands decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.CurrentCommitHead, - Name = d.Trim(), + Name = d, }); } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) @@ -133,7 +83,7 @@ namespace SourceGit.Commands decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.LocalBranchHead, - Name = d.Substring(11).Trim(), + Name = d.Substring(11), }); } else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) @@ -141,7 +91,7 @@ namespace SourceGit.Commands decorators.Add(new Models.Decorator() { Type = Models.DecoratorType.RemoteBranchHead, - Name = d.Substring(13).Trim(), + Name = d.Substring(13), }); } } @@ -149,19 +99,12 @@ namespace SourceGit.Commands decorators.Sort((l, r) => { if (l.Type != r.Type) - { return (int)l.Type - (int)r.Type; - } else - { - return l.Name.CompareTo(r.Name); - } + return string.Compare(l.Name, r.Name, StringComparison.Ordinal); }); return isHeadOfCurrent; } - - private Models.Commit _commit = new Models.Commit(); - private bool isSkipingGpgsig = false; } } diff --git a/src/Commands/QueryStashes.cs b/src/Commands/QueryStashes.cs index 5362f87b..6d089f8e 100644 --- a/src/Commands/QueryStashes.cs +++ b/src/Commands/QueryStashes.cs @@ -1,64 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Collections.Generic; namespace SourceGit.Commands { - public partial class QueryStashes : Command + public class QueryStashes : Command { - - [GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")] - private static partial Regex REG_STASH(); - public QueryStashes(string repo) { WorkingDirectory = repo; Context = repo; - Args = "stash list --pretty=raw"; + Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s"; } public List Result() { Exec(); - if (_current != null) - _stashes.Add(_current); return _stashes; } protected override void OnReadline(string line) { - if (line.StartsWith("commit ", StringComparison.Ordinal)) + switch (_nextLineIdx) { - if (_current != null && !string.IsNullOrEmpty(_current.Name)) + case 0: + _current = new Models.Stash() { SHA = line }; _stashes.Add(_current); - _current = new Models.Stash() { SHA = line.Substring(7, 8) }; - return; + break; + case 1: + _current.Time = ulong.Parse(line); + break; + case 2: + _current.Name = line; + break; + case 3: + _current.Message = line; + break; } - if (_current == null) - return; - - if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) - { - var match = REG_STASH().Match(line); - if (match.Success) - _current.Name = match.Groups[1].Value; - } - else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) - { - _current.Message = line.Substring(16); - } - else if (line.StartsWith("author ", StringComparison.Ordinal)) - { - Models.User user = Models.User.Invalid; - ulong time = 0; - Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); - _current.Author = user; - _current.Time = time; - } + _nextLineIdx++; + if (_nextLineIdx > 3) + _nextLineIdx = 0; } private readonly List _stashes = new List(); private Models.Stash _current = null; + private int _nextLineIdx = 0; } } diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index 428c10d1..3c4460ea 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -29,9 +29,10 @@ namespace SourceGit.Commands } } - public bool Update() + public bool Update(Action outputHandler) { Args = $"submodule update --rebase --remote"; + _outputHandler = outputHandler; return Exec(); } diff --git a/src/Converters/ChangeViewModeConverters.cs b/src/Converters/ChangeViewModeConverters.cs index 01bc1774..a5b07bca 100644 --- a/src/Converters/ChangeViewModeConverters.cs +++ b/src/Converters/ChangeViewModeConverters.cs @@ -19,14 +19,5 @@ namespace SourceGit.Converters return App.Current?.FindResource("Icons.Tree") as StreamGeometry; } }); - - public static readonly FuncValueConverter IsList = - new FuncValueConverter(v => v == Models.ChangeViewMode.List); - - public static readonly FuncValueConverter IsGrid = - new FuncValueConverter(v => v == Models.ChangeViewMode.Grid); - - public static readonly FuncValueConverter IsTree = - new FuncValueConverter(v => v == Models.ChangeViewMode.Tree); } } diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs index 9f3d9447..e19cb37c 100644 --- a/src/Converters/DecoratorTypeConverters.cs +++ b/src/Converters/DecoratorTypeConverters.cs @@ -38,8 +38,8 @@ namespace SourceGit.Converters }); public static readonly FuncValueConverter ToFontWeight = - new FuncValueConverter(v => - v is Models.DecoratorType.CurrentBranchHead or Models.DecoratorType.CurrentCommitHead + new FuncValueConverter(v => + v is Models.DecoratorType.CurrentBranchHead or Models.DecoratorType.CurrentCommitHead ? FontWeight.Bold : FontWeight.Regular ); } diff --git a/src/Converters/LauncherPageConverters.cs b/src/Converters/LauncherPageConverters.cs deleted file mode 100644 index 05eec2b1..00000000 --- a/src/Converters/LauncherPageConverters.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; - -using Avalonia.Collections; -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class LauncherPageConverters - { - public static readonly FuncMultiValueConverter ToTabSeperatorVisible = - new FuncMultiValueConverter(v => - { - if (v == null) - return false; - - var array = new List(); - array.AddRange(v); - if (array.Count != 3) - return false; - - var self = array[0] as ViewModels.LauncherPage; - if (self == null) - return false; - - var selected = array[1] as ViewModels.LauncherPage; - var collections = array[2] as AvaloniaList; - - if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected))) - { - return false; - } - else - { - return true; - } - }); - } -} diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index aa687f23..ac1785c0 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -30,17 +30,11 @@ namespace SourceGit.Converters { var theme = (string)value; if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) - { return ThemeVariant.Light; - } else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) - { return ThemeVariant.Dark; - } else - { return ThemeVariant.Default; - } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) @@ -70,7 +64,7 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToShortSHA = new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); - + public static readonly FuncValueConverter UnderRecommendGitVersion = new(v => { diff --git a/src/Converters/WindowStateConverters.cs b/src/Converters/WindowStateConverters.cs index 7122dc1f..05f0b1cd 100644 --- a/src/Converters/WindowStateConverters.cs +++ b/src/Converters/WindowStateConverters.cs @@ -12,30 +12,20 @@ namespace SourceGit.Converters new FuncValueConverter(state => { if (OperatingSystem.IsWindows() && state == WindowState.Maximized) - { return new Thickness(6); - } else if (OperatingSystem.IsLinux() && state != WindowState.Maximized) - { return new Thickness(6); - } else - { return new Thickness(0); - } }); public static readonly FuncValueConverter ToTitleBarHeight = new FuncValueConverter(state => { if (state == WindowState.Maximized) - { return new GridLength(OperatingSystem.IsMacOS() ? 34 : 30); - } else - { return new GridLength(38); - } }); public static readonly FuncValueConverter IsNormal = diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index d8355e81..363b4b08 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -13,7 +13,6 @@ namespace SourceGit.Models public User Committer { get; set; } = User.Invalid; public ulong CommitterTime { get; set; } = 0; public string Subject { get; set; } = string.Empty; - public string Message { get; set; } = string.Empty; public List Parents { get; set; } = new List(); public List Decorators { get; set; } = new List(); public bool HasDecorators => Decorators.Count > 0; @@ -25,31 +24,8 @@ namespace SourceGit.Models public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd"); public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); - public bool IsCommitterVisible - { - get => Author != Committer || AuthorTime != CommitterTime; - } - - public bool IsCurrentHead - { - get => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; - } - - public string FullMessage - { - get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}"; - } - - public static void ParseUserAndTime(string data, ref User user, ref ulong time) - { - var userEndIdx = data.IndexOf('>', StringComparison.Ordinal); - if (userEndIdx < 0) - return; - - var timeEndIdx = data.IndexOf(' ', userEndIdx + 2); - user = User.FindOrAdd(data.Substring(0, userEndIdx)); - time = timeEndIdx < 0 ? 0 : ulong.Parse(data.Substring(userEndIdx + 2, timeEndIdx - userEndIdx - 2)); - } + public bool IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime; + public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 201a1e69..6b26eba5 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -2,11 +2,23 @@ using System.Collections.Generic; using Avalonia; +using Avalonia.Media; namespace SourceGit.Models { public class CommitGraph { + public static readonly Pen[] Pens = [ + new Pen(Brushes.Orange, 2), + new Pen(Brushes.ForestGreen, 2), + new Pen(Brushes.Gold, 2), + new Pen(Brushes.Magenta, 2), + new Pen(Brushes.Red, 2), + new Pen(Brushes.Gray, 2), + new Pen(Brushes.Turquoise, 2), + new Pen(Brushes.Olive, 2), + ]; + public class Path { public List Points = new List(); @@ -101,12 +113,12 @@ namespace SourceGit.Models public List Links { get; set; } = new List(); public List Dots { get; set; } = new List(); - public static CommitGraph Parse(List commits, double rowHeight, int colorCount) + public static CommitGraph Parse(List commits, int colorCount) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; - double UNIT_HEIGHT = rowHeight; - double HALF_HEIGHT = rowHeight / 2; + double UNIT_HEIGHT = 28; + double HALF_HEIGHT = 14; var temp = new CommitGraph(); var unsolved = new List(); diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index d9d21031..a0a02c6f 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.RegularExpressions; +using Avalonia; using Avalonia.Media.Imaging; namespace SourceGit.Models @@ -62,6 +63,7 @@ namespace SourceGit.Models { public string File { get; set; } = string.Empty; public List Lines { get; set; } = new List(); + public Vector SyncScrollOffset { get; set; } = Vector.Zero; public int MaxLineNumber = 0; public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) @@ -568,8 +570,11 @@ namespace SourceGit.Models public Bitmap Old { get; set; } = null; public Bitmap New { get; set; } = null; - public string OldSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0"; - public string NewSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; + public long OldFileSize { get; set; } = 0; + public long NewFileSize { get; set; } = 0; + + public string OldImageSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0"; + public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; } public class NoOrEOLChange @@ -582,10 +587,16 @@ namespace SourceGit.Models public string New { get; set; } = string.Empty; } + public class SubmoduleRevision + { + public Commit Commit { get; set; } = null; + public string FullMessage { get; set; } = string.Empty; + } + public class SubmoduleDiff { - public Commit Old { get; set; } = null; - public Commit New { get; set; } = null; + public SubmoduleRevision Old { get; set; } = null; + public SubmoduleRevision New { get; set; } = null; } public class DiffResult diff --git a/src/Models/FileTreeNode.cs b/src/Models/FileTreeNode.cs deleted file mode 100644 index ad1298c9..00000000 --- a/src/Models/FileTreeNode.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace SourceGit.Models -{ - public class FileTreeNode - { - public string FullPath { get; set; } = string.Empty; - public bool IsFolder { get; set; } = false; - public bool IsExpanded { get; set; } = false; - public object Backend { get; set; } = null; - public List Children { get; set; } = new List(); - - public static List Build(List changes, bool expanded) - { - var nodes = new List(); - var folders = new Dictionary(); - - foreach (var c in changes) - { - var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal); - if (sepIdx == -1) - { - nodes.Add(new FileTreeNode() - { - FullPath = c.Path, - Backend = c, - IsFolder = false, - IsExpanded = false - }); - } - else - { - FileTreeNode lastFolder = null; - var start = 0; - - while (sepIdx != -1) - { - var folder = c.Path.Substring(0, sepIdx); - if (folders.TryGetValue(folder, out var value)) - { - lastFolder = value; - } - else if (lastFolder == null) - { - lastFolder = new FileTreeNode() - { - FullPath = folder, - Backend = null, - IsFolder = true, - IsExpanded = expanded - }; - nodes.Add(lastFolder); - folders.Add(folder, lastFolder); - } - else - { - var cur = new FileTreeNode() - { - FullPath = folder, - Backend = null, - IsFolder = true, - IsExpanded = expanded - }; - folders.Add(folder, cur); - lastFolder.Children.Add(cur); - lastFolder = cur; - } - - start = sepIdx + 1; - sepIdx = c.Path.IndexOf('/', start); - } - - lastFolder.Children.Add(new FileTreeNode() - { - FullPath = c.Path, - Backend = c, - IsFolder = false, - IsExpanded = false - }); - } - } - - folders.Clear(); - Sort(nodes); - return nodes; - } - - public static List Build(List files, bool expanded) - { - var nodes = new List(); - var folders = new Dictionary(); - - foreach (var f in files) - { - var sepIdx = f.Path.IndexOf('/', StringComparison.Ordinal); - if (sepIdx == -1) - { - nodes.Add(new FileTreeNode() - { - FullPath = f.Path, - Backend = f, - IsFolder = false, - IsExpanded = false - }); - } - else - { - FileTreeNode lastFolder = null; - var start = 0; - - while (sepIdx != -1) - { - var folder = f.Path.Substring(0, sepIdx); - if (folders.TryGetValue(folder, out var value)) - { - lastFolder = value; - } - else if (lastFolder == null) - { - lastFolder = new FileTreeNode() - { - FullPath = folder, - Backend = null, - IsFolder = true, - IsExpanded = expanded - }; - nodes.Add(lastFolder); - folders.Add(folder, lastFolder); - } - else - { - var cur = new FileTreeNode() - { - FullPath = folder, - Backend = null, - IsFolder = true, - IsExpanded = expanded - }; - folders.Add(folder, cur); - lastFolder.Children.Add(cur); - lastFolder = cur; - } - - start = sepIdx + 1; - sepIdx = f.Path.IndexOf('/', start); - } - - lastFolder.Children.Add(new FileTreeNode() - { - FullPath = f.Path, - Backend = f, - IsFolder = false, - IsExpanded = false - }); - } - } - - folders.Clear(); - Sort(nodes); - return nodes; - } - - private static void Sort(List nodes) - { - nodes.Sort((l, r) => - { - if (l.IsFolder == r.IsFolder) - { - return l.FullPath.CompareTo(r.FullPath); - } - else - { - return l.IsFolder ? -1 : 1; - } - }); - - foreach (var node in nodes) - { - if (node.Children.Count > 1) - Sort(node.Children); - } - } - } -} diff --git a/src/Models/GPGFormat.cs b/src/Models/GPGFormat.cs index bf3b3678..0ba4e9e2 100644 --- a/src/Models/GPGFormat.cs +++ b/src/Models/GPGFormat.cs @@ -2,24 +2,18 @@ namespace SourceGit.Models { - public class GPGFormat(string name, string value, string desc) + public class GPGFormat(string name, string value, string desc, string program, bool needFindProgram) { public string Name { get; set; } = name; public string Value { get; set; } = value; public string Desc { get; set; } = desc; + public string Program { get; set; } = program; + public bool NeedFindProgram { get; set; } = needFindProgram; - public static readonly GPGFormat OPENPGP = new GPGFormat("OPENPGP", "openpgp", "DEFAULT"); - - public static readonly GPGFormat SSH = new GPGFormat("SSH", "ssh", "Git >= 2.34.0"); - - public static readonly List Supported = new List() { - OPENPGP, - SSH, - }; - - public bool Equals(GPGFormat other) - { - return Value == other.Value; - } + public static readonly List Supported = [ + new GPGFormat("OPENPGP", "openpgp", "DEFAULT", "gpg", true), + new GPGFormat("X.509", "x509", "", "gpgsm", true), + new GPGFormat("SSH", "ssh", "Requires Git >= 2.34.0", "ssh-keygen", false), + ]; } } diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 022da0fe..cbb1e7f1 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -10,6 +10,7 @@ namespace SourceGit.Models public static readonly List Supported = new List() { new Locale("English", "en_US"), new Locale("简体中文", "zh_CN"), + new Locale("繁體中文", "zh_TW"), }; public Locale(string name, string key) diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 2376959a..2fab0f2f 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -8,7 +8,6 @@ namespace SourceGit.Models public string Name { get; set; } = ""; public string SHA { get; set; } = ""; - public User Author { get; set; } = User.Invalid; public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs index b016d739..be9d7d7c 100644 --- a/src/Models/TreeDataGridSelectionModel.cs +++ b/src/Models/TreeDataGridSelectionModel.cs @@ -42,23 +42,15 @@ namespace SourceGit.Models public void Select(IEnumerable items) { - var sets = new HashSet(); - foreach (var item in items) - sets.Add(item); - using (BatchUpdate()) { Clear(); - int num = _source.Rows.Count; - for (int i = 0; i < num; ++i) + foreach (var selected in items) { - var m = _source.Rows[i].Model as TModel; - if (m != null && sets.Contains(m)) - { - var idx = _source.Rows.RowIndexToModelIndex(i); + var idx = GetModelIndex(_source.Items, selected, IndexPath.Unselected); + if (!idx.Equals(IndexPath.Unselected)) Select(idx); - } } } } @@ -190,7 +182,7 @@ namespace SourceGit.Models { var focus = _source.Rows[row.RowIndex]; if (focus is IExpander expander && HasChildren(focus)) - expander.IsExpanded = !expander.IsExpanded; + expander.IsExpanded = !expander.IsExpanded; else _rowDoubleTapped?.Invoke(this, e); @@ -431,6 +423,30 @@ namespace SourceGit.Models return _childrenGetter?.Invoke(node); } + private IndexPath GetModelIndex(IEnumerable collection, TModel model, IndexPath parent) + { + int i = 0; + + foreach (var item in collection) + { + var index = parent.Append(i); + if (item != null && item == model) + return index; + + var children = GetChildren(item); + if (children != null) + { + var findInChildren = GetModelIndex(children, model, index); + if (!findInChildren.Equals(IndexPath.Unselected)) + return findInChildren; + } + + i++; + } + + return IndexPath.Unselected; + } + private bool HasChildren(IRow row) { var children = GetChildren(row.Model as TModel); @@ -439,7 +455,7 @@ namespace SourceGit.Models foreach (var c in children) return true; } - + return false; } } diff --git a/src/Models/User.cs b/src/Models/User.cs index 5a93e135..cb0d21cd 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; namespace SourceGit.Models { @@ -27,8 +28,8 @@ namespace SourceGit.Models { return _caches.GetOrAdd(data, key => { - var nameEndIdx = key.IndexOf('<', System.StringComparison.Ordinal); - var name = nameEndIdx >= 2 ? key.Substring(0, nameEndIdx - 1) : string.Empty; + var nameEndIdx = key.IndexOf('±', StringComparison.Ordinal); + var name = nameEndIdx > 0 ? key.Substring(0, nameEndIdx) : string.Empty; var email = key.Substring(nameEndIdx + 1); return new User() { Name = name, Email = email }; diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index b6369398..8f43742c 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -9,7 +9,6 @@ M30 0 30 30 0 15z M0 0 0 30 30 15z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z - M512 945c-238 0-433-195-433-433S274 79 512 79c238 0 433 195 433 433S750 945 512 945M512 0C228 0 0 228 0 512s228 512 512 512 512-228 512-512-228-512-512-512zM752 477H364l128-128a38 38 0 000-55 38 38 0 00-55 0l-185 183a55 55 0 00-16 39c0 16 6 30 16 39l185 185a39 39 0 0028 12 34 34 0 0028-14 38 38 0 000-55l-128-128h386c22 0 41-18 41-39a39 39 0 00-39-39 M576 832C576 867 547 896 512 896 477 896 448 867 448 832 448 797 477 768 512 768 547 768 576 797 576 832ZM512 256C477 256 448 285 448 320L448 640C448 675 477 704 512 704 547 704 576 675 576 640L576 320C576 285 547 256 512 256ZM1024 896C1024 967 967 1024 896 1024L128 1024C57 1024 0 967 0 896 0 875 5 855 14 837L14 837 398 69 398 69C420 28 462 0 512 0 562 0 604 28 626 69L1008 835C1018 853 1024 874 1024 896ZM960 896C960 885 957 875 952 865L952 864 951 863 569 98C557 77 536 64 512 64 488 64 466 77 455 99L452 105 92 825 93 825 71 867C66 876 64 886 64 896 64 931 93 960 128 960L896 960C931 960 960 931 960 896Z M608 0q48 0 88 23t63 63 23 87v70h55q35 0 67 14t57 38 38 57 14 67V831q0 34-14 66t-38 57-57 38-67 13H426q-34 0-66-13t-57-38-38-57-14-66v-70h-56q-34 0-66-14t-57-38-38-57-13-67V174q0-47 23-87T109 23 196 0h412m175 244H426q-46 0-86 22T278 328t-26 85v348H608q47 0 86-22t63-62 25-85l1-348m-269 318q18 0 31 13t13 31-13 31-31 13-31-13-13-31 13-31 31-13m0-212q13 0 22 9t11 22v125q0 14-9 23t-22 10-23-7-11-22l-1-126q0-13 10-23t23-10z m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z @@ -98,4 +97,5 @@ M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z + M248 221a77 77 0 00-30-21c-18-7-40-10-68-5a224 224 0 00-45 13c-5 2-10 5-15 8l-3 2v68l11-9c10-8 21-14 34-19 13-5 26-7 39-7 12 0 21 3 28 10 6 6 9 16 9 29l-62 9c-14 2-26 6-36 11a80 80 0 00-25 20c-7 8-12 17-15 27-6 21-6 44 1 65a70 70 0 0041 43c10 4 21 6 34 6a80 80 0 0063-28v22h64V298c0-16-2-31-6-44a91 91 0 00-18-33zm-41 121v15c0 8-1 15-4 22a48 48 0 01-24 29 44 44 0 01-33 2 29 29 0 01-10-6 25 25 0 01-6-9 30 30 0 01-2-12c0-5 1-9 2-14a21 21 0 015-9 28 28 0 0110-7 83 83 0 0120-5l42-6zm323-68a144 144 0 00-16-42 87 87 0 00-28-29 75 75 0 00-41-11 73 73 0 00-44 14c-6 5-12 11-17 17V64H326v398h59v-18c8 10 18 17 30 21 6 2 13 3 21 3 16 0 31-4 43-11 12-7 23-18 31-31a147 147 0 0019-46 248 248 0 006-57c0-17-2-33-5-49zm-55 49c0 15-1 28-4 39-2 11-6 20-10 27a41 41 0 01-15 15 37 37 0 01-36 1 44 44 0 01-13-12 59 59 0 01-9-18A76 76 0 01384 352v-33c0-10 1-20 4-29 2-8 6-15 10-22a43 43 0 0115-13 37 37 0 0119-5 35 35 0 0132 18c4 6 7 14 9 23 2 9 3 20 3 31zM154 634a58 58 0 0120-15c14-6 35-7 49-1 7 3 13 6 20 12l21 17V572l-6-4a124 124 0 00-58-14c-20 0-38 4-54 11-16 7-30 17-41 30-12 13-20 29-26 46-6 17-9 36-9 57 0 18 3 36 8 52 6 16 14 30 24 42 10 12 23 21 38 28 15 7 32 10 50 10 15 0 28-2 39-5 11-3 21-8 30-14l5-4v-57l-13 6a26 26 0 01-5 2c-3 1-6 2-8 3-2 1-15 6-15 6-4 2-9 3-14 4a63 63 0 01-38-4 53 53 0 01-20-14 70 70 0 01-13-24 111 111 0 01-5-34c0-13 2-26 5-36 3-10 8-19 14-26zM896 384h-256V320h288c21 1 32 12 32 32v384c0 18-12 32-32 32H504l132 133-45 45-185-185c-16-21-16-25 0-45l185-185L637 576l-128 128H896V384z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index edf3fe1a..856577d7 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -93,10 +93,7 @@ Search Changes ... FILES LFS File - Search Files ... Submodule - Tag - Tree INFORMATION AUTHOR CHANGED @@ -166,6 +163,7 @@ SUBMODULE NEW Syntax Highlighting + Line Word Wrap Open In Merge Tool Decrease Number of Visible Lines Increase Number of Visible Lines @@ -285,6 +283,7 @@ Paste Preference APPEARANCE + Custom Color Schema Default Font Default Font Size Monospace Font @@ -314,7 +313,7 @@ Commit GPG signing Tag GPG signing GPG Format - Install Path + Program Install Path Input path for installed gpg program User Signing Key User's gpg signing key @@ -385,7 +384,9 @@ ADD REMOTE RESOLVE Search Commit - Search Author/Committer/Message/SHA + Search By + Information + File Statistics SUBMODULES ADD SUBMODULE @@ -458,6 +459,8 @@ Delete${0}$ Push${0}$ URL : + Update Submodules + Run `submodule update` command for this repository. Warning Create Group Create Sub-Group diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 015cb6cc..e5b46759 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -96,10 +96,7 @@ 查找变更... 文件列表 LFS文件 - 查找文件... 子模块 - 标签文件 - 子树 基本信息 修改者 变更列表 @@ -169,6 +166,7 @@ 子模块 新增 语法高亮 + 自动换行 使用外部合并工具查看 减少可见的行数 增加可见的行数 @@ -288,6 +286,7 @@ 粘贴 偏好设置 外观配置 + 自定义配色文件 缺省字体 默认字体大小 等宽字体 @@ -316,9 +315,9 @@ GPG签名 启用提交签名 启用标签签名 - GPG签名格式 - 可执行文件位置 - gpg.exe所在路径 + 签名格式 + 签名程序位置 + 签名程序所在路径 用户签名KEY 输入签名提交所使用的KEY 外部合并工具 @@ -388,7 +387,9 @@ 添加远程 解决冲突 查找提交 - 支持搜索作者/提交者/主题/指纹 + 搜索途径 + 摘要 + 文件 提交统计 子模块列表 添加子模块 @@ -461,6 +462,8 @@ 删除${0}$ 推送${0}$ 仓库地址 : + 更新子模块 + 为此仓库执行`submodule update`命令,更新所有的子模块。 警告 新建分组 新建子分组 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml new file mode 100644 index 00000000..ae7a6d47 --- /dev/null +++ b/src/Resources/Locales/zh_TW.axaml @@ -0,0 +1,500 @@ + + + + + 關於軟體 + 關於本軟體 + • 專案依賴於 + © 2024 sourcegit-scm + • 文字編輯器使用 + • 等寬字型來自於 + • 專案原始碼地址 + 開源免費的Git客戶端 + 應用補丁(apply) + 錯誤 + 輸出錯誤,並終止應用補丁 + 更多錯誤 + 與【錯誤】級別相似,但輸出內容更多 + 補丁檔案 : + 選擇補丁檔案 + 忽略空白符號 + 忽略 + 關閉所有警告 + 應用補丁 + 警告 + 應用補丁,輸出關於空白符的警告 + 空白符號處理 : + 存檔(archive) ... + 存檔檔案路徑: + 選擇存檔檔案的存放路徑 + 指定的提交: + 存檔 + 不跟蹤更改的檔案 + 沒有不跟蹤更改的檔案 + 移除 + 二進位制檔案不支援該操作!!! + 逐行追溯(blame) + 選中檔案不支援該操作!!! + 檢出(checkout)${0}$ + 與當前HEAD比較 + 與本地工作樹比較 + 複製分支名 + 刪除${0}$ + 刪除選中的 {0} 個分支 + 放棄所有更改 + 快進(fast-forward)到${0}$ + GIT工作流 - 完成${0}$ + 合併${0}$到${1}$ + 拉回(pull)${0}$ + 拉回(pull)${0}$內容至${1}$ + 推送(push)${0}$ + 變基(rebase)${0}$分支至${1}$ + 重新命名${0}$ + 切換上游分支... + 取消追蹤 + 位元組 + 取 消 + 切換變更顯示模式 + 檔名+路徑列表模式 + 全路徑列表模式 + 檔案目錄樹形結構模式 + 檢出(checkout)分支 + 檢出(checkout)提交 + 注意:執行該操作後,當前HEAD會變為遊離(detached)狀態! + 提交 : + 目標分支 : + 未提交更改 : + 丟棄更改 + 不做處理 + 儲藏並自動恢復 + 挑選(cherry-pick)此提交 + 提交ID : + 提交變化 + 挑選提交 + 丟棄儲藏確認 + 您正在丟棄所有的儲藏,一經操作,無法回退,是否繼續? + 克隆遠端倉庫 + 額外引數 : + 其他克隆引數,選填。 + 本地倉庫名 : + 本地倉庫目錄的名字,選填。 + 父級目錄 : + 遠端倉庫 : + 關閉 + 挑選(cherry-pick)此提交 + 檢出此提交 + 與當前HEAD比較 + 與本地工作樹比較 + 複製提交指紋 + 變基(rebase)${0}$到此處 + 重置(reset)${0}$到此處 + 回滾此提交 + 編輯提交資訊 + 另存為補丁 ... + 合併此提交到上一個提交 + 變更對比 + 查詢變更... + 檔案列表 + LFS檔案 + 子模組 + 基本資訊 + 修改者 + 變更列表 + 提交者 + 提交資訊 + 父提交 + 相關引用 + 提交指紋 + 倉庫配置 + 電子郵箱 + 郵箱地址 + HTTP代理 + HTTP網路代理 + 使用者名稱 + 應用於本倉庫的使用者名稱 + 複製 + 複製路徑 + 複製檔名 + 新建分支 + 新分支基於 : + 完成後切換到新分支 + 未提交更改 : + 丟棄更改 + 不做處理 + 儲藏並自動恢復 + 新分支名 : + 填寫分支名稱。 + 建立本地分支 + 新建標籤 + 標籤位於 : + 使用GPG簽名 + 標籤描述 : + 選填。 + 標籤名 : + 推薦格式 :v1.0.0-alpha + 推送到所有遠端倉庫 + 型別 : + 附註標籤 + 輕量標籤 + 剪下 + 刪除分支確認 + 分支名 : + 您正在刪除遠端上的分支,請務必小心!!! + 同時刪除遠端分支${0}$ + 刪除多個分支 + 您正在嘗試一次性刪除多個分支,請務必仔細檢查後再執行操作! + 刪除遠端確認 + 遠端名 : + 目標 : + 刪除分組確認 + 刪除倉庫確認 + 刪除子模組確認 + 子模組路徑 : + 刪除標籤確認 + 標籤名 : + 同時刪除遠端倉庫中的此標籤 + 二進位制檔案 + 當前大小 + 原始大小 + 複製 + 檔案許可權已變化 + LFS物件變更 + 下一個差異 + 沒有變更或僅有換行符差異 + 上一個差異 + 分列對比 + 子模組 + 新增 + 語法高亮 + 自動換行 + 使用外部合併工具檢視 + 減少可見的行數 + 增加可見的行數 + 請選擇需要對比的檔案 + 使用外部比對工具檢視 + 放棄更改確認 + 所有本地址未提交的修改。 + 需要放棄的變更 : + 總計{0}項選中更改 + 本操作不支援回退,請確認後繼續!!! + 書籤 : + 名稱 : + 目標 : + 編輯分組 + 編輯倉庫 + 快進(fast-forward,無需checkout) + 拉取(fetch) + 拉取所有的遠端倉庫 + 自動清理遠端已刪除分支 + 遠端倉庫 : + 拉取遠端倉庫內容 + 不跟蹤此檔案的更改 + 放棄更改... + 放棄 {0} 個檔案的更改... + 放棄選中的更改 + 使用外部合併工具開啟 + 另存為補丁... + 暫存(add)... + 暫存(add){0} 個檔案... + 暫存選中的更改 + 儲藏(stash)... + 儲藏(stash)選中的 {0} 個檔案... + 從暫存中移除 + 從暫存中移除 {0} 個檔案 + 從暫存中移除選中的更改 + 使用 THEIRS (checkout --theirs) + 使用 MINE (checkout --ours) + 檔案歷史 + 過濾 + GIT工作流 + 開發分支 : + 特性分支 : + 特性分支名字首 : + 結束特性分支 + 結束脩復分支 + 結束版本分支 + 目標分支 : + 修復分支 : + 修復分支名字首 : + 初始化GIT工作流 + 保留分支 + 釋出分支 : + 版本分支 : + 版本分支名字首 : + 開始特性分支... + 開始特性分支 + 開始修復分支... + 開始修復分支 + 輸入分支名 + 開始版本分支... + 開始版本分支 + 版本標籤字首 : + 歷史記錄 + 切換橫向/縱向顯示 + 切換曲線/折線顯示 + 查詢提交指紋、資訊、作者。回車鍵開始,ESC鍵取消 + 清空 + 已選中 {0} 項提交 + 快捷鍵參考 + 全域性快捷鍵 + 取消彈出面板 + 關閉當前頁面 + 切換到上一個頁面 + 切換到下一個頁面 + 新建頁面 + 開啟偏好設定面板 + 倉庫頁面快捷鍵 + 重新載入倉庫狀態 + 將選中的變更暫存或從暫存列表中移除 + 開啟歷史搜尋 + 顯示本地更改 + 顯示歷史記錄 + 顯示儲藏列表 + 文字編輯器 + 關閉搜尋 + 定位到下一個匹配搜尋的位置 + 定位到上一個匹配搜尋的位置 + 開啟搜尋 + 初始化新倉庫 + 路徑 : + 選擇目錄不是有效的Git倉庫。是否需要在此目錄執行`git init`操作? + 挑選(Cherry-Pick)操作進行中。點選【終止】回滾到操作前的狀態。 + 合併操作進行中。點選【終止】回滾到操作前的狀態。 + 變基(Rebase)操作進行中。點選【終止】回滾到操作前的狀態。 + 回滾提交操作進行中。點選【終止】回滾到操作前的狀態。 + Source Git + 出錯了 + 系統提示 + 主選單 + 合併分支 + 目標分支 : + 合併方式 : + 合併分支 : + 名稱 : + GIT尚未配置。請開啟【偏好設定】配置GIT路徑。 + 系統提示 + 選擇資料夾 + 開啟檔案... + 選填。 + 新建空白頁 + 設定書籤 + 關閉標籤頁 + 關閉其他標籤頁 + 關閉右側標籤頁 + 複製倉庫路徑 + 新標籤頁 + 貼上 + 偏好設定 + 外觀配置 + 自訂配色檔 + 預設字型 + 預設字型大小 + 等寬字型 + 主題 + 通用配置 + 頭像服務 + 啟動時檢測軟體更新 + 顯示語言 + 最大歷史提交數 + 啟動時恢復上次開啟的倉庫 + 使用固定寬度的標題欄標籤 + GIT配置 + 啟用定時自動拉取遠端更新 + 自動拉取間隔 + 分鐘 + 自動換行轉換 + 預設克隆路徑 + 郵箱 + 預設GIT使用者郵箱 + 安裝路徑 + 終端Shell + 使用者名稱 + 預設GIT使用者名稱 + Git 版本 + 本軟體要求GIT最低版本為2.23.0 + GPG簽名 + 啟用提交簽名 + 啟用標籤簽名 + GPG簽名格式 + 可執行檔案位置 + gpg.exe所在路徑 + 使用者簽名KEY + 輸入簽名提交所使用的KEY + 外部合併工具 + 對比模式啟動引數 + 合併模式啟動引數 + 安裝路徑 + 填寫工具可執行檔案所在位置 + 工具 + 拉回(pull) + 拉取分支 : + 本地分支 : + 未提交更改 : + 丟棄更改 + 不做處理 + 儲藏並自動恢復 + 遠端 : + 拉回(拉取併合並) + 使用變基方式合併分支 + 推送(push) + 啟用強制推送 + 本地分支 : + 遠端倉庫 : + 推送到遠端倉庫 + 遠端分支 : + 跟蹤遠端分支 + 同時推送標籤 + 推送標籤到遠端倉庫 + 推送到所有遠端倉庫 + 遠端倉庫 : + 標籤 : + 退出 + 變基(rebase)操作 + 自動儲藏並恢復本地變更 + 目標提交 : + 分支 : + 重新載入 + 新增遠端倉庫 + 編輯遠端倉庫 + 遠端名 : + 唯一遠端名 + 倉庫地址 : + 遠端倉庫的地址 + 複製遠端地址 + 刪除 ... + 編輯 ... + 拉取(fetch)更新 ... + 清理遠端已刪除分支 + 目標 : + 分支重新命名 + 新的名稱 : + 新的分支名不能與現有分支名相同 + 分支 : + 終止合併 + 清理本倉庫(GC) + 本操作將執行`gc`,對於啟用LFS的倉庫也會執行`lfs prune`。 + 配置本倉庫 + 下一步 + 在檔案瀏覽器中開啟 + 過濾顯示分支 + 本地分支 + 定位HEAD + 新建分支 + 在 {0} 中開啟 + 使用外部工具開啟 + 重新載入 + 遠端列表 + 新增遠端 + 解決衝突 + 查詢提交 + 查詢方式 + 摘要 + 檔案 + 提交統計 + 子模組列表 + 新增子模組 + 更新子模組 + 標籤列表 + 新建標籤 + 在終端中開啟 + 工作區 + 遠端倉庫地址 + 重置(reset)當前分支到指定版本 + 重置模式 : + 提交 : + 當前分支 : + 在檔案瀏覽器中檢視 + 回滾操作確認 + 目標提交 : + 回滾後提交更改 + 編輯提交資訊 + 提交資訊: + 提交: + 執行操作中,請耐心等待... + 保 存 + 另存為... + 補丁已成功儲存! + 檢測更新... + 檢測到軟體有版本更新: + 獲取最新版本資訊失敗! + 下 載 + 忽略此版本 + 軟體更新 + 當前已是最新版本。 + 合併HEAD到上一個提交 + 當前提交 : + 修改提交資訊: + 合併到 : + SSH金鑰 : + SSH金鑰檔案 + 開 始 + 儲藏(stash) + 包含未跟蹤的檔案 + 資訊 : + 選填,用於命名此儲藏 + 儲藏本地變更 + 應用(apply) + 刪除(drop) + 應用並刪除(pop) + 丟棄儲藏確認 + 丟棄儲藏 : + 儲藏列表 + 檢視變更 + 儲藏列表 + 提交統計 + 提交次數 + 提交者 + 本月 + 本週 + 本年 + 提交次數: + 提交者: + 子模組 + 新增子模組 + 複製路徑 + 拉取子孫模組 + 開啟倉庫 + 相對倉庫路徑 : + 本地存放的相對路徑。 + 刪除子模組 + 確 定 + 複製標籤名 + 刪除${0}$ + 推送${0}$ + 倉庫地址 : + 更新子模組 + 本操作將執行 `submodule update` 。 + 警告 + 新建分組 + 新建子分組 + 克隆遠端倉庫 + 刪除 + 支援拖放目錄新增。支援自定義分組。 + 編輯 + 開啟本地倉庫 + 開啟終端 + 快速查詢倉庫... + 排序 + 本地更改 + 修補(--amend) + 現在您已可將其加入暫存區中 + 提交 + 提交併推送 + 填寫提交資訊 + CTRL + Enter + 檢測到衝突 + 檔案衝突已解決 + 最近輸入的提交資訊 + 顯示未跟蹤檔案 + 歷史提交資訊 + 沒有提交資訊記錄 + 已暫存 + 從暫存區移除選中 + 從暫存區移除所有 + 未暫存 + 暫存選中 + 暫存所有 + 檢視忽略變更檔案 + 請選中衝突檔案,開啟右鍵選單,選擇合適的解決方式 + 本地工作樹 + diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index c373d891..114ac72d 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1045,32 +1045,33 @@ - - - - - + + + + + - + HorizontalAlignment="Center" + IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" /> + + + + + + + + + + - + - + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 75806e59..a7373acb 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -16,7 +16,7 @@ namespace SourceGit.Views { var datagrid = sender as DataGrid; detail.ActivePageIndex = 1; - detail.SelectedChanges = new () { datagrid.SelectedItem as Models.Change }; + detail.SelectedChanges = new() { datagrid.SelectedItem as Models.Change }; } e.Handled = true; diff --git a/src/Views/ContextMenuExtension.cs b/src/Views/ContextMenuExtension.cs index c03a20de..6e6dbf20 100644 --- a/src/Views/ContextMenuExtension.cs +++ b/src/Views/ContextMenuExtension.cs @@ -8,8 +8,9 @@ namespace SourceGit.Views { public static void OpenContextMenu(this Control control, ContextMenu menu) { - if (menu == null) return; - + if (menu == null) + return; + menu.PlacementTarget = control; menu.Closing += OnContextMenuClosing; // Clear context menu because it is dynamic. diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index 9705715c..081abdf0 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -30,7 +30,7 @@ - + @@ -41,7 +41,7 @@ - + + + + + - + - + @@ -157,7 +167,7 @@ - + @@ -166,58 +176,70 @@ - - + + - + + + - + - + + + - - - + + + + + - - - - 0,0,0,4 - 0 - 0 - 8 - 16 - 16 - - + + + + + + + + 0,0,0,4 + 0 + 0 + 8 + 16 + 16 + + + - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 650d52e2..a726f57c 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -23,7 +23,7 @@ IsReadOnly="True" HeadersVisibility="None" Focusable="False" - RowHeight="{Binding DataGridRowHeight}" + RowHeight="28" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" LayoutUpdated="OnCommitDataGridLayoutUpdated" @@ -68,7 +68,7 @@ diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 508645d4..37ad441d 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -69,17 +69,6 @@ namespace SourceGit.Views public class CommitGraph : Control { - public static readonly Pen[] Pens = [ - new Pen(Brushes.Orange, 2), - new Pen(Brushes.ForestGreen, 2), - new Pen(Brushes.Gold, 2), - new Pen(Brushes.Magenta, 2), - new Pen(Brushes.Red, 2), - new Pen(Brushes.Gray, 2), - new Pen(Brushes.Turquoise, 2), - new Pen(Brushes.Olive, 2), - ]; - public static readonly StyledProperty GraphProperty = AvaloniaProperty.Register(nameof(Graph)); @@ -151,7 +140,7 @@ namespace SourceGit.Views if (dot.Center.Y > bottom) break; - context.DrawEllipse(dotFill, Pens[dot.Color], dot.Center, 3, 3); + context.DrawEllipse(dotFill, Models.CommitGraph.Pens[dot.Color], dot.Center, 3, 3); } } @@ -168,7 +157,7 @@ namespace SourceGit.Views continue; var geo = new StreamGeometry(); - var pen = Pens[line.Color]; + var pen = Models.CommitGraph.Pens[line.Color]; using (var ctx = geo.Open()) { var started = false; @@ -238,7 +227,7 @@ namespace SourceGit.Views ctx.QuadraticBezierTo(link.Control, link.End); } - context.DrawGeometry(null, Pens[link.Color], geo); + context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo); } } } diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index 4ba10c30..16e242bc 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -125,11 +125,9 @@ CommandParameter="{Binding}" InputGesture="{OnPlatform Ctrl+W, macOS=⌘+W}"/> + Command="{Binding #me.((vm:Launcher)DataContext).CloseOtherTabs}"/> + Command="{Binding #me.((vm:Launcher)DataContext).CloseRightTabs}"/> @@ -210,15 +208,11 @@ - - - - - - - - - + diff --git a/src/Views/LoadingIcon.axaml.cs b/src/Views/LoadingIcon.axaml.cs index 5a7e0014..62efed1d 100644 --- a/src/Views/LoadingIcon.axaml.cs +++ b/src/Views/LoadingIcon.axaml.cs @@ -18,7 +18,7 @@ namespace SourceGit.Views { base.OnLoaded(e); - if (IsVisible) + if (IsVisible) StartAnim(); } @@ -37,7 +37,7 @@ namespace SourceGit.Views if (IsVisible) StartAnim(); else - StopAnim(); + StopAnim(); } } diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 46279470..d7bc08a6 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -144,7 +144,7 @@ - + + + + + + + + @@ -409,7 +424,7 @@ - + + SelectedItem="{Binding #me.GPGFormat, Mode=TwoWay}"> @@ -433,12 +448,14 @@ + Margin="0,0,16,0" + IsVisible="{Binding #me.GPGFormat.NeedFindProgram}"/> + Watermark="{DynamicResource Text.Preference.GPG.Path.Placeholder}" + IsVisible="{Binding #me.GPGFormat.NeedFindProgram}"> - @@ -461,17 +493,17 @@ - + @@ -497,7 +529,27 @@ - + + + + + + + + + + + @@ -540,7 +593,7 @@ - new Commands.Submodule(repo.FullPath).Update()); - iconSubmoduleUpdate.Classes.Remove("rotating"); - repo.SetWatcherEnabled(true); - } - - e.Handled = true; - } - private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) { if (node == null || node.IsRemote) return; - + if (node.IsFolder) { foreach (var child in node.Children) diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 871a632a..2d466a08 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -14,7 +14,7 @@ Classes="bold" Text="{DynamicResource Text.Configure}"/> - + - - - - - - - - - - - - - - - diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 82fd5265..27976d26 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -20,8 +20,8 @@ IsHitTestVisible="False" User="{Binding StartPoint.Author}"/> - - + + @@ -44,8 +44,8 @@ IsHitTestVisible="False" User="{Binding Author}"/> - - + + @@ -56,8 +56,8 @@ - - + + diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 82d2651c..9c999061 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -17,48 +17,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + Children { get; set; } = new List(); + + public bool IsFolder => Backend != null && Backend.Type == Models.ObjectType.Tree; + public string Name => Backend != null ? Path.GetFileName(Backend.Path) : string.Empty; + } + + public class RevisionFileTreeView : UserControl + { + public static readonly StyledProperty RevisionProperty = + AvaloniaProperty.Register(nameof(Revision), null); + + public string Revision + { + get => GetValue(RevisionProperty); + set => SetValue(RevisionProperty, value); + } + + public Models.Object SelectedObject + { + get; + private set; + } = null; + + protected override Type StyleKeyOverride => typeof(UserControl); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RevisionProperty) + { + SelectedObject = null; + + if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable) + disposable.Dispose(); + + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + { + Content = null; + GC.Collect(); + return; + } + + var objects = vm.GetRevisionFilesUnderFolder(null); + if (objects == null || objects.Count == 0) + { + Content = null; + GC.Collect(); + return; + } + + var toplevelObjects = new List(); + foreach (var obj in objects) + toplevelObjects.Add(new RevisionFileTreeNode() { Backend = obj }); + + toplevelObjects.Sort((l, r) => + { + if (l.IsFolder == r.IsFolder) + return l.Name.CompareTo(r.Name); + return l.IsFolder ? -1 : 1; + }); + + var template = this.FindResource("RevisionFileTreeNodeTemplate") as IDataTemplate; + var source = new HierarchicalTreeDataGridSource(toplevelObjects) + { + Columns = + { + new HierarchicalExpanderColumn( + new TemplateColumn(null, template, null, GridLength.Auto), + GetChildrenOfTreeNode, + x => x.IsFolder, + x => x.IsExpanded) + } + }; + + var selection = new Models.TreeDataGridSelectionModel(source, GetChildrenOfTreeNode); + selection.SingleSelect = true; + selection.SelectionChanged += (s, _) => + { + if (s is Models.TreeDataGridSelectionModel model) + { + var node = model.SelectedItem; + var detail = DataContext as ViewModels.CommitDetail; + + if (node != null && !node.IsFolder) + { + SelectedObject = node.Backend; + detail.ViewRevisionFile(node.Backend); + } + else + { + SelectedObject = null; + detail.ViewRevisionFile(null); + } + } + }; + + source.Selection = selection; + Content = new TreeDataGrid() + { + AutoDragDropRows = false, + ShowColumnHeaders = false, + CanUserResizeColumns = false, + CanUserSortColumns = false, + Source = source, + }; + + GC.Collect(); + } + } + + private List GetChildrenOfTreeNode(RevisionFileTreeNode node) + { + if (!node.IsFolder) + return null; + + if (node.Children.Count > 0) + return node.Children; + + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return null; + + var objects = vm.GetRevisionFilesUnderFolder(node.Backend.Path + "/"); + if (objects == null || objects.Count == 0) + return null; + + foreach (var obj in objects) + node.Children.Add(new RevisionFileTreeNode() { Backend = obj }); + + node.Children.Sort((l, r) => + { + if (l.IsFolder == r.IsFolder) + return l.Name.CompareTo(r.Name); + return l.IsFolder ? -1 : 1; + }); + + return node.Children; + } + } + public class RevisionImageFileView : Control { public static readonly StyledProperty SourceProperty = @@ -59,9 +208,7 @@ namespace SourceGit.Views var source = Source; if (source != null) - { context.DrawImage(source, new Rect(source.Size), new Rect(8, 8, Bounds.Width - 16, Bounds.Height - 16)); - } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -79,9 +226,7 @@ namespace SourceGit.Views { var source = Source; if (source == null) - { return availableSize; - } var w = availableSize.Width - 16; var h = availableSize.Height - 16; @@ -89,13 +234,9 @@ namespace SourceGit.Views if (size.Width <= w) { if (size.Height <= h) - { return new Size(size.Width + 16, size.Height + 16); - } else - { return new Size(h * size.Width / size.Height + 16, availableSize.Height); - } } else { @@ -130,12 +271,6 @@ namespace SourceGit.Views base.OnLoaded(e); TextArea.TextView.ContextRequested += OnTextViewContextRequested; - - _textMate = Models.TextMateHelper.CreateForEditor(this); - if (DataContext is Models.RevisionTextFile source) - { - Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); - } } protected override void OnUnloaded(RoutedEventArgs e) @@ -143,13 +278,6 @@ namespace SourceGit.Views base.OnUnloaded(e); TextArea.TextView.ContextRequested -= OnTextViewContextRequested; - - if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } - GC.Collect(); } @@ -159,20 +287,9 @@ namespace SourceGit.Views var source = DataContext as Models.RevisionTextFile; if (source != null) - { Text = source.Content; - Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); - } - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null) - { - Models.TextMateHelper.SetThemeByApp(_textMate); - } + else + Text = string.Empty; } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) @@ -202,8 +319,6 @@ namespace SourceGit.Views TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } - - private TextMate.Installation _textMate = null; } public partial class RevisionFiles : UserControl @@ -213,15 +328,14 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnFileContextRequested(object sender, ContextRequestedEventArgs e) + private void OnRevisionFileTreeViewContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.CommitDetail vm && sender is TreeDataGrid tree) + if (DataContext is ViewModels.CommitDetail vm && sender is RevisionFileTreeView view) { - var selected = tree.RowSelection.SelectedItem as Models.FileTreeNode; - if (selected != null && !selected.IsFolder && selected.Backend is Models.Object obj) + if (view.SelectedObject != null && view.SelectedObject.Type != Models.ObjectType.Tree) { - var menu = vm.CreateRevisionFileContextMenu(obj); - tree.OpenContextMenu(menu); + var menu = vm.CreateRevisionFileContextMenu(view.SelectedObject); + view.OpenContextMenu(menu); } } diff --git a/src/Views/TextDiffView.axaml b/src/Views/TextDiffView.axaml index b97cfedb..f435b2e2 100644 --- a/src/Views/TextDiffView.axaml +++ b/src/Views/TextDiffView.axaml @@ -7,31 +7,40 @@ xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.TextDiffView" + x:Name="ThisControl" Background="{DynamicResource Brush.Contents}"> - + SyncScrollOffset="{Binding #ThisControl.SyncScrollOffset, Mode=TwoWay}" + UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" + WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"/> KnownLayer.Background; public LineBackgroundRenderer(CombinedTextDiffPresenter editor) @@ -139,7 +135,7 @@ namespace SourceGit.Views { if (line.FirstDocumentLine == null) continue; - + var index = line.FirstDocumentLine.LineNumber; if (index > _editor.DiffData.Lines.Count) break; @@ -159,11 +155,11 @@ namespace SourceGit.Views switch (type) { case Models.TextDiffLineType.None: - return BG_EMPTY; + return _editor.LineBGEmpty; case Models.TextDiffLineType.Added: - return BG_ADDED; + return _editor.LineBGAdd; case Models.TextDiffLineType.Deleted: - return BG_DELETED; + return _editor.LineBGDeleted; default: return null; } @@ -174,9 +170,6 @@ namespace SourceGit.Views public class LineStyleTransformer : DocumentColorizingTransformer { - private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(90, 0, 255, 0)); - private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(80, 255, 0, 0)); - public LineStyleTransformer(CombinedTextDiffPresenter editor) { _editor = editor; @@ -202,7 +195,7 @@ namespace SourceGit.Views if (info.Highlights.Count > 0) { - var bg = info.Type == Models.TextDiffLineType.Added ? HL_ADDED : HL_DELETED; + var bg = info.Type == Models.TextDiffLineType.Added ? _editor.SecondaryLineBGAdd : _editor.SecondaryLineBGDeleted; foreach (var highlight in info.Highlights) { ChangeLinePart(line.Offset + highlight.Start, line.Offset + highlight.Start + highlight.Count, v => @@ -225,6 +218,51 @@ namespace SourceGit.Views set => SetValue(DiffDataProperty, value); } + public static readonly StyledProperty LineBGEmptyProperty = + AvaloniaProperty.Register(nameof(LineBGEmpty), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0))); + + public IBrush LineBGEmpty + { + get => GetValue(LineBGEmptyProperty); + set => SetValue(LineBGEmptyProperty, value); + } + + public static readonly StyledProperty LineBGAddProperty = + AvaloniaProperty.Register(nameof(LineBGAdd), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0))); + + public IBrush LineBGAdd + { + get => GetValue(LineBGAddProperty); + set => SetValue(LineBGAddProperty, value); + } + + public static readonly StyledProperty LineBGDeletedProperty = + AvaloniaProperty.Register(nameof(LineBGDeleted), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0))); + + public IBrush LineBGDeleted + { + get => GetValue(LineBGDeletedProperty); + set => SetValue(LineBGDeletedProperty, value); + } + + public static readonly StyledProperty SecondaryLineBGAddProperty = + AvaloniaProperty.Register(nameof(SecondaryLineBGAdd), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0))); + + public IBrush SecondaryLineBGAdd + { + get => GetValue(SecondaryLineBGAddProperty); + set => SetValue(SecondaryLineBGAddProperty, value); + } + + public static readonly StyledProperty SecondaryLineBGDeletedProperty = + AvaloniaProperty.Register(nameof(SecondaryLineBGDeleted), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0))); + + public IBrush SecondaryLineBGDeleted + { + get => GetValue(SecondaryLineBGDeletedProperty); + set => SetValue(SecondaryLineBGDeletedProperty, value); + } + public static readonly StyledProperty SecondaryFGProperty = AvaloniaProperty.Register(nameof(SecondaryFG), Brushes.Gray); @@ -260,7 +298,6 @@ namespace SourceGit.Views IsReadOnly = true; ShowLineNumbers = false; - WordWrap = false; TextArea.LeftMargins.Add(new LineNumberMargin(this, true) { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); @@ -500,10 +537,6 @@ namespace SourceGit.Views public class LineBackgroundRenderer : IBackgroundRenderer { - private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(60, 0, 0, 0)); - private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); - private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)); - public KnownLayer Layer => KnownLayer.Background; public LineBackgroundRenderer(SingleSideTextDiffPresenter editor) @@ -522,7 +555,7 @@ namespace SourceGit.Views { if (line.FirstDocumentLine == null) continue; - + var index = line.FirstDocumentLine.LineNumber; if (index > infos.Count) break; @@ -542,11 +575,11 @@ namespace SourceGit.Views switch (type) { case Models.TextDiffLineType.None: - return BG_EMPTY; + return _editor.LineBGEmpty; case Models.TextDiffLineType.Added: - return BG_ADDED; + return _editor.LineBGAdd; case Models.TextDiffLineType.Deleted: - return BG_DELETED; + return _editor.LineBGDeleted; default: return null; } @@ -557,9 +590,6 @@ namespace SourceGit.Views public class LineStyleTransformer : DocumentColorizingTransformer { - private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(90, 0, 255, 0)); - private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(80, 255, 0, 0)); - public LineStyleTransformer(SingleSideTextDiffPresenter editor) { _editor = editor; @@ -586,7 +616,7 @@ namespace SourceGit.Views if (info.Highlights.Count > 0) { - var bg = info.Type == Models.TextDiffLineType.Added ? HL_ADDED : HL_DELETED; + var bg = info.Type == Models.TextDiffLineType.Added ? _editor.LineBGAdd : _editor.LineBGDeleted; foreach (var highlight in info.Highlights) { ChangeLinePart(line.Offset + highlight.Start, line.Offset + highlight.Start + highlight.Count, v => @@ -618,6 +648,51 @@ namespace SourceGit.Views set => SetValue(DiffDataProperty, value); } + public static readonly StyledProperty LineBGEmptyProperty = + AvaloniaProperty.Register(nameof(LineBGEmpty), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0))); + + public IBrush LineBGEmpty + { + get => GetValue(LineBGEmptyProperty); + set => SetValue(LineBGEmptyProperty, value); + } + + public static readonly StyledProperty LineBGAddProperty = + AvaloniaProperty.Register(nameof(LineBGAdd), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0))); + + public IBrush LineBGAdd + { + get => GetValue(LineBGAddProperty); + set => SetValue(LineBGAddProperty, value); + } + + public static readonly StyledProperty LineBGDeletedProperty = + AvaloniaProperty.Register(nameof(LineBGDeleted), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0))); + + public IBrush LineBGDeleted + { + get => GetValue(LineBGDeletedProperty); + set => SetValue(LineBGDeletedProperty, value); + } + + public static readonly StyledProperty SecondaryLineBGAddProperty = + AvaloniaProperty.Register(nameof(SecondaryLineBGAdd), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0))); + + public IBrush SecondaryLineBGAdd + { + get => GetValue(SecondaryLineBGAddProperty); + set => SetValue(SecondaryLineBGAddProperty, value); + } + + public static readonly StyledProperty SecondaryLineBGDeletedProperty = + AvaloniaProperty.Register(nameof(SecondaryLineBGDeleted), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0))); + + public IBrush SecondaryLineBGDeleted + { + get => GetValue(SecondaryLineBGDeletedProperty); + set => SetValue(SecondaryLineBGDeletedProperty, value); + } + public static readonly StyledProperty SecondaryFGProperty = AvaloniaProperty.Register(nameof(SecondaryFG), Brushes.Gray); @@ -653,7 +728,6 @@ namespace SourceGit.Views IsReadOnly = true; ShowLineNumbers = false; - WordWrap = false; TextArea.LeftMargins.Add(new LineNumberMargin(this) { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); @@ -738,7 +812,7 @@ namespace SourceGit.Views }; menu.Items.Add(copy); - + TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } @@ -842,6 +916,15 @@ namespace SourceGit.Views set => SetValue(UseSideBySideDiffProperty, value); } + public static readonly StyledProperty SyncScrollOffsetProperty = + AvaloniaProperty.Register(nameof(SyncScrollOffset)); + + public Vector SyncScrollOffset + { + get => GetValue(SyncScrollOffsetProperty); + set => SetValue(SyncScrollOffsetProperty, value); + } + public TextDiffView() { InitializeComponent(); @@ -1083,20 +1166,31 @@ namespace SourceGit.Views { base.OnPropertyChanged(change); - if (change.Property == TextDiffProperty || change.Property == UseSideBySideDiffProperty) + var data = TextDiff; + if (data == null) { - if (TextDiff == null) - { - Content = null; - } - else if (UseSideBySideDiff) - { + Content = null; + SyncScrollOffset = Vector.Zero; + return; + } + + if (change.Property == TextDiffProperty) + { + if (UseSideBySideDiff) Content = new ViewModels.TwoSideTextDiff(TextDiff); - } else - { Content = TextDiff; - } + + SetCurrentValue(SyncScrollOffsetProperty, TextDiff.SyncScrollOffset); + } + else if (change.Property == UseSideBySideDiffProperty) + { + if (UseSideBySideDiff) + Content = new ViewModels.TwoSideTextDiff(TextDiff); + else + Content = TextDiff; + + SetCurrentValue(SyncScrollOffsetProperty, Vector.Zero); } } diff --git a/src/Views/UpdateSubmodules.axaml b/src/Views/UpdateSubmodules.axaml new file mode 100644 index 00000000..5ea93fac --- /dev/null +++ b/src/Views/UpdateSubmodules.axaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/Views/UpdateSubmodules.axaml.cs b/src/Views/UpdateSubmodules.axaml.cs new file mode 100644 index 00000000..165c809a --- /dev/null +++ b/src/Views/UpdateSubmodules.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class UpdateSubmodules : UserControl + { + public UpdateSubmodules() + { + InitializeComponent(); + } + } +}