Merge branch 'release/v8.30'

This commit is contained in:
leo 2024-09-16 10:55:40 +08:00
commit 94f75d7017
No known key found for this signature in database
67 changed files with 2408 additions and 981 deletions

View file

@ -34,6 +34,8 @@ Opensource Git GUI client.
* GitFlow
* Git LFS
* Issue Link
* Workspace
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
@ -83,6 +85,20 @@ For **Linux** users:
* `xdg-open` must be installed to support open native file manager.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
## OpenAI
This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
For `OpenAI`:
* `Server` must be `https://api.openai.com/v1/chat/completions`
For other AI service:
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`
* The `API Key` is optional that depends on the service
## External Tools

View file

@ -1 +1 @@
8.29
8.30

View file

@ -46,6 +46,8 @@ namespace SourceGit
[JsonSerializable(typeof(Models.ExternalToolPaths))]
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
[JsonSerializable(typeof(Models.ThemeOverrides))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]

View file

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace SourceGit.Commands
{
/// <summary>
/// A C# version of https://github.com/anjerodev/commitollama
/// </summary>
public class GenerateCommitMessage
{
public class GetDiffContent : Command
{
public GetDiffContent(string repo, Models.DiffOption opt)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --diff-algorithm=minimal {opt}";
}
}
public GenerateCommitMessage(string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
{
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
_onProgress = onProgress;
}
public string Result()
{
try
{
var summaries = new List<string>();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return "";
_onProgress?.Invoke($"Analyzing {change.Path}...");
var summary = GenerateChangeSummary(change);
summaries.Add(summary);
}
if (_cancelToken.IsCancellationRequested)
return "";
_onProgress?.Invoke($"Generating commit message...");
var builder = new StringBuilder();
builder.Append(GenerateSubject(string.Join("", summaries)));
builder.Append("\n");
foreach (var summary in summaries)
{
builder.Append("\n- ");
builder.Append(summary.Trim());
}
return builder.ToString();
}
catch (Exception e)
{
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
return "";
}
}
private string GenerateChangeSummary(Models.Change change)
{
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
var prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits.");
prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:");
prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points.");
prompt.AppendLine("- Do not mention the route of file that has been change.");
prompt.AppendLine("- Simply describe the MAIN GOAL of the changes.");
prompt.AppendLine("- Output directly the summary in plain text.`");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private string GenerateSubject(string summary)
{
var prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits messages.");
prompt.AppendLine("Your only goal is to retrieve a single commit message.");
prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:");
prompt.AppendLine("- Assign the commit {type} according to the next conditions:");
prompt.AppendLine(" feat: Only when adding a new feature.");
prompt.AppendLine(" fix: When fixing a bug.");
prompt.AppendLine(" docs: When updating documentation.");
prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.");
prompt.AppendLine(" test: When adding or updating tests. ");
prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. ");
prompt.AppendLine(" revert: When undoing a previous commit.");
prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.");
prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer.");
prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.");
prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters.");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private string _repo;
private List<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onProgress;
}
}

View file

@ -1,19 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace SourceGit.Converters
{
public static class BookmarkConverters
{
public static readonly FuncValueConverter<int, IBrush> ToBrush =
new FuncValueConverter<int, IBrush>(bookmark =>
{
if (bookmark == 0)
return Application.Current?.FindResource("Brush.FG1") as IBrush;
else
return Models.Bookmarks.Brushes[bookmark];
});
}
}

View file

@ -1,5 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace SourceGit.Converters
{
@ -28,5 +30,14 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
public static readonly FuncValueConverter<int, IBrush> ToBookmarkBrush =
new FuncValueConverter<int, IBrush>(bookmark =>
{
if (bookmark == 0)
return Application.Current?.FindResource("Brush.FG1") as IBrush;
else
return Models.Bookmarks.Brushes[bookmark];
});
}
}

136
src/Models/OpenAI.cs Normal file
View file

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
namespace SourceGit.Models
{
public class OpenAIChatMessage
{
[JsonPropertyName("role")]
public string Role
{
get;
set;
}
[JsonPropertyName("content")]
public string Content
{
get;
set;
}
}
public class OpenAIChatChoice
{
[JsonPropertyName("index")]
public int Index
{
get;
set;
}
[JsonPropertyName("message")]
public OpenAIChatMessage Message
{
get;
set;
}
}
public class OpenAIChatResponse
{
[JsonPropertyName("choices")]
public List<OpenAIChatChoice> Choices
{
get;
set;
} = [];
}
public class OpenAIChatRequest
{
[JsonPropertyName("model")]
public string Model
{
get;
set;
}
[JsonPropertyName("messages")]
public List<OpenAIChatMessage> Messages
{
get;
set;
} = [];
public void AddMessage(string role, string content)
{
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
}
}
public static class OpenAI
{
public static string Server
{
get;
set;
}
public static string ApiKey
{
get;
set;
}
public static string Model
{
get;
set;
}
public static bool IsValid
{
get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model);
}
public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
{
var chat = new OpenAIChatRequest() { Model = Model };
chat.AddMessage("system", prompt);
chat.AddMessage("user", question);
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
client.DefaultRequestHeaders.Add("Content-Type", "application/json");
if (!string.IsNullOrEmpty(ApiKey))
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest));
try
{
var task = client.PostAsync(Server, req, cancellation);
task.Wait();
var rsp = task.Result;
if (!rsp.IsSuccessStatusCode)
throw new Exception($"AI service returns error code {rsp.StatusCode}");
var reader = rsp.Content.ReadAsStringAsync(cancellation);
reader.Wait();
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
}
catch
{
if (cancellation.IsCancellationRequested)
return null;
throw;
}
}
}
}

View file

@ -10,6 +10,9 @@ namespace SourceGit.Models
public class RevisionImageFile
{
public Bitmap Image { get; set; } = null;
public long FileSize { get; set; } = 0;
public string ImageType { get; set; } = string.Empty;
public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0";
}
public class RevisionTextFile

View file

@ -1,10 +0,0 @@
namespace SourceGit.Models
{
public enum Shell
{
Default = 0,
PowerShell,
CommandPrompt,
DefaultShellOfWindowsTerminal,
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace SourceGit.Models
{
public class ShellOrTerminal
{
public string Type { get; set; }
public string Name { get; set; }
public string Exec { get; set; }
public Bitmap Icon
{
get
{
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/ShellIcons/{Type}.png", UriKind.RelativeOrAbsolute));
return new Bitmap(icon);
}
}
public static readonly List<ShellOrTerminal> Supported;
static ShellOrTerminal()
{
if (OperatingSystem.IsWindows())
{
Supported = new List<ShellOrTerminal>()
{
new ShellOrTerminal("git-bash", "Git Bash", "bash.exe"),
new ShellOrTerminal("pwsh", "PowerShell", "pwsh.exe|powershell.exe"),
new ShellOrTerminal("cmd", "Command Prompt", "cmd.exe"),
new ShellOrTerminal("wt", "Windows Terminal", "wt.exe")
};
}
else if (OperatingSystem.IsMacOS())
{
Supported = new List<ShellOrTerminal>()
{
new ShellOrTerminal("mac-terminal", "Terminal", ""),
new ShellOrTerminal("iterm2", "iTerm", ""),
};
}
else
{
Supported = new List<ShellOrTerminal>()
{
new ShellOrTerminal("gnome-terminal", "Gnome Terminal", "gnome-terminal"),
new ShellOrTerminal("konsole", "Konsole", "konsole"),
new ShellOrTerminal("xfce4-terminal", "Xfce4 Terminal", "xfce4-terminal"),
new ShellOrTerminal("lxterminal", "LXTerminal", "lxterminal"),
new ShellOrTerminal("deepin-terminal", "Deepin Terminal", "deepin-terminal"),
new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"),
new ShellOrTerminal("foot", "Foot", "foot"),
};
}
}
public ShellOrTerminal(string type, string name, string exec)
{
Type = type;
Name = name;
Exec = exec;
}
}
}

View file

@ -11,31 +11,8 @@ namespace SourceGit.Native
[SupportedOSPlatform("linux")]
internal class Linux : OS.IBackend
{
class Terminal
{
public string FilePath { get; set; }
public string OpenArgFormat { get; set; }
public Terminal(string exec, string fmt)
{
FilePath = exec;
OpenArgFormat = fmt;
}
public void Open(string dir)
{
Process.Start(FilePath, string.Format(OpenArgFormat, dir));
}
}
public Linux()
{
_xdgOpenPath = FindExecutable("xdg-open");
_terminal = FindTerminal();
}
public void SetupApp(AppBuilder builder)
{
{
builder.With(new X11PlatformOptions()
{
EnableIme = true,
@ -47,6 +24,20 @@ namespace SourceGit.Native
return FindExecutable("git");
}
public string FindTerminal(Models.ShellOrTerminal shell)
{
var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathes)
{
var test = Path.Combine(path, shell.Exec);
if (File.Exists(test))
return test;
}
return string.Empty;
}
public List<Models.ExternalTool> FindExternalTools()
{
var finder = new Models.ExternalToolsFinder();
@ -61,50 +52,40 @@ namespace SourceGit.Native
public void OpenBrowser(string url)
{
if (string.IsNullOrEmpty(_xdgOpenPath))
App.RaiseException("", $"Can NOT find `xdg-open` command!!!");
else
Process.Start(_xdgOpenPath, $"\"{url}\"");
Process.Start("xdg-open", $"\"{url}\"");
}
public void OpenInFileManager(string path, bool select)
{
if (string.IsNullOrEmpty(_xdgOpenPath))
{
App.RaiseException("", $"Can NOT find `xdg-open` command!!!");
return;
}
if (Directory.Exists(path))
{
Process.Start(_xdgOpenPath, $"\"{path}\"");
Process.Start("xdg-open", $"\"{path}\"");
}
else
{
var dir = Path.GetDirectoryName(path);
if (Directory.Exists(dir))
Process.Start(_xdgOpenPath, $"\"{dir}\"");
Process.Start("xdg-open", $"\"{dir}\"");
}
}
public void OpenTerminal(string workdir)
{
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
if (_terminal == null)
App.RaiseException(dir, $"Only supports gnome-terminal/konsole/xfce4-terminal/lxterminal/deepin-terminal/mate-terminal/foot!");
else
_terminal.Open(dir);
if (string.IsNullOrEmpty(OS.ShellOrTerminal) || !File.Exists(OS.ShellOrTerminal))
{
App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured.");
return;
}
var startInfo = new ProcessStartInfo();
startInfo.WorkingDirectory = string.IsNullOrEmpty(workdir) ? "~" : workdir;
startInfo.FileName = OS.ShellOrTerminal;
Process.Start(startInfo);
}
public void OpenWithDefaultEditor(string file)
{
if (string.IsNullOrEmpty(_xdgOpenPath))
{
App.RaiseException("", $"Can NOT find `xdg-open` command!!!");
return;
}
var proc = Process.Start(_xdgOpenPath, $"\"{file}\"");
var proc = Process.Start("xdg-open", $"\"{file}\"");
if (proc != null)
{
proc.WaitForExit();
@ -130,51 +111,10 @@ namespace SourceGit.Native
return string.Empty;
}
private Terminal FindTerminal()
{
var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathes)
{
var test = Path.Combine(path, "gnome-terminal");
if (File.Exists(test))
return new Terminal(test, "--working-directory=\"{0}\"");
test = Path.Combine(path, "konsole");
if (File.Exists(test))
return new Terminal(test, "--workdir \"{0}\"");
test = Path.Combine(path, "xfce4-terminal");
if (File.Exists(test))
return new Terminal(test, "--working-directory=\"{0}\"");
test = Path.Combine(path, "lxterminal");
if (File.Exists(test))
return new Terminal(test, "--working-directory=\"{0}\"");
test = Path.Combine(path, "deepin-terminal");
if (File.Exists(test))
return new Terminal(test, "--work-directory \"{0}\"");
test = Path.Combine(path, "mate-terminal");
if (File.Exists(test))
return new Terminal(test, "--working-directory=\"{0}\"");
test = Path.Combine(path, "foot");
if (File.Exists(test))
return new Terminal(test, "--working-directory=\"{0}\"");
}
return null;
}
private string FindJetBrainsFleet()
{
var path = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}/JetBrains/Toolbox/apps/fleet/bin/Fleet";
return File.Exists(path) ? path : FindExecutable("fleet");
}
private string _xdgOpenPath = null;
private Terminal _terminal = null;
}
}

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
using System.Text;
using Avalonia;
@ -12,12 +11,6 @@ namespace SourceGit.Native
[SupportedOSPlatform("macOS")]
internal class MacOS : OS.IBackend
{
enum TerminalType
{
Default,
iTerm2,
}
public void SetupApp(AppBuilder builder)
{
builder.With(new MacOSPlatformOptions()
@ -31,6 +24,19 @@ namespace SourceGit.Native
return File.Exists("/usr/bin/git") ? "/usr/bin/git" : string.Empty;
}
public string FindTerminal(Models.ShellOrTerminal shell)
{
switch (shell.Type)
{
case "mac-terminal":
return "Terminal";
case "iterm2":
return "iTerm";
}
return "InvalidTerminal";
}
public List<Models.ExternalTool> FindExternalTools()
{
var finder = new Models.ExternalToolsFinder();
@ -58,54 +64,14 @@ namespace SourceGit.Native
public void OpenTerminal(string workdir)
{
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
dir = dir.Replace(" ", "\\ ");
var terminal = DetectTerminal();
var cmdBuilder = new StringBuilder();
switch (terminal)
{
case TerminalType.iTerm2:
cmdBuilder.AppendLine("on run argv");
cmdBuilder.AppendLine(" tell application \"iTerm2\"");
cmdBuilder.AppendLine(" create window with default profile");
cmdBuilder.AppendLine(" tell the current session of the current window");
cmdBuilder.AppendLine($" write text \"cd {dir}\"");
cmdBuilder.AppendLine(" end tell");
cmdBuilder.AppendLine(" end tell");
cmdBuilder.AppendLine("end run");
break;
default:
cmdBuilder.AppendLine("on run argv");
cmdBuilder.AppendLine(" tell application \"Terminal\"");
cmdBuilder.AppendLine($" do script \"cd {dir}\"");
cmdBuilder.AppendLine(" activate");
cmdBuilder.AppendLine(" end tell");
cmdBuilder.AppendLine("end run");
break;
}
var tmp = Path.GetTempFileName();
File.WriteAllText(tmp, cmdBuilder.ToString());
var proc = Process.Start("osascript", $"\"{tmp}\"");
if (proc != null)
proc.Exited += (_, _) => File.Delete(tmp);
else
File.Delete(tmp);
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var dir = string.IsNullOrEmpty(workdir) ? home : workdir;
Process.Start("open", $"-a {OS.ShellOrTerminal} \"{dir}\"");
}
public void OpenWithDefaultEditor(string file)
{
Process.Start("open", $"\"{file}\"");
}
private TerminalType DetectTerminal()
{
if (Directory.Exists("/Applications/iTerm.app"))
return TerminalType.iTerm2;
return TerminalType.Default;
}
}
}

View file

@ -13,6 +13,7 @@ namespace SourceGit.Native
void SetupApp(AppBuilder builder);
string FindGitExecutable();
string FindTerminal(Models.ShellOrTerminal shell);
List<Models.ExternalTool> FindExternalTools();
void OpenTerminal(string workdir);
@ -23,6 +24,7 @@ namespace SourceGit.Native
public static string DataDir { get; private set; } = string.Empty;
public static string GitExecutable { get; set; } = string.Empty;
public static string ShellOrTerminal { get; set; } = string.Empty;
public static List<Models.ExternalTool> ExternalTools { get; set; } = [];
static OS()
@ -45,29 +47,6 @@ namespace SourceGit.Native
}
}
public static Models.Shell GetShell()
{
if (OperatingSystem.IsWindows())
return (_backend as Windows)!.Shell;
return Models.Shell.Default;
}
public static bool SetShell(Models.Shell shell)
{
if (OperatingSystem.IsWindows())
{
var windows = (_backend as Windows)!;
if (windows.Shell != shell)
{
windows.Shell = shell;
return true;
}
}
return false;
}
public static void SetupApp(AppBuilder builder)
{
_backend.SetupApp(builder);
@ -95,6 +74,14 @@ namespace SourceGit.Native
return _backend.FindGitExecutable();
}
public static void SetShellOrTerminal(Models.ShellOrTerminal shellOrTerminal)
{
if (shellOrTerminal == null)
ShellOrTerminal = string.Empty;
else
ShellOrTerminal = _backend.FindTerminal(shellOrTerminal);
}
public static void OpenInFileManager(string path, bool select = false)
{
_backend.OpenInFileManager(path, select);
@ -107,7 +94,10 @@ namespace SourceGit.Native
public static void OpenTerminal(string workdir)
{
_backend.OpenTerminal(workdir);
if (string.IsNullOrEmpty(ShellOrTerminal))
App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured.");
else
_backend.OpenTerminal(workdir);
}
public static void OpenWithDefaultEditor(string file)

View file

@ -53,12 +53,6 @@ namespace SourceGit.Native
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags);
public Models.Shell Shell
{
get;
set;
} = Models.Shell.Default;
public void SetupApp(AppBuilder builder)
{
// Fix drop shadow issue on Windows 10
@ -98,6 +92,51 @@ namespace SourceGit.Native
return null;
}
public string FindTerminal(Models.ShellOrTerminal shell)
{
switch (shell.Type)
{
case "git-bash":
if (string.IsNullOrEmpty(OS.GitExecutable))
break;
var binDir = Path.GetDirectoryName(OS.GitExecutable)!;
var bash = Path.Combine(binDir, "bash.exe");
if (!File.Exists(bash))
break;
return bash;
case "pwsh":
var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine,
Microsoft.Win32.RegistryView.Registry64);
var pwsh = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pwsh.exe");
if (pwsh != null)
{
var path = pwsh.GetValue(null) as string;
if (File.Exists(path))
return path;
}
var pwshFinder = new StringBuilder("powershell.exe", 512);
if (PathFindOnPath(pwshFinder, null))
return pwshFinder.ToString();
break;
case "cmd":
return "C:\\Windows\\System32\\cmd.exe";
case "wt":
var wtFinder = new StringBuilder("wt.exe", 512);
if (PathFindOnPath(wtFinder, null))
return wtFinder.ToString();
break;
}
return string.Empty;
}
public List<Models.ExternalTool> FindExternalTools()
{
var finder = new Models.ExternalToolsFinder();
@ -119,56 +158,15 @@ namespace SourceGit.Native
public void OpenTerminal(string workdir)
{
if (string.IsNullOrEmpty(workdir) || !Path.Exists(workdir))
if (string.IsNullOrEmpty(OS.ShellOrTerminal) || !File.Exists(OS.ShellOrTerminal))
{
workdir = ".";
App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured.");
return;
}
var startInfo = new ProcessStartInfo();
startInfo.WorkingDirectory = workdir;
switch (Shell)
{
case Models.Shell.Default:
if (string.IsNullOrEmpty(OS.GitExecutable))
{
App.RaiseException(workdir, $"Can NOT found bash.exe");
return;
}
var binDir = Path.GetDirectoryName(OS.GitExecutable)!;
var bash = Path.Combine(binDir, "bash.exe");
if (!File.Exists(bash))
{
App.RaiseException(workdir, $"Can NOT found bash.exe under '{binDir}'");
return;
}
startInfo.FileName = bash;
break;
case Models.Shell.PowerShell:
startInfo.FileName = ChoosePowerShell();
startInfo.Arguments = startInfo.FileName.EndsWith("pwsh.exe") ? $"-WorkingDirectory \"{workdir}\" -Nologo" : "-Nologo";
break;
case Models.Shell.CommandPrompt:
startInfo.FileName = "cmd";
break;
case Models.Shell.DefaultShellOfWindowsTerminal:
var wt = FindWindowsTerminalApp();
if (!File.Exists(wt))
{
App.RaiseException(workdir, $"Can NOT found wt.exe on your system!");
return;
}
startInfo.FileName = wt;
startInfo.Arguments = $"-d \"{workdir}\"";
break;
default:
App.RaiseException(workdir, $"Bad shell configuration!");
return;
}
startInfo.FileName = OS.ShellOrTerminal;
Process.Start(startInfo);
}
@ -218,52 +216,6 @@ namespace SourceGit.Native
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
}
// There are two versions of PowerShell : pwsh.exe (preferred) and powershell.exe (system default)
private string ChoosePowerShell()
{
if (!string.IsNullOrEmpty(_powershellPath))
return _powershellPath;
var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine,
Microsoft.Win32.RegistryView.Registry64);
var pwsh = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pwsh.exe");
if (pwsh != null)
{
var path = pwsh.GetValue(null) as string;
if (File.Exists(path))
{
_powershellPath = path;
return _powershellPath;
}
}
var finder = new StringBuilder("powershell.exe", 512);
if (PathFindOnPath(finder, null))
{
_powershellPath = finder.ToString();
return _powershellPath;
}
return string.Empty;
}
private string FindWindowsTerminalApp()
{
if (!string.IsNullOrEmpty(_wtPath))
return _wtPath;
var finder = new StringBuilder("wt.exe", 512);
if (PathFindOnPath(finder, null))
{
_wtPath = finder.ToString();
return _wtPath;
}
return string.Empty;
}
#region EXTERNAL_EDITOR_FINDER
private string FindVSCode()
{
@ -385,8 +337,5 @@ namespace SourceGit.Native
ILFree(pidl);
}
}
private string _powershellPath = string.Empty;
private string _wtPath = string.Empty;
}
}

View file

@ -1,4 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StreamGeometry x:Key="Icons.AIAssist">M951 419a255 255 0 00-22-209 258 258 0 00-278-124A259 259 0 00213 178a255 255 0 00-171 124 258 258 0 0032 303 255 255 0 0022 210 258 258 0 00278 124A255 255 0 00566 1024a258 258 0 00246-179 256 256 0 00171-124 258 258 0 00-32-302zM566 957a191 191 0 01-123-44l6-3 204-118a34 34 0 0017-29v-287l86 50a3 3 0 012 2v238a192 192 0 01-192 192zM154 781a191 191 0 01-23-129l6 4 204 118a33 33 0 0033 0l249-144v99a3 3 0 01-1 3L416 851a192 192 0 01-262-70zM100 337a191 191 0 01101-84V495a33 33 0 0017 29l248 143-86 50a3 3 0 01-3 0l-206-119A192 192 0 01100 336zm708 164-249-145L645 307a3 3 0 013 0l206 119a192 192 0 01-29 346v-242a34 34 0 00-17-28zm86-129-6-4-204-119a33 33 0 00-33 0L401 394V294a3 3 0 011-3l206-119a192 192 0 01285 199zm-539 176-86-50a3 3 0 01-2-2V259a192 192 0 01315-147l-6 3-204 118a34 34 0 00-17 29zm47-101 111-64 111 64v128l-111 64-111-64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Archive">M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z</StreamGeometry>
<StreamGeometry x:Key="Icons.Binary">M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z</StreamGeometry>
<StreamGeometry x:Key="Icons.Blame">M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z</StreamGeometry>
@ -119,6 +120,7 @@
<StreamGeometry x:Key="Icons.Window.Maximize">M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z</StreamGeometry>
<StreamGeometry x:Key="Icons.Window.Restore">M256 128l0 192L64 320l0 576 704 0 0-192 192 0L960 128 256 128zM704 832 128 832 128 384l576 0L704 832zM896 640l-128 0L768 320 320 320 320 192l576 0L896 640z</StreamGeometry>
<StreamGeometry x:Key="Icons.WordWrap">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Workspace">M128 691H6V38h838v160h-64V102H70v525H128zM973 806H154V250h819v557zm-755-64h691V314H218v429zM365 877h448v64h-448z</StreamGeometry>
<StreamGeometry x:Key="Icons.Worktree">M512 0C229 0 0 72 0 160v128C0 376 229 448 512 448s512-72 512-160v-128C1024 72 795 0 512 0zM512 544C229 544 0 472 0 384v192c0 88 229 160 512 160s512-72 512-160V384c0 88-229 160-512 160zM512 832c-283 0-512-72-512-160v192C0 952 229 1024 512 1024s512-72 512-160v-192c0 88-229 160-512 160z</StreamGeometry>
<StreamGeometry x:Key="Icons.Worktree.Add">M640 725 768 725 768 597 853 597 853 725 981 725 981 811 853 811 853 939 768 939 768 811 640 811 640 725M384 128C573 128 725 204 725 299 725 393 573 469 384 469 195 469 43 393 43 299 43 204 195 128 384 128M43 384C43 478 195 555 384 555 573 555 725 478 725 384L725 512 683 512 683 595C663 612 640 627 610 640L555 640 555 660C504 675 446 683 384 683 195 683 43 606 43 512L43 384M43 597C43 692 195 768 384 768 446 768 504 760 555 745L555 873C504 888 446 896 384 896 195 896 43 820 43 725L43 597Z</StreamGeometry>
</ResourceDictionary>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -20,6 +20,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Standard ist der Zielordnername.</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Branch verfolgen:</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Remote-Branch verfolgen</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI Assistent</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Verwende OpenAI, um Commit-Nachrichten zu generieren</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Fehler</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Fehler werfen und anwenden des Patches verweigern</x:String>
@ -147,6 +149,9 @@
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP Proxy für dieses Repository</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">Benutzername</x:String>
<x:String x:Key="Text.Configure.User.Placeholder" xml:space="preserve">Benutzername für dieses Repository</x:String>
<x:String x:Key="Text.ConfigureWorkspace" xml:space="preserve">Arbeitsplätze</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">Name</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">Farbe</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">Kopieren</x:String>
<x:String x:Key="Text.CopyAllText" xml:space="preserve">Kopiere gesamten Text</x:String>
<x:String x:Key="Text.CopyMessage" xml:space="preserve">COMMIT-NACHRICHT KOPIEREN</x:String>
@ -320,6 +325,7 @@
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">REPOSITORY</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Commit" xml:space="preserve">Gestagte Änderungen committen</x:String>
<x:String x:Key="Text.Hotkeys.Repo.CommitAndPush" xml:space="preserve">Gestagte Änderungen committen und pushen</x:String>
<x:String x:Key="Text.Hotkeys.Repo.DiscardSelected" xml:space="preserve">Ausgewählte Änderungen verwerfen</x:String>
<x:String x:Key="Text.Hotkeys.Repo.GoHome" xml:space="preserve">Dashboard Modus (Standard)</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Refresh" xml:space="preserve">Erzwinge Neuladen des Repositorys</x:String>
<x:String x:Key="Text.Hotkeys.Repo.StageOrUnstageSelected" xml:space="preserve">Ausgewählte Änderungen stagen/unstagen</x:String>
@ -354,6 +360,8 @@
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Ziel-Branch:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">Quell-Branch:</x:String>
<x:String x:Key="Text.MoveRepositoryNode" xml:space="preserve">Bewege Repository Knoten</x:String>
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">Wähle Vorgänger-Knoten für:</x:String>
<x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">Git wurde NICHT konfiguriert. Gehe bitte zuerst in die [Einstellungen] und konfiguriere Git.</x:String>
<x:String x:Key="Text.Notice" xml:space="preserve">BENACHRICHTIGUNG</x:String>
@ -379,6 +387,10 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">Leztes Jahr</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">Vor {0} Jahren</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Einstellungen</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Schlüssel</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">Modell</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">DARSTELLUNG</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Standardschriftart</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">Standardschriftgröße</x:String>
@ -388,11 +400,14 @@
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">Design-Anpassungen</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">Fixe Tab-Breite in Titelleiste</x:String>
<x:String x:Key="Text.Preference.Appearance.UseNativeWindowFrame" xml:space="preserve">Verwende nativen Fensterrahmen</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Installationspfad</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Installationspfad zum Diff/Merge Tool</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">ALLGEMEIN</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">Beim Starten nach Updates suchen</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Sprache</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">Commit-Historie</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">Zuletzt geöffnete Tabs beim Starten wiederherstellen</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Längenvorgabe für Commit-Nachrichten</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">Remotes automatisch fetchen</x:String>
@ -403,7 +418,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">Benutzer Email</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">Globale Git Benutzer Email</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">Installationspfad</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">Benutzername</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">Globaler Git Benutzername</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Git Version</x:String>
@ -416,10 +430,6 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">Installationspfad zum GPG Programm</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">Benutzer Signierungsschlüssel</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">GPG Benutzer Signierungsschlüssel</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Installationspfad</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Installationspfad zum Diff/Merge Tool</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Remote löschen</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Ziel:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Worktrees löschen</x:String>
@ -482,7 +492,7 @@
<x:String x:Key="Text.Repository.Configure" xml:space="preserve">Repository Einstellungen</x:String>
<x:String x:Key="Text.Repository.Continue" xml:space="preserve">WEITER</x:String>
<x:String x:Key="Text.Repository.Explore" xml:space="preserve">Öffne im Datei-Browser</x:String>
<x:String x:Key="Text.Repository.Filter" xml:space="preserve">Suche Branches &amp; Tags &amp; Submodules</x:String>
<x:String x:Key="Text.Repository.Filter" xml:space="preserve">Suche Branches &amp; Tags &amp; Submodule</x:String>
<x:String x:Key="Text.Repository.FilterCommitPrefix" xml:space="preserve">GEFILTERT:</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">LOKALE BRANCHES</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">Zum HEAD wechseln</x:String>
@ -526,6 +536,8 @@
<x:String x:Key="Text.Save" xml:space="preserve">SPEICHERN</x:String>
<x:String x:Key="Text.SaveAs" xml:space="preserve">Speichern als...</x:String>
<x:String x:Key="Text.SaveAsPatchSuccess" xml:space="preserve">Patch wurde erfolgreich gespeichert!</x:String>
<x:String x:Key="Text.ScanRepositories" xml:space="preserve">Durchsuche Repositories</x:String>
<x:String x:Key="Text.ScanRepositories.RootDir" xml:space="preserve">Hauptverzeichnis:</x:String>
<x:String x:Key="Text.SelfUpdate" xml:space="preserve">Suche nach Updates...</x:String>
<x:String x:Key="Text.SelfUpdate.Available" xml:space="preserve">Neue Version ist verfügbar: </x:String>
<x:String x:Key="Text.SelfUpdate.Error" xml:space="preserve">Suche nach Updates fehlgeschlagen!</x:String>
@ -585,9 +597,11 @@
<x:String x:Key="Text.Welcome.Delete" xml:space="preserve">Lösche</x:String>
<x:String x:Key="Text.Welcome.DragDropTip" xml:space="preserve">DRAG &amp; DROP VON ORDNER UNTERSTÜTZT. BENUTZERDEFINIERTE GRUPPIERUNG UNTERSTÜTZT.</x:String>
<x:String x:Key="Text.Welcome.Edit" xml:space="preserve">Bearbeiten</x:String>
<x:String x:Key="Text.Welcome.Move" xml:space="preserve">Bewege in eine andere Gruppe</x:String>
<x:String x:Key="Text.Welcome.OpenAllInNode" xml:space="preserve">Öffne alle Repositories</x:String>
<x:String x:Key="Text.Welcome.OpenOrInit" xml:space="preserve">Öffne Repository</x:String>
<x:String x:Key="Text.Welcome.OpenTerminal" xml:space="preserve">Öffne Terminal</x:String>
<x:String x:Key="Text.Welcome.ScanDefaultCloneDir" xml:space="preserve">Clone Standardordner erneut nach Repositories durchsuchen</x:String>
<x:String x:Key="Text.Welcome.Search" xml:space="preserve">Suche Repositories...</x:String>
<x:String x:Key="Text.Welcome.Sort" xml:space="preserve">Sortieren</x:String>
<x:String x:Key="Text.WorkingCopy" xml:space="preserve">Änderungen</x:String>
@ -617,6 +631,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN</x:String>
<x:String x:Key="Text.WorkingCopy.UseCommitTemplate" xml:space="preserve">Template: ${0}$</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus.</x:String>
<x:String x:Key="Text.Workspace" xml:space="preserve">ARBEITSPLATZ: </x:String>
<x:String x:Key="Text.Workspace.Configure" xml:space="preserve">Arbeitsplätze konfigurieren...</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">WORKTREE</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">Pfad kopieren</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Sperren</x:String>

View file

@ -17,6 +17,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI Assistant</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use OpenAI to generate commit message</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>
@ -146,6 +148,9 @@
<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>
<x:String x:Key="Text.Configure.User.Placeholder" xml:space="preserve">User name for this repository</x:String>
<x:String x:Key="Text.ConfigureWorkspace" xml:space="preserve">Workspaces</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">Name</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">Color</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">Copy</x:String>
<x:String x:Key="Text.CopyAllText" xml:space="preserve">Copy All Text</x:String>
<x:String x:Key="Text.CopyMessage" xml:space="preserve">COPY MESSAGE</x:String>
@ -381,6 +386,10 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">Last year</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">Model</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APPEARANCE</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Default Font</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">Default Font Size</x:String>
@ -390,11 +399,14 @@
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">Theme Overrides</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">Use fixed tab width in titlebar</x:String>
<x:String x:Key="Text.Preference.Appearance.UseNativeWindowFrame" xml:space="preserve">Use native window frame</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Input path for diff/merge tool</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">GENERAL</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">Check for updates on startup</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Language</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">History Commits</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">Restore last opened tab(s) on startup</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Subject Guide Length</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">Fetch remotes automatically</x:String>
@ -405,7 +417,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">User Email</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">Global git user email</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">User Name</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">Global git user name</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Git version</x:String>
@ -418,10 +429,10 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">Input path for installed gpg program</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">User Signing Key</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">User's gpg signing key</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Input path for diff/merge tool</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.Preference.Integration" xml:space="preserve">INTEGRATION</x:String>
<x:String x:Key="Text.Preference.Shell" xml:space="preserve">SHELL/TERMINAL</x:String>
<x:String x:Key="Text.Preference.Shell.Type" xml:space="preserve">Shell/Terminal</x:String>
<x:String x:Key="Text.Preference.Shell.Path" xml:space="preserve">Path</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Prune Remote</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Prune Worktrees</x:String>
@ -624,6 +635,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">VIEW ASSUME UNCHANGED</x:String>
<x:String x:Key="Text.WorkingCopy.UseCommitTemplate" xml:space="preserve">Template: ${0}$</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">Right-click the selected file(s), and make your choice to resolve conflicts.</x:String>
<x:String x:Key="Text.Workspace" xml:space="preserve">WORKSPACE: </x:String>
<x:String x:Key="Text.Workspace.Configure" xml:space="preserve">Configure Workspaces...</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">WORKTREE</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">Copy Path</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>

View file

@ -387,11 +387,14 @@
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">Dérogations de thème</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">Utiliser des onglets de taille fixe dans la barre de titre</x:String>
<x:String x:Key="Text.Preference.Appearance.UseNativeWindowFrame" xml:space="preserve">Utiliser un cadre de fenêtre natif</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">OUTIL DIFF/MERGE</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Chemin d'installation</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Saisir le chemin d'installation de l'outil diff/merge</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Outil</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">GÉNÉRAL</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">Vérifier les mises à jour au démarrage</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Language</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">Historique de commits</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">Restaurer les onglets au démarrage</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Guide de longueur du sujet</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">Fetch les dépôts distants automatiquement</x:String>
@ -402,7 +405,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">E-mail utilsateur</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">E-mail utilsateur global</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">Chemin d'installation</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">Nom d'utilisateur</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">Nom d'utilisateur global</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Version de Git</x:String>
@ -415,10 +417,6 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">Saisir le chemin d'installation vers le programme GPG</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">Clé de signature de l'utilisateur</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">Clé de signature GPG de l'utilisateur</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">OUTIL DIFF/MERGE</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Chemin d'installation</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Saisir le chemin d'installation de l'outil diff/merge</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Outil</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Élaguer une branche distant</x:String> <!-- If it is indeed about a branch -->
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Cible :</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Élaguer les Worktrees</x:String>

View file

@ -381,11 +381,14 @@
<x:String x:Key="Text.Preference.Appearance.Theme" xml:space="preserve">Tema</x:String>
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">Sobrescrever Tema</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">Usar largura fixa da aba na barra de título</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">FERRAMENTA DE DIF/MERGE</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Caminho de Instalação</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Insira o caminho para a ferramenta de dif/merge</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Ferramenta</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">GERAL</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">Verificar atualizações na inicialização</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Idioma</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">Commits do Histórico</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">Restaurar as últimas abas abertas na inicialização</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Comprimento do Guia de Assunto</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">Buscar remotos automaticamente</x:String>
@ -396,7 +399,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">E-mail do Usuário</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">E-mail global do usuário git</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">Caminho de Instalação</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">Nome do Usuário</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">Nome global do usuário git</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Versão do Git</x:String>
@ -409,10 +411,6 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">Insira o caminho do programa gpg instalado</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">Chave de Assinatura do Usuário</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">Chave de assinatura gpg do usuário</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">FERRAMENTA DE DIF/MERGE</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">Caminho de Instalação</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">Insira o caminho para a ferramenta de dif/merge</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Ferramenta</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Prunar Remoto</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Alvo:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Podar Worktrees</x:String>

View file

@ -20,6 +20,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI助手</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用OpenAI助手生成提交信息</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
@ -149,6 +151,9 @@
<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>
<x:String x:Key="Text.Configure.User.Placeholder" xml:space="preserve">应用于本仓库的用户名</x:String>
<x:String x:Key="Text.ConfigureWorkspace" xml:space="preserve">工作区</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">名称</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">颜色</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">复制</x:String>
<x:String x:Key="Text.CopyAllText" xml:space="preserve">复制全部文本</x:String>
<x:String x:Key="Text.CopyMessage" xml:space="preserve">复制内容</x:String>
@ -393,11 +398,14 @@
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">主题自定义</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">主标签使用固定宽度</x:String>
<x:String x:Key="Text.Preference.Appearance.UseNativeWindowFrame" xml:space="preserve">使用系统默认窗体样式</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">对比/合并工具</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">填写工具可执行文件所在位置</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">通用配置</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">启动时检测软件更新</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">显示语言</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">最大历史提交数</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">启动时恢复上次打开的仓库</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">SUBJECT字数检测</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">启用定时自动拉取远程更新</x:String>
@ -408,7 +416,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">邮箱</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">默认GIT用户邮箱</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">终端Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">用户名</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">默认GIT用户名</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Git 版本</x:String>
@ -421,10 +428,10 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">签名程序所在路径</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">用户签名KEY</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">输入签名提交所使用的KEY</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">对比/合并工具</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">填写工具可执行文件所在位置</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Preference.Integration" xml:space="preserve">第三方工具集成</x:String>
<x:String x:Key="Text.Preference.Shell" xml:space="preserve">终端/SHELL</x:String>
<x:String x:Key="Text.Preference.Shell.Type" xml:space="preserve">终端/SHELL</x:String>
<x:String x:Key="Text.Preference.Shell.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">清理远程已删除分支</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目标 </x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">清理工作树</x:String>
@ -626,6 +633,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">查看忽略变更文件</x:String>
<x:String x:Key="Text.WorkingCopy.UseCommitTemplate" xml:space="preserve">模板:${0}$</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">请选中冲突文件,打开右键菜单,选择合适的解决方式</x:String>
<x:String x:Key="Text.Workspace" xml:space="preserve">工作区:</x:String>
<x:String x:Key="Text.Workspace.Configure" xml:space="preserve">配置工作区...</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">本地工作树</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">复制工作树路径</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">锁定工作树</x:String>

View file

@ -20,6 +20,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">選填。預設使用目標資料夾名稱。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI 助理</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 OpenAI 產生提交訊息</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String>
@ -149,6 +151,9 @@
<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>
<x:String x:Key="Text.Configure.User.Placeholder" xml:space="preserve">用於本存放庫的使用者名稱</x:String>
<x:String x:Key="Text.ConfigureWorkspace" xml:space="preserve">工作區</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">名稱</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">顏色</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">複製</x:String>
<x:String x:Key="Text.CopyAllText" xml:space="preserve">複製全部內容</x:String>
<x:String x:Key="Text.CopyMessage" xml:space="preserve">複製內容</x:String>
@ -358,7 +363,7 @@
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式:</x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">合併分支:</x:String>
<x:String x:Key="Text.MoveRepositoryNode" xml:space="preserve">調整存放庫分組</x:String>
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">請選擇目標分組</x:String>
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">請選擇目標分組:</x:String>
<x:String x:Key="Text.Name" xml:space="preserve">名稱:</x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">尚未設定 Git。請開啟 [偏好設定] 以設定 Git 路徑。</x:String>
<x:String x:Key="Text.Notice" xml:space="preserve">系統提示</x:String>
@ -384,6 +389,10 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} 年前</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好設定</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OpenAI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">伺服器</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API 金鑰</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外觀設定</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">預設字型</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">預設字型大小</x:String>
@ -393,11 +402,14 @@
<x:String x:Key="Text.Preference.Appearance.ThemeOverrides" xml:space="preserve">自訂主題</x:String>
<x:String x:Key="Text.Preference.Appearance.UseFixedTabWidth" xml:space="preserve">使用固定寬度的分頁標籤</x:String>
<x:String x:Key="Text.Preference.Appearance.UseNativeWindowFrame" xml:space="preserve">使用系統原生預設視窗樣式</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">對比/合併工具</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">填寫可執行檔案所在路徑</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">一般設定</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">啟動時檢查軟體更新</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">顯示語言</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">最大歷史提交數</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">啟動時還原上次開啟的存放庫</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">提交標題字數偵測</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">Git 設定</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">啟用定時自動提取 (fetch) 遠端更新</x:String>
@ -408,7 +420,6 @@
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">電子郵件</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">預設 Git 使用者電子郵件</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">終端 Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">使用者名稱</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">預設 Git 使用者名稱</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Git 版本</x:String>
@ -421,10 +432,10 @@
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">填寫 gpg.exe 所在路徑</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">使用者簽章金鑰</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">填寫簽章提交所使用的金鑰</x:String>
<x:String x:Key="Text.Preference.DiffMerge" xml:space="preserve">對比/合併工具</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Path.Placeholder" xml:space="preserve">填寫可執行檔案所在路徑</x:String>
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Preference.Integration" xml:space="preserve">第三方工具整合</x:String>
<x:String x:Key="Text.Preference.Shell" xml:space="preserve">終端機/Shell</x:String>
<x:String x:Key="Text.Preference.Shell.Type" xml:space="preserve">終端機/Shell</x:String>
<x:String x:Key="Text.Preference.Shell.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">清理遠端已刪除分支</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目標:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">清理工作區</x:String>
@ -627,6 +638,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">檢視不追蹤變更的檔案</x:String>
<x:String x:Key="Text.WorkingCopy.UseCommitTemplate" xml:space="preserve">範本: ${0}$</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式</x:String>
<x:String x:Key="Text.Workspace" xml:space="preserve">工作區:</x:String>
<x:String x:Key="Text.Workspace.Configure" xml:space="preserve">設定工作區...</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">本機工作區</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">複製工作區路徑</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">鎖定工作區</x:String>

View file

@ -466,21 +466,6 @@
<Setter Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</Style>
<Style Selector="Button.no_border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector="Button.no_border /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="Button.no_border:pointerover Path">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}"/>
</Style>
<Style Selector="Button.no_border:pressed">
<Setter Property="RenderTransform" Value="scale(1.0)"/>
</Style>
<Style Selector="Button.flat">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border2}"/>

View file

@ -20,7 +20,7 @@
<RepositoryType>Public</RepositoryType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PropertyGroup Condition="'$(Configuration)' == 'Release' and '$(SourceGitNoAot)' != 'true'">
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -176,19 +177,17 @@ namespace SourceGit.ViewModels
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _commit.SHA, file.Path);
var bitmap = stream.Length > 0 ? new Bitmap(stream) : null;
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap };
});
var fileSize = stream.Length;
var bitmap = fileSize > 0 ? new Bitmap(stream) : null;
var imageType = Path.GetExtension(file.Path).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType };
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = image);
}
else
{
var size = new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).Result();
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
});
var binary = new Models.RevisionBinaryFile() { Size = size };
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = binary);
}
return;
@ -202,7 +201,6 @@ namespace SourceGit.ViewModels
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
obj.Object.Oid = matchLFS.Groups[1].Value;
obj.Object.Size = long.Parse(matchLFS.Groups[2].Value);
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = obj);
}
else
@ -220,14 +218,8 @@ namespace SourceGit.ViewModels
if (commit != null)
{
var body = new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).Result();
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionSubmodule()
{
Commit = commit,
FullMessage = body,
};
});
var submodule = new Models.RevisionSubmodule() { Commit = commit, FullMessage = body };
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = submodule);
}
else
{

View file

@ -0,0 +1,60 @@
using System;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class ConfigureWorkspace : ObservableObject
{
public AvaloniaList<Workspace> Workspaces
{
get;
private set;
}
public Workspace Selected
{
get => _selected;
set
{
if (SetProperty(ref _selected, value))
CanDeleteSelected = value != null && !value.IsActive;
}
}
public bool CanDeleteSelected
{
get => _canDeleteSelected;
private set => SetProperty(ref _canDeleteSelected, value);
}
public ConfigureWorkspace()
{
Workspaces = new AvaloniaList<Workspace>();
Workspaces.AddRange(Preference.Instance.Workspaces);
}
public void Add()
{
var workspace = new Workspace();
workspace.Name = $"Unnamed {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
workspace.Color = 4278221015;
Preference.Instance.Workspaces.Add(workspace);
Workspaces.Add(workspace);
Selected = workspace;
}
public void Delete()
{
if (_selected == null || _selected.IsActive)
return;
Preference.Instance.Workspaces.Remove(_selected);
Workspaces.Remove(_selected);
}
private Workspace _selected = null;
private bool _canDeleteSelected = false;
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -120,8 +121,10 @@ namespace SourceGit.ViewModels
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file);
var bitmap = stream.Length > 0 ? new Bitmap(stream) : null;
var image = new Models.RevisionImageFile() { Image = bitmap };
var fileSize = stream.Length;
var bitmap = fileSize > 0 ? new Bitmap(stream) : null;
var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, image));
}
else

View file

@ -61,6 +61,30 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _detailContext, value);
}
public GridLength LeftArea
{
get => _leftArea;
set => SetProperty(ref _leftArea, value);
}
public GridLength RightArea
{
get => _rightArea;
set => SetProperty(ref _rightArea, value);
}
public GridLength TopArea
{
get => _topArea;
set => SetProperty(ref _topArea, value);
}
public GridLength BottomArea
{
get => _bottomArea;
set => SetProperty(ref _bottomArea, value);
}
public Histories(Repository repo)
{
_repo = repo;
@ -800,5 +824,10 @@ namespace SourceGit.ViewModels
private Models.Commit _autoSelectedCommit = null;
private long _navigationId = 0;
private object _detailContext = null;
private GridLength _leftArea = new GridLength(1, GridUnitType.Star);
private GridLength _rightArea = new GridLength(1, GridUnitType.Star);
private GridLength _topArea = new GridLength(1, GridUnitType.Star);
private GridLength _bottomArea = new GridLength(1, GridUnitType.Star);
}
}

View file

@ -17,51 +17,49 @@ namespace SourceGit.ViewModels
private set;
}
public Workspace ActiveWorkspace
{
get => _activeWorkspace;
private set => SetProperty(ref _activeWorkspace, value);
}
public LauncherPage ActivePage
{
get => _activePage;
set
{
if (SetProperty(ref _activePage, value))
{
PopupHost.Active = value;
if (!_ignoreIndexChange && value is { Data: Repository })
ActiveWorkspace.ActiveIdx = Pages.IndexOf(value);
}
}
}
public Launcher(string startupRepo)
{
_ignoreIndexChange = true;
Pages = new AvaloniaList<LauncherPage>();
AddNewTab();
var pref = Preference.Instance;
if (!string.IsNullOrEmpty(startupRepo))
if (string.IsNullOrEmpty(startupRepo))
{
var test = new Commands.QueryRepositoryRootPath(startupRepo).ReadToEnd();
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
{
Pages[0].Notifications.Add(new Models.Notification
{
IsError = true,
Message = $"Given path: '{startupRepo}' is NOT a valid repository!"
});
return;
}
ActiveWorkspace = pref.GetActiveWorkspace();
var normalized = test.StdOut.Trim().Replace("\\", "/");
var node = pref.FindOrAddNodeByRepositoryPath(normalized, null, false);
Welcome.Instance.Refresh();
OpenRepositoryInTab(node, null);
}
else if (pref.RestoreTabs)
{
foreach (var id in pref.OpenedTabs)
var repos = ActiveWorkspace.Repositories.ToArray();
foreach (var repo in repos)
{
var node = pref.FindNode(id);
var node = pref.FindNode(repo);
if (node == null)
{
node = new RepositoryNode()
{
Id = id,
Name = Path.GetFileName(id),
Id = repo,
Name = Path.GetFileName(repo),
Bookmark = 0,
IsRepository = true,
};
@ -70,34 +68,58 @@ namespace SourceGit.ViewModels
OpenRepositoryInTab(node, null);
}
var lastActiveIdx = pref.LastActiveTabIdx;
if (lastActiveIdx >= 0 && lastActiveIdx < Pages.Count)
ActivePage = Pages[lastActiveIdx];
}
}
public void Quit()
{
var pref = Preference.Instance;
pref.OpenedTabs.Clear();
if (pref.RestoreTabs)
{
foreach (var page in Pages)
var activeIdx = ActiveWorkspace.ActiveIdx;
if (activeIdx >= 0 && activeIdx < Pages.Count)
{
if (page.Node.IsRepository)
pref.OpenedTabs.Add(page.Node.Id);
ActivePage = Pages[activeIdx];
}
else
{
ActivePage = Pages[0];
ActiveWorkspace.ActiveIdx = 0;
}
}
else
{
ActiveWorkspace = new Workspace() { Name = "Unnamed", Color = 4278221015 };
foreach (var w in pref.Workspaces)
w.IsActive = false;
var test = new Commands.QueryRepositoryRootPath(startupRepo).ReadToEnd();
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
{
Pages[0].Notifications.Add(new Models.Notification
{
IsError = true,
Message = $"Given path: '{startupRepo}' is NOT a valid repository!"
});
}
else
{
var normalized = test.StdOut.Trim().Replace("\\", "/");
var node = pref.FindOrAddNodeByRepositoryPath(normalized, null, false);
Welcome.Instance.Refresh();
OpenRepositoryInTab(node, null);
}
}
pref.LastActiveTabIdx = Pages.IndexOf(ActivePage);
_ignoreIndexChange = false;
}
public void Quit(double width, double height)
{
var pref = Preference.Instance;
pref.Layout.LauncherWidth = width;
pref.Layout.LauncherHeight = height;
pref.Save();
foreach (var page in Pages)
{
if (page.Data is Repository repo)
repo.Close();
}
_ignoreIndexChange = true;
foreach (var one in Pages)
CloseRepositoryInTab(one, false);
_ignoreIndexChange = false;
}
public void AddNewTab()
@ -142,6 +164,7 @@ namespace SourceGit.ViewModels
var last = Pages[0];
if (last.Data is Repository repo)
{
ActiveWorkspace.Repositories.Remove(repo.FullPath);
Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath);
repo.Close();
@ -247,6 +270,7 @@ namespace SourceGit.ViewModels
};
repo.Open();
ActiveWorkspace.AddRepository(repo.FullPath);
Models.AutoFetchManager.Instance.AddRepository(repo.FullPath);
if (page == null)
@ -294,6 +318,46 @@ namespace SourceGit.ViewModels
_activePage.Notifications.Add(notification);
}
public ContextMenu CreateContextForWorkspace()
{
var pref = Preference.Instance;
var menu = new ContextMenu();
for (var i = 0; i < pref.Workspaces.Count; i++)
{
var workspace = pref.Workspaces[i];
var icon = App.CreateMenuIcon(workspace.IsActive ? "Icons.Check" : "Icons.Workspace");
icon.Fill = workspace.Brush;
var item = new MenuItem();
item.Header = workspace.Name;
item.Icon = icon;
item.Click += (_, e) =>
{
if (!workspace.IsActive)
SwitchWorkspace(workspace);
e.Handled = true;
};
menu.Items.Add(item);
}
menu.Items.Add(new MenuItem() { Header = "-" });
var configure = new MenuItem();
configure.Header = App.Text("Workspace.Configure");
configure.Click += (_, e) =>
{
App.OpenDialog(new Views.ConfigureWorkspace() { DataContext = new ConfigureWorkspace() });
e.Handled = true;
};
menu.Items.Add(configure);
return menu;
}
public ContextMenu CreateContextForPageTab(LauncherPage page)
{
if (page == null)
@ -369,10 +433,63 @@ namespace SourceGit.ViewModels
return menu;
}
private void CloseRepositoryInTab(LauncherPage page)
private void SwitchWorkspace(Workspace to)
{
_ignoreIndexChange = true;
var pref = Preference.Instance;
foreach (var w in pref.Workspaces)
w.IsActive = false;
ActiveWorkspace = to;
to.IsActive = true;
foreach (var one in Pages)
CloseRepositoryInTab(one, false);
Pages.Clear();
AddNewTab();
var repos = to.Repositories.ToArray();
foreach (var repo in repos)
{
var node = pref.FindNode(repo);
if (node == null)
{
node = new RepositoryNode()
{
Id = repo,
Name = Path.GetFileName(repo),
Bookmark = 0,
IsRepository = true,
};
}
OpenRepositoryInTab(node, null);
}
var activeIdx = to.ActiveIdx;
if (activeIdx >= 0 && activeIdx < Pages.Count)
{
ActivePage = Pages[activeIdx];
}
else
{
ActivePage = Pages[0];
to.ActiveIdx = 0;
}
_ignoreIndexChange = false;
GC.Collect();
}
private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = true)
{
if (page.Data is Repository repo)
{
if (removeFromWorkspace)
ActiveWorkspace.Repositories.Remove(repo.FullPath);
Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath);
repo.Close();
}
@ -380,6 +497,8 @@ namespace SourceGit.ViewModels
page.Data = null;
}
private Workspace _activeWorkspace = null;
private LauncherPage _activePage = null;
private bool _ignoreIndexChange = false;
}
}

View file

@ -18,7 +18,9 @@ namespace SourceGit.ViewModels
if (_instance == null)
{
_isLoading = true;
if (!File.Exists(_savePath))
var path = Path.Combine(Native.OS.DataDir, "preference.json");
if (!File.Exists(path))
{
_instance = new Preference();
}
@ -26,7 +28,7 @@ namespace SourceGit.ViewModels
{
try
{
_instance = JsonSerializer.Deserialize(File.ReadAllText(_savePath), JsonCodeGen.Default.Preference);
_instance = JsonSerializer.Deserialize(File.ReadAllText(path), JsonCodeGen.Default.Preference);
}
catch
{
@ -39,6 +41,9 @@ namespace SourceGit.ViewModels
if (!_instance.IsGitConfigured())
_instance.GitInstallPath = Native.OS.FindGitExecutable();
if (_instance.Workspaces.Count == 0)
_instance.Workspaces.Add(new Workspace() { Name = "Default", Color = 4278221015 });
return _instance;
}
}
@ -133,12 +138,6 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _subjectGuideLength, value);
}
public bool RestoreTabs
{
get => _restoreTabs;
set => SetProperty(ref _restoreTabs, value);
}
public bool UseFixedTabWidth
{
get => _useFixedTabWidth;
@ -230,16 +229,6 @@ namespace SourceGit.ViewModels
}
}
public Models.Shell GitShell
{
get => Native.OS.GetShell();
set
{
if (Native.OS.SetShell(value))
OnPropertyChanged();
}
}
public string GitDefaultCloneDir
{
get => _gitDefaultCloneDir;
@ -275,6 +264,36 @@ namespace SourceGit.ViewModels
}
}
public int ShellOrTerminal
{
get => _shellOrTerminal;
set
{
if (SetProperty(ref _shellOrTerminal, value))
{
if (value >= 0 && value < Models.ShellOrTerminal.Supported.Count)
Native.OS.SetShellOrTerminal(Models.ShellOrTerminal.Supported[value]);
else
Native.OS.SetShellOrTerminal(null);
OnPropertyChanged(nameof(ShellOrTerminalPath));
}
}
}
public string ShellOrTerminalPath
{
get => Native.OS.ShellOrTerminal;
set
{
if (value != Native.OS.ShellOrTerminal)
{
Native.OS.ShellOrTerminal = value;
OnPropertyChanged();
}
}
}
public int ExternalMergeToolType
{
get => _externalMergeToolType;
@ -298,24 +317,57 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _externalMergeToolPath, value);
}
public string OpenAIServer
{
get => Models.OpenAI.Server;
set
{
if (value != Models.OpenAI.Server)
{
Models.OpenAI.Server = value;
OnPropertyChanged();
}
}
}
public string OpenAIApiKey
{
get => Models.OpenAI.ApiKey;
set
{
if (value != Models.OpenAI.ApiKey)
{
Models.OpenAI.ApiKey = value;
OnPropertyChanged();
}
}
}
public string OpenAIModel
{
get => Models.OpenAI.Model;
set
{
if (value != Models.OpenAI.Model)
{
Models.OpenAI.Model = value;
OnPropertyChanged();
}
}
}
public List<RepositoryNode> RepositoryNodes
{
get;
set;
} = [];
public List<string> OpenedTabs
public List<Workspace> Workspaces
{
get;
set;
} = [];
public int LastActiveTabIdx
{
get;
set;
} = 0;
public double LastCheckUpdateTime
{
get => _lastCheckUpdateTime;
@ -343,6 +395,19 @@ namespace SourceGit.ViewModels
return true;
}
public Workspace GetActiveWorkspace()
{
foreach (var w in Workspaces)
{
if (w.IsActive)
return w;
}
var first = Workspaces[0];
first.IsActive = true;
return first;
}
public void AddNode(RepositoryNode node, RepositoryNode to, bool save)
{
var collection = to == null ? RepositoryNodes : to.SubNodes;
@ -425,8 +490,12 @@ namespace SourceGit.ViewModels
public void Save()
{
if (_isLoading)
return;
var file = Path.Combine(Native.OS.DataDir, "preference.json");
var data = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preference);
File.WriteAllText(_savePath, data);
File.WriteAllText(file, data);
}
private RepositoryNode FindNodeRecursive(string id, List<RepositoryNode> collection)
@ -478,7 +547,6 @@ namespace SourceGit.ViewModels
private static Preference _instance = null;
private static bool _isLoading = false;
private static readonly string _savePath = Path.Combine(Native.OS.DataDir, "preference.json");
private string _locale = "en_US";
private string _theme = "Default";
@ -492,7 +560,6 @@ namespace SourceGit.ViewModels
private int _maxHistoryCommits = 20000;
private int _subjectGuideLength = 50;
private bool _restoreTabs = false;
private bool _useFixedTabWidth = true;
private bool _check4UpdatesOnStartup = true;
@ -513,6 +580,7 @@ namespace SourceGit.ViewModels
private string _gitDefaultCloneDir = string.Empty;
private int _shellOrTerminal = -1;
private int _externalMergeToolType = 0;
private string _externalMergeToolPath = string.Empty;
}

View file

@ -210,7 +210,7 @@ namespace SourceGit.ViewModels
}
if (!autoSelectedBranch)
SelectedBranch = branches.Count > 0 ? branches[0] : null;
SelectedBranch = null;
}
private readonly Repository _repo = null;

View file

@ -490,10 +490,11 @@ namespace SourceGit.ViewModels
return;
}
if (autoStart)
PopupHost.ShowAndStartPopup(new Pull(this, null));
var pull = new Pull(this, null);
if (autoStart && pull.SelectedBranch != null)
PopupHost.ShowAndStartPopup(pull);
else
PopupHost.ShowPopup(new Pull(this, null));
PopupHost.ShowPopup(pull);
}
public void Push(bool autoStart)

View file

@ -138,22 +138,22 @@ namespace SourceGit.ViewModels
public void Save()
{
SetIfChanged("user.name", UserName);
SetIfChanged("user.email", UserEmail);
SetIfChanged("commit.gpgsign", GPGCommitSigningEnabled ? "true" : "false");
SetIfChanged("tag.gpgsign", GPGTagSigningEnabled ? "true" : "false");
SetIfChanged("user.signingkey", GPGUserSigningKey);
SetIfChanged("http.proxy", HttpProxy);
SetIfChanged("user.name", UserName, "");
SetIfChanged("user.email", UserEmail, "");
SetIfChanged("commit.gpgsign", GPGCommitSigningEnabled ? "true" : "false", "false");
SetIfChanged("tag.gpgsign", GPGTagSigningEnabled ? "true" : "false", "false");
SetIfChanged("user.signingkey", GPGUserSigningKey, "");
SetIfChanged("http.proxy", HttpProxy, "");
}
private void SetIfChanged(string key, string value)
private void SetIfChanged(string key, string value, string defValue)
{
bool changed = false;
if (_cached.TryGetValue(key, out var old))
{
changed = old != value;
}
else if (!string.IsNullOrEmpty(value))
else if (!string.IsNullOrEmpty(value) && value != defValue)
{
changed = true;
}

View file

@ -33,6 +33,11 @@ namespace SourceGit.ViewModels
public class WorkingCopy : ObservableObject
{
public string RepoPath
{
get => _repo.FullPath;
}
public bool IncludeUntracked
{
get => _repo.IncludeUntracked;
@ -320,23 +325,24 @@ namespace SourceGit.ViewModels
PopupHost.ShowPopup(new StashChanges(_repo, _cached, true));
}
public void StageSelected()
public void StageSelected(Models.Change next)
{
StageChanges(_selectedUnstaged);
SelectedUnstaged = [];
StageChanges(_selectedUnstaged, next);
}
public void StageAll()
{
StageChanges(_unstaged);
SelectedUnstaged = [];
StageChanges(_unstaged, null);
}
public async void StageChanges(List<Models.Change> changes)
public async void StageChanges(List<Models.Change> changes, Models.Change next)
{
if (_unstaged.Count == 0 || changes.Count == 0)
return;
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
_selectedUnstaged = next != null ? [next] : [];
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count)
@ -357,23 +363,24 @@ namespace SourceGit.ViewModels
IsStaging = false;
}
public void UnstageSelected()
public void UnstageSelected(Models.Change next)
{
UnstageChanges(_selectedStaged);
SelectedStaged = [];
UnstageChanges(_selectedStaged, next);
}
public void UnstageAll()
{
UnstageChanges(_staged);
SelectedStaged = [];
UnstageChanges(_staged, null);
}
public async void UnstageChanges(List<Models.Change> changes)
public async void UnstageChanges(List<Models.Change> changes, Models.Change next)
{
if (_staged.Count == 0 || changes.Count == 0)
return;
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
_selectedStaged = next != null ? [next] : [];
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (_useAmend)
@ -494,7 +501,7 @@ namespace SourceGit.ViewModels
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
StageChanges(_selectedUnstaged);
StageChanges(_selectedUnstaged, null);
e.Handled = true;
};
@ -818,7 +825,7 @@ namespace SourceGit.ViewModels
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
StageChanges(_selectedUnstaged);
StageChanges(_selectedUnstaged, null);
e.Handled = true;
};
@ -912,7 +919,7 @@ namespace SourceGit.ViewModels
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) =>
{
UnstageChanges(_selectedStaged);
UnstageChanges(_selectedStaged, null);
e.Handled = true;
};
@ -1081,7 +1088,7 @@ namespace SourceGit.ViewModels
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) =>
{
UnstageChanges(_selectedStaged);
UnstageChanges(_selectedStaged, null);
e.Handled = true;
};

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Workspace : ObservableObject
{
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public uint Color
{
get => _color;
set
{
if (SetProperty(ref _color, value))
Brush = new SolidColorBrush(value);
}
}
public List<string> Repositories
{
get;
set;
} = new List<string>();
public int ActiveIdx
{
get;
set;
} = 0;
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
[JsonIgnore]
public IBrush Brush
{
get => _brush;
private set => SetProperty(ref _brush, value);
}
public void AddRepository(string repo)
{
if (!Repositories.Contains(repo))
Repositories.Add(repo);
}
private string _name = string.Empty;
private uint _color = 0;
private bool _isActive = false;
private IBrush _brush = null;
}
}

View file

@ -0,0 +1,60 @@
<v:ChromelessWindow xmlns="https://github.com/avaloniaui"
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:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
x:Class="SourceGit.Views.AIAssistant"
x:DataType="vm:WorkingCopy"
x:Name="ThisControl"
Icon="/App.ico"
Title="{DynamicResource Text.AIAssistant}"
Width="400" SizeToContent="Height"
CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,Auto">
<!-- TitleBar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="30" IsVisible="{Binding !#ThisControl.UseSystemWindowFrame}">
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
PointerPressed="BeginMoveWindow"/>
<Path Grid.Column="0"
Width="14" Height="14"
Margin="10,0,0,0"
Data="{StaticResource Icons.AIAssist}"
IsVisible="{OnPlatform True, macOS=False}"/>
<v:CaptionButtonsMacOS Grid.Column="0"
Margin="0,2,0,0"
IsCloseButtonOnly="True"
IsVisible="{OnPlatform False, macOS=True}"/>
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
Classes="bold"
Text="{DynamicResource Text.AIAssistant}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
<v:CaptionButtons Grid.Column="2"
IsCloseButtonOnly="True"
IsVisible="{OnPlatform True, macOS=False}"/>
</Grid>
<!-- Animated Icon -->
<v:LoadingIcon Grid.Row="1"
Width="24" Height="24"
Margin="0,16,0,0"/>
<!-- Message -->
<TextBlock Grid.Row="2"
x:Name="ProgressMessage"
Margin="16"
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</Grid>
</v:ChromelessWindow>

View file

@ -0,0 +1,61 @@
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
namespace SourceGit.Views
{
public partial class AIAssistant : ChromelessWindow
{
public AIAssistant()
{
_cancel = new CancellationTokenSource();
InitializeComponent();
ProgressMessage.Text = "Generating commit message... Please wait!";
}
public void GenerateCommitMessage()
{
if (DataContext is ViewModels.WorkingCopy vm)
{
Task.Run(() =>
{
var message = new Commands.GenerateCommitMessage(vm.RepoPath, vm.Staged, _cancel.Token, SetDescription).Result();
if (_cancel.IsCancellationRequested)
return;
Dispatcher.UIThread.Invoke(() =>
{
if (DataContext is ViewModels.WorkingCopy wc)
wc.CommitMessage = message;
Close();
});
}, _cancel.Token);
}
}
protected override void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
_cancel.Cancel();
}
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
BeginMoveDrag(e);
}
private void SetDescription(string message)
{
Dispatcher.UIThread.Invoke(() =>
{
ProgressMessage.Text = message;
});
}
private CancellationTokenSource _cancel;
}
}

View file

@ -50,14 +50,14 @@
</Border>
<!-- Body -->
<Grid Grid.Row="2">
<Grid Grid.Row="2" Background="{DynamicResource Brush.Contents}">
<!-- Blame View -->
<v:BlameTextEditor HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Margin="4,0,4,4"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
Background="{DynamicResource Brush.Contents}"
Background="Transparent"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Monospace}"
BlameData="{Binding Data}"/>

View file

@ -174,7 +174,11 @@ namespace SourceGit.Views
var rect = new Rect(0, y, shaLink.Width, shaLink.Height);
if (rect.Contains(pos))
{
_editor.OnCommitSHAClicked(info.CommitSHA);
if (DataContext is ViewModels.Blame blame)
{
blame.NavigateToCommit(info.CommitSHA);
}
e.Handled = true;
break;
}
@ -229,6 +233,9 @@ namespace SourceGit.Views
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) });
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
TextArea.Caret.PositionChanged += OnTextAreaCaretPositionChanged;
TextArea.LayoutUpdated += OnTextAreaLayoutUpdated;
TextArea.PointerWheelChanged += OnTextAreaPointerWheelChanged;
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
TextArea.TextView.Margin = new Thickness(4, 0);
@ -236,11 +243,35 @@ namespace SourceGit.Views
TextArea.TextView.Options.EnableEmailHyperlinks = false;
}
public void OnCommitSHAClicked(string sha)
public override void Render(DrawingContext context)
{
if (DataContext is ViewModels.Blame blame)
base.Render(context);
if (string.IsNullOrEmpty(_highlight))
return;
var view = TextArea.TextView;
if (view == null || !view.VisualLinesValid)
return;
var color = (Color)this.FindResource("SystemAccentColor")!;
var brush = new SolidColorBrush(color, 0.4);
foreach (var line in view.VisualLines)
{
blame.NavigateToCommit(sha);
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var lineNumber = line.FirstDocumentLine.LineNumber;
if (lineNumber >= BlameData.LineInfos.Count)
break;
var info = BlameData.LineInfos[lineNumber - 1];
if (info.CommitSHA != _highlight)
continue;
var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - view.VerticalOffset;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - view.VerticalOffset;
context.FillRectangle(brush, new Rect(0, startY, Bounds.Width, endY - startY));
}
}
@ -249,6 +280,9 @@ namespace SourceGit.Views
base.OnUnloaded(e);
TextArea.LeftMargins.Clear();
TextArea.Caret.PositionChanged -= OnTextAreaCaretPositionChanged;
TextArea.LayoutUpdated -= OnTextAreaLayoutUpdated;
TextArea.PointerWheelChanged -= OnTextAreaPointerWheelChanged;
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
@ -281,6 +315,31 @@ namespace SourceGit.Views
}
}
private void OnTextAreaCaretPositionChanged(object sender, EventArgs e)
{
if (!TextArea.IsFocused)
return;
var caret = TextArea.Caret;
if (caret == null || caret.Line >= BlameData.LineInfos.Count)
return;
_highlight = BlameData.LineInfos[caret.Line - 1].CommitSHA;
InvalidateVisual();
}
private void OnTextAreaLayoutUpdated(object sender, EventArgs e)
{
if (TextArea.IsFocused)
InvalidateVisual();
}
private void OnTextAreaPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
if (!TextArea.IsFocused && !string.IsNullOrEmpty(_highlight))
Focus();
}
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
{
var selected = SelectedText;
@ -325,6 +384,7 @@ namespace SourceGit.Views
}
private TextMate.Installation _textMate = null;
private string _highlight = string.Empty;
}
public partial class Blame : ChromelessWindow

View file

@ -132,6 +132,69 @@ namespace SourceGit.Views
}
}
public Models.Change GetNextChangeWithoutSelection()
{
var selected = SelectedChanges;
var changes = Changes;
if (selected == null || selected.Count == 0)
return changes.Count > 0 ? changes[0] : null;
if (selected.Count == changes.Count)
return null;
var set = new HashSet<string>();
foreach (var c in selected)
set.Add(c.Path);
if (Content is ViewModels.ChangeCollectionAsTree tree)
{
var lastUnselected = -1;
for (int i = tree.Rows.Count - 1; i >= 0; i--)
{
var row = tree.Rows[i];
if (!row.IsFolder)
{
if (set.Contains(row.FullPath))
{
if (lastUnselected == -1)
continue;
else
break;
}
else
{
lastUnselected = i;
}
}
}
if (lastUnselected != -1)
return tree.Rows[lastUnselected].Change;
}
else
{
var lastUnselected = -1;
for (int i = changes.Count - 1; i >= 0; i--)
{
if (set.Contains(changes[i].Path))
{
if (lastUnselected == -1)
continue;
else
break;
}
else
{
lastUnselected = i;
}
}
if (lastUnselected != -1)
return changes[lastUnselected];
}
return null;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

247
src/Views/ColorPicker.cs Normal file
View file

@ -0,0 +1,247 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
namespace SourceGit.Views
{
public class ColorPicker : Control
{
public static readonly StyledProperty<uint> ValueProperty =
AvaloniaProperty.Register<ColorPicker, uint>(nameof(Value), 0);
public uint Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
// Values are copied from Avalonia: src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs
private static readonly Color[,] COLOR_TABLE = new Color[,]
{
{
Color.FromArgb(255, 255, 67, 67), /* #FF4343 */
Color.FromArgb(255, 209, 52, 56), /* #D13438 */
Color.FromArgb(255, 239, 105, 80), /* #EF6950 */
Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */
Color.FromArgb(255, 202, 80, 16), /* #CA5010 */
Color.FromArgb(255, 247, 99, 12), /* #F7630C */
Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */
Color.FromArgb(255, 255, 185, 0), /* #FFB900 */
},
{
Color.FromArgb(255, 231, 72, 86), /* #E74856 */
Color.FromArgb(255, 232, 17, 35), /* #E81123 */
Color.FromArgb(255, 234, 0, 94), /* #EA005E */
Color.FromArgb(255, 195, 0, 82), /* #C30052 */
Color.FromArgb(255, 227, 0, 140), /* #E3008C */
Color.FromArgb(255, 191, 0, 119), /* #BF0077 */
Color.FromArgb(255, 194, 57, 179), /* #C239B3 */
Color.FromArgb(255, 154, 0, 137), /* #9A0089 */
},
{
Color.FromArgb(255, 0, 120, 215), /* #0078D7 */
Color.FromArgb(255, 0, 99, 177), /* #0063B1 */
Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */
Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */
Color.FromArgb(255, 135, 100, 184), /* #8764B8 */
Color.FromArgb(255, 116, 77, 169), /* #744DA9 */
Color.FromArgb(255, 177, 70, 194), /* #B146C2 */
Color.FromArgb(255, 136, 23, 152), /* #881798 */
},
{
Color.FromArgb(255, 0, 153, 188), /* #0099BC */
Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */
Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */
Color.FromArgb(255, 3, 131, 135), /* #038387 */
Color.FromArgb(255, 0, 178, 148), /* #00B294 */
Color.FromArgb(255, 1, 133, 116), /* #018574 */
Color.FromArgb(255, 0, 204, 106), /* #00CC6A */
Color.FromArgb(255, 16, 137, 62), /* #10893E */
},
{
Color.FromArgb(255, 122, 117, 116), /* #7A7574 */
Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */
Color.FromArgb(255, 104, 118, 138), /* #68768A */
Color.FromArgb(255, 81, 92, 107), /* #515C6B */
Color.FromArgb(255, 86, 124, 115), /* #567C73 */
Color.FromArgb(255, 72, 104, 96), /* #486860 */
Color.FromArgb(255, 73, 130, 5), /* #498205 */
Color.FromArgb(255, 16, 124, 16), /* #107C10 */
},
{
Color.FromArgb(255, 118, 118, 118), /* #767676 */
Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */
Color.FromArgb(255, 105, 121, 126), /* #69797E */
Color.FromArgb(255, 74, 84, 89), /* #4A5459 */
Color.FromArgb(255, 100, 124, 100), /* #647C64 */
Color.FromArgb(255, 82, 94, 84), /* #525E54 */
Color.FromArgb(255, 132, 117, 69), /* #847545 */
Color.FromArgb(255, 126, 115, 95), /* #7E735F */
}
};
static ColorPicker()
{
ValueProperty.Changed.AddClassHandler<ColorPicker>((c, _) => c.UpdateColors());
}
public override void Render(DrawingContext context)
{
base.Render(context);
// Color table.
{
// Colors
for (int i = 0; i < 6; i++)
{
for (int j = 0; j < 8; j++)
context.FillRectangle(new SolidColorBrush(COLOR_TABLE[i, j]), new Rect(j * 32, i * 32, 32, 32));
}
// Borders
var border = this.FindResource("Brush.Border0") as IBrush;
var pen = new Pen(border, 0.4);
for (int i = 1; i < 6; i++)
context.DrawLine(pen, new Point(0, i * 32), new Point(256, i * 32));
for (int j = 1; j < 8; j++)
context.DrawLine(pen, new Point(j * 32, 0), new Point(j * 32, 192));
// Selected
if (_hightlightedTableRect is { } rect)
context.DrawRectangle(new Pen(Brushes.White, 2), rect);
}
// Palette picker
{
context.DrawRectangle(Brushes.Transparent, null, new Rect(0, 200, 256, 32), 4, 4, _shadow);
context.DrawRectangle(new SolidColorBrush(_darkestColor), null, _darkestRect);
context.FillRectangle(new SolidColorBrush(_darkerColor), _darkerRect);
context.FillRectangle(new SolidColorBrush(_darkColor), _darkRect);
context.FillRectangle(new SolidColorBrush(_lightColor), _lightRect);
context.FillRectangle(new SolidColorBrush(_lighterColor), _lighterRect);
context.DrawRectangle(new SolidColorBrush(_lightestColor), null, _lightestRect);
context.DrawRectangle(new SolidColorBrush(_color), null, new Rect(96, 200 - 4, 64, 40), 4, 4, _shadow);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty)
{
UpdateColors();
InvalidateVisual();
}
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
_hightlightedTableRect = null;
}
protected override Size MeasureOverride(Size availableSize)
{
return new Size(256, 256);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var p = e.GetPosition(this);
if (_colorTableRect.Contains(p))
{
var col = (int)Math.Floor(p.X / 32.0);
var row = (int)Math.Floor(p.Y / 32.0);
var rect = new Rect(col * 32 + 2, row * 32 + 2, 28, 28);
if (!rect.Equals(_hightlightedTableRect))
{
_hightlightedTableRect = rect;
SetCurrentValue(ValueProperty, COLOR_TABLE[row, col].ToUInt32());
}
return;
}
if (_darkestRect.Rect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _darkestColor.ToUInt32());
}
else if (_darkerRect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _darkerColor.ToUInt32());
}
else if (_darkRect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _darkColor.ToUInt32());
}
else if (_lightRect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _lightColor.ToUInt32());
}
else if (_lighterRect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _lighterColor.ToUInt32());
}
else if (_lightestRect.Rect.Contains(p))
{
_hightlightedTableRect = null;
SetCurrentValue(ValueProperty, _lightestColor.ToUInt32());
}
}
private void UpdateColors()
{
_color = Color.FromUInt32(Value);
var hsvColor = _color.ToHsv();
_darkestColor = GetNextColor(hsvColor, -0.3);
_darkerColor = GetNextColor(hsvColor, -0.2);
_darkColor = GetNextColor(hsvColor, -0.1);
_lightColor = GetNextColor(hsvColor, 0.1);
_lighterColor = GetNextColor(hsvColor, 0.2);
_lightestColor = GetNextColor(hsvColor, 0.3);
}
private Color GetNextColor(HsvColor c, double step)
{
var v = c.V;
v += step;
v = Math.Round(v, 2);
var newColor = new HsvColor(c.A, c.H, c.S, v);
return newColor.ToRgb();
}
private BoxShadows _shadow = BoxShadows.Parse("0 0 6 0 #A9000000");
private Rect _colorTableRect = new Rect(0, 0, 32 * 8, 32 * 6);
private RoundedRect _darkestRect = new RoundedRect(new Rect(0, 200, 32, 32), new CornerRadius(4, 0, 0, 4));
private Rect _darkerRect = new Rect(32, 200, 32, 32);
private Rect _darkRect = new Rect(64, 200, 32, 32);
private Rect _lightRect = new Rect(160, 200, 32, 32);
private Rect _lighterRect = new Rect(192, 200, 32, 32);
private RoundedRect _lightestRect = new RoundedRect(new Rect(224, 200, 32, 32), new CornerRadius(0, 4, 4, 0));
private Rect? _hightlightedTableRect = null;
private Color _darkestColor;
private Color _darkerColor;
private Color _darkColor;
private Color _color;
private Color _lightColor;
private Color _lighterColor;
private Color _lightestColor;
}
}

View file

@ -0,0 +1,122 @@
<v:ChromelessWindow xmlns="https://github.com/avaloniaui"
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"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.ConfigureWorkspace"
x:DataType="vm:ConfigureWorkspace"
x:Name="ThisControl"
Icon="/App.ico"
Title="{DynamicResource Text.ConfigureWorkspace}"
SizeToContent="WidthAndHeight"
CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto" MinWidth="494">
<!-- TitleBar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="30" IsVisible="{Binding !#ThisControl.UseSystemWindowFrame}">
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
PointerPressed="BeginMoveWindow"/>
<Path Grid.Column="0"
Width="14" Height="14"
Data="{StaticResource Icons.Workspace}"
Margin="10,0,0,0"
IsVisible="{OnPlatform True, macOS=False}"/>
<v:CaptionButtonsMacOS Grid.Column="0"
Margin="0,2,0,0"
IsCloseButtonOnly="True"
IsVisible="{OnPlatform False, macOS=True}"/>
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
Classes="bold"
Text="{DynamicResource Text.ConfigureWorkspace}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
<v:CaptionButtons Grid.Column="2"
IsCloseButtonOnly="True"
IsVisible="{OnPlatform True, macOS=False}"/>
</Grid>
<!-- BODY -->
<Grid Grid.Row="1" ColumnDefinitions="200,16,256" Height="324" Margin="8">
<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 Workspaces}"
SelectedItem="{Binding Selected, Mode=TwoWay}"
SelectionMode="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="vm:Workspace">
<Grid ColumnDefinitions="Auto,*,Auto">
<Path Grid.Column="0" Margin="4,0" Width="14" Height="14" Data="{StaticResource Icons.Workspace}" Fill="{Binding Brush}"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="4,0" TextTrimming="CharacterEllipsis"/>
<Path Grid.Column="2"
Margin="4,0"
Width="14" Height="14"
Data="{StaticResource Icons.Check}"
Fill="{DynamicResource Brush.FG1}"
IsVisible="{Binding IsActive}"/>
</Grid>
</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" Command="{Binding Add}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
<Button Classes="icon_button" Command="{Binding Delete}" IsEnabled="{Binding CanDeleteSelected}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Column="2" Content="{Binding Selected}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:Workspace">
<Grid RowDefinitions="Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Text="{DynamicResource Text.ConfigureWorkspace.Name}"/>
<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,4" Text="{DynamicResource Text.ConfigureWorkspace.Color}"/>
<v:ColorPicker Grid.Row="3" HorizontalAlignment="Left" Value="{Binding Color, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</Grid>
</v:ChromelessWindow>

View file

@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace SourceGit.Views
{
public partial class ConfigureWorkspace : ChromelessWindow
{
public ConfigureWorkspace()
{
InitializeComponent();
}
protected override void OnClosing(WindowClosingEventArgs e)
{
ViewModels.Preference.Instance.Save();
base.OnClosing(e);
}
private void BeginMoveWindow(object _, PointerPressedEventArgs e)
{
BeginMoveDrag(e);
}
}
}

View file

@ -24,7 +24,7 @@
Text="{DynamicResource Text.DeleteRepositoryNode.Target}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,0,8,0"
Fill="{Binding Node.Bookmark, Converter={x:Static c:BookmarkConverters.ToBrush}}"
Fill="{Binding Node.Bookmark, Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
HorizontalAlignment="Left" VerticalAlignment="Center"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding Node.IsRepository}"/>

View file

@ -40,7 +40,7 @@
<DataTemplate>
<Border Height="20" VerticalAlignment="Center">
<Path Width="12" Height="12"
Fill="{Binding Converter={x:Static c:BookmarkConverters.ToBrush}}"
Fill="{Binding Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="{StaticResource Icons.Bookmark}"/>
</Border>

View file

@ -10,8 +10,19 @@
x:Class="SourceGit.Views.Histories"
x:DataType="vm:Histories"
x:Name="ThisControl">
<v:LayoutableGrid RowDefinitions="*,3,*" ColumnDefinitions="*,3,*"
UseHorizontal="{Binding Source={x:Static vm:Preference.Instance}, Path=UseTwoColumnsLayoutInHistories}">
<v:LayoutableGrid UseHorizontal="{Binding Source={x:Static vm:Preference.Instance}, Path=UseTwoColumnsLayoutInHistories}">
<v:LayoutableGrid.RowDefinitions>
<RowDefinition Height="{Binding TopArea, Mode=TwoWay}"/>
<RowDefinition Height="3"/>
<RowDefinition Height="{Binding BottomArea, Mode=TwoWay}"/>
</v:LayoutableGrid.RowDefinitions>
<v:LayoutableGrid.ColumnDefinitions>
<ColumnDefinition Width="{Binding LeftArea, Mode=TwoWay}"/>
<ColumnDefinition Width="3"/>
<ColumnDefinition Width="{Binding RightArea, Mode=TwoWay}"/>
</v:LayoutableGrid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid RowDefinitions="24,*">
<!-- Headers -->

344
src/Views/ImageContainer.cs Normal file
View file

@ -0,0 +1,344 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
namespace SourceGit.Views
{
public class ImageContainer : Control
{
public override void Render(DrawingContext context)
{
if (_bgBrush == null)
{
var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB);
var bg = new DrawingGroup()
{
Children =
{
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) },
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) },
}
};
_bgBrush = new DrawingBrush(bg)
{
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute),
Stretch = Stretch.None,
TileMode = TileMode.Tile,
};
}
context.FillRectangle(_bgBrush, new Rect(Bounds.Size));
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property.Name == "ActualThemeVariant")
{
_bgBrush = null;
InvalidateVisual();
}
}
private DrawingBrush _bgBrush = null;
}
public class ImageView : ImageContainer
{
public static readonly StyledProperty<Bitmap> ImageProperty =
AvaloniaProperty.Register<ImageView, Bitmap>(nameof(Image));
public Bitmap Image
{
get => GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Image is { } image)
context.DrawImage(image, new Rect(0, 0, Bounds.Width, Bounds.Height));
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ImageProperty)
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
if (Image is { } image)
{
var imageSize = image.Size;
var scaleW = availableSize.Width / imageSize.Width;
var scaleH = availableSize.Height / imageSize.Height;
var scale = Math.Min(scaleW, scaleH);
return new Size(scale * imageSize.Width, scale * imageSize.Height);
}
return availableSize;
}
}
public class ImageSwipeControl : ImageContainer
{
public static readonly StyledProperty<double> AlphaProperty =
AvaloniaProperty.Register<ImageSwipeControl, double>(nameof(Alpha), 0.5);
public double Alpha
{
get => GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
public static readonly StyledProperty<Bitmap> OldImageProperty =
AvaloniaProperty.Register<ImageSwipeControl, Bitmap>(nameof(OldImage));
public Bitmap OldImage
{
get => GetValue(OldImageProperty);
set => SetValue(OldImageProperty, value);
}
public static readonly StyledProperty<Bitmap> NewImageProperty =
AvaloniaProperty.Register<ImageSwipeControl, Bitmap>(nameof(NewImage));
public Bitmap NewImage
{
get => GetValue(NewImageProperty);
set => SetValue(NewImageProperty, value);
}
static ImageSwipeControl()
{
AffectsMeasure<ImageSwipeControl>(OldImageProperty, NewImageProperty);
AffectsRender<ImageSwipeControl>(AlphaProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var alpha = Alpha;
var w = Bounds.Width;
var h = Bounds.Height;
var x = w * alpha;
var left = OldImage;
if (left != null && alpha > 0)
{
var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height);
var dst = new Rect(0, 0, x, h);
context.DrawImage(left, src, dst);
}
var right = NewImage;
if (right != null && alpha < 1)
{
var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height);
var dst = new Rect(x, 0, w - x, h);
context.DrawImage(right, src, dst);
}
context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height));
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var p = e.GetPosition(this);
var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height);
var pointer = e.GetCurrentPoint(this);
if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p))
{
_pressedOnSlider = true;
Cursor = new Cursor(StandardCursorType.SizeWestEast);
e.Pointer.Capture(this);
e.Handled = true;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
_pressedOnSlider = false;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
var w = Bounds.Width;
var p = e.GetPosition(this);
if (_pressedOnSlider)
{
SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w);
}
else
{
var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height);
if (hitbox.Contains(p))
{
if (!_lastInSlider)
{
_lastInSlider = true;
Cursor = new Cursor(StandardCursorType.SizeWestEast);
}
}
else
{
if (_lastInSlider)
{
_lastInSlider = false;
Cursor = null;
}
}
}
}
protected override Size MeasureOverride(Size availableSize)
{
var left = OldImage;
var right = NewImage;
if (left == null)
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
if (right == null)
return GetDesiredSize(left.Size, availableSize);
var ls = GetDesiredSize(left.Size, availableSize);
var rs = GetDesiredSize(right.Size, availableSize);
return ls.Width > rs.Width ? ls : rs;
}
private Size GetDesiredSize(Size img, Size available)
{
var sw = available.Width / img.Width;
var sh = available.Height / img.Height;
var scale = Math.Min(sw, sh);
return new Size(scale * img.Width, scale * img.Height);
}
private bool _pressedOnSlider = false;
private bool _lastInSlider = false;
}
public class ImageBlendControl : ImageContainer
{
public static readonly StyledProperty<double> AlphaProperty =
AvaloniaProperty.Register<ImageBlendControl, double>(nameof(Alpha), 1.0);
public double Alpha
{
get => GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
public static readonly StyledProperty<Bitmap> OldImageProperty =
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(OldImage));
public Bitmap OldImage
{
get => GetValue(OldImageProperty);
set => SetValue(OldImageProperty, value);
}
public static readonly StyledProperty<Bitmap> NewImageProperty =
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(NewImage));
public Bitmap NewImage
{
get => GetValue(NewImageProperty);
set => SetValue(NewImageProperty, value);
}
static ImageBlendControl()
{
AffectsMeasure<ImageBlendControl>(OldImageProperty, NewImageProperty);
AffectsRender<ImageBlendControl>(AlphaProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
var alpha = Alpha;
var left = OldImage;
var right = NewImage;
var drawLeft = left != null && alpha < 1.0;
var drawRight = right != null && alpha > 0;
if (drawLeft && drawRight)
{
using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi))
{
var rtRect = new Rect(rt.Size);
using (var dc = rt.CreateDrawingContext())
{
using (dc.PushRenderOptions(RO_SRC))
using (dc.PushOpacity(1 - alpha))
dc.DrawImage(left, rtRect);
using (dc.PushRenderOptions(RO_DST))
using (dc.PushOpacity(alpha))
dc.DrawImage(right, rtRect);
}
context.DrawImage(rt, rtRect, rect);
}
}
else if (drawLeft)
{
using (context.PushOpacity(1 - alpha))
context.DrawImage(left, rect);
}
else if (drawRight)
{
using (context.PushOpacity(alpha))
context.DrawImage(right, rect);
}
}
protected override Size MeasureOverride(Size availableSize)
{
var left = OldImage;
var right = NewImage;
if (left == null)
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
if (right == null)
return GetDesiredSize(left.Size, availableSize);
var ls = GetDesiredSize(left.Size, availableSize);
var rs = GetDesiredSize(right.Size, availableSize);
return ls.Width > rs.Width ? ls : rs;
}
private Size GetDesiredSize(Size img, Size available)
{
var sw = available.Width / img.Width;
var sh = available.Height / img.Height;
var scale = Math.Min(sw, sh);
return new Size(scale * img.Width, scale * img.Height);
}
private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
}
}

View file

@ -28,17 +28,14 @@
</Border>
<TextBlock Classes="primary" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
<TextBlock Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</StackPanel>
<Border Grid.Row="1" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
<Border Background="{DynamicResource Brush.Popup}" HorizontalAlignment="Center" Padding="8">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
<Grid>
<v:ImageContainer/>
<Image Source="{Binding Old}" Stretch="Uniform" VerticalAlignment="Center"/>
</Grid>
<v:ImageView Image="{Binding Old}"/>
</Border>
</Border>
</Border>
@ -51,17 +48,14 @@
</Border>
<TextBlock Classes="primary" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
<TextBlock Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</StackPanel>
<Border Grid.Row="1" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
<Border Background="{DynamicResource Brush.Popup}" HorizontalAlignment="Center" Padding="8">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
<Grid>
<v:ImageContainer/>
<Image Source="{Binding New}" Stretch="Uniform" VerticalAlignment="Center"/>
</Grid>
<v:ImageView Image="{Binding New}"/>
</Border>
</Border>
</Border>
@ -81,7 +75,7 @@
</Border>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Grid.Column="3" Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
@ -89,16 +83,16 @@
</Border>
<TextBlock Grid.Column="5" Classes="primary" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="6" Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="6" Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Grid.Column="7" Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</Grid>
<Border Grid.Row="1" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
<Border HorizontalAlignment="Center" Background="{DynamicResource Brush.Window}">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
<v:ImagesSwipeControl OldImage="{Binding Old}"
NewImage="{Binding New}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
<v:ImageSwipeControl OldImage="{Binding Old}"
NewImage="{Binding New}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Border>
</Border>
</Border>
@ -117,7 +111,7 @@
</Border>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Grid.Column="3" Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
@ -125,7 +119,7 @@
</Border>
<TextBlock Grid.Column="5" Classes="primary" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="6" Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="6" Classes="primary" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
<TextBlock Grid.Column="7" Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</Grid>

View file

@ -5,300 +5,12 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
namespace SourceGit.Views
{
public class ImageContainer : Control
{
public override void Render(DrawingContext context)
{
if (_bgBrush == null)
{
var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB);
var bg = new DrawingGroup()
{
Children =
{
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) },
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) },
}
};
_bgBrush = new DrawingBrush(bg)
{
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute),
Stretch = Stretch.None,
TileMode = TileMode.Tile,
};
}
context.FillRectangle(_bgBrush, new Rect(Bounds.Size));
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property.Name == "ActualThemeVariant")
{
_bgBrush = null;
InvalidateVisual();
}
}
private DrawingBrush _bgBrush = null;
}
public class ImagesSwipeControl : ImageContainer
{
public static readonly StyledProperty<double> AlphaProperty =
AvaloniaProperty.Register<ImagesSwipeControl, double>(nameof(Alpha), 0.5);
public double Alpha
{
get => GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
public static readonly StyledProperty<Bitmap> OldImageProperty =
AvaloniaProperty.Register<ImagesSwipeControl, Bitmap>(nameof(OldImage));
public Bitmap OldImage
{
get => GetValue(OldImageProperty);
set => SetValue(OldImageProperty, value);
}
public static readonly StyledProperty<Bitmap> NewImageProperty =
AvaloniaProperty.Register<ImagesSwipeControl, Bitmap>(nameof(NewImage));
public Bitmap NewImage
{
get => GetValue(NewImageProperty);
set => SetValue(NewImageProperty, value);
}
static ImagesSwipeControl()
{
AffectsMeasure<ImagesSwipeControl>(OldImageProperty, NewImageProperty);
AffectsRender<ImagesSwipeControl>(AlphaProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var alpha = Alpha;
var w = Bounds.Width;
var h = Bounds.Height;
var x = w * alpha;
var left = OldImage;
if (left != null && alpha > 0)
{
var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height);
var dst = new Rect(0, 0, x, h);
context.DrawImage(left, src, dst);
}
var right = NewImage;
if (right != null && alpha < 1)
{
var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height);
var dst = new Rect(x, 0, w - x, h);
context.DrawImage(right, src, dst);
}
context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height));
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var p = e.GetPosition(this);
var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height);
var pointer = e.GetCurrentPoint(this);
if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p))
{
_pressedOnSlider = true;
Cursor = new Cursor(StandardCursorType.SizeWestEast);
e.Pointer.Capture(this);
e.Handled = true;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
_pressedOnSlider = false;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
var w = Bounds.Width;
var p = e.GetPosition(this);
if (_pressedOnSlider)
{
SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w);
}
else
{
var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height);
if (hitbox.Contains(p))
{
if (!_lastInSlider)
{
_lastInSlider = true;
Cursor = new Cursor(StandardCursorType.SizeWestEast);
}
}
else
{
if (_lastInSlider)
{
_lastInSlider = false;
Cursor = null;
}
}
}
}
protected override Size MeasureOverride(Size availableSize)
{
var left = OldImage;
var right = NewImage;
if (left == null)
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
if (right == null)
return GetDesiredSize(left.Size, availableSize);
var ls = GetDesiredSize(left.Size, availableSize);
var rs = GetDesiredSize(right.Size, availableSize);
return ls.Width > rs.Width ? ls : rs;
}
private Size GetDesiredSize(Size img, Size available)
{
var sw = available.Width / img.Width;
var sh = available.Height / img.Height;
var scale = Math.Min(sw, sh);
return new Size(scale * img.Width, scale * img.Height);
}
private bool _pressedOnSlider = false;
private bool _lastInSlider = false;
}
public class ImageBlendControl : ImageContainer
{
public static readonly StyledProperty<double> AlphaProperty =
AvaloniaProperty.Register<ImageBlendControl, double>(nameof(Alpha), 1.0);
public double Alpha
{
get => GetValue(AlphaProperty);
set => SetValue(AlphaProperty, value);
}
public static readonly StyledProperty<Bitmap> OldImageProperty =
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(OldImage));
public Bitmap OldImage
{
get => GetValue(OldImageProperty);
set => SetValue(OldImageProperty, value);
}
public static readonly StyledProperty<Bitmap> NewImageProperty =
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(NewImage));
public Bitmap NewImage
{
get => GetValue(NewImageProperty);
set => SetValue(NewImageProperty, value);
}
static ImageBlendControl()
{
AffectsMeasure<ImageBlendControl>(OldImageProperty, NewImageProperty);
AffectsRender<ImageBlendControl>(AlphaProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
var alpha = Alpha;
var left = OldImage;
var right = NewImage;
var drawLeft = left != null && alpha < 1.0;
var drawRight = right != null && alpha > 0;
if (drawLeft && drawRight)
{
using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi))
{
var rtRect = new Rect(rt.Size);
using (var dc = rt.CreateDrawingContext())
{
using (dc.PushRenderOptions(RO_SRC))
using (dc.PushOpacity(1 - alpha))
dc.DrawImage(left, rtRect);
using (dc.PushRenderOptions(RO_DST))
using (dc.PushOpacity(alpha))
dc.DrawImage(right, rtRect);
}
context.DrawImage(rt, rtRect, rect);
}
}
else if (drawLeft)
{
using (context.PushOpacity(1 - alpha))
context.DrawImage(left, rect);
}
else if (drawRight)
{
using (context.PushOpacity(alpha))
context.DrawImage(right, rect);
}
}
protected override Size MeasureOverride(Size availableSize)
{
var left = OldImage;
var right = NewImage;
if (left == null)
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
if (right == null)
return GetDesiredSize(left.Size, availableSize);
var ls = GetDesiredSize(left.Size, availableSize);
var rs = GetDesiredSize(right.Size, availableSize);
return ls.Width > rs.Width ? ls : rs;
}
private Size GetDesiredSize(Size img, Size available)
{
var sw = available.Width / img.Width;
var sh = available.Height / img.Height;
var scale = Math.Min(sw, sh);
return new Size(scale * img.Width, scale * img.Height);
}
private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
}
public partial class ImageDiffView : UserControl
{

View file

@ -20,9 +20,9 @@
</Grid.RowDefinitions>
<!-- Custom TitleBar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto">
<!-- Bottom border -->
<Border Grid.Column="0" Grid.ColumnSpan="3"
<Border Grid.Column="0" Grid.ColumnSpan="4"
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
DoubleTapped="OnTitleBarDoubleTapped"
@ -69,11 +69,24 @@
<Path Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
</Button>
<!-- Workspace Switcher -->
<Button Grid.Column="1" Classes="icon_button" VerticalAlignment="Bottom" Click="OnOpenWorkspaceMenu">
<ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{DynamicResource Text.Workspace}" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}"/>
<TextBlock Text="{Binding ActiveWorkspace.Name}"/>
</StackPanel>
</ToolTip.Tip>
<Path Width="14" Height="14"
Data="{StaticResource Icons.Workspace}"
Fill="{Binding ActiveWorkspace.Brush}"/>
</Button>
<!-- Pages Tabs-->
<v:LauncherTabBar Grid.Column="1" Height="30" VerticalAlignment="Bottom"/>
<v:LauncherTabBar Grid.Column="2" Height="30" VerticalAlignment="Bottom"/>
<!-- Caption Buttons (Windows/Linux)-->
<Border Grid.Column="2" Margin="32,0,0,0" IsVisible="{Binding #ThisControl.IsRightCaptionButtonsVisible}">
<Border Grid.Column="3" Margin="32,0,0,0" IsVisible="{Binding #ThisControl.IsRightCaptionButtonsVisible}">
<v:CaptionButtons Height="30" VerticalAlignment="Top"/>
</Border>
</Grid>

View file

@ -3,6 +3,7 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views
@ -220,13 +221,7 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
var pref = ViewModels.Preference.Instance;
pref.Layout.LauncherWidth = Width;
pref.Layout.LauncherHeight = Height;
var vm = DataContext as ViewModels.Launcher;
vm?.Quit();
(DataContext as ViewModels.Launcher)?.Quit(Width, Height);
base.OnClosing(e);
}
@ -248,6 +243,17 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e)
{
if (sender is Button btn && DataContext is ViewModels.Launcher launcher)
{
var menu = launcher.CreateContextForWorkspace();
btn.OpenContextMenu(menu);
}
e.Handled = true;
}
private KeyModifiers _unhandledModifiers = KeyModifiers.None;
}
}

View file

@ -52,7 +52,7 @@
<Grid Width="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFixedTabWidth, Converter={x:Static c:BoolConverters.ToPageTabWidth}}" Height="30" ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Path Grid.Column="0"
Width="12" Height="12" Margin="12,0"
Fill="{Binding Node.Bookmark, Converter={x:Static c:BookmarkConverters.ToBrush}}"
Fill="{Binding Node.Bookmark, Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding Node.IsRepository}"
IsHitTestVisible="False"/>

View file

@ -52,7 +52,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.General}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.General.Locale}"
HorizontalAlignment="Right"
@ -66,10 +66,25 @@
SelectedItem="{Binding Locale, Mode=TwoWay, Converter={x:Static c:StringConverters.ToLocale}}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.DefaultCloneDir}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding GitDefaultCloneDir, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectDefaultCloneDir">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{DynamicResource Text.Preference.General.SubjectGuideLength}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<NumericUpDown Grid.Row="1" Grid.Column="1"
<NumericUpDown Grid.Row="2" Grid.Column="1"
Minimum="50" Maximum="1000" Increment="1"
Height="28"
Padding="4"
@ -78,11 +93,11 @@
CornerRadius="3"
Value="{Binding SubjectGuideLength, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Preference.General.MaxHistoryCommits}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<Grid Grid.Row="2" Grid.Column="1" ColumnDefinitions="*,64">
<Grid Grid.Row="3" Grid.Column="1" ColumnDefinitions="*,64">
<Slider Grid.Column="0"
Minimum="20000" Maximum="100000"
TickPlacement="BottomRight" TickFrequency="5000"
@ -97,14 +112,36 @@
Text="{Binding MaxHistoryCommits}"/>
</Grid>
<CheckBox Grid.Row="3" Grid.Column="1"
Content="{DynamicResource Text.Preference.General.RestoreTabs}"
IsChecked="{Binding RestoreTabs, Mode=TwoWay}"/>
<CheckBox Grid.Row="4" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preference.General.Check4UpdatesOnStartup}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=Check4UpdatesOnStartup, Mode=TwoWay}"/>
<CheckBox Grid.Row="5" Grid.Column="1"
Content="{DynamicResource Text.Preference.Git.AutoFetch}"
IsChecked="{Binding GitAutoFetch, Mode=TwoWay}"/>
<TextBlock Grid.Row="6" Grid.Column="0"
IsVisible="{Binding GitAutoFetch}"
Text="{DynamicResource Text.Preference.Git.AutoFetchInterval}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<Grid Grid.Row="6" Grid.Column="1" Height="32" ColumnDefinitions="*,Auto" IsVisible="{Binding GitAutoFetch}">
<NumericUpDown Grid.Column="0"
Minimum="1" Maximum="60" Increment="1"
Height="28"
Padding="4"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}"
CornerRadius="3"
ParsingNumberStyle="Integer"
FormatString="0"
Value="{Binding GitAutoFetchInterval, Mode=TwoWay, FallbackValue=10}"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="5,0,0,0"
Text="{DynamicResource Text.Preference.Git.AutoFetchIntervalSuffix}" />
</Grid>
</Grid>
</TabItem>
@ -198,7 +235,7 @@
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.Git}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,Auto,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.Path}"
HorizontalAlignment="Right"
@ -230,85 +267,31 @@
</Border>
</StackPanel>
<Border Grid.Row="2" Grid.Column="0"
Height="32"
IsVisible="{OnPlatform False, Windows=True}">
<TextBlock Text="{DynamicResource Text.Preference.Git.Shell}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
</Border>
<ComboBox Grid.Row="2" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
RenderOptions.BitmapInterpolationMode="HighQuality"
FontSize="{Binding DefaultFontSize, Mode=OneWay}"
SelectedIndex="{Binding GitShell, Mode=TwoWay}"
IsVisible="{OnPlatform False, Windows=True}">
<ComboBox.Items>
<Grid ColumnDefinitions="Auto,*">
<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/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/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/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>
</ComboBox>
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.DefaultCloneDir}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="3" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding GitDefaultCloneDir, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectDefaultCloneDir">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="4" Grid.Column="0"
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.User}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="4" Grid.Column="1"
<TextBox Grid.Row="2" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding #ThisControl.DefaultUser, Mode=TwoWay}"
Watermark="{DynamicResource Text.Preference.Git.User.Placeholder}"/>
<TextBlock Grid.Row="5" Grid.Column="0"
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.Email}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="5" Grid.Column="1"
<TextBox Grid.Row="3" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding #ThisControl.DefaultEmail, Mode=TwoWay}"
Watermark="{DynamicResource Text.Preference.Git.Email.Placeholder}"/>
<TextBlock Grid.Row="6" Grid.Column="0"
<TextBlock Grid.Row="4" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.CRLF}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<ComboBox Grid.Row="6" Grid.Column="1"
<ComboBox Grid.Row="4" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
@ -323,32 +306,6 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="7" Grid.Column="1"
Content="{DynamicResource Text.Preference.Git.AutoFetch}"
IsChecked="{Binding GitAutoFetch, Mode=TwoWay}"/>
<TextBlock Grid.Row="8" Grid.Column="0"
IsVisible="{Binding GitAutoFetch}"
Text="{DynamicResource Text.Preference.Git.AutoFetchInterval}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<Grid Grid.Row="8" Grid.Column="1" Height="32" ColumnDefinitions="*,Auto" IsVisible="{Binding GitAutoFetch}">
<NumericUpDown Grid.Column="0"
Minimum="1" Maximum="60" Increment="1"
Height="28"
Padding="4"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}"
CornerRadius="3"
ParsingNumberStyle="Integer"
FormatString="0"
Value="{Binding GitAutoFetchInterval, Mode=TwoWay, FallbackValue=10}"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="5,0,0,0"
Text="{DynamicResource Text.Preference.Git.AutoFetchIntervalSuffix}" />
</Grid>
</Grid>
</TabItem>
@ -418,51 +375,142 @@
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.DiffMerge}"/>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.Integration}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.DiffMerge.Type}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<ComboBox Grid.Row="0" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
RenderOptions.BitmapInterpolationMode="HighQuality"
FontSize="{Binding DefaultFontSize}"
ItemsSource="{Binding Source={x:Static m:ExternalMerger.Supported}}"
SelectedIndex="{Binding ExternalMergeToolType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:ExternalMerger}">
<Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="{Binding IconImage}" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Margin="8" MaxWidth="580" Orientation="Vertical" Grid.IsSharedSizeScope="True">
<TextBlock Classes="bold" Text="{DynamicResource Text.Preference.Shell}"/>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.Shell.Type}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<ComboBox Grid.Row="0" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={x:Static m:ShellOrTerminal.Supported}}"
SelectedIndex="{Binding ShellOrTerminal, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:ShellOrTerminal}">
<Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="{Binding Icon}" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.DiffMerge.Path}"
HorizontalAlignment="Right"
Margin="0,0,16,0"
IsVisible="{Binding ExternalMergeToolType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding ExternalMergeToolPath, Mode=TwoWay}"
Watermark="{DynamicResource Text.Preference.DiffMerge.Path.Placeholder}"
IsVisible="{Binding ExternalMergeToolType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectExternalMergeTool">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Grid>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.Shell.Path}"
HorizontalAlignment="Right"
Margin="0,0,16,0"
IsVisible="{OnPlatform True, macOS=False}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding ShellOrTerminalPath, Mode=TwoWay}"
IsVisible="{OnPlatform True, macOS=False}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectShellOrTerminal">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Grid>
<TextBlock Classes="bold" Margin="0,24,0,0" Text="{DynamicResource Text.Preference.DiffMerge}"/>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.DiffMerge.Type}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<ComboBox Grid.Row="0" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
RenderOptions.BitmapInterpolationMode="HighQuality"
FontSize="{Binding DefaultFontSize}"
ItemsSource="{Binding Source={x:Static m:ExternalMerger.Supported}}"
SelectedIndex="{Binding ExternalMergeToolType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:ExternalMerger}">
<Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="16" Height="16" Source="{Binding IconImage}" RenderOptions.BitmapInterpolationMode="HighQuality"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.DiffMerge.Path}"
HorizontalAlignment="Right"
Margin="0,0,16,0"
IsVisible="{Binding ExternalMergeToolType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding ExternalMergeToolPath, Mode=TwoWay}"
Watermark="{DynamicResource Text.Preference.DiffMerge.Path.Placeholder}"
IsVisible="{Binding ExternalMergeToolType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectExternalMergeTool">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Grid>
<TextBlock Classes="bold" Margin="0,24,0,0" Text="{DynamicResource Text.Preference.AI}"/>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,32,32">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.Server}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding OpenAIServer, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.Model}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding OpenAIModel, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.ApiKey}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="2" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding OpenAIApiKey, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</TabItem>
</TabControl>
</Border>

View file

@ -128,16 +128,16 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
var config = new Commands.Config(null).ListAll();
SetIfChanged(config, "user.name", DefaultUser);
SetIfChanged(config, "user.email", DefaultEmail);
SetIfChanged(config, "user.signingkey", GPGUserKey);
SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null);
SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false");
SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false");
SetIfChanged(config, "gpg.format", GPGFormat.Value);
SetIfChanged(config, "user.name", DefaultUser, "");
SetIfChanged(config, "user.email", DefaultEmail, "");
SetIfChanged(config, "user.signingkey", GPGUserKey, "");
SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null, null);
SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false", "false");
SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false", "false");
SetIfChanged(config, "gpg.format", GPGFormat.Value, "openpgp");
if (!GPGFormat.Value.Equals("ssh", StringComparison.Ordinal))
SetIfChanged(config, $"gpg.{GPGFormat.Value}.program", GPGExecutableFile);
SetIfChanged(config, $"gpg.{GPGFormat.Value}.program", GPGExecutableFile, "");
base.OnClosing(e);
}
@ -183,7 +183,7 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void SelectDefaultCloneDir(object _1, RoutedEventArgs _2)
private async void SelectDefaultCloneDir(object _, RoutedEventArgs e)
{
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
try
@ -194,13 +194,15 @@ namespace SourceGit.Views
ViewModels.Preference.Instance.GitDefaultCloneDir = selected[0].Path.LocalPath;
}
}
catch (Exception e)
catch (Exception ex)
{
App.RaiseException(string.Empty, $"Failed to select default clone directory: {e.Message}");
App.RaiseException(string.Empty, $"Failed to select default clone directory: {ex.Message}");
}
e.Handled = true;
}
private async void SelectGPGExecutable(object _1, RoutedEventArgs _2)
private async void SelectGPGExecutable(object _, RoutedEventArgs e)
{
var patterns = new List<string>();
if (OperatingSystem.IsWindows())
@ -219,14 +221,39 @@ namespace SourceGit.Views
{
GPGExecutableFile = selected[0].Path.LocalPath;
}
e.Handled = true;
}
private async void SelectExternalMergeTool(object _1, RoutedEventArgs _2)
private async void SelectShellOrTerminal(object _, RoutedEventArgs e)
{
var type = ViewModels.Preference.Instance.ShellOrTerminal;
if (type == -1)
return;
var shell = Models.ShellOrTerminal.Supported[type];
var options = new FilePickerOpenOptions()
{
FileTypeFilter = [new FilePickerFileType(shell.Name) { Patterns = [shell.Exec] }],
AllowMultiple = false,
};
var selected = await StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1)
{
ViewModels.Preference.Instance.ShellOrTerminalPath = selected[0].Path.LocalPath;
}
e.Handled = true;
}
private async void SelectExternalMergeTool(object _, RoutedEventArgs e)
{
var type = ViewModels.Preference.Instance.ExternalMergeToolType;
if (type < 0 || type >= Models.ExternalMerger.Supported.Count)
{
ViewModels.Preference.Instance.ExternalMergeToolType = 0;
e.Handled = true;
return;
}
@ -242,14 +269,16 @@ namespace SourceGit.Views
{
ViewModels.Preference.Instance.ExternalMergeToolPath = selected[0].Path.LocalPath;
}
e.Handled = true;
}
private void SetIfChanged(Dictionary<string, string> cached, string key, string value)
private void SetIfChanged(Dictionary<string, string> cached, string key, string value, string defValue)
{
bool changed = false;
if (cached.TryGetValue(key, out var old))
changed = old != value;
else if (!string.IsNullOrEmpty(value))
else if (!string.IsNullOrEmpty(value) && value != defValue)
changed = true;
if (changed)

View file

@ -225,7 +225,7 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="m:IssueTrackerRule">
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding Name}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

View file

@ -23,16 +23,25 @@
</DataTemplate>
<DataTemplate DataType="m:RevisionImageFile">
<Border Margin="0,8" VerticalAlignment="Center" HorizontalAlignment="Center" Effect="drop-shadow(0 0 8 #A0000000)">
<Border Background="{DynamicResource Brush.Window}">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
<Grid>
<v:ImageContainer/>
<Image Source="{Binding Image}" Stretch="Uniform" VerticalAlignment="Center" RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Grid>
<Grid RowDefinitions="*,Auto" Margin="0,8" VerticalAlignment="Center" HorizontalAlignment="Center">
<Border Grid.Row="0" Effect="drop-shadow(0 0 8 #A0000000)">
<Border Background="{DynamicResource Brush.Window}">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
<v:ImageView Image="{Binding Image}"/>
</Border>
</Border>
</Border>
</Border>
<StackPanel Grid.Row="1" Margin="0,8,0,0" Orientation="Horizontal" HorizontalAlignment="Center">
<Border Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Classes="primary" Text="{Binding ImageType}" Margin="8,0" FontSize="10" Foreground="{DynamicResource Brush.BadgeFG}"/>
</Border>
<TextBlock Classes="primary" Text="{Binding ImageSize}" Margin="8,0,0,0"/>
<TextBlock Classes="primary" Text="{Binding FileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
<TextBlock Classes="primary" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate DataType="m:RevisionLFSObject">

View file

@ -117,7 +117,7 @@
<Path Grid.Column="1"
Width="14" Height="14"
Fill="{Binding Bookmark, Converter={x:Static c:BookmarkConverters.ToBrush}}"
Fill="{Binding Bookmark, Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
HorizontalAlignment="Center"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding IsRepository}"/>

View file

@ -5,7 +5,7 @@
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="SourceGit.Views.WorkingCopy"
x:DataType="vm:WorkingCopy">
<Grid>
@ -42,7 +42,7 @@
Classes="icon_button"
Width="26" Height="14"
Padding="0"
Command="{Binding StageSelected}">
Click="OnStageSelectedButtonClicked">
<ToolTip.Tip>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.WorkingCopy.Unstaged.Stage}" VerticalAlignment="Center"/>
@ -64,6 +64,8 @@
<!-- Unstaged Changes -->
<v:ChangeCollectionView Grid.Row="1"
x:Name="UnstagedChangesView"
Focusable="True"
IsUnstagedChange="True"
SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}"
@ -81,7 +83,7 @@
<TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Staged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/>
<v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/>
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Command="{Binding UnstageSelected}">
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Click="OnUnstageSelectedButtonClicked">
<ToolTip.Tip>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.WorkingCopy.Staged.Unstage}" VerticalAlignment="Center"/>
@ -98,6 +100,8 @@
<!-- Staged Changes -->
<v:ChangeCollectionView Grid.Row="3"
x:Name="StagedChangesView"
Focusable="True"
SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}"
@ -173,34 +177,45 @@
<v:CommitMessageTextBox Grid.Row="2" Text="{Binding CommitMessage, Mode=TwoWay}"/>
<!-- Commit Options -->
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto,Auto">
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto">
<Button Grid.Column="0"
Classes="no_border"
Classes="icon_button"
Margin="4,0,0,0" Padding="0"
Click="OnOpenCommitMessagePicker">
<Grid ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
<TextBlock Grid.Column="1" Margin="8,0,0,0" Text="{DynamicResource Text.WorkingCopy.CommitMessageHelper}"/>
</Grid>
Click="OnOpenCommitMessagePicker"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.CommitMessageHelper}"
ToolTip.Placement="Top"
ToolTip.VerticalOffset="0">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
</Button>
<CheckBox Grid.Column="1"
<Button Grid.Column="1"
Classes="icon_button"
Width="32"
Margin="4,2,0,0"
Click="OnOpenAIAssist"
ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}"
ToolTip.Placement="Top"
ToolTip.VerticalOffset="0">
<Path Width="15" Height="15" Data="{StaticResource Icons.AIAssist}"/>
</Button>
<CheckBox Grid.Column="2"
Height="24"
Margin="12,0,0,0"
Margin="4,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding AutoStageBeforeCommit, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.AutoStage}"/>
<CheckBox Grid.Column="2"
<CheckBox Grid.Column="3"
Height="24"
Margin="12,0,0,0"
Margin="8,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding UseAmend, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.Amend}"/>
<v:LoadingIcon Grid.Column="4" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>
<v:LoadingIcon Grid.Column="5" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>
<Button Grid.Column="5"
<Button Grid.Column="6"
Classes="flat primary"
Content="{DynamicResource Text.WorkingCopy.Commit}"
Height="28"
@ -208,9 +223,11 @@
Padding="8,0"
Command="{Binding Commit}"
HotKey="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"
ToolTip.Tip="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"/>
ToolTip.Tip="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"
ToolTip.Placement="Top"
ToolTip.VerticalOffset="0"/>
<Button Grid.Column="6"
<Button Grid.Column="7"
Classes="flat"
Content="{DynamicResource Text.WorkingCopy.CommitAndPush}"
Height="28"
@ -219,6 +236,8 @@
Command="{Binding CommitWithPush}"
HotKey="{OnPlatform Ctrl+Shift+Enter, macOS=⌘+Shift+Enter}"
ToolTip.Tip="{OnPlatform Ctrl+Shift+Enter, macOS=⌘+Shift+Enter}"
ToolTip.Placement="Top"
ToolTip.VerticalOffset="0"
IsVisible="{Binding IsCommitWithPushVisible}"/>
</Grid>
</Grid>

View file

@ -46,7 +46,9 @@ namespace SourceGit.Views
{
if (DataContext is ViewModels.WorkingCopy vm)
{
vm.StageSelected();
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
e.Handled = true;
}
}
@ -55,7 +57,9 @@ namespace SourceGit.Views
{
if (DataContext is ViewModels.WorkingCopy vm)
{
vm.UnstageSelected();
var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
e.Handled = true;
}
}
@ -66,7 +70,9 @@ namespace SourceGit.Views
{
if (e.Key is Key.Space or Key.Enter)
{
vm.StageSelected();
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
e.Handled = true;
return;
}
@ -84,9 +90,60 @@ namespace SourceGit.Views
{
if (DataContext is ViewModels.WorkingCopy vm && e.Key is Key.Space or Key.Enter)
{
vm.UnstageSelected();
var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
e.Handled = true;
}
}
private void OnStageSelectedButtonClicked(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.WorkingCopy vm)
{
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
}
e.Handled = true;
}
private void OnUnstageSelectedButtonClicked(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.WorkingCopy vm)
{
var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
}
e.Handled = true;
}
private void OnOpenAIAssist(object _, RoutedEventArgs e)
{
if (!Models.OpenAI.IsValid)
{
App.RaiseException(null, "Bad configuration for OpenAI");
return;
}
if (DataContext is ViewModels.WorkingCopy vm)
{
if (vm.Staged is { Count: > 0 })
{
var dialog = new AIAssistant() { DataContext = vm };
dialog.GenerateCommitMessage();
App.OpenDialog(dialog);
}
else
{
App.RaiseException(null, "No files added to commit!");
}
}
e.Handled = true;
}
}
}