Merge branch 'develop' into bugfixes/enhance-german-translation

This commit is contained in:
warappa 2024-08-06 21:05:27 +02:00
commit 67ebc5ae03
83 changed files with 953 additions and 341 deletions

View file

@ -99,3 +99,41 @@ jobs:
with:
name: sourcegit.linux-x64
path: sourcegit.linux-x64.tar
build-linux-arm64:
name: Build Linux (arm64)
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Configure arm64 packages
run: |
sudo dpkg --add-architecture arm64
echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted' \
| sudo tee /etc/apt/sources.list.d/arm64.list
sudo sed -i -e 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
sudo sed -i -e 's/^deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list
- name: Install cross-compiling dependencies
run: |
sudo apt-get update
sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
- name: Build
run: dotnet build -c Release
- name: Publish
run: dotnet publish src/SourceGit.csproj -c Release -o publish -r linux-arm64
- name: Rename Executable File
run: mv publish/SourceGit publish/sourcegit
- name: Packing Program
run: tar -cvf sourcegit.linux-arm64.tar -C publish/ .
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: sourcegit.linux-arm64
path: sourcegit.linux-arm64.tar

View file

@ -15,6 +15,7 @@ Opensource Git GUI client.
* GIT commands with GUI
* Clone/Fetch/Pull/Push...
* Merge/Rebase/Reset/Revert/Amend/Cherry-pick...
* Amend/Reword
* Interactive rebase (Basic)
* Branches
* Remotes
@ -30,8 +31,9 @@ Opensource Git GUI client.
* Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
* GitFlow support
* Git LFS support
* GitFlow
* Git LFS
* Issue Link
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.

View file

@ -1 +1 @@
8.23
8.24

View file

@ -6,3 +6,4 @@ Icon=/usr/share/icons/sourcegit.png
Terminal=false
Type=Application
Categories=Development
MimeType=inode/directory;

View file

@ -208,8 +208,8 @@ namespace SourceGit
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow?.Clipboard is { } clipbord)
await clipbord.SetTextAsync(data);
if (desktop.MainWindow?.Clipboard is { } clipboard)
await clipboard.SetTextAsync(data);
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,116 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
{
public class IssueTrackerMatch
{
public int Start { get; set; } = 0;
public int Length { get; set; } = 0;
public string URL { get; set; } = "";
public bool Intersect(int start, int length)
{
if (start == Start)
return true;
if (start < Start)
return start + length > Start;
return start < Start + Length;
}
}
public class IssueTrackerRule : ObservableObject
{
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string RegexString
{
get => _regexString;
set
{
if (SetProperty(ref _regexString, value))
{
try
{
_regex = null;
_regex = new Regex(_regexString, RegexOptions.Multiline);
}
catch
{
// Ignore errors.
}
}
OnPropertyChanged(nameof(IsRegexValid));
}
}
public bool IsRegexValid
{
get => _regex != null;
}
public string URLTemplate
{
get => _urlTemplate;
set => SetProperty(ref _urlTemplate, value);
}
public void Matches(List<IssueTrackerMatch> outs, string message)
{
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return;
var matches = _regex.Matches(message);
for (var i = 0; i < matches.Count; i++)
{
var match = matches[i];
if (!match.Success)
continue;
var start = match.Index;
var len = match.Length;
var intersect = false;
foreach (var exist in outs)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
var range = new IssueTrackerMatch();
range.Start = start;
range.Length = len;
range.URL = _urlTemplate;
for (var j = 1; j < match.Groups.Count; j++)
{
var group = match.Groups[j];
if (group.Success)
range.URL = range.URL.Replace($"${j}", group.Value);
}
outs.Add(range);
}
}
private string _name;
private string _regexString;
private string _urlTemplate;
private Regex _regex = null;
}
}

View file

@ -76,6 +76,12 @@ namespace SourceGit.Models
set;
} = new AvaloniaList<string>();
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
{
get;
set;
} = new AvaloniaList<IssueTrackerRule>();
public void PushCommitMessage(string message)
{
var existIdx = CommitMessages.IndexOf(message);
@ -93,5 +99,50 @@ namespace SourceGit.Models
CommitMessages.Insert(0, message);
}
public IssueTrackerRule AddNewIssueTracker()
{
var rule = new IssueTrackerRule()
{
Name = "New Issue Tracker",
RegexString = "#(\\d+)",
URLTemplate = "https://xxx/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddGithubIssueTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "Github ISSUE",
RegexString = "#(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddJiraIssueTracker()
{
var rule = new IssueTrackerRule()
{
Name = "Jira Tracker",
RegexString = "PROJ-(\\d+)",
URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public void RemoveIssueTracker(IssueTrackerRule rule)
{
if (rule != null)
IssueTrackerRules.Remove(rule);
}
}
}

View file

@ -52,6 +52,7 @@
<StreamGeometry x:Key="Icons.Info">M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z</StreamGeometry>
<StreamGeometry x:Key="Icons.Init">M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z</StreamGeometry>
<StreamGeometry x:Key="Icons.InteractiveRebase">M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z</StreamGeometry>
<StreamGeometry x:Key="Icons.Issue">M922 39H102A65 65 0 0039 106v609a65 65 0 0063 68h94v168a34 34 0 0019 31 30 30 0 0012 3 30 30 0 0022-10l182-192H922a65 65 0 0063-68V106A65 65 0 00922 39zM288 378h479a34 34 0 010 68H288a34 34 0 010-68zm0-135h479a34 34 0 010 68H288a34 34 0 010-68zm0 270h310a34 34 0 010 68H288a34 34 0 010-68z</StreamGeometry>
<StreamGeometry x:Key="Icons.Password">M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutHorizontal">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutVertical">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z</StreamGeometry>
@ -67,6 +68,7 @@
<StreamGeometry x:Key="Icons.MacOS.Maximize">M0 4 0 20 16 20 0 4M4 0 20 0 20 16 4 0z</StreamGeometry>
<StreamGeometry x:Key="Icons.Menu">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Merge">M824 645V307c0-56-46-102-102-102h-102V102l-154 154 154 154V307h102v338c-46 20-82 67-82 123 0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123zm-51 195c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72zM384 256c0-72-61-133-133-133-72 0-133 61-133 133 0 56 36 102 82 123v266C154 666 118 712 118 768c0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123V379C348 358 384 312 384 256zM323 768c0 41-31 72-72 72-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72zM251 328c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72z</StreamGeometry>
<StreamGeometry x:Key="Icons.Move">M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z</StreamGeometry>
<StreamGeometry x:Key="Icons.OpenWith">M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z</StreamGeometry>
<StreamGeometry x:Key="Icons.Paste">M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Plus">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</StreamGeometry>

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -124,6 +124,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">Repository Configure</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">Email Address</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">Email address</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE TRACKER</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">Add Sample Github Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">Add Sample Jira Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">New Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">Issue Regex Expression:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">Rule Name:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">Result URL:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">Please use $1, $2 to access regex groups values.</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP Proxy</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP proxy used by this repository</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">User Name</x:String>

View file

@ -127,6 +127,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">仓库配置</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">电子邮箱</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">邮箱地址</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE追踪</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增匹配Github Issue规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增匹配Jira规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自定义规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">匹配ISSUE的正则表达式 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">规则名 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">为ISSUE生成的URL链接 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在URL中使用$1$2等变量填入正则表达式匹配的内容</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP网络代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">用户名</x:String>

View file

@ -127,6 +127,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">倉庫配置</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">電子郵箱</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">郵箱地址</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE追蹤</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增匹配Github Issue規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增匹配Jira規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自定義規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">匹配ISSUE的正則表達式 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">規則名 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">為ISSUE生成的URL連結 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在URL中使用$1$2等變數填入正則表示式匹配的內容</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP網路代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">使用者名稱</x:String>

View file

@ -164,6 +164,7 @@
<Setter Property="Foreground" Value="{DynamicResource Brush.FG1}"/>
<Setter Property="Background" Value="{DynamicResource Brush.Popup}"/>
<Setter Property="VerticalOffset" Value="-8"/>
<Setter Property="TextBlock.TextDecorations" Value=""/>
<Setter Property="Template">
<ControlTemplate>
<Grid Effect="drop-shadow(0 0 8 #80000000)">
@ -276,6 +277,13 @@
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style Selector="TextBlock.issue_link">
<Setter Property="Foreground" Value="{DynamicResource Brush.Link}"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="TextBlock.issue_link:pointerover">
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
<Style Selector="SelectableTextBlock">
<Setter Property="HorizontalAlignment" Value="Left"/>
@ -781,7 +789,7 @@
ContentTemplate="{TemplateBinding HeaderTemplate}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
RecognizesAccessKey="True"/>
RecognizesAccessKey="False"/>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="2"
Classes="CaptionTextBlockStyle"

View file

@ -32,6 +32,7 @@
<Color x:Key="Color.Diff.DeletedBG">#80FF9797</Color>
<Color x:Key="Color.Diff.AddedHighlight">#A7E1A7</Color>
<Color x:Key="Color.Diff.DeletedHighlight">#F19B9D</Color>
<Color x:Key="Color.Link">#0000EE</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
@ -65,6 +66,7 @@
<Color x:Key="Color.Diff.DeletedBG">#C0633F3E</Color>
<Color x:Key="Color.Diff.AddedHighlight">#A0308D3C</Color>
<Color x:Key="Color.Diff.DeletedHighlight">#A09F4247</Color>
<Color x:Key="Color.Link">#4DAAFC</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
@ -100,4 +102,5 @@
<SolidColorBrush x:Key="Brush.Diff.DeletedBG" Color="{DynamicResource Color.Diff.DeletedBG}"/>
<SolidColorBrush x:Key="Brush.Diff.AddedHighlight" Color="{DynamicResource Color.Diff.AddedHighlight}"/>
<SolidColorBrush x:Key="Brush.Diff.DeletedHighlight" Color="{DynamicResource Color.Diff.DeletedHighlight}"/>
<SolidColorBrush x:Key="Brush.Link" Color="{DynamicResource Color.Link}"/>
</ResourceDictionary>

View file

@ -29,10 +29,11 @@
<ItemGroup>
<AvaloniaResource Include="App.ico" />
<AvaloniaResource Include="Resources/ExternalToolIcons/*" />
<AvaloniaResource Include="Resources/ExternalToolIcons/JetBrains/*" />
<AvaloniaResource Include="Resources/Fonts/*" />
<AvaloniaResource Include="Resources/ShellIcons/*" />
<AvaloniaResource Include="Resources/Images/*" />
<AvaloniaResource Include="Resources/Images/ExternalToolIcons/*" />
<AvaloniaResource Include="Resources/Images/ExternalToolIcons/JetBrains/*" />
<AvaloniaResource Include="Resources/Images/ShellIcons/*" />
</ItemGroup>
<ItemGroup>
@ -40,11 +41,11 @@
<PackageReference Include="Avalonia.Desktop" Version="11.1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.1" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.1" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.1.1" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.60" />
</ItemGroup>
<ItemGroup>

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
@ -88,9 +89,15 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _viewRevisionFileContent, value);
}
public CommitDetail(string repo)
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => _issueTrackerRules;
}
public CommitDetail(string repo, AvaloniaList<Models.IssueTrackerRule> issueTrackerRules)
{
_repo = repo;
_issueTrackerRules = issueTrackerRules;
}
public void Cleanup()
@ -242,7 +249,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path, _issueTrackerRules) };
window.Show();
ev.Handled = true;
};
@ -305,7 +312,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path, _issueTrackerRules) };
window.Show();
ev.Handled = true;
};
@ -457,6 +464,7 @@ namespace SourceGit.ViewModels
};
private string _repo;
private AvaloniaList<Models.IssueTrackerRule> _issueTrackerRules = null;
private int _activePageIndex = 0;
private Models.Commit _commit = null;
private string _fullMessage = string.Empty;

View file

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@ -54,11 +54,11 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _detailContext, value);
}
public FileHistories(string repo, string file)
public FileHistories(string repo, string file, AvaloniaList<Models.IssueTrackerRule> issueTrackerRules)
{
_repo = repo;
_file = file;
_detailContext = new CommitDetail(repo);
_detailContext = new CommitDetail(repo, issueTrackerRules);
Task.Run(() =>
{

View file

@ -1,7 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
@ -55,6 +55,11 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _detailContext, value);
}
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => _repo.Settings.IssueTrackerRules;
}
public Histories(Repository repo)
{
_repo = repo;
@ -94,7 +99,7 @@ namespace SourceGit.ViewModels
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
var commitDetail = new CommitDetail(_repo.FullPath, _repo.Settings.IssueTrackerRules);
commitDetail.Commit = commit;
DetailContext = commitDetail;
}
@ -122,7 +127,7 @@ namespace SourceGit.ViewModels
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
var commitDetail = new CommitDetail(_repo.FullPath, _repo.Settings.IssueTrackerRules);
commitDetail.Commit = commit;
DetailContext = commitDetail;
}

View file

@ -114,7 +114,7 @@ namespace SourceGit.ViewModels
Current = current;
On = on;
IsLoading = true;
DetailContext = new CommitDetail(repoPath);
DetailContext = new CommitDetail(repoPath, repo.Settings.IssueTrackerRules);
Task.Run(() =>
{

View file

@ -49,9 +49,9 @@ namespace SourceGit.ViewModels
return Task.Run(() =>
{
var succ = new Commands.Rebase(_repo.FullPath, _revision, AutoStash).Exec();
new Commands.Rebase(_repo.FullPath, _revision, AutoStash).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
return true;
});
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
@ -41,6 +42,17 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _httpProxy, value);
}
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => _repo.Settings.IssueTrackerRules;
}
public Models.IssueTrackerRule SelectedIssueTrackerRule
{
get => _selectedIssueTrackerRule;
set => SetProperty(ref _selectedIssueTrackerRule, value);
}
public RepositoryConfigure(Repository repo)
{
_repo = repo;
@ -65,6 +77,39 @@ namespace SourceGit.ViewModels
HttpProxy = string.Empty;
}
public void AddSampleGithubIssueTracker()
{
foreach (var remote in _repo.Remotes)
{
if (remote.URL.Contains("github.com", System.StringComparison.Ordinal))
{
if (remote.TryGetVisitURL(out string url))
{
SelectedIssueTrackerRule = _repo.Settings.AddGithubIssueTracker(url);
return;
}
}
}
SelectedIssueTrackerRule = _repo.Settings.AddGithubIssueTracker(null);
}
public void AddSampleJiraIssueTracker()
{
SelectedIssueTrackerRule = _repo.Settings.AddJiraIssueTracker();
}
public void NewIssueTracker()
{
SelectedIssueTrackerRule = _repo.Settings.AddNewIssueTracker();
}
public void RemoveSelectedIssueTracker()
{
_repo.Settings.RemoveIssueTracker(_selectedIssueTrackerRule);
SelectedIssueTrackerRule = null;
}
public void Save()
{
SetIfChanged("user.name", UserName);
@ -96,5 +141,6 @@ namespace SourceGit.ViewModels
private readonly Repository _repo = null;
private readonly Dictionary<string, string> _cached = null;
private string _httpProxy;
private Models.IssueTrackerRule _selectedIssueTrackerRule = null;
}
}

View file

@ -135,7 +135,7 @@ namespace SourceGit.ViewModels
if (value == null || value.Count == 0)
{
if (_selectedStaged == null || _selectedStaged.Count == 0)
SetDetail(null);
SetDetail(null, true);
}
else
{
@ -143,9 +143,9 @@ namespace SourceGit.ViewModels
SelectedStaged = [];
if (value.Count == 1)
SetDetail(value[0]);
SetDetail(value[0], true);
else
SetDetail(null);
SetDetail(null, true);
}
}
}
@ -161,7 +161,7 @@ namespace SourceGit.ViewModels
if (value == null || value.Count == 0)
{
if (_selectedUnstaged == null || _selectedUnstaged.Count == 0)
SetDetail(null);
SetDetail(null, false);
}
else
{
@ -169,9 +169,9 @@ namespace SourceGit.ViewModels
SelectedUnstaged = [];
if (value.Count == 1)
SetDetail(value[0]);
SetDetail(value[0], false);
else
SetDetail(null);
SetDetail(null, false);
}
}
}
@ -218,7 +218,24 @@ namespace SourceGit.ViewModels
public bool SetData(List<Models.Change> changes)
{
if (!IsChanged(_cached, changes))
{
// Just force refresh selected changes.
Dispatcher.UIThread.Invoke(() =>
{
if (_selectedUnstaged.Count == 1)
SetDetail(_selectedUnstaged[0], true);
else if (_selectedStaged.Count == 1)
SetDetail(_selectedStaged[0], false);
else
SetDetail(null, false);
});
return _cached.Find(x => x.IsConflit) != null;
}
_cached = changes;
_count = _cached.Count;
var unstaged = new List<Models.Change>();
var staged = new List<Models.Change>();
@ -227,12 +244,12 @@ namespace SourceGit.ViewModels
var lastSelectedUnstaged = new HashSet<string>();
var lastSelectedStaged = new HashSet<string>();
if (_selectedUnstaged != null)
if (_selectedUnstaged != null && _selectedUnstaged.Count > 0)
{
foreach (var c in _selectedUnstaged)
lastSelectedUnstaged.Add(c.Path);
}
else if (_selectedStaged != null)
else if (_selectedStaged != null && _selectedStaged.Count > 0)
{
foreach (var c in _selectedStaged)
lastSelectedStaged.Add(c.Path);
@ -258,21 +275,21 @@ namespace SourceGit.ViewModels
selectedStaged.Add(c);
}
_count = changes.Count;
Dispatcher.UIThread.Invoke(() =>
{
_isLoadingData = true;
Unstaged = unstaged;
Staged = staged;
SelectedUnstaged = selectedUnstaged;
SelectedStaged = selectedStaged;
_isLoadingData = false;
if (selectedUnstaged.Count > 0)
SelectedUnstaged = selectedUnstaged;
else if (selectedStaged.Count > 0)
SelectedStaged = selectedStaged;
if (selectedUnstaged.Count == 1)
SetDetail(selectedUnstaged[0], true);
else if (selectedStaged.Count == 1)
SetDetail(selectedStaged[0], false);
else
SetDetail(null);
SetDetail(null, false);
// Try to load merge message from MERGE_MSG
if (string.IsNullOrEmpty(_commitMessage))
@ -317,7 +334,6 @@ namespace SourceGit.ViewModels
if (_unstaged.Count == 0 || changes.Count == 0)
return;
SetDetail(null);
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count)
@ -355,7 +371,6 @@ namespace SourceGit.ViewModels
if (_staged.Count == 0 || changes.Count == 0)
return;
SetDetail(null);
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (_useAmend)
@ -541,7 +556,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path, _repo.Settings.IssueTrackerRules) };
window.Show();
e.Handled = true;
};
@ -1155,12 +1170,11 @@ namespace SourceGit.ViewModels
}
}
private void SetDetail(Models.Change change)
private void SetDetail(Models.Change change, bool isUnstaged)
{
if (_isLoadingData)
return;
var isUnstaged = _selectedUnstaged != null && _selectedUnstaged.Count > 0;
if (change == null)
DetailContext = null;
else if (change.IsConflit && isUnstaged)
@ -1243,10 +1257,8 @@ namespace SourceGit.ViewModels
return;
}
_repo.Settings.PushCommitMessage(_commitMessage);
SetDetail(null);
IsCommitting = true;
_repo.Settings.PushCommitMessage(_commitMessage);
_repo.SetWatcherEnabled(false);
var autoStage = AutoStageBeforeCommit;
@ -1257,6 +1269,7 @@ namespace SourceGit.ViewModels
{
if (succ)
{
SelectedStaged = [];
CommitMessage = string.Empty;
UseAmend = false;
@ -1273,6 +1286,24 @@ namespace SourceGit.ViewModels
});
}
private bool IsChanged(List<Models.Change> old, List<Models.Change> cur)
{
if (old.Count != cur.Count)
return true;
var oldSet = new HashSet<string>();
foreach (var c in old)
oldSet.Add($"{c.Path}\n{c.WorkTree}\n{c.Index}");
foreach (var c in cur)
{
if (!oldSet.Contains($"{c.Path}\n{c.WorkTree}\n{c.Index}"))
return true;
}
return false;
}
private Repository _repo = null;
private bool _isLoadingData = false;
private bool _isStaging = false;

View file

@ -1,7 +1,5 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Avalonia;
using Avalonia.Controls;
@ -41,10 +39,7 @@ namespace SourceGit.Views
refetch.Click += (_, _) =>
{
if (User != null)
{
Models.AvatarManager.Request(_emailMD5, true);
InvalidateVisual();
}
Models.AvatarManager.Request(User.Email, true);
};
ContextMenu = new ContextMenu();
@ -59,7 +54,7 @@ namespace SourceGit.Views
return;
var corner = (float)Math.Max(2, Bounds.Width / 16);
var img = Models.AvatarManager.Request(_emailMD5);
var img = Models.AvatarManager.Request(User.Email, false);
if (img != null)
{
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
@ -74,9 +69,9 @@ namespace SourceGit.Views
}
}
public void OnAvatarResourceChanged(string md5)
public void OnAvatarResourceChanged(string email)
{
if (_emailMD5 == md5)
if (User.Email.Equals(email, StringComparison.Ordinal))
{
InvalidateVisual();
}
@ -97,10 +92,7 @@ namespace SourceGit.Views
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();
@ -108,15 +100,6 @@ namespace SourceGit.Views
foreach (var c in chars)
sum += Math.Abs(c);
var lowered = avatar.User.Email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered));
var builder = new StringBuilder();
foreach (var c in hash)
builder.Append(c.ToString("x2"));
var md5 = builder.ToString();
if (avatar._emailMD5 == null || avatar._emailMD5 != md5)
avatar._emailMD5 = md5;
avatar._fallbackBrush = new LinearGradientBrush
{
GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length],
@ -139,6 +122,5 @@ namespace SourceGit.Views
private FormattedText _fallbackLabel = null;
private LinearGradientBrush _fallbackBrush = null;
private string _emailMD5 = null;
}
}

View file

@ -24,9 +24,7 @@
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border2}"
DoubleTapped="MaximizeOrRestoreWindow"
PointerPressed="BeginMoveWindow"
PointerMoved="MoveWindow"
PointerReleased="EndMoveWindow"/>
PointerPressed="BeginMoveWindow"/>
<!-- Caption Buttons (macOS) -->
<Border Grid.Column="0" IsVisible="{OnPlatform False, macOS=True}">

View file

@ -330,8 +330,6 @@ namespace SourceGit.Views
private void MaximizeOrRestoreWindow(object _, TappedEventArgs e)
{
_pressedTitleBar = false;
if (WindowState == WindowState.Maximized)
WindowState = WindowState.Normal;
else
@ -342,34 +340,10 @@ namespace SourceGit.Views
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
if (e.ClickCount != 2)
_pressedTitleBar = true;
}
if (e.ClickCount == 1)
BeginMoveDrag(e);
private void MoveWindow(object _, PointerEventArgs e)
{
if (!_pressedTitleBar || e.Source == null)
return;
var visual = (Visual)e.Source;
if (visual == null)
return;
#pragma warning disable CS0618
BeginMoveDrag(new PointerPressedEventArgs(
e.Source,
e.Pointer,
visual,
e.GetPosition(visual),
e.Timestamp,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
e.KeyModifiers));
#pragma warning restore CS0618
}
private void EndMoveWindow(object _1, PointerReleasedEventArgs _2)
{
_pressedTitleBar = false;
e.Handled = true;
}
protected override void OnClosed(EventArgs e)
@ -377,7 +351,5 @@ namespace SourceGit.Views
base.OnClosed(e);
GC.Collect();
}
private bool _pressedTitleBar = false;
}
}

View file

@ -26,9 +26,7 @@
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border2}"
DoubleTapped="MaximizeOrRestoreWindow"
PointerPressed="BeginMoveWindow"
PointerMoved="MoveWindow"
PointerReleased="EndMoveWindow"/>
PointerPressed="BeginMoveWindow"/>
<!-- Caption Buttons (macOS) -->
<Border Grid.Column="0" IsVisible="{OnPlatform False, macOS=True}">

View file

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@ -13,8 +12,6 @@ namespace SourceGit.Views
private void MaximizeOrRestoreWindow(object _, TappedEventArgs e)
{
_pressedTitleBar = false;
if (WindowState == WindowState.Maximized)
WindowState = WindowState.Normal;
else
@ -25,34 +22,10 @@ namespace SourceGit.Views
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
if (e.ClickCount != 2)
_pressedTitleBar = true;
}
if (e.ClickCount == 1)
BeginMoveDrag(e);
private void MoveWindow(object _, PointerEventArgs e)
{
if (!_pressedTitleBar || e.Source == null)
return;
var visual = (Visual)e.Source;
if (visual == null)
return;
#pragma warning disable CS0618
BeginMoveDrag(new PointerPressedEventArgs(
e.Source,
e.Pointer,
visual,
e.GetPosition(visual),
e.Timestamp,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
e.KeyModifiers));
#pragma warning restore CS0618
}
private void EndMoveWindow(object _1, PointerReleasedEventArgs _2)
{
_pressedTitleBar = false;
e.Handled = true;
}
private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e)
@ -73,7 +46,5 @@ namespace SourceGit.Views
e.Handled = true;
}
private bool _pressedTitleBar = false;
}
}

View file

@ -11,31 +11,31 @@ namespace SourceGit.Views
InitializeComponent();
}
private void MinimizeWindow(object _1, RoutedEventArgs _2)
private void MinimizeWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.WindowState = WindowState.Minimized;
}
e.Handled = true;
}
private void MaximizeOrRestoreWindow(object _1, RoutedEventArgs _2)
private void MaximizeOrRestoreWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
e.Handled = true;
}
private void CloseWindow(object _1, RoutedEventArgs _2)
private void CloseWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.Close();
}
e.Handled = true;
}
}
}

View file

@ -11,31 +11,31 @@ namespace SourceGit.Views
InitializeComponent();
}
private void MinimizeWindow(object _1, RoutedEventArgs _2)
private void MinimizeWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.WindowState = WindowState.Minimized;
}
e.Handled = true;
}
private void MaximizeOrRestoreWindow(object _1, RoutedEventArgs _2)
private void MaximizeOrRestoreWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
e.Handled = true;
}
private void CloseWindow(object _1, RoutedEventArgs _2)
private void CloseWindow(object _, RoutedEventArgs e)
{
var window = this.FindAncestorOfType<Window>();
if (window != null)
{
window.Close();
}
e.Handled = true;
}
}
}

View file

@ -92,7 +92,12 @@
<!-- Messages -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
<SelectableTextBlock Grid.Row="3" Grid.Column="1" Margin="12,5,8,0" Classes="primary" Text="{Binding #ThisControl.Message}" TextWrapping="Wrap"/>
<v:CommitMessagePresenter Grid.Row="3" Grid.Column="1"
Margin="12,5,8,0"
Classes="primary"
Message="{Binding #ThisControl.Message}"
IssueTrackerRules="{Binding #ThisControl.IssueTrackerRules}"
TextWrapping="Wrap"/>
</Grid>
</StackPanel>
</DataTemplate>

View file

@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
@ -24,6 +25,15 @@ namespace SourceGit.Views
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<CommitBaseInfo, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => GetValue(IssueTrackerRulesProperty);
set => SetValue(IssueTrackerRulesProperty, value);
}
public CommitBaseInfo()
{
InitializeComponent();

View file

@ -19,7 +19,9 @@
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!-- Base Information -->
<v:CommitBaseInfo Content="{Binding Commit}" Message="{Binding FullMessage}"/>
<v:CommitBaseInfo Content="{Binding Commit}"
Message="{Binding FullMessage}"
IssueTrackerRules="{Binding IssueTrackerRules}"/>
<!-- Line -->
<Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}"/>
@ -32,7 +34,7 @@
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="MinHeight" Value="26"/>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
@ -45,7 +47,7 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">
<Grid Background="Transparent" Height="26" ColumnDefinitions="36,*" ContextRequested="OnChangeContextRequested" DoubleTapped="OnChangeDoubleTapped">
<Grid Background="Transparent" Height="24" ColumnDefinitions="36,*" ContextRequested="OnChangeContextRequested" DoubleTapped="OnChangeDoubleTapped">
<v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14"
HorizontalAlignment="Left"

View file

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
namespace SourceGit.Views
{
public class CommitMessagePresenter : SelectableTextBlock
{
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<CommitMessagePresenter, string>(nameof(Message));
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<CommitMessagePresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => GetValue(IssueTrackerRulesProperty);
set => SetValue(IssueTrackerRulesProperty, value);
}
protected override Type StyleKeyOverride => typeof(SelectableTextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MessageProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines.Clear();
var message = Message;
if (string.IsNullOrEmpty(message))
return;
var rules = IssueTrackerRules;
if (rules == null || rules.Count == 0)
{
Inlines.Add(new Run(message));
return;
}
var matches = new List<Models.IssueTrackerMatch>();
foreach (var rule in rules)
rule.Matches(matches, message);
if (matches.Count == 0)
{
Inlines.Add(new Run(message));
return;
}
matches.Sort((l, r) => l.Start - r.Start);
int pos = 0;
foreach (var match in matches)
{
if (match.Start > pos)
Inlines.Add(new Run(message.Substring(pos, match.Start - pos)));
var link = new TextBlock();
link.SetValue(TextProperty, message.Substring(match.Start, match.Length));
link.SetValue(ToolTip.TipProperty, match.URL);
link.Classes.Add("issue_link");
link.PointerPressed += OnLinkPointerPressed;
Inlines.Add(link);
pos = match.Start + match.Length;
}
if (pos < message.Length)
Inlines.Add(new Run(message.Substring(pos)));
}
}
private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is TextBlock text)
{
var tooltip = text.GetValue(ToolTip.TipProperty) as string;
if (!string.IsNullOrEmpty(tooltip))
Native.OS.OpenBrowser(tooltip);
e.Handled = true;
}
}
}
}

View file

@ -26,9 +26,7 @@
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
DoubleTapped="MaximizeOrRestoreWindow"
PointerPressed="BeginMoveWindow"
PointerMoved="MoveWindow"
PointerReleased="EndMoveWindow"/>
PointerPressed="BeginMoveWindow"/>
<!-- Caption Buttons (macOS) -->
<Border Grid.Column="0" IsVisible="{OnPlatform False, macOS=True}">

View file

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@ -13,8 +12,6 @@ namespace SourceGit.Views
private void MaximizeOrRestoreWindow(object _, TappedEventArgs e)
{
_pressedTitleBar = false;
if (WindowState == WindowState.Maximized)
WindowState = WindowState.Normal;
else
@ -25,36 +22,10 @@ namespace SourceGit.Views
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
if (e.ClickCount != 2)
_pressedTitleBar = true;
if (e.ClickCount == 1)
BeginMoveDrag(e);
e.Handled = true;
}
private void MoveWindow(object _, PointerEventArgs e)
{
if (!_pressedTitleBar || e.Source == null)
return;
var visual = (Visual)e.Source;
if (visual == null)
return;
#pragma warning disable CS0618
BeginMoveDrag(new PointerPressedEventArgs(
e.Source,
e.Pointer,
visual,
e.GetPosition(visual),
e.Timestamp,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
e.KeyModifiers));
#pragma warning restore CS0618
}
private void EndMoveWindow(object _1, PointerReleasedEventArgs _2)
{
_pressedTitleBar = false;
}
private bool _pressedTitleBar = false;
}
}

View file

@ -27,10 +27,12 @@
ColumnHeaderHeight="24"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ClipboardCopyMode="None"
LayoutUpdated="OnCommitDataGridLayoutUpdated"
SelectionChanged="OnCommitDataGridSelectionChanged"
ContextRequested="OnCommitDataGridContextRequested"
DoubleTapped="OnCommitDataGridDoubleTapped">
DoubleTapped="OnCommitDataGridDoubleTapped"
KeyDown="OnCommitDataGridKeyDown">
<DataGrid.Styles>
<Style Selector="DataGridColumnHeader">
<Setter Property="MinHeight" Value="24"/>
@ -80,10 +82,11 @@
FontSize="10"
VerticalAlignment="Center"/>
<TextBlock Classes="primary"
Text="{Binding Subject}"
Opacity="{Binding Opacity}"
FontWeight="{Binding FontWeight}"/>
<v:CommitSubjectPresenter Classes="primary"
Subject="{Binding Subject}"
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"
Opacity="{Binding Opacity}"
FontWeight="{Binding FontWeight}"/>
</StackPanel>
</Border>
</DataTemplate>

View file

@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -70,6 +74,93 @@ namespace SourceGit.Views
}
}
public class CommitSubjectPresenter : TextBlock
{
public static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
public string Subject
{
get => GetValue(SubjectProperty);
set => SetValue(SubjectProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => GetValue(IssueTrackerRulesProperty);
set => SetValue(IssueTrackerRulesProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines.Clear();
var subject = Subject;
if (string.IsNullOrEmpty(subject))
return;
var rules = IssueTrackerRules;
if (rules == null || rules.Count == 0)
{
Inlines.Add(new Run(subject));
return;
}
var matches = new List<Models.IssueTrackerMatch>();
foreach (var rule in rules)
rule.Matches(matches, subject);
if (matches.Count == 0)
{
Inlines.Add(new Run(subject));
return;
}
matches.Sort((l, r) => l.Start - r.Start);
int pos = 0;
foreach (var match in matches)
{
if (match.Start > pos)
Inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
var link = new TextBlock();
link.SetValue(TextProperty, subject.Substring(match.Start, match.Length));
link.SetValue(ToolTip.TipProperty, match.URL);
link.Classes.Add("issue_link");
link.PointerPressed += OnLinkPointerPressed;
Inlines.Add(link);
pos = match.Start + match.Length;
}
if (pos < subject.Length)
Inlines.Add(new Run(subject.Substring(pos)));
}
}
private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is TextBlock text)
{
var tooltip = text.GetValue(ToolTip.TipProperty) as string;
if (!string.IsNullOrEmpty(tooltip))
Native.OS.OpenBrowser(tooltip);
e.Handled = true;
}
}
}
public class CommitTimeTextBlock : TextBlock
{
public static readonly StyledProperty<bool> ShowAsDateTimeProperty =
@ -368,6 +459,16 @@ namespace SourceGit.Views
set => SetValue(NavigationIdProperty, value);
}
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get
{
if (DataContext is ViewModels.Histories histories)
return histories.IssueTrackerRules;
return null;
}
}
static Histories()
{
NavigationIdProperty.Changed.AddClassHandler<Histories>((h, _) =>
@ -419,5 +520,24 @@ namespace SourceGit.Views
}
e.Handled = true;
}
private void OnCommitDataGridKeyDown(object sender, KeyEventArgs e)
{
if (sender is DataGrid grid &&
grid.SelectedItems is { Count: > 0 } selected &&
e.Key == Key.C &&
e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
var builder = new StringBuilder();
foreach (var item in selected)
{
if (item is Models.Commit commit)
builder.AppendLine($"{commit.SHA.Substring(0, 10)} - {commit.Subject}");
}
App.CopyText(builder.ToString());
e.Handled = true;
}
}
}
}

View file

@ -73,7 +73,6 @@
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
DragDrop.AllowDrop="True"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
@ -82,6 +81,19 @@
VerticalScrollBarVisibility="Auto"
KeyDown="OnDataGridKeyDown">
<DataGrid.Columns>
<DataGridTemplateColumn Width="16" Header="DragHandler">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
<Border Background="Transparent"
Margin="4,0,0,0"
Loaded="OnSetupRowHeaderDragDrop"
PointerPressed="OnRowHeaderPointerPressed">
<Path Width="14" Height="14" Data="{StaticResource Icons.Move}" Fill="{DynamicResource Brush.FG2}"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Option">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">

View file

@ -21,6 +21,55 @@ namespace SourceGit.Views
Close();
}
private void OnSetupRowHeaderDragDrop(object sender, RoutedEventArgs e)
{
if (sender is Border border)
{
DragDrop.SetAllowDrop(border, true);
border.AddHandler(DragDrop.DragOverEvent, OnRowHeaderDragOver);
}
}
private void OnRowHeaderPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is Border border && border.DataContext is ViewModels.InteractiveRebaseItem item)
{
var data = new DataObject();
data.Set("InteractiveRebaseItem", item);
DragDrop.DoDragDrop(e, data, DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link);
}
}
private void OnRowHeaderDragOver(object sender, DragEventArgs e)
{
if (DataContext is ViewModels.InteractiveRebase vm &&
e.Data.Get("InteractiveRebaseItem") is ViewModels.InteractiveRebaseItem src &&
sender is Border { DataContext: ViewModels.InteractiveRebaseItem dst } border &&
src != dst)
{
e.DragEffects = DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link;
var p = e.GetPosition(border);
if (p.Y > border.Bounds.Height * 0.33 && p.Y < border.Bounds.Height * 0.67)
{
var srcIdx = vm.Items.IndexOf(src);
var dstIdx = vm.Items.IndexOf(dst);
if (srcIdx < dstIdx)
{
for (var i = srcIdx; i < dstIdx; i++)
vm.MoveItemDown(src);
}
else
{
for (var i = srcIdx; i > dstIdx; i--)
vm.MoveItemUp(src);
}
}
e.Handled = true;
}
}
private void OnMoveItemUp(object sender, RoutedEventArgs e)
{
if (sender is Control control && DataContext is ViewModels.InteractiveRebase vm)

View file

@ -25,9 +25,7 @@
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
DoubleTapped="OnTitleBarDoubleTapped"
PointerPressed="BeginMoveWindow"
PointerMoved="MoveWindow"
PointerReleased="EndMoveWindow"/>
PointerPressed="BeginMoveWindow"/>
<!-- Caption Buttons (macOS) -->
<Border Grid.Column="0" VerticalAlignment="Stretch" Margin="2,0,8,3" IsVisible="{OnPlatform False, macOS=True}">

View file

@ -163,8 +163,6 @@ namespace SourceGit.Views
private void OnTitleBarDoubleTapped(object _, TappedEventArgs e)
{
_pressedTitleBar = false;
if (WindowState == WindowState.Maximized)
WindowState = WindowState.Normal;
else
@ -175,36 +173,10 @@ namespace SourceGit.Views
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
if (e.ClickCount != 2)
_pressedTitleBar = true;
if (e.ClickCount == 1)
BeginMoveDrag(e);
e.Handled = true;
}
private void MoveWindow(object _, PointerEventArgs e)
{
if (!_pressedTitleBar || e.Source == null)
return;
var visual = (Visual)e.Source;
if (visual == null)
return;
#pragma warning disable CS0618
BeginMoveDrag(new PointerPressedEventArgs(
e.Source,
e.Pointer,
visual,
e.GetPosition(visual),
e.Timestamp,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
e.KeyModifiers));
#pragma warning restore CS0618
}
private void EndMoveWindow(object _1, PointerReleasedEventArgs _2)
{
_pressedTitleBar = false;
}
private bool _pressedTitleBar = false;
}
}

View file

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

View file

@ -488,7 +488,7 @@
</TextBox.InnerRightContent>
</TextBox>
<Popup PlacementTarget="#TxtSearchCommitsBox"
<Popup PlacementTarget="{Binding #TxtSearchCommitsBox}"
Placement="BottomEdgeAlignedLeft"
HorizontalOffset="-8" VerticalAlignment="-8"
IsOpen="{Binding IsSearchCommitSuggestionOpen}">

View file

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@ -47,79 +48,167 @@
</Grid>
<!-- Body -->
<Grid Grid.Row="1" Margin="16,8,16,0" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.User}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.User.Placeholder}"
Text="{Binding UserName, Mode=TwoWay}"
v:AutoFocusBehaviour.IsEnabled="True"/>
<TabControl Grid.Row="1">
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Configure.Git}"/>
</TabItem.Header>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.Email}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.Email.Placeholder}"
Text="{Binding UserEmail, Mode=TwoWay}"/>
<Grid Margin="16,4,16,8" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.User}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.User.Placeholder}"
Text="{Binding UserName, Mode=TwoWay}"
v:AutoFocusBehaviour.IsEnabled="True"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.Proxy}"/>
<TextBox Grid.Row="2" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.Proxy.Placeholder}"
Text="{Binding HttpProxy, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" IsVisible="{Binding HttpProxy, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Command="{Binding ClearHttpProxy}">
<Path Width="16" Height="16" Margin="0,0,0,0" Data="{StaticResource Icons.Clear}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.Email}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.Email.Placeholder}"
Text="{Binding UserEmail, Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Preference.GPG.UserKey}"/>
<TextBox Grid.Row="3" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Preference.GPG.UserKey.Placeholder}"
Text="{Binding GPGUserSigningKey, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Configure.Proxy}"/>
<TextBox Grid.Row="2" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Configure.Proxy.Placeholder}"
Text="{Binding HttpProxy, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" IsVisible="{Binding HttpProxy, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Command="{Binding ClearHttpProxy}">
<Path Width="16" Height="16" Margin="0,0,0,0" Data="{StaticResource Icons.Clear}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<CheckBox Grid.Row="4" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.CommitEnabled}"
IsChecked="{Binding GPGCommitSigningEnabled, Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Preference.GPG.UserKey}"/>
<TextBox Grid.Row="3" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Preference.GPG.UserKey.Placeholder}"
Text="{Binding GPGUserSigningKey, Mode=TwoWay}"/>
<CheckBox Grid.Row="5" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.TagEnabled}"
IsChecked="{Binding GPGTagSigningEnabled, Mode=TwoWay}"/>
</Grid>
<CheckBox Grid.Row="4" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.CommitEnabled}"
IsChecked="{Binding GPGCommitSigningEnabled, Mode=TwoWay}"/>
<!-- Options -->
<StackPanel Grid.Row="2"
Margin="8,4,8,8"
Height="32"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Classes="flat primary"
Width="80"
Content="{DynamicResource Text.Sure}"
Click="SaveAndClose"
HotKey="Enter"/>
<Button Classes="flat"
Width="80"
Margin="8,0,0,0"
Content="{DynamicResource Text.Cancel}"
Click="CloseWindow"/>
</StackPanel>
<CheckBox Grid.Row="5" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.TagEnabled}"
IsChecked="{Binding GPGTagSigningEnabled, Mode=TwoWay}"/>
</Grid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Configure.IssueTracker}"/>
</TabItem.Header>
<Grid ColumnDefinitions="200,*" Height="250" Margin="0,8,0,16">
<Border Grid.Column="0"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}">
<Grid RowDefinitions="*,1,Auto">
<ListBox Grid.Row="0"
Background="Transparent"
ItemsSource="{Binding IssueTrackerRules}"
SelectedItem="{Binding SelectedIssueTrackerRule, Mode=TwoWay}"
SelectionMode="AlwaysSelected,Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:IssueTrackerRule">
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle Grid.Row="1" Height="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Background="{DynamicResource Brush.ToolBar}">
<Button Classes="icon_button">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.NewRule}" Command="{Binding NewIssueTracker}"/>
<MenuItem Header="-"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleGithub}" Command="{Binding AddSampleGithubIssueTracker}"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleJira}" Command="{Binding AddSampleJiraIssueTracker}"/>
</MenuFlyout>
</Button.Flyout>
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>
</Button>
<Rectangle Grid.Row="1" Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
<Button Classes="icon_button" Command="{Binding RemoveSelectedIssueTracker}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Rectangle Grid.Row="1" Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Column="1" Content="{Binding SelectedIssueTrackerRule}" Margin="16,0,0,0">
<ContentControl.Content>
<Binding Path="SelectedIssueTrackerRule">
<Binding.TargetNullValue>
<Path Width="64" Height="64"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Issue}"/>
</Binding.TargetNullValue>
</Binding>
</ContentControl.Content>
<ContentControl.DataTemplates>
<DataTemplate DataType="m:IssueTrackerRule">
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Text="{DynamicResource Text.Configure.IssueTracker.RuleName}"/>
<TextBox Grid.Row="1" Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Margin="0,12,0,0" Text="{DynamicResource Text.Configure.IssueTracker.Regex}"/>
<TextBox Grid.Row="3" Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding RegexString, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Path Margin="4,0" Width="12" Height="12" Data="{StaticResource Icons.Error}" Fill="OrangeRed" IsVisible="{Binding !IsRegexValid}"/>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="4" Margin="0,12,0,0" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate}"/>
<TextBox Grid.Row="5" Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding URLTemplate, Mode=TwoWay}"/>
<TextBlock Grid.Row="6" Margin="0,2,0,0" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate.Tip}" Foreground="{DynamicResource Brush.FG2}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</TabItem>
</TabControl>
</Grid>
</v:ChromelessWindow>

View file

@ -16,11 +16,6 @@ namespace SourceGit.Views
}
private void CloseWindow(object _1, RoutedEventArgs _2)
{
Close();
}
private void SaveAndClose(object _1, RoutedEventArgs _2)
{
(DataContext as ViewModels.RepositoryConfigure)?.Save();
Close();