refactor<*>: rewrite all with AvaloniaUI

This commit is contained in:
leo 2024-02-06 15:08:37 +08:00
parent 0136904612
commit 2a62596999
521 changed files with 19780 additions and 23244 deletions

87
src/Views/About.axaml Normal file
View file

@ -0,0 +1,87 @@
<Window 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:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.About"
x:DataType="v:About"
Title="{DynamicResource Text.About}"
Background="{DynamicResource Brush.Window}"
SizeToContent="WidthAndHeight"
CanResize="False"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome">
<Grid RowDefinitions="30,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto">
<Path Grid.Column="0"
Width="14" Height="14"
Margin="10,0,0,0"
Data="{StaticResource Icons.Info}"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle, Converter={x:Static BoolConverters.Not}}"/>
<Grid Grid.Column="0" Classes="caption_button_box" Margin="2,4,0,0" IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle}">
<Button Classes="caption_button_macos" Click="CloseWindow">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Close}"/>
<Path Height="7" Width="7" Stretch="Fill" Data="{StaticResource Icons.MacOS.Close}"/>
</Grid>
</Button>
</Grid>
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
Classes="bold"
Text="{DynamicResource Text.About}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
<Button Grid.Column="2"
Classes="caption_button"
Click="CloseWindow"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle, Converter={x:Static BoolConverters.Not}}">
<Path Data="{StaticResource Icons.Window.Close}"/>
</Button>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<Image Grid.Column="0"
Width="200" Height="200"
Margin="8,0"
Source="/App.ico"
HorizontalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="0,20,32,0">
<StackPanel Height="48" Orientation="Horizontal">
<TextBlock Classes="bold" Text="SourceGit" FontSize="32" />
<Border Margin="12,0,0,0" Height="20" CornerRadius="10" Background="{DynamicResource Brush.Accent1}">
<Border.Effect>
<DropShadowEffect OffsetX="0" OffsetY="0" BlurRadius="6" Color="Black" Opacity=".3"/>
</Border.Effect>
<TextBlock Classes="monospace" Margin="8,0" Text="{Binding Version}" FontSize="12" Foreground="White"/>
</Border>
</StackPanel>
<TextBlock Margin="2,0,0,0" Text="{DynamicResource Text.About.Copyright}" Foreground="{DynamicResource Brush.FG2}"/>
<StackPanel Orientation="Vertical" Margin="0,28,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{DynamicResource Text.About.BuildWith}" />
<TextBlock Text="Avalonia UI" Cursor="Hand" Foreground="{DynamicResource Brush.Accent1}" TextDecorations="Underline" PointerPressed="OnVisitAvaloniaUI"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="{DynamicResource Text.About.Editor}" />
<TextBlock Text="OneWare.AvaloniaEdit" Cursor="Hand" Foreground="{DynamicResource Brush.Accent1}" TextDecorations="Underline" PointerPressed="OnVisitAvaloniaEdit"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="{DynamicResource Text.About.Fonts}" />
<TextBlock Text="JetBrains Mono" Cursor="Hand" Foreground="{DynamicResource Brush.Accent1}" TextDecorations="Underline" PointerPressed="OnVisitJetBrainsMonoFont"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</Window>

39
src/Views/About.axaml.cs Normal file
View file

@ -0,0 +1,39 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using System.Reflection;
namespace SourceGit.Views {
public partial class About : Window {
public string Version {
get;
private set;
}
public About() {
var ver = Assembly.GetExecutingAssembly().GetName().Version;
Version = $"{ver.Major}.{ver.Minor}";
DataContext = this;
InitializeComponent();
}
private void CloseWindow(object sender, RoutedEventArgs e) {
Close();
}
private void OnVisitAvaloniaUI(object sender, PointerPressedEventArgs e) {
Native.OS.OpenBrowser("https://www.avaloniaui.net/");
e.Handled = true;
}
private void OnVisitAvaloniaEdit(object sender, PointerPressedEventArgs e) {
Native.OS.OpenBrowser("https://www.nuget.org/packages/OneWare.AvaloniaEdit");
e.Handled = true;
}
private void OnVisitJetBrainsMonoFont(object sender, PointerPressedEventArgs e) {
Native.OS.OpenBrowser("https://www.jetbrains.com/lp/mono/");
e.Handled = true;
}
}
}

View file

@ -1,123 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.About"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
Title="{DynamicResource Text.About}"
Width="420" SizeToContent="Height"
ResizeMode="NoResize">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Help}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.About}"/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="Quit"
Width="28"
IconSize="10"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<!-- Content -->
<StackPanel Grid.Row="2" Orientation="Vertical">
<!-- LOGO -->
<Image
Margin="0,16,0,0"
Width="64" Height="64"
Source="pack://application:,,,/App.ico"/>
<!-- Title -->
<TextBlock
Margin="0,24,0,8"
Text="{DynamicResource Text.About.Title}"
HorizontalAlignment="Center"
FontSize="18"/>
<!-- Version -->
<TextBlock
x:Name="version"
Margin="0,0,0,8"
HorizontalAlignment="Center"
FontSize="11"
Text="VERSION: v1.0"/>
<DataGrid
x:Name="hotkeys"
Grid.Row="2"
Margin="16,8"
Background="{DynamicResource Brush.Contents}"
GridLinesVisibility="All"
HorizontalGridLinesBrush="{DynamicResource Brush.Border0}"
VerticalGridLinesBrush="{DynamicResource Brush.Border0}"
HeadersVisibility="Column"
RowHeight="24"
ColumnHeaderHeight="24"
IsHitTestVisible="False"
BorderThickness="1,1,0,0"
BorderBrush="{DynamicResource Brush.Border0}">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
<Border BorderThickness="0,0,1,1" BorderBrush="{DynamicResource Brush.Border0}" Background="{DynamicResource Brush.Window}">
<TextBlock
Text="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="DemiBold"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="100" Header="{DynamicResource Text.Hotkeys.Col.Key}" Binding="{Binding .Key}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
<DataGridTextColumn Width="*" Header="{DynamicResource Text.Hotkeys.Col.Desc}" Binding="{Binding .Desc}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
</DataGrid.Columns>
</DataGrid>
<!-- Official site -->
<TextBlock HorizontalAlignment="Center" Margin="0,4,0,16">
<TextBlock Text="© 2023" Margin="0" Padding="0"/>
<Hyperlink NavigateUri="https://github.com/sourcegit-scm/sourcegit.git" RequestNavigate="OnRequestNavigate" ToolTip="https://github.com/sourcegit-scm/sourcegit.git">
<Run Text="SourceGit."/>
</Hyperlink>
<TextBlock Text="All rights reserved." Margin="0" Padding="0"/>
</TextBlock>
</StackPanel>
</Grid>
</controls:Window>

View file

@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
namespace SourceGit.Views {
/// <summary>
/// 关于对话框
/// </summary>
public partial class About : Controls.Window {
public class Keymap {
public string Key { get; set; }
public string Desc { get; set; }
public Keymap(string k, string d) { Key = k; Desc = App.Text($"Hotkeys.{d}"); }
}
public About() {
InitializeComponent();
var asm = Assembly.GetExecutingAssembly().GetName();
version.Text = $"VERSION : v{asm.Version.Major}.{asm.Version.Minor}";
hotkeys.ItemsSource = new List<Keymap>() {
new Keymap("CTRL + T", "NewTab"),
new Keymap("CTRL + W", "CloseTab"),
new Keymap("CTRL + TAB", "NextTab"),
new Keymap("CTRL + [1-9]", "SwitchTo"),
new Keymap("CTRL + F", "Search"),
new Keymap("F5", "Refresh"),
new Keymap("SPACE", "ToggleStage"),
new Keymap("ESC", "CancelPopup"),
};
}
private void OnRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) {
var info = new ProcessStartInfo("cmd", $"/c start {e.Uri.AbsoluteUri}");
info.CreateNoWindow = true;
Process.Start(info);
}
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
}
}

57
src/Views/AddRemote.axaml Normal file
View file

@ -0,0 +1,57 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.AddRemote"
x:DataType="vm:AddRemote">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Remote.AddTitle}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,Auto" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Remote.Name}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.Remote.Name.Placeholder}"
Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Remote.URL}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.Remote.URL.Placeholder}"
Text="{Binding Url, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.SSHKey}"
IsVisible="{Binding UseSSH}"/>
<Grid Grid.Row="2" Grid.Column="2" Height="32" ColumnDefinitions="*,Auto" IsVisible="{Binding UseSSH}">
<TextBox Grid.Column="0"
x:Name="txtSSHKey"
Height="26"
CornerRadius="3"
Watermark="{DynamicResource Text.SSHKey.Placeholder}"
Text="{Binding SSHKey, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectSSHKey">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views {
public partial class AddRemote : UserControl {
public AddRemote() {
InitializeComponent();
}
private async void SelectSSHKey(object sender, RoutedEventArgs e) {
var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] };
var toplevel = TopLevel.GetTopLevel(this);
var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1) {
txtSSHKey.Text = selected[0].Path.LocalPath;
}
e.Handled = true;
}
}
}

View file

@ -0,0 +1,43 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.AddSubmodule"
x:DataType="vm:AddSubmodule">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Submodule.Add}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.URL}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.RepositoryURL}"
Text="{Binding Url, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Submodule.RelativePath}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.Submodule.RelativePath.Placeholder}"
Text="{Binding RelativePath, Mode=TwoWay}"/>
<CheckBox Grid.Row="2" Grid.Column="1"
Content="{DynamicResource Text.Submodule.FetchNested}"
IsChecked="{Binding Recursive, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class AddSubmodule : UserControl {
public AddSubmodule() {
InitializeComponent();
}
}
}

58
src/Views/Apply.axaml Normal file
View file

@ -0,0 +1,58 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Apply"
x:DataType="vm:Apply">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Apply.Title}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Apply.File}"/>
<Grid Grid.Row="0" Grid.Column="1" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
x:Name="txtPatchFile"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Apply.File.Placeholder}"
Text="{Binding PatchFile, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectPatchFile">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Apply.WS}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding WhiteSpaceModes}"
SelectedItem="{Binding SelectedWhiteSpaceMode, Mode=TwoWay}"
IsEnabled="{Binding !IgnoreWhiteSpace}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:ApplyWhiteSpaceMode}">
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Desc}" Margin="8,0,0,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="2" Grid.Column="1"
Content="{DynamicResource Text.Apply.IgnoreWS}"
IsChecked="{Binding IgnoreWhiteSpace, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

24
src/Views/Apply.axaml.cs Normal file
View file

@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views {
public partial class Apply : UserControl {
public Apply() {
InitializeComponent();
}
private async void SelectPatchFile(object sender, RoutedEventArgs e) {
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [ new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }] };
var selected = await topLevel.StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1) {
txtPatchFile.Text = selected[0].Path.LocalPath;
}
e.Handled = true;
}
}
}

63
src/Views/Archive.axaml Normal file
View file

@ -0,0 +1,63 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.Archive"
x:DataType="vm:Archive">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Archive.Title}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Archive.Revision}"/>
<ContentControl Grid.Column="1" Content="{Binding BasedOn}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" FontFamily="{StaticResource JetBrainsMono}" Foreground="DarkOrange" Margin="8,0,0,0"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Subject}}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Tag">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Tag}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" Margin="8,0,0,0"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Archive.File}"/>
<Grid Grid.Row="1" Grid.Column="1" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
x:Name="txtSaveFile"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Archive.File.Placeholder}"
Text="{Binding SaveFile, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectOutputFile">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views {
public partial class Archive : UserControl {
public Archive() {
InitializeComponent();
}
private async void SelectOutputFile(object sender, RoutedEventArgs e) {
var options = new FilePickerSaveOptions() { DefaultExtension = ".zip", FileTypeChoices = [ new FilePickerFileType("ZIP") { Patterns = [ "*.zip" ]}] };
var toplevel = TopLevel.GetTopLevel(this);
var selected = await toplevel.StorageProvider.SaveFilePickerAsync(options);
if (selected != null) {
txtSaveFile.Text = selected.Path.LocalPath;
}
e.Handled = true;
}
}
}

View file

@ -1,100 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.AssumeUnchanged"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
Title="{DynamicResource Text.AssumeUnchanged}"
Width="600" MinHeight="200" SizeToContent="Height"
ResizeMode="NoResize">
<Window.Resources>
<Style x:Key="Style.DataGridRow.Change" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Ignores}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.AssumeUnchanged}"/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="OnQuit"
Width="28"
IconSize="10"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<DataGrid
Grid.Row="2"
x:Name="list"
Margin="8,4"
RowHeight="24"
SelectionMode="Extended"
SelectionUnit="FullRow"
Background="{DynamicResource Brush.Contents}"
ItemsSource="{Binding ElementName=me, Path=Files}"
RowStyle="{StaticResource Style.DataGridRow.Change}"
Visibility="Collapsed">
<DataGrid.Columns>
<DataGridTemplateColumn IsReadOnly="True" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Margin="8,0" Text="{Binding .}" FontSize="14"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="32">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:IconButton
Grid.Column="6"
Click="Remove"
Width="12" Height="12"
Margin="4,0"
Icon="{StaticResource Icon.Close}"
ToolTip="{DynamicResource Text.AssumeUnchanged.Remove}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="2" x:Name="mask" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed">
<Path Width="48" Height="48" Margin="0,32,0,8" Data="{StaticResource Icon.Empty}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Margin="0,0,0,32" Text="{DynamicResource Text.AssumeUnchanged.Empty}" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
</Grid>
</controls:Window>

View file

@ -1,64 +0,0 @@
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views {
/// <summary>
/// 管理不跟踪变更的文件
/// </summary>
public partial class AssumeUnchanged : Controls.Window {
private string repo = null;
public ObservableCollection<string> Files { get; set; }
public AssumeUnchanged(string repo) {
this.repo = repo;
this.Files = new ObservableCollection<string>();
InitializeComponent();
Task.Run(() => {
var unchanged = new Commands.AssumeUnchanged(repo).View();
Dispatcher.Invoke(() => {
if (unchanged.Count > 0) {
foreach (var file in unchanged) Files.Add(file);
mask.Visibility = Visibility.Collapsed;
list.Visibility = Visibility.Visible;
list.ItemsSource = Files;
} else {
list.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Visible;
}
});
});
}
private void OnQuit(object sender, RoutedEventArgs e) {
Close();
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void Remove(object sender, RoutedEventArgs e) {
var btn = sender as Button;
if (btn == null) return;
var file = btn.DataContext as string;
if (file == null) return;
new Commands.AssumeUnchanged(repo).Remove(file);
Files.Remove(file);
if (Files.Count == 0) {
list.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Visible;
}
e.Handled = true;
}
}
}

View file

@ -0,0 +1,105 @@
<Window 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:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.AssumeUnchangedManager"
x:DataType="vm:AssumeUnchangedManager"
Title="{DynamicResource Text.AssumeUnchanged}"
Background="{DynamicResource Brush.Window}"
Width="600" Height="400"
CanResize="False"
WindowStartupLocation="CenterOwner"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome">
<Grid RowDefinitions="30,1,*">
<!-- TitleBar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource Brush.TitleBar}">
<Path Grid.Column="0"
Width="14" Height="14"
Margin="10,0,0,0"
Data="{StaticResource Icons.File.Ignore}"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle, Converter={x:Static BoolConverters.Not}}"/>
<Grid Grid.Column="0" Classes="caption_button_box" Margin="2,4,0,0" IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle}">
<Button Classes="caption_button_macos" Click="CloseWindow">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Close}"/>
<Path Height="7" Width="7" Stretch="Fill" Data="{StaticResource Icons.MacOS.Close}"/>
</Grid>
</Button>
</Grid>
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
Classes="bold"
Text="{DynamicResource Text.AssumeUnchanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
<Button Grid.Column="2"
Classes="caption_button"
Click="CloseWindow"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle, Converter={x:Static BoolConverters.Not}}">
<Path Data="{StaticResource Icons.Window.Close}"/>
</Button>
</Grid>
<!-- Line -->
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="1" Fill="{DynamicResource Brush.Border0}"/>
<!-- Unchanged Files -->
<DataGrid Grid.Row="2"
Margin="8"
Background="{DynamicResource Brush.Contents}"
ItemsSource="{Binding Files}"
SelectionMode="Single"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="8,0,4,0" Data="{StaticResource Icons.File}"/>
<TextBlock Text="{Binding}" Margin="4,0"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Classes="icon_button"
Command="{Binding $parent[v:AssumeUnchangedManager].DataContext.(vm:AssumeUnchangedManager).Remove}"
CommandParameter="{Binding}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Clear}"/>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Empty -->
<StackPanel Grid.Row="2"
Orientation="Vertical"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding Files.Count, Converter={x:Static c:IntConverters.IsZero}}">
<Path Width="48" Height="48" Data="{StaticResource Icons.Empty}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0" Text="{DynamicResource Text.AssumeUnchanged.Empty}" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
</Grid>
</Window>

View file

@ -0,0 +1,14 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace SourceGit.Views {
public partial class AssumeUnchangedManager : Window {
public AssumeUnchangedManager() {
InitializeComponent();
}
private void CloseWindow(object sender, RoutedEventArgs e) {
Close();
}
}
}

122
src/Views/Avatar.cs Normal file
View file

@ -0,0 +1,122 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace SourceGit.Views {
public class Avatar : Control, Models.IAvatarHost {
private static readonly GradientStops[] FALLBACK_GRADIENTS = [
new GradientStops() { new GradientStop(Colors.Orange, 0), new GradientStop(Color.FromRgb(255, 213, 134), 1) },
new GradientStops() { new GradientStop(Colors.DodgerBlue, 0), new GradientStop(Colors.LightSkyBlue, 1) },
new GradientStops() { new GradientStop(Colors.LimeGreen, 0), new GradientStop(Color.FromRgb(124, 241, 124), 1) },
new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
];
public static readonly StyledProperty<FontFamily> FallbackFontFamilyProperty =
AvaloniaProperty.Register<Avatar, FontFamily>(nameof(FallbackFontFamily));
public FontFamily FallbackFontFamily {
get => GetValue(FallbackFontFamilyProperty);
set => SetValue(FallbackFontFamilyProperty, value);
}
public static readonly StyledProperty<Models.User> UserProperty =
AvaloniaProperty.Register<Avatar, Models.User>(nameof(User));
public Models.User User {
get => GetValue(UserProperty);
set => SetValue(UserProperty, value);
}
static Avatar() {
AffectsRender<Avatar>(FallbackFontFamilyProperty);
UserProperty.Changed.AddClassHandler<Avatar>(OnUserPropertyChanged);
}
public Avatar() {
var refetch = new MenuItem() { Header = App.Text("RefetchAvatar") };
refetch.Click += (o, e) => {
if (User != null) {
_image = Models.AvatarManager.Request(_emailMD5, true);
InvalidateVisual();
}
};
ContextMenu = new ContextMenu();
ContextMenu.Items.Add(refetch);
Models.AvatarManager.Subscribe(this);
}
public override void Render(DrawingContext context) {
if (User == null) return;
float corner = (float)Math.Max(2, Bounds.Width / 16);
if (_image != null) {
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.PushClip(new RoundedRect(rect, corner));
context.DrawImage(_image, rect);
} else {
Point textOrigin = new Point((Bounds.Width - _fallbackLabel.Width) * 0.5, (Bounds.Height - _fallbackLabel.Height) * 0.5);
context.DrawRectangle(_fallbackBrush, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner);
context.DrawText(_fallbackLabel, textOrigin);
}
}
public void OnAvatarResourceReady(string md5, Bitmap bitmap) {
if (_emailMD5 == md5) {
_image = bitmap;
InvalidateVisual();
}
}
private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e) {
if (avatar.User == null) {
avatar._emailMD5 = null;
return;
}
var placeholder = string.IsNullOrWhiteSpace(avatar.User.Name) ? "?" : avatar.User.Name.Substring(0, 1);
var chars = placeholder.ToCharArray();
var sum = 0;
foreach (var c in chars) sum += Math.Abs(c);
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(avatar.User.Email.ToLower().Trim()));
var builder = new StringBuilder();
foreach (var c in hash) builder.Append(c.ToString("x2"));
var md5 = builder.ToString();
if (avatar._emailMD5 != md5) {
avatar._emailMD5 = md5;
avatar._image = Models.AvatarManager.Request(md5, false);
}
avatar._fallbackBrush = new LinearGradientBrush {
GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length],
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
};
var typeface = avatar.FallbackFontFamily == null ? Typeface.Default : new Typeface(avatar.FallbackFontFamily);
avatar._fallbackLabel = new FormattedText(
placeholder,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
avatar.Width * 0.65,
Brushes.White);
avatar.InvalidateVisual();
}
private FormattedText _fallbackLabel = null;
private LinearGradientBrush _fallbackBrush = null;
private string _emailMD5 = null;
private Bitmap _image = null;
}
}

76
src/Views/Blame.axaml Normal file
View file

@ -0,0 +1,76 @@
<Window 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="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Blame"
x:Name="me"
x:DataType="vm:Blame"
Title="{DynamicResource Text.Blame}"
WindowStartupLocation="CenterOwner"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border0}"
Background="{DynamicResource Brush.TitleBar}"
MinWidth="1280" MinHeight="720"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome">
<Grid Margin="{Binding #me.WindowState, Converter={x:Static c:WindowStateConverters.ToContentMargin}}">
<Grid.RowDefinitions>
<RowDefinition Height="{Binding #me.WindowState, Converter={x:Static c:WindowStateConverters.ToTitleBarHeight}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<!-- Bottom border -->
<Rectangle Grid.Column="0" Grid.ColumnSpan="5" Fill="{DynamicResource Brush.Border0}" Height="1" VerticalAlignment="Bottom"/>
<!-- Caption Buttons (macOS) -->
<Border Grid.Column="0" IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle}">
<v:CaptionButtonsMacOS/>
</Border>
<!-- Icon -->
<Path Grid.Column="1" Margin="8,0,0,0" Width="12" Height="12" Data="{StaticResource Icons.Blame}"/>
<!-- Title -->
<TextBlock Grid.Column="2" Margin="8,0" Text="{DynamicResource Text.Blame}" FontWeight="Bold" IsHitTestVisible="False" VerticalAlignment="Center"/>
<!-- Blame information -->
<TextBlock Grid.Column="3" Text="{Binding Title}" FontSize="11" FontStyle="Italic" IsHitTestVisible="False" VerticalAlignment="Center"/>
<!-- Caption Buttons (Windows) -->
<Border Grid.Column="4" IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=UseMacOSStyle, Converter={x:Static BoolConverters.Not}}">
<v:CaptionButtons/>
</Border>
</Grid>
<Grid Grid.Row="1" Background="{DynamicResource Brush.Window}">
<!-- Blame View -->
<v:BlameTextEditor HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="0"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{StaticResource JetBrainsMono}"
FontSize="12"
BlameData="{Binding Data}"/>
<!-- Not supported mask (for binary files) -->
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding IsBinary}">
<Path Width="64" Height="64" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Error}"/>
<TextBlock Margin="0,16,0,0" FontSize="18" FontWeight="Bold" HorizontalAlignment="Center" Foreground="{DynamicResource Brush.FG2}" Text="{DynamicResource Text.BlameTypeNotSupported}"/>
</StackPanel>
</Grid>
<!-- Loading -->
<Grid Grid.Row="1" Background="{DynamicResource Brush.Window}" IsVisible="{Binding Data, Converter={x:Static ObjectConverters.IsNull}}">
<Path Width="48" Height="48" Classes="rotating" Data="{StaticResource Icons.Loading}" IsVisible="{Binding Data, Converter={x:Static ObjectConverters.IsNull}}"/>
</Grid>
</Grid>
</Window>

261
src/Views/Blame.axaml.cs Normal file
View file

@ -0,0 +1,261 @@
using Avalonia;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Styling;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Rendering;
using AvaloniaEdit.TextMate;
using AvaloniaEdit.Utils;
using System;
using System.Globalization;
using System.IO;
using TextMateSharp.Grammars;
namespace SourceGit.Views {
public class BlameTextEditor : TextEditor {
public class CommitInfoMargin : AbstractMargin {
public CommitInfoMargin(BlameTextEditor editor) {
_editor = editor;
ClipToBounds = true;
}
public override void Render(DrawingContext context) {
if (_editor.BlameData == null) return;
var view = TextView;
if (view != null && view.VisualLinesValid) {
var typeface = view.CreateTypeface();
var underlinePen = new Pen(Brushes.DarkOrange, 1);
foreach (var line in view.VisualLines) {
var lineNumber = line.FirstDocumentLine.LineNumber;
if (lineNumber > _editor.BlameData.LineInfos.Count) break;
var info = _editor.BlameData.LineInfos[lineNumber - 1];
if (!info.IsFirstInGroup) continue;
var x = 0.0;
var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset;
var shaLink = new FormattedText(
info.CommitSHA,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
_editor.FontSize,
Brushes.DarkOrange);
context.DrawText(shaLink, new Point(x, y));
context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2));
x += shaLink.Width + 8;
var time = new FormattedText(
info.Time,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
_editor.FontSize,
_editor.Foreground);
context.DrawText(time, new Point(x, y));
x += time.Width + 8;
var author = new FormattedText(
info.Author,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
_editor.FontSize,
_editor.Foreground);
context.DrawText(author, new Point(x, y));
}
}
}
protected override Size MeasureOverride(Size availableSize) {
return new Size(250, 0);
}
protected override void OnPointerPressed(PointerPressedEventArgs e) {
base.OnPointerPressed(e);
var view = TextView;
if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid) {
var pos = e.GetPosition(this);
var typeface = view.CreateTypeface();
foreach (var line in view.VisualLines) {
var lineNumber = line.FirstDocumentLine.LineNumber;
if (lineNumber >= _editor.BlameData.LineInfos.Count) break;
var info = _editor.BlameData.LineInfos[lineNumber - 1];
if (!info.IsFirstInGroup) continue;
var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset;
var shaLink = new FormattedText(
info.CommitSHA,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
_editor.FontSize,
Brushes.DarkOrange);
var rect = new Rect(0, y, shaLink.Width, shaLink.Height);
if (rect.Contains(pos)) {
_editor.OnCommitSHAClicked(info.CommitSHA);
e.Handled = true;
break;
}
}
}
}
private BlameTextEditor _editor = null;
}
public class VerticalSeperatorMargin : AbstractMargin {
public VerticalSeperatorMargin(BlameTextEditor editor) {
_editor = editor;
}
public override void Render(DrawingContext context) {
var pen = new Pen(_editor.BorderBrush, 1);
context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height));
}
protected override Size MeasureOverride(Size availableSize) {
return new Size(1, 0);
}
private BlameTextEditor _editor = null;
}
public static readonly StyledProperty<Models.BlameData> BlameDataProperty =
AvaloniaProperty.Register<BlameTextEditor, Models.BlameData>(nameof(BlameData));
public Models.BlameData BlameData {
get => GetValue(BlameDataProperty);
set => SetValue(BlameDataProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextEditor);
public BlameTextEditor() : base(new TextArea(), new TextDocument()) {
IsReadOnly = true;
ShowLineNumbers = false;
WordWrap = false;
}
public void OnCommitSHAClicked(string sha) {
if (DataContext is ViewModels.Blame blame) {
blame.NavigateToCommit(sha);
}
}
protected override void OnLoaded(RoutedEventArgs e) {
base.OnLoaded(e);
TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) });
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) });
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
TextArea.TextView.Margin = new Thickness(4, 0);
if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) {
_registryOptions = new RegistryOptions(ThemeName.DarkPlus);
} else {
_registryOptions = new RegistryOptions(ThemeName.LightPlus);
}
_textMate = this.InstallTextMate(_registryOptions);
if (BlameData != null) {
_textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File)));
}
}
protected override void OnUnloaded(RoutedEventArgs e) {
base.OnUnloaded(e);
TextArea.LeftMargins.Clear();
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
_registryOptions = null;
_textMate.Dispose();
_textMate = null;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) {
base.OnPropertyChanged(change);
if (change.Property == BlameDataProperty) {
if (BlameData != null) {
Text = BlameData.Content;
if (_textMate != null) _textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File)));
} else {
Text = string.Empty;
}
} else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null && _textMate != null) {
if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) {
_textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.DarkPlus));
} else {
_textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.LightPlus));
}
}
}
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) {
var selected = SelectedText;
if (string.IsNullOrEmpty(selected)) return;
var icon = new Avalonia.Controls.Shapes.Path();
icon.Width = 10;
icon.Height = 10;
icon.Stretch = Stretch.Uniform;
icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry;
var copy = new MenuItem();
copy.Header = App.Text("Copy");
copy.Icon = icon;
copy.Click += (o, ev) => {
App.CopyText(selected);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(copy);
menu.Open(TextArea.TextView);
e.Handled = true;
}
private RegistryOptions _registryOptions = null;
private TextMate.Installation _textMate = null;
}
public partial class Blame : Window {
public Blame() {
if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
Owner = desktop.MainWindow;
}
InitializeComponent();
}
protected override void OnClosed(EventArgs e) {
base.OnClosed(e);
GC.Collect();
}
private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) {
if (DataContext is ViewModels.Blame blame) {
var txt = sender as TextBlock;
blame.NavigateToCommit(txt.Text);
}
e.Handled = true;
}
}
}

View file

@ -1,192 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.Blame"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:models="clr-namespace:SourceGit.Models"
mc:Ignorable="d"
Title="{DynamicResource Text.Blame}"
WindowStartupLocation="CenterOwner"
Height="600" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title Bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Commit}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Blame}"/>
<!-- Description -->
<TextBlock
Grid.Column="2"
x:Name="txtFile"
Margin="8,0,0,0"
FontFamily="{Binding Source={x:Static models:Preference.Instance}, Path=General.FontFamilyContent, Mode=OneWay}"
Foreground="{DynamicResource Brush.FG2}"
FontSize="11"/>
<!-- Window Commands -->
<StackPanel Grid.Column="4" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<controls:IconButton Click="Minimize" Width="48" IconSize="10" Icon="{StaticResource Icon.Minimize}" HoverBackground="#40000000" Opacity="1"/>
<ToggleButton Style="{StaticResource Style.ToggleButton.MaxOrRestore}" Width="48" IsChecked="{Binding ElementName=me, Path=IsMaximized}"/>
<controls:IconButton Click="Quit" Width="48" IconSize="10" Icon="{StaticResource Icon.Close}" HoverBackground="Red" Opacity="1"/>
</StackPanel>
</Grid>
<!-- Line -->
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<!-- Viewer -->
<DataGrid
Grid.Row="2"
x:Name="blame"
GridLinesVisibility="Vertical"
VerticalGridLinesBrush="{DynamicResource Brush.Border2}"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"
FrozenColumnCount="2"
RowHeight="16"
SelectionUnit="FullRow"
SelectionMode="Single"
SizeChanged="OnViewerSizeChanged"
SelectionChanged="OnSelectionChanged">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnViewerRequestBringIntoView"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Transparent"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="RenderOptions.ClearTypeHint" Value="Enabled"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Transparent"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="LineNumber" Text="{Binding Line.LineNumber}" HorizontalAlignment="Right" Padding="8,0"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="LineNumber" Property="Foreground" Value="DarkOrange"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="BG">
<Border x:Name="Content" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Margin="4,0" MaxWidth="300">
<TextBlock>
<Hyperlink NavigateUri="{Binding Line.CommitSHA}" RequestNavigate="GotoCommit" Foreground="DarkOrange" ToolTip="GOTO COMMIT">
<Run Text="{Binding Line.CommitSHA, Mode=OneWay}"/>
</Hyperlink>
</TextBlock>
<TextBlock Text="{Binding Line.Time}" Margin="8,0"/>
<TextBlock Text="{Binding Line.Author}" Foreground="DarkOrange"/>
</StackPanel>
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFirstLine}" Value="True">
<Setter TargetName="Content" Property="BorderThickness" Value="0"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsFirstLineInGroup}" Value="False">
<Setter TargetName="Content" Property="Visibility" Value="Hidden"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="BG" Property="Background" Value="#44000000"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="SizeToCells" MinWidth="1" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border x:Name="BG" BorderThickness="0">
<TextBlock Text="{Binding Line.Content}" Style="{DynamicResource Style.TextBlock.LineContent}"/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="BG" Property="Background" Value="#44000000"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel
x:Name="notSupport"
Grid.Row="3"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Background="{DynamicResource Brush.Window}"
Visibility="Collapsed">
<Path Width="64" Height="64" Data="{StaticResource Icon.Error}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Text="{DynamicResource Text.BlameTypeNotSupported}" Margin="0,16,0,0" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
<!-- Loading -->
<controls:Loading x:Name="loading" Grid.Row="3" Width="48" Height="48" IsAnimating="True"/>
</Grid>
</controls:Window>

View file

@ -1,173 +0,0 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
namespace SourceGit.Views {
/// <summary>
/// 逐行追溯
/// </summary>
public partial class Blame : Controls.Window {
/// <summary>
/// DataGrid数据源结构
/// </summary>
public class Record : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 原始Blame行数据
/// </summary>
public Models.BlameLine Line { get; set; }
/// <summary>
/// 是否是第一行
/// </summary>
public bool IsFirstLine { get; set; } = false;
/// <summary>
/// 前一行与本行的提交不同
/// </summary>
public bool IsFirstLineInGroup { get; set; } = false;
/// <summary>
/// 是否当前选中,会影响背景色
/// </summary>
private bool isSelected = false;
public bool IsSelected {
get { return isSelected; }
set {
if (isSelected != value) {
isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
}
/// <summary>
/// Blame数据
/// </summary>
public ObservableCollection<Record> Records { get; set; }
public Blame(string repo, string file, string revision) {
InitializeComponent();
this.repo = repo;
Records = new ObservableCollection<Record>();
txtFile.Text = $"{file}@{revision.Substring(0, 8)}";
Task.Run(() => {
var lfs = new Commands.LFS(repo).IsFiltered(file);
if (lfs) {
Dispatcher.Invoke(() => {
loading.IsAnimating = false;
loading.Visibility = Visibility.Collapsed;
notSupport.Visibility = Visibility.Visible;
});
return;
}
var rs = new Commands.Blame(repo, file, revision).Result();
if (rs.IsBinary) {
Dispatcher.Invoke(() => {
loading.IsAnimating = false;
loading.Visibility = Visibility.Collapsed;
notSupport.Visibility = Visibility.Visible;
});
} else {
string lastSHA = null;
foreach (var line in rs.Lines) {
var r = new Record();
r.Line = line;
r.IsSelected = false;
if (line.CommitSHA != lastSHA) {
lastSHA = line.CommitSHA;
r.IsFirstLineInGroup = true;
} else {
r.IsFirstLineInGroup = false;
}
Records.Add(r);
}
if (Records.Count > 0) Records[0].IsFirstLine = true;
Dispatcher.Invoke(() => {
loading.IsAnimating = false;
loading.Visibility = Visibility.Collapsed;
blame.ItemsSource = Records;
});
}
});
}
#region WINDOW_COMMANDS
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
#endregion
#region EVENTS
private T GetVisualChild<T>(DependencyObject parent) where T : Visual {
T child = null;
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++) {
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null) {
child = GetVisualChild<T>(v);
}
if (child != null) {
break;
}
}
return child;
}
private void OnViewerSizeChanged(object sender, SizeChangedEventArgs e) {
var total = blame.ActualWidth;
var offset = blame.NonFrozenColumnsViewportHorizontalOffset;
var minWidth = total - offset;
var scroller = GetVisualChild<ScrollViewer>(blame);
if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8;
blame.Columns[2].MinWidth = minWidth;
blame.Columns[2].Width = DataGridLength.SizeToCells;
blame.UpdateLayout();
}
private void OnViewerRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
var r = blame.SelectedItem as Record;
if (r == null) return;
foreach (var one in Records) {
one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA;
}
}
private void GotoCommit(object sender, RequestNavigateEventArgs e) {
Models.Watcher.Get(repo).NavigateTo(e.Uri.OriginalString);
e.Handled = true;
}
#endregion
private string repo = null;
}
}

View file

@ -0,0 +1,23 @@
<UserControl 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:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CaptionButtons">
<Grid Classes="caption_button_box">
<StackPanel Orientation="Horizontal">
<Button Classes="caption_button" Click="MinimizeWindow">
<Path Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Button Classes="caption_button" Click="MaximizeOrRestoreWindow">
<Path Data="{Binding $parent[Window].WindowState, Converter={x:Static c:WindowStateConverters.ToMaxOrRestoreIcon}}"/>
</Button>
<Button Classes="caption_button" Click="CloseWindow">
<Path Data="{StaticResource Icons.Window.Close}"/>
</Button>
</StackPanel>
</Grid>
</UserControl>

View file

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views {
public partial class CaptionButtons : UserControl {
public CaptionButtons() {
InitializeComponent();
}
private void MinimizeWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.WindowState = WindowState.Minimized;
}
}
private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
}
private void CloseWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.Close();
}
}
}
}

View file

@ -0,0 +1,32 @@
<UserControl 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:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CaptionButtonsMacOS">
<Grid Classes="caption_button_box">
<StackPanel Orientation="Horizontal">
<Button Classes="caption_button_macos" Click="CloseWindow" Margin="10,0,0,0">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Close}"/>
<Path Height="7" Width="7" Stretch="Fill" Data="{StaticResource Icons.MacOS.Close}"/>
</Grid>
</Button>
<Button Classes="caption_button_macos" Click="MinimizeWindow">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Minimize}"/>
<Path Height="2" Width="8" Stretch="Fill" Data="{StaticResource Icons.MacOS.Minimize}"/>
</Grid>
</Button>
<Button Classes="caption_button_macos" Click="MaximizeOrRestoreWindow">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Maximize}"/>
<Path Height="8" Width="8" Stretch="Fill" Data="{StaticResource Icons.MacOS.Maximize}}"/>
</Grid>
</Button>
</StackPanel>
</Grid>
</UserControl>

View file

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views {
public partial class CaptionButtonsMacOS : UserControl {
public CaptionButtonsMacOS() {
InitializeComponent();
}
private void MinimizeWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.WindowState = WindowState.Minimized;
}
}
private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
}
private void CloseWindow(object sender, RoutedEventArgs e) {
var window = this.FindAncestorOfType<Window>();
if (window != null) {
window.Close();
}
}
}
}

View file

@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using System;
using System.Globalization;
namespace SourceGit.Views {
public class ChangeStatusIcon : Control {
private static readonly IBrush[] BACKGROUNDS = [
Brushes.Transparent,
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
new LinearGradientBrush {
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
},
];
private static readonly string[] INDICATOR = ["?", "±", "+", "", "➜", "❏", "U", "★"];
public static readonly StyledProperty<bool> IsWorkingCopyChangeProperty =
AvaloniaProperty.Register<Avatar, bool>(nameof(IsWorkingCopyChange));
public bool IsWorkingCopyChange {
get => GetValue(IsWorkingCopyChangeProperty);
set => SetValue(IsWorkingCopyChangeProperty, value);
}
public static readonly StyledProperty<Models.Change> ChangeProperty =
AvaloniaProperty.Register<Avatar, Models.Change>(nameof(Change));
public Models.Change Change {
get => GetValue(ChangeProperty);
set => SetValue(ChangeProperty, value);
}
public static readonly StyledProperty<FontFamily> IconFontFamilyProperty =
AvaloniaProperty.Register<Avatar, FontFamily>(nameof(IconFontFamily));
public FontFamily IconFontFamily {
get => GetValue(IconFontFamilyProperty);
set => SetValue(IconFontFamilyProperty, value);
}
static ChangeStatusIcon() {
AffectsRender<ChangeStatusIcon>(IsWorkingCopyChangeProperty, ChangeProperty, IconFontFamilyProperty);
}
public override void Render(DrawingContext context) {
if (Change == null || Bounds.Width <= 0) return;
var typeface = IconFontFamily == null ? Typeface.Default : new Typeface(IconFontFamily);
IBrush background = null;
string indicator;
if (IsWorkingCopyChange) {
if (Change.IsConflit) {
background = Brushes.OrangeRed;
indicator = "!";
} else {
background = BACKGROUNDS[(int)Change.WorkTree];
indicator = INDICATOR[(int)Change.WorkTree];
}
} else {
background = BACKGROUNDS[(int)Change.Index];
indicator = INDICATOR[(int)Change.Index];
}
var txt = new FormattedText(
indicator,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
Bounds.Width * 0.8,
Brushes.White);
float corner = (float)Math.Max(2, Bounds.Width / 16);
Point textOrigin = new Point((Bounds.Width - txt.Width) * 0.5, (Bounds.Height - txt.Height) * 0.5);
context.DrawRectangle(background, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner);
context.DrawText(txt, textOrigin);
}
}
}

View file

@ -0,0 +1,35 @@
<UserControl 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:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.ChangeViewModeSwitcher"
x:DataType="v:ChangeViewModeSwitcher">
<Button Classes="icon_button" Padding="0" Margin="0" HorizontalAlignment="Center" VerticalAlignment="Center" ToolTip.Tip="{DynamicResource Text.ChangeDisplayMode}">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{DynamicResource Text.ChangeDisplayMode.List}" Command="{Binding SwitchMode}" CommandParameter="{x:Static m:ChangeViewMode.List}">
<MenuItem.Icon>
<Path Width="12" Height="12" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.List}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource Text.ChangeDisplayMode.Grid}" Command="{Binding SwitchMode}" CommandParameter="{x:Static m:ChangeViewMode.Grid}">
<MenuItem.Icon>
<Path Width="12" Height="12" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Grid}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource Text.ChangeDisplayMode.Tree}" Command="{Binding SwitchMode}" CommandParameter="{x:Static m:ChangeViewMode.Tree}">
<MenuItem.Icon>
<Path Width="12" Height="12" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Tree}"/>
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
<Path Stretch="Uniform" Fill="{DynamicResource Brush.FG2}" Data="{Binding ViewMode, Converter={x:Static c:ChangeViewModeConverters.ToIcon}}"/>
</Button>
</UserControl>

View file

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class ChangeViewModeSwitcher : UserControl {
public static readonly StyledProperty<Models.ChangeViewMode> ViewModeProperty =
AvaloniaProperty.Register<ChangeViewModeSwitcher, Models.ChangeViewMode>(nameof(ViewMode));
public Models.ChangeViewMode ViewMode {
get => GetValue(ViewModeProperty);
set => SetValue(ViewModeProperty, value);
}
public ChangeViewModeSwitcher() {
DataContext = this;
InitializeComponent();
}
public void SwitchMode(object param) {
ViewMode = (Models.ChangeViewMode)param;
}
}
}

19
src/Views/Checkout.axaml Normal file
View file

@ -0,0 +1,19 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Checkout"
x:DataType="vm:Checkout">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Checkout}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,16,0,0">
<TextBlock Text="{DynamicResource Text.Checkout.Target}"/>
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Branch}"/>
</StackPanel>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class Checkout : UserControl {
public Checkout() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,32 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CherryPick"
x:DataType="vm:CherryPick">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.CherryPick}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CherryPick.Commit}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Target.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" FontFamily="{StaticResource JetBrainsMono}" Foreground="DarkOrange" Margin="8,0,0,0"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Target.Subject}}" Margin="4,0,0,0"/>
</StackPanel>
<CheckBox Grid.Row="1" Grid.Column="1"
Content="{DynamicResource Text.CherryPick.CommitChanges}"
IsChecked="{Binding AutoCommit, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class CherryPick : UserControl {
public CherryPick() {
InitializeComponent();
}
}
}

19
src/Views/Cleanup.axaml Normal file
View file

@ -0,0 +1,19 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.Cleanup"
x:DataType="vm:Cleanup">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Repository.Clean}"/>
<TextBlock Text="{DynamicResource Text.Repository.CleanTips}"
Margin="0,16,0,0"
HorizontalAlignment="Center"/>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class Cleanup : UserControl {
public Cleanup() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,18 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.ClearStashes"
x:DataType="vm:ClearStashes">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.ClearStashes}"/>
<TextBlock Margin="0,16,0,8"
Text="{DynamicResource Text.ClearStashes.Message}"
HorizontalAlignment="Center"/>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class ClearStashes : UserControl {
public ClearStashes() {
InitializeComponent();
}
}
}

54
src/Views/Clone.axaml Normal file
View file

@ -0,0 +1,54 @@
<UserControl 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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Clone"
x:DataType="vm:Clone">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock Classes="bold" FontSize="18" Text="{DynamicResource Text.Clone}"/>
<Grid Margin="8,16,0,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.Clone.RemoteURL}"/>
<TextBox Grid.Column="1" CornerRadius="3" Text="{Binding Remote, Mode=TwoWay}"/>
</Grid>
<Grid Margin="8,4,0,0" Height="28" ColumnDefinitions="120,*" IsVisible="{Binding UseSSH}">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.SSHKey}"/>
<Grid Grid.Column="1" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
x:Name="txtSSHKey"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.SSHKey.Placeholder}"
Text="{Binding SSHKey, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectSSHKey">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
</Grid>
<Grid Margin="8,4,0,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.Clone.ParentFolder}"/>
<Grid Grid.Column="1" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
x:Name="txtParentFolder"
Height="28"
CornerRadius="3"
Text="{Binding ParentFolder, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectParentFolder">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
</Grid>
<Grid Margin="8,4,0,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.Clone.LocalName}"/>
<TextBox Grid.Column="1" CornerRadius="3" Watermark="{DynamicResource Text.Clone.LocalName.Placeholder}" Text="{Binding Local, Mode=TwoWay}"/>
</Grid>
<Grid Margin="8,4,0,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.Clone.AdditionalParam}"/>
<TextBox Grid.Column="1" CornerRadius="3" Watermark="{DynamicResource Text.Clone.AdditionalParam.Placeholder}" Text="{Binding ExtraArgs, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

33
src/Views/Clone.axaml.cs Normal file
View file

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views {
public partial class Clone : UserControl {
public Clone() {
InitializeComponent();
}
private async void SelectParentFolder(object sender, RoutedEventArgs e) {
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
var toplevel = TopLevel.GetTopLevel(this);
var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options);
if (selected.Count == 1) {
txtParentFolder.Text = selected[0].Path.LocalPath;
}
e.Handled = true;
}
private async void SelectSSHKey(object sender, RoutedEventArgs e) {
var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] };
var toplevel = TopLevel.GetTopLevel(this);
var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1) {
txtSSHKey.Text = selected[0].Path.LocalPath;
}
e.Handled = true;
}
}
}

View file

@ -1,261 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.Clone"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:validations="clr-namespace:SourceGit.Views.Validations"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
Title="{DynamicResource Text.Clone}"
Width="500" SizeToContent="Height"
ResizeMode="NoResize">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="None"/>
</Window.TaskbarItemInfo>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Pull}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Clone}"/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="OnQuit"
Width="28"
IconSize="10"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<Grid Grid.Row="2">
<!-- Body -->
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition x:Name="rowSSHKey" Height="0"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="48"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Clone.RemoteURL}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<controls:TextEdit
Grid.Row="0" Grid.Column="1"
x:Name="txtUrl"
Height="24"
Placeholder="{DynamicResource Text.Clone.RemoteURL.Placeholder}"
TextChanged="OnUrlChanged">
<controls:TextEdit.Text>
<Binding ElementName="me" Path="Uri" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<validations:GitURL/>
</Binding.ValidationRules>
</Binding>
</controls:TextEdit.Text>
</controls:TextEdit>
<TextBlock
Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.SSHKey}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<controls:TextEdit
Grid.Column="0"
x:Name="txtSSHKey"
Height="24"
Placeholder="{DynamicResource Text.SSHKey.Placeholder}"/>
<controls:IconButton
Grid.Column="1"
Click="OnSelectSSHKey"
Width="24" Height="24"
Margin="2,0,0,0" IconSize="16"
BorderBrush="{DynamicResource Brush.Border1}"
BorderThickness="1"
Icon="{StaticResource Icon.Folder.Open}"/>
</Grid>
<TextBlock
Grid.Row="2" Grid.Column="0"
Text="{DynamicResource Text.Clone.Folder}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<Grid Grid.Row="2" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<controls:TextEdit
Grid.Column="0"
x:Name="txtFolder"
Height="24"
Placeholder="{DynamicResource Text.Clone.Folder.Placeholder}">
<controls:TextEdit.Text>
<Binding ElementName="me" Path="Folder" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<validations:CloneDir/>
</Binding.ValidationRules>
</Binding>
</controls:TextEdit.Text>
</controls:TextEdit>
<controls:IconButton
Grid.Column="1"
Click="OnFolderSelectorClick"
Width="24" Height="24"
Margin="2,0,0,0" IconSize="16"
BorderBrush="{DynamicResource Brush.Border1}"
BorderThickness="1"
Icon="{StaticResource Icon.Folder.Open}"/>
</Grid>
<TextBlock
Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Clone.LocalName}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<controls:TextEdit
Grid.Row="3" Grid.Column="1"
Height="24"
x:Name="txtLocal"
Placeholder="{DynamicResource Text.Clone.LocalName.Placeholder}">
<controls:TextEdit.Text>
<Binding ElementName="me" Path="LocalName" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<validations:LocalRepositoryName/>
</Binding.ValidationRules>
</Binding>
</controls:TextEdit.Text>
</controls:TextEdit>
<TextBlock
Grid.Row="4" Grid.Column="0"
Text="{DynamicResource Text.Clone.RemoteName}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<controls:TextEdit
Grid.Row="4" Grid.Column="1"
x:Name="txtRemote"
Height="24"
Placeholder="{DynamicResource Text.Clone.RemoteName.Placeholder}">
<controls:TextEdit.Text>
<Binding ElementName="me" Path="RemoteName" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<validations:RemoteName x:Name="ruleRemote"/>
</Binding.ValidationRules>
</Binding>
</controls:TextEdit.Text>
</controls:TextEdit>
<TextBlock
Grid.Row="5" Grid.Column="0"
Text="{DynamicResource Text.Clone.AdditionalParam}"
Margin="0,0,4,0"
HorizontalAlignment="Right"/>
<controls:TextEdit
Grid.Row="5" Grid.Column="1"
Height="24"
Placeholder="{DynamicResource Text.Clone.AdditionalParam.Placeholder}"
Text="{Binding ElementName=me, Path=ExtraArgs, Mode=TwoWay}"/>
<StackPanel
Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
Height="32"
Orientation="Horizontal"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Click="OnStart" Width="80" Content="{DynamicResource Text.Start}" BorderBrush="{DynamicResource Brush.FG1}" Background="{DynamicResource Brush.Accent1}" FontWeight="Bold"/>
<Button Click="OnQuit" Width="80" Margin="8,0,0,0" Content="{DynamicResource Text.Cancel}" FontWeight="Bold"/>
</StackPanel>
</Grid>
<!-- Progress -->
<Border x:Name="progress" Visibility="Collapsed" Background="{DynamicResource Brush.Popup}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:Loading x:Name="processing" Width="48" Height="48"/>
<TextBlock x:Name="txtProgress" Margin="0,16,0,0"/>
</StackPanel>
</Border>
<!-- Exception -->
<Grid x:Name="exception" Margin="8" Visibility="Collapsed" Background="{DynamicResource Brush.Window}">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
<RowDefinition Height="48"/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Text="{DynamicResource Text.Launcher.Error}"
FontWeight="Bold"/>
<controls:TextEdit
Grid.Row="1"
x:Name="txtError"
IsReadOnly="true"
BorderThickness="0"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
MaxHeight="80"
Margin="0,8"
VerticalAlignment="Top"/>
<Button
Grid.Row="2"
Height="26"
Margin="4,0" Padding="8,0"
Click="OnCloseException"
Content="{DynamicResource Text.Sure}"
Background="{DynamicResource Brush.Accent1}"
BorderBrush="{DynamicResource Brush.FG1}"
BorderThickness="1"
HorizontalAlignment="Right"/>
</Grid>
</Grid>
</Grid>
</controls:Window>

View file

@ -1,131 +0,0 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shell;
namespace SourceGit.Views {
/// <summary>
/// 克隆
/// </summary>
public partial class Clone : Controls.Window {
public string Uri { get; set; }
public string Folder { get; set; }
public string LocalName { get; set; }
public string RemoteName { get; set; }
public string ExtraArgs { get; set; }
public Clone() {
Folder = Models.Preference.Instance.Git.DefaultCloneDir;
InitializeComponent();
ruleRemote.IsOptional = true;
}
#region EVENTS
private void OnQuit(object sender, RoutedEventArgs e) {
Close();
}
private async void OnStart(object s, RoutedEventArgs e) {
var checks = new Controls.TextEdit[] { txtUrl, txtFolder, txtLocal, txtRemote };
foreach (var edit in checks) {
edit.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(edit)) return;
}
TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate;
progress.Visibility = Visibility.Visible;
processing.IsAnimating = true;
var sshKey = txtSSHKey.Text;
var succ = await Task.Run(() => {
var extras = string.IsNullOrEmpty(ExtraArgs) ? "" : ExtraArgs;
if (!string.IsNullOrEmpty(RemoteName)) extras += $" --origin {RemoteName}";
var cloneRs = new Commands.Clone(Folder, Uri, LocalName, sshKey, extras, msg => {
Dispatcher.Invoke(() => txtProgress.Text = msg);
}, err => {
Dispatcher.Invoke(() => txtError.Text = err);
}).Exec();
if (!cloneRs) return false;
var path = Folder;
if (!string.IsNullOrEmpty(LocalName)) {
path = Path.GetFullPath(Path.Combine(path, LocalName));
} else {
var name = Path.GetFileName(Uri);
if (name.EndsWith(".git")) name = name.Substring(0, name.Length - 4);
path = Path.GetFullPath(Path.Combine(path, name));
}
if (!Directory.Exists(path)) {
Dispatcher.Invoke(() => txtError.Text = $"Folder {path} not found!");
return false;
}
if (!string.IsNullOrEmpty(sshKey)) {
var config = new Commands.Config(path);
var remote = "origin";
if (!string.IsNullOrEmpty(RemoteName)) remote = RemoteName;
config.Set($"remote.{remote}.sshkey", sshKey);
}
var gitDir = new Commands.QueryGitDir(path).Result();
var repo = Models.Preference.Instance.AddRepository(path, gitDir);
if (repo != null) Dispatcher.Invoke(() => Models.Watcher.Open(repo));
return true;
});
TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None;
progress.Visibility = Visibility.Collapsed;
processing.IsAnimating = false;
if (succ) {
Close();
} else {
exception.Visibility = Visibility.Visible;
}
}
private void OnFolderSelectorClick(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
Folder = dialog.SelectedPath;
txtFolder.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
}
}
private void OnSelectSSHKey(object sender, RoutedEventArgs e) {
var initPath = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "..", ".ssh"));
if (!Directory.Exists(initPath)) Directory.CreateDirectory(initPath);
var dialog = new OpenFileDialog();
dialog.Filter = $"SSH Private Key|*";
dialog.Title = App.Text("SSHKey");
dialog.InitialDirectory = initPath;
dialog.CheckFileExists = true;
dialog.Multiselect = false;
if (dialog.ShowDialog() == true) txtSSHKey.Text = dialog.FileName;
}
private void OnUrlChanged(object sender, TextChangedEventArgs e) {
var isSSHProtocal = Validations.GitURL.IsSSH(txtUrl.Text);
if (isSSHProtocal) {
rowSSHKey.Height = new GridLength(32, GridUnitType.Pixel);
} else {
rowSSHKey.Height = new GridLength(0, GridUnitType.Pixel);
}
}
private void OnCloseException(object s, RoutedEventArgs e) {
exception.Visibility = Visibility.Collapsed;
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,115 @@
<UserControl 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="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitBaseInfo">
<UserControl.DataTemplates>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Vertical">
<!-- Author & Committer -->
<Grid ColumnDefinitions="96,*,96,*" Margin="0,8">
<!-- Author -->
<v:Avatar Grid.Column="0"
Width="64" Height="64"
HorizontalAlignment="Right"
FallbackFontFamily="{StaticResource JetBrainsMono}"
User="{Binding Author}"/>
<StackPanel Grid.Column="1" Margin="16,0,8,0" Orientation="Vertical">
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.CommitDetail.Info.Author}"/>
<StackPanel Orientation="Horizontal" Margin="0,10,0,8">
<SelectableTextBlock Text="{Binding Author.Name}" FontSize="12" Margin="2,0,8,0"/>
<SelectableTextBlock Text="{Binding Author.Email}" FontSize="12" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
<SelectableTextBlock Text="{Binding AuthorTimeStr}" Margin="2,0,0,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
<!-- Committer -->
<v:Avatar Grid.Column="2"
Width="64" Height="64"
HorizontalAlignment="Right"
FallbackFontFamily="{StaticResource JetBrainsMono}"
User="{Binding Committer}"
IsVisible="{Binding IsCommitterVisible}"/>
<StackPanel Grid.Column="3" Margin="16,0,8,0" Orientation="Vertical" IsVisible="{Binding IsCommitterVisible}">
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.CommitDetail.Info.Committer}"/>
<StackPanel Orientation="Horizontal" Margin="0,10,0,8">
<SelectableTextBlock Text="{Binding Committer.Name}" FontSize="12" Margin="2,0,8,0"/>
<SelectableTextBlock Text="{Binding Committer.Email}" FontSize="12" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
<SelectableTextBlock Text="{Binding CommitterTimeStr}" Margin="2,0,0,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
</Grid>
<!-- Line -->
<Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}" VerticalAlignment="Center"/>
<!-- Base Information -->
<Grid RowDefinitions="24,Auto,Auto,Auto" ColumnDefinitions="96,*">
<!-- SHA -->
<TextBlock Grid.Row="0" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.SHA}" />
<SelectableTextBlock Grid.Row="0" Grid.Column="1" Text="{Binding SHA}" Margin="12,0,0,0" FontSize="12" VerticalAlignment="Center"/>
<!-- PARENTS -->
<TextBlock Grid.Row="1" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Parents}" IsVisible="{Binding Parents.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<ItemsControl Grid.Row="1" Grid.Column="1" Height="24" Margin="12,0,0,0" ItemsSource="{Binding Parents}" IsVisible="{Binding Parents.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding, Converter={x:Static c:StringConverters.ToShortSHA}}"
FontSize="12"
Foreground="DarkOrange"
TextDecorations="Underline"
Margin="0,0,16,0"
PointerPressed="OnParentSHAPressed"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- REFS -->
<TextBlock Grid.Row="2" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Refs}" IsVisible="{Binding Decorators.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<ItemsControl Grid.Row="2" Grid.Column="1" Height="24" Margin="12,0,0,0" ItemsSource="{Binding Decorators}" IsVisible="{Binding Decorators.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type m:Decorator}">
<Border Height="16" Margin="0,0,6,0" CornerRadius="2" ClipToBounds="True">
<StackPanel Orientation="Horizontal">
<Border Background="{DynamicResource Brush.Decorator}" Width="16">
<Path Width="8" Height="8" Stretch="Fill" Data="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToIcon}}" Fill="{DynamicResource Brush.DecoratorIcon}"/>
</Border>
<Border Background="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToBackground}}">
<TextBlock Text="{Binding Name}" FontSize="10" Margin="4,0" Foreground="Black"/>
</Border>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Messages -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
<ScrollViewer Grid.Row="3" Grid.Column="1" Margin="12,4,0,0" MaxHeight="100" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<SelectableTextBlock Text="{Binding FullMessage}" FontSize="12" TextWrapping="Wrap"/>
</ScrollViewer>
</Grid>
<!-- Line -->
<Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</UserControl.DataTemplates>
</UserControl>

View file

@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace SourceGit.Views {
public partial class CommitBaseInfo : UserControl {
public CommitBaseInfo() {
InitializeComponent();
}
private void OnParentSHAPressed(object sender, PointerPressedEventArgs e) {
if (DataContext is ViewModels.CommitDetail detail) {
detail.NavigateTo((sender as Control).DataContext as string);
}
e.Handled = true;
}
}
}

View file

@ -0,0 +1,183 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitChanges"
x:DataType="vm:CommitDetail">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="256" MinWidth="200" MaxWidth="480"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" RowDefinitions="26,*">
<!-- Search & Display Mode -->
<Grid Grid.Row="0" ColumnDefinitions="24,*,24,24">
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="Transparent"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"/>
<Path Grid.Column="0"
Width="14" Height="14"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Search}"/>
<TextBox Grid.Column="1"
Classes="no_background no_border"
Height="24"
FontSize="12"
CornerRadius="0"
Watermark="{DynamicResource Text.CommitDetail.Changes.Search}"
Text="{Binding SearchChangeFilter, Mode=TwoWay}"/>
<Button Grid.Column="2"
Classes="icon_button"
IsVisible="{Binding SearchChangeFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding ClearSearchChangeFilter}">
<Path Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Clear}"/>
</Button>
<v:ChangeViewModeSwitcher Grid.Column="3"
Width="18" Height="18"
HorizontalAlignment="Right"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode, Mode=TwoWay}"/>
</Grid>
<!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<Grid>
<DataGrid Background="Transparent"
ItemsSource="{Binding VisibleChanges}"
SelectedItem="{Binding SelectedChange, Mode=TwoWay}"
SelectionMode="Single"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
SelectionChanged="OnDataGridSelectionChanged"
ContextRequested="OnDataGridContextRequested"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode, Converter={x:Static c:ChangeViewModeConverters.IsList}}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding}" IconFontFamily="{StaticResource JetBrainsMono}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="PATH">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path}" Margin="4,0,0,0" FontSize="12"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid Background="Transparent"
ItemsSource="{Binding VisibleChanges}"
SelectedItem="{Binding SelectedChange, Mode=TwoWay}"
SelectionMode="Single"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
SelectionChanged="OnDataGridSelectionChanged"
ContextRequested="OnDataGridContextRequested"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode, Converter={x:Static c:ChangeViewModeConverters.IsGrid}}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding}" IconFontFamily="{StaticResource JetBrainsMono}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="FILE_NAME">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path, Converter={x:Static c:PathConverters.PureFileName}}" Margin="4,0,0,0" FontSize="12" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="FOLDER_PATH">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path, Converter={x:Static c:PathConverters.PureDirectoryName}}" Margin="4,0,0,0" FontSize="12" Foreground="{DynamicResource Brush.FG2}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TreeView ItemsSource="{Binding ChangeTree}"
SelectedItem="{Binding SelectedChangeNode, Mode=TwoWay}"
AutoScrollToSelectedItem="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ContextRequested="OnTreeViewContextRequested"
IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode, Converter={x:Static c:ChangeViewModeConverters.IsTree}}">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:FileTreeNode">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}" x:DataType="{x:Type vm:FileTreeNode}">
<Grid Height="24" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/>
<v:ChangeStatusIcon Grid.Column="0" Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding Backend}" IconFontFamily="{StaticResource JetBrainsMono}" IsVisible="{Binding !IsFolder}"/>
<TextBlock Grid.Column="1" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" FontSize="12" Margin="6,0,0,0"/>
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Border>
</Grid>
<GridSplitter Grid.Column="1"
Width="1" MinWidth=".5"
HorizontalAlignment="Center" VerticalAlignment="Stretch"
Background="Transparent"/>
<Grid Grid.Column="2" Margin="4,0,0,0">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Path Width="64" Height="64" Data="{StaticResource Icons.Diff}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0"
Text="{DynamicResource Text.Diff.Welcome}"
FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource Brush.FG2}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<ContentControl Content="{Binding DiffContext}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:DiffContext">
<v:DiffView/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,39 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class CommitChanges : UserControl {
public CommitChanges() {
InitializeComponent();
}
private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (sender is DataGrid datagrid && datagrid.IsVisible && datagrid.SelectedItem != null) {
datagrid.ScrollIntoView(datagrid.SelectedItem, null);
}
e.Handled = true;
}
private void OnDataGridContextRequested(object sender, ContextRequestedEventArgs e) {
if (sender is DataGrid datagrid && datagrid.SelectedItem != null) {
var detail = DataContext as ViewModels.CommitDetail;
var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change);
menu.Open(datagrid);
}
e.Handled = true;
}
private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) {
if (sender is TreeView view && view.SelectedItem != null) {
var detail = DataContext as ViewModels.CommitDetail;
var node = view.SelectedItem as ViewModels.FileTreeNode;
if (node != null && !node.IsFolder) {
var menu = detail.CreateChangeContextMenu(node.Backend as Models.Change);
menu.Open(view);
}
}
e.Handled = true;
}
}
}

View file

@ -0,0 +1,76 @@
<UserControl 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="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitDetail"
x:DataType="vm:CommitDetail"
Background="{DynamicResource Brush.Window}">
<TabControl SelectedIndex="{Binding ActivePageIndex, Mode=TwoWay}" Padding="4">
<!-- Information Page -->
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.CommitDetail.Info}"/>
</TabItem.Header>
<Grid RowDefinitions="Auto,*">
<!-- Base Information -->
<v:CommitBaseInfo Grid.Row="0" Content="{Binding Commit}"/>
<!-- Change List -->
<DataGrid Grid.Row="1"
Background="Transparent"
ItemsSource="{Binding Changes}"
SelectionMode="Single"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
Margin="80,0,8,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ContextRequested="OnChangeListContextRequested"
DoubleTapped="OnChangeListDoubleTapped">
<DataGrid.Columns>
<DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding}" IconFontFamily="{StaticResource JetBrainsMono}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="PATH">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path}" Margin="8,0,0,0" FontSize="12"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.CommitDetail.Changes}"/>
</TabItem.Header>
<v:CommitChanges/>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.CommitDetail.Files}"/>
</TabItem.Header>
<v:RevisionFiles/>
</TabItem>
</TabControl>
</UserControl>

View file

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace SourceGit.Views {
public partial class CommitDetail : UserControl {
public CommitDetail() {
InitializeComponent();
}
private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) {
if (DataContext is ViewModels.CommitDetail detail) {
var datagrid = sender as DataGrid;
detail.ActivePageIndex = 1;
detail.SelectedChange = datagrid.SelectedItem as Models.Change;
}
e.Handled = true;
}
private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) {
if (DataContext is ViewModels.CommitDetail detail) {
var datagrid = sender as DataGrid;
var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change);
menu.Open(datagrid);
}
e.Handled = true;
}
}
}

View file

@ -1,78 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.ConfirmDialog"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
Width="500" SizeToContent="Height"
ResizeMode="NoResize">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Help}"/>
<!-- Title -->
<TextBlock Grid.Column="1" x:Name="txtTitle" Text=""/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="OnQuit"
Width="28"
IconSize="10"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<Grid Grid.Row="2">
<!-- Body -->
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="48"/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0" Grid.Column="0"
x:Name="txtMessage"
Margin="8, 16"
HorizontalAlignment="Center"
TextWrapping="Wrap"/>
<StackPanel
Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
Height="32"
Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Click="OnSure" Width="80" Content="{DynamicResource Text.Sure}" FontWeight="Bold"/>
<Button x:Name="btnCancel" Click="OnQuit" Width="80" Margin="8,0,0,0" Content="{DynamicResource Text.Cancel}" FontWeight="Bold"/>
</StackPanel>
</Grid>
</Grid>
</Grid>
</controls:Window>

View file

@ -1,49 +0,0 @@
using System;
using System.Windows;
namespace SourceGit.Views {
/// <summary>
/// 通用的确认弹出框
/// </summary>
public partial class ConfirmDialog : Controls.Window {
private Action cbOK;
private Action cbCancel;
public ConfirmDialog(string title, string message) {
Owner = App.Current.MainWindow;
cbOK = null;
cbCancel = null;
InitializeComponent();
txtTitle.Text = title;
txtMessage.Text = message;
btnCancel.Visibility = Visibility.Collapsed;
}
public ConfirmDialog(string title, string message, Action onOk, Action onCancel = null) {
Owner = App.Current.MainWindow;
cbOK = onOk;
cbCancel = onCancel;
InitializeComponent();
txtTitle.Text = title;
txtMessage.Text = message;
btnCancel.Visibility = Visibility.Visible;
}
private void OnSure(object sender, RoutedEventArgs e) {
cbOK?.Invoke();
Close();
}
private void OnQuit(object sender, RoutedEventArgs e) {
cbCancel?.Invoke();
Close();
}
}
}

View file

@ -1,250 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SourceGit.Views.Controls {
/// <summary>
/// 头像控件
/// </summary>
public class Avatar : Image {
/// <summary>
/// 显示FallbackLabel时的背景色
/// </summary>
private static readonly Brush[] BACKGROUND_BRUSHES = new Brush[] {
new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90),
new LinearGradientBrush(Colors.DodgerBlue, Colors.LightSkyBlue, 90),
new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90),
new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90),
new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90),
};
/// <summary>
/// 头像资源本地缓存路径
/// </summary>
public static readonly string CACHE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"avatars");
/// <summary>
/// 邮件属性定义
/// </summary>
public static readonly DependencyProperty EmailProperty = DependencyProperty.Register(
"Email",
typeof(string),
typeof(Avatar),
new PropertyMetadata(null, OnEmailChanged));
/// <summary>
/// 邮件属性
/// </summary>
public string Email {
get { return (string)GetValue(EmailProperty); }
set { SetValue(EmailProperty, value); }
}
/// <summary>
/// 下载头像失败时显示的Label属性定义
/// </summary>
public static readonly DependencyProperty FallbackLabelProperty = DependencyProperty.Register(
"FallbackLabel",
typeof(string),
typeof(Avatar),
new PropertyMetadata("?", OnFallbackLabelChanged));
/// <summary>
/// 下载头像失败时显示的Label属性
/// </summary>
public string FallbackLabel {
get { return (string)GetValue(FallbackLabelProperty); }
set { SetValue(FallbackLabelProperty, value); }
}
private static event Action<string> RefetchRequested;
private static event Action<string> FetchCompleted;
private static Dictionary<string, BitmapImage> loaded = new Dictionary<string, BitmapImage>();
private static Task loader = null;
private int colorIdx = 0;
private FormattedText label = null;
public Avatar() {
RefetchRequested += email => {
if (email == Email) {
Source = null;
InvalidateVisual();
}
};
FetchCompleted += email => {
if (email == Email) {
Source = loaded[Email];
InvalidateVisual();
}
};
SetValue(RenderOptions.BitmapScalingModeProperty, BitmapScalingMode.HighQuality);
SetValue(RenderOptions.ClearTypeHintProperty, ClearTypeHint.Auto);
var refetch = new MenuItem();
refetch.Header = App.Text("Dashboard.Refresh");
refetch.Click += (o, e) => Refetch();
ContextMenu = new ContextMenu();
ContextMenu.Items.Add(refetch);
}
/// <summary>
/// 手动刷新
/// </summary>
private void Refetch() {
byte[] hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(Email.ToLower().Trim()));
string md5 = "";
for (int i = 0; i < hash.Length; i++) md5 += hash[i].ToString("x2");
md5 = md5.ToLower();
string filePath = Path.Combine(CACHE_PATH, md5);
if (File.Exists(filePath)) File.Delete(filePath);
RefetchRequested?.Invoke(Email);
if (loaded.ContainsKey(Email)) loaded.Remove(Email);
OnEmailChanged(this, new DependencyPropertyChangedEventArgs(EmailProperty, null, Email));
}
/// <summary>
/// 渲染实现
/// </summary>
/// <param name="dc"></param>
protected override void OnRender(DrawingContext dc) {
var corner = Math.Max(2, Width / 16);
if (Source == null && label != null) {
var offsetX = (double)0;
if (HorizontalAlignment == HorizontalAlignment.Right) {
offsetX = -Width * 0.5;
} else if (HorizontalAlignment == HorizontalAlignment.Left) {
offsetX = Width * 0.5;
}
Brush brush = BACKGROUND_BRUSHES[colorIdx];
dc.DrawRoundedRectangle(brush, null, new Rect(-Width * 0.5 + offsetX, -Height * 0.5, Width, Height), corner, corner);
dc.DrawText(label, new Point(label.Width * -0.5 + offsetX, label.Height * -0.5));
} else {
dc.PushClip(new RectangleGeometry(new Rect(0, 0, Width, Height), corner, corner));
base.OnRender(dc);
}
}
/// <summary>
/// 显示文本变化时触发
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnFallbackLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Avatar a = d as Avatar;
if (a == null) return;
var placeholder = a.FallbackLabel.Length > 0 ? a.FallbackLabel.Substring(0, 1) : "?";
a.colorIdx = 0;
a.label = new FormattedText(
placeholder,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily(Models.Preference.Instance.General.FontFamilyWindow), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
a.Width * 0.65,
Brushes.White,
VisualTreeHelper.GetDpi(a).PixelsPerDip);
var chars = placeholder.ToCharArray();
foreach (var ch in chars) a.colorIdx += Math.Abs(ch);
a.colorIdx = a.colorIdx % BACKGROUND_BRUSHES.Length;
if (a.Source == null) a.InvalidateVisual();
}
/// <summary>
/// 邮件变化时触发
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnEmailChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Avatar a = d as Avatar;
if (a == null) return;
if (a.Source != null) {
a.Source = null;
a.InvalidateVisual();
}
var email = e.NewValue as string;
if (string.IsNullOrEmpty(email)) return;
if (loaded.ContainsKey(email)) {
a.Source = loaded[email];
return;
}
byte[] hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(email.ToLower().Trim()));
string md5 = "";
for (int i = 0; i < hash.Length; i++) md5 += hash[i].ToString("x2");
md5 = md5.ToLower();
string filePath = Path.Combine(CACHE_PATH, md5);
if (File.Exists(filePath)) {
var img = LoadFromFile(filePath);
loaded.Add(email, img);
a.Source = img;
return;
}
loaded.Add(email, null);
Action job = () => {
try {
var req = WebRequest.CreateHttp($"https://cravatar.cn/avatar/{md5}?d=404");
req.Timeout = 2000;
req.Method = "GET";
var rsp = req.GetResponse() as HttpWebResponse;
if (rsp != null && rsp.StatusCode == HttpStatusCode.OK) {
using (var reader = rsp.GetResponseStream()) {
using (var writer = File.OpenWrite(filePath)) {
reader.CopyTo(writer);
}
}
a.Dispatcher.Invoke(() => {
loaded[email] = LoadFromFile(filePath);
FetchCompleted?.Invoke(email);
});
}
} catch {}
};
if (loader != null && !loader.IsCompleted) {
loader = loader.ContinueWith(t => { job(); });
} else {
loader = Task.Run(job);
}
}
private static BitmapImage LoadFromFile(string file) {
var img = new BitmapImage();
img.BeginInit();
img.StreamSource = new MemoryStream(File.ReadAllBytes(file));
img.EndInit();
return img;
}
}
}

View file

@ -1,52 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 徽章
/// </summary>
public class Badge : Border {
private TextBlock label = null;
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
"Label",
typeof(string),
typeof(Border),
new PropertyMetadata("", OnLabelChanged));
public string Label {
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
public Badge() {
Width = double.NaN;
Height = 18;
CornerRadius = new CornerRadius(9);
VerticalAlignment = VerticalAlignment.Center;
Visibility = Visibility.Collapsed;
SetResourceReference(BackgroundProperty, "Brush.Badge");
label = new TextBlock();
label.FontSize = 10;
label.HorizontalAlignment = HorizontalAlignment.Center;
label.Margin = new Thickness(9, 0, 9, 0);
Child = label;
}
private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Badge badge = d as Badge;
if (badge != null) {
var text = e.NewValue as string;
if (string.IsNullOrEmpty(text) || text == "0") {
badge.Visibility = Visibility.Collapsed;
} else {
badge.label.Text = text;
badge.Visibility = Visibility.Visible;
}
}
}
}
}

View file

@ -1,105 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SourceGit.Views.Controls {
/// <summary>
/// 用于切换变更显示模式的按钮
/// </summary>
public class ChangeDisplaySwitcher : Button {
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
"Mode",
typeof(Models.Change.DisplayMode),
typeof(ChangeDisplaySwitcher),
new PropertyMetadata(Models.Change.DisplayMode.Tree, OnModeChanged));
public Models.Change.DisplayMode Mode {
get { return (Models.Change.DisplayMode)GetValue(ModeProperty); }
set { SetValue(ModeProperty, value); }
}
public static readonly RoutedEvent ModeChangedEvent = EventManager.RegisterRoutedEvent(
"ModeChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ChangeDisplaySwitcher));
public event RoutedEventHandler ModeChanged {
add { AddHandler(ModeChangedEvent, value); }
remove { RemoveHandler(ModeChangedEvent, value); }
}
private Path icon = null;
public ChangeDisplaySwitcher() {
icon = new Path();
icon.Data = FindResource("Icon.Tree") as Geometry;
icon.SetResourceReference(Path.FillProperty, "Brush.FG2");
Content = icon;
Style = FindResource("Style.Button") as Style;
BorderThickness = new Thickness(0);
ToolTip = App.Text("ChangeDisplayMode");
Click += OnClicked;
}
private void OnClicked(object sender, RoutedEventArgs e) {
if (ContextMenu != null) {
ContextMenu.IsOpen = true;
e.Handled = true;
return;
}
var menu = new ContextMenu();
menu.Placement = PlacementMode.Bottom;
menu.PlacementTarget = this;
menu.StaysOpen = false;
menu.Focusable = true;
FillMenu(menu, "ChangeDisplayMode.Tree", "Icon.Tree", Models.Change.DisplayMode.Tree);
FillMenu(menu, "ChangeDisplayMode.List", "Icon.List", Models.Change.DisplayMode.List);
FillMenu(menu, "ChangeDisplayMode.Grid", "Icon.Grid", Models.Change.DisplayMode.Grid);
ContextMenu = menu;
ContextMenu.IsOpen = true;
e.Handled = true;
}
private void FillMenu(ContextMenu menu, string header, string icon, Models.Change.DisplayMode useMode) {
var iconMode = new Path();
iconMode.Width = 12;
iconMode.Height = 12;
iconMode.Data = FindResource(icon) as Geometry;
iconMode.SetResourceReference(Path.FillProperty, "Brush.FG2");
var item = new MenuItem();
item.Icon = iconMode;
item.Header = App.Text(header);
item.Click += (o, e) => Mode = useMode;
menu.Items.Add(item);
}
private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var elem = d as ChangeDisplaySwitcher;
if (elem != null) {
switch (elem.Mode) {
case Models.Change.DisplayMode.Tree:
elem.icon.Data = elem.FindResource("Icon.Tree") as Geometry;
break;
case Models.Change.DisplayMode.List:
elem.icon.Data = elem.FindResource("Icon.List") as Geometry;
break;
case Models.Change.DisplayMode.Grid:
elem.icon.Data = elem.FindResource("Icon.Grid") as Geometry;
break;
}
elem.RaiseEvent(new RoutedEventArgs(ModeChangedEvent));
}
}
}
}

View file

@ -1,106 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 变更状态图标
/// </summary>
class ChangeStatusIcon : FrameworkElement {
public static readonly Brush[] Backgrounds = new Brush[] {
Brushes.Transparent,
new LinearGradientBrush(Color.FromRgb(238, 160, 14), Color.FromRgb(228, 172, 67), 90),
new LinearGradientBrush(Color.FromRgb(47, 185, 47), Color.FromRgb(75, 189, 75), 90),
new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90),
new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90),
new LinearGradientBrush(Color.FromRgb(238, 160, 14), Color.FromRgb(228, 172, 67), 90),
new LinearGradientBrush(Color.FromRgb(238, 160, 14), Color.FromRgb(228, 172, 67), 90),
new LinearGradientBrush(Color.FromRgb(47, 185, 47), Color.FromRgb(75, 189, 75), 90),
};
public static readonly string[] Labels = new string[] {
"?",
"±",
"+",
"",
"➜",
"❏",
"U",
"★",
};
public static readonly DependencyProperty ChangeProperty = DependencyProperty.Register(
"Change",
typeof(Models.Change),
typeof(ChangeStatusIcon),
new PropertyMetadata(null, ForceDirty));
public Models.Change Change {
get { return (Models.Change)GetValue(ChangeProperty); }
set { SetValue(ChangeProperty, value); }
}
public static readonly DependencyProperty IsLocalChangeProperty = DependencyProperty.Register(
"IsLocalChange",
typeof(bool),
typeof(ChangeStatusIcon),
new PropertyMetadata(false, ForceDirty));
public bool IsLocalChange {
get { return (bool)GetValue(IsLocalChangeProperty); }
set { SetValue(IsLocalChangeProperty, value); }
}
private Brush background;
private FormattedText label;
public ChangeStatusIcon() {
HorizontalAlignment = HorizontalAlignment.Center;
VerticalAlignment = VerticalAlignment.Center;
}
protected override void OnRender(DrawingContext dc) {
if (background == null || label == null) return;
var corner = Math.Max(2, Width / 16);
dc.DrawRoundedRectangle(background, null, new Rect(0, 0, Width, Height), corner, corner);
dc.DrawText(label, new Point((Width - label.Width) * 0.5, 0));
}
private static void ForceDirty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var icon = d as ChangeStatusIcon;
if (icon == null) return;
if (icon.Change == null) {
icon.background = null;
icon.label = null;
return;
}
string txt;
if (icon.IsLocalChange) {
if (icon.Change.IsConflit) {
icon.background = Brushes.OrangeRed;
txt = "!";
} else {
icon.background = Backgrounds[(int)icon.Change.WorkTree];
txt = Labels[(int)icon.Change.WorkTree];
}
} else {
icon.background = Backgrounds[(int)icon.Change.Index];
txt = Labels[(int)icon.Change.Index];
}
icon.label = new FormattedText(
txt,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily(Models.Preference.Instance.General.FontFamilyWindow), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
icon.Width * 0.8,
new SolidColorBrush(Color.FromRgb(241, 241, 241)),
VisualTreeHelper.GetDpi(icon).PixelsPerDip);
icon.InvalidateVisual();
}
}
}

View file

@ -1,180 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 绘制提交频率柱状图
/// </summary>
public class Chart : FrameworkElement {
public static readonly double MAX_SHAPE_WIDTH = 24;
public static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register(
"LineBrush",
typeof(Brush),
typeof(Chart),
new PropertyMetadata(Brushes.White));
public Brush LineBrush {
get { return (Brush)GetValue(LineBrushProperty); }
set { SetValue(LineBrushProperty, value); }
}
public static readonly DependencyProperty ChartBrushProperty = DependencyProperty.Register(
"ChartBrush",
typeof(Brush),
typeof(Chart),
new PropertyMetadata(Brushes.White));
public Brush ChartBrush {
get { return (Brush)GetValue(ChartBrushProperty); }
set { SetValue(ChartBrushProperty, value); }
}
private List<Models.StatisticSample> samples = new List<Models.StatisticSample>();
private List<Rect> hitboxes = new List<Rect>();
private int maxV = 0;
/// <summary>
/// 设置绘制数据
/// </summary>
/// <param name="samples">数据源</param>
public void SetData(List<Models.StatisticSample> samples) {
this.samples = samples;
this.hitboxes.Clear();
maxV = 0;
foreach (var s in samples) {
if (maxV < s.Count) maxV = s.Count;
}
if (maxV <= 5) {
maxV = 5;
} else if (maxV <= 10) {
maxV = 10;
} else if (maxV <= 50) {
maxV = 50;
} else if (maxV <= 100) {
maxV = 100;
} else if (maxV <= 200) {
maxV = 200;
} else if (maxV <= 500) {
maxV = 500;
} else {
maxV = (int)Math.Ceiling(maxV / 500.0) * 500;
}
InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e) {
base.OnMouseMove(e);
InvalidateVisual();
}
protected override void OnRender(DrawingContext dc) {
base.OnRender(dc);
var font = new FontFamily("Consolas");
var culture = CultureInfo.CurrentCulture;
var typeface = new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var ppi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
var pen = new Pen(LineBrush, 1);
// 坐标系绘制
var maxLabel = new FormattedText($"{maxV}", culture, FlowDirection.LeftToRight, typeface, 12.0, LineBrush, ppi);
var horizonStart = maxLabel.Width + 8;
var labelHeight = 32;
dc.DrawText(maxLabel, new Point(0, - maxLabel.Height * 0.5));
dc.DrawLine(pen, new Point(horizonStart, 0), new Point(horizonStart, ActualHeight - labelHeight));
dc.DrawLine(pen, new Point(horizonStart, ActualHeight - labelHeight), new Point(ActualWidth, ActualHeight - labelHeight));
if (samples.Count == 0) return;
// 绘制纵坐标数值参考线
var stepX = (ActualWidth - horizonStart) / samples.Count;
var stepV = (ActualHeight - labelHeight) / 5;
var labelStepV = maxV / 5;
var gridPen = new Pen(LineBrush, 1) { DashStyle = DashStyles.Dash };
for (int i = 1; i < 5; i++) {
var vLabel = new FormattedText(
$"{maxV - i * labelStepV}",
culture,
FlowDirection.LeftToRight,
typeface,
12.0,
LineBrush,
ppi);
var dashHeight = i * stepV;
var vy = Math.Max(0, dashHeight - vLabel.Height * 0.5);
dc.PushOpacity(.1);
dc.DrawLine(gridPen, new Point(horizonStart + 1, dashHeight), new Point(ActualWidth, dashHeight));
dc.Pop();
dc.DrawText(vLabel, new Point(horizonStart - vLabel.Width - 8, vy));
}
// 先计算一下当前每个样本的碰撞区域,用于当鼠标移动上去时显示数值
if (hitboxes.Count == 0) {
var shapeWidth = Math.Min(32, stepX - 4);
for (int i = 0; i < samples.Count; i++) {
var h = samples[i].Count * (ActualHeight - labelHeight) / maxV;
var x = horizonStart + 1 + stepX * i + (stepX - shapeWidth) * 0.5;
var y = ActualHeight - labelHeight - h;
hitboxes.Add(new Rect(x, y, shapeWidth, h));
}
}
// 绘制样本
for (int i = 0; i < samples.Count; i++) {
var hLabel = new FormattedText(
samples[i].Name,
culture,
FlowDirection.LeftToRight,
typeface,
10.0,
LineBrush,
ppi);
var rect = hitboxes[i];
var xLabel = rect.X - (hLabel.Width - rect.Width) * 0.5;
var yLabel = ActualHeight - labelHeight + 4;
dc.DrawRectangle(ChartBrush, null, rect);
if (stepX < 32) {
dc.PushTransform(new TranslateTransform(xLabel, yLabel));
dc.PushTransform(new RotateTransform(45, hLabel.Width * 0.5, hLabel.Height * 0.5));
dc.DrawText(hLabel, new Point(0, 0));
dc.Pop();
dc.Pop();
} else {
dc.DrawText(hLabel, new Point(xLabel, yLabel));
}
}
// 当鼠标移动上去时显示数值
var mouse = Mouse.GetPosition(this);
for (int i = 0; i < samples.Count; i++) {
var rect = hitboxes[i];
if (rect.Contains(mouse)) {
var tooltip = new FormattedText(
$"{samples[i].Count}",
culture,
FlowDirection.LeftToRight,
typeface,
12.0,
FindResource("Brush.FG1") as Brush,
ppi);
var tx = rect.X - (tooltip.Width - rect.Width) * 0.5;
var ty = rect.Y - tooltip.Height - 4;
dc.DrawText(tooltip, new Point(tx, ty));
break;
}
}
}
}
}

View file

@ -1,341 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 提交线路图
/// </summary>
public class CommitGraph : FrameworkElement {
public static readonly Pen[] PENS = new Pen[] {
new Pen(Brushes.Orange, 2),
new Pen(Brushes.ForestGreen, 2),
new Pen(Brushes.Gold, 2),
new Pen(Brushes.Magenta, 2),
new Pen(Brushes.Red, 2),
new Pen(Brushes.Gray, 2),
new Pen(Brushes.Turquoise, 2),
new Pen(Brushes.Olive, 2),
};
public static readonly double UNIT_WIDTH = 12;
public static readonly double HALF_WIDTH = 6;
public static readonly double UNIT_HEIGHT = 24;
public static readonly double HALF_HEIGHT = 12;
public class Path {
public List<Point> Points = new List<Point>();
public int Color = 0;
}
public class PathHelper {
public string Next;
public bool IsMerged;
public double LastX;
public double LastY;
public double EndY;
public Path Path;
public PathHelper(string next, bool isMerged, int color, Point start) {
Next = next;
IsMerged = isMerged;
LastX = start.X;
LastY = start.Y;
EndY = LastY;
Path = new Path();
Path.Color = color % PENS.Length;
Path.Points.Add(start);
}
public PathHelper(string next, bool isMerged, int color, Point start, Point to) {
Next = next;
IsMerged = isMerged;
LastX = to.X;
LastY = to.Y;
EndY = LastY;
Path = new Path();
Path.Color = color % PENS.Length;
Path.Points.Add(start);
Path.Points.Add(to);
}
public void Add(double x, double y, bool isEnd = false) {
if (x > LastX) {
Add(new Point(LastX, LastY));
Add(new Point(x, y - HALF_HEIGHT));
if (isEnd) Add(new Point(x, y));
} else if (x < LastX) {
if (y > LastY + HALF_HEIGHT) Add(new Point(LastX, LastY + HALF_HEIGHT));
Add(new Point(x, y));
} else if (isEnd) {
Add(new Point(x, y));
}
LastX = x;
LastY = y;
}
private void Add(Point p) {
if (EndY < p.Y) {
Path.Points.Add(p);
EndY = p.Y;
}
}
}
public class Link {
public Point Start;
public Point Control;
public Point End;
public int Color;
}
public class Dot {
public Point Center;
public int Color;
}
public class Data {
public List<Path> Paths = new List<Path>();
public List<Link> Links = new List<Link>();
public List<Dot> Dots = new List<Dot>();
}
private Data data = null;
private double startY = 0;
public CommitGraph() {
Models.Theme.AddListener(this, InvalidateVisual);
IsHitTestVisible = false;
ClipToBounds = true;
}
public void SetOffset(double offset) {
startY = offset;
InvalidateVisual();
}
public void SetData(List<Models.Commit> commits, bool isSearchResult = false) {
if (isSearchResult) {
foreach (var c in commits) c.Margin = new Thickness(0);
data = null;
return;
}
var temp = new Data();
var unsolved = new List<PathHelper>();
var mapUnsolved = new Dictionary<string, PathHelper>();
var ended = new List<PathHelper>();
var offsetY = -HALF_HEIGHT;
var colorIdx = 0;
foreach (var commit in commits) {
var major = null as PathHelper;
var isMerged = commit.IsMerged;
var oldCount = unsolved.Count;
// 更新Y坐标
offsetY += UNIT_HEIGHT;
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) {
if (l.Next == commit.SHA) {
if (major == null) {
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0) {
major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
} else {
major.Next = "ENDED";
ended.Add(l);
}
major.Add(offsetX, offsetY);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY);
}
}
// 处理本提交为非当前分支HEAD的情况创建新依赖线路
if (major == null && commit.Parents.Count > 0) {
offsetX += UNIT_WIDTH;
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major);
temp.Paths.Add(major.Path);
colorIdx++;
}
// 确定本提交的点的位置
Point position = new Point(offsetX, offsetY);
if (major != null) {
major.IsMerged = isMerged;
position.X = major.LastX;
position.Y = offsetY;
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
} else {
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
}
// 处理本提交的其他依赖
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (mapUnsolved.ContainsKey(parent)) {
var l = mapUnsolved[parent];
var link = new Link();
link.Start = position;
link.End = new Point(l.LastX, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Color = l.Path.Color;
temp.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
// 防止有下一个提交有ended线时新的分支线与旧线重合
var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT));
unsolved.Add(l);
temp.Paths.Add(l.Path);
colorIdx++;
}
}
// 处理已终止的线
foreach (var l in ended) {
l.Add(position.X, position.Y, true);
unsolved.Remove(l);
}
// 加入本次提交
commit.IsMerged = isMerged;
commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH), 0, 0, 0);
// 清理
ended.Clear();
mapUnsolved.Clear();
}
// 处理尚未终结的线
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
if (path.Path.Points.Count == 1 && path.Path.Points[0].Y == endY) continue;
path.Add((i + 0.5) * UNIT_WIDTH, endY, true);
}
unsolved.Clear();
Dispatcher.Invoke(() => {
data = temp;
InvalidateVisual();
});
}
protected override void OnRender(DrawingContext dc) {
if (data == null) return;
dc.PushTransform(new TranslateTransform(0, -startY));
// 计算边界
var top = startY;
var bottom = startY + ActualHeight;
// 绘制线
DrawCurves(dc, top, bottom);
// 绘制点
var dotFill = FindResource("Brush.Contents") as Brush;
foreach (var dot in data.Dots) {
if (dot.Center.Y < top) continue;
if (dot.Center.Y > bottom) break;
dc.DrawEllipse(dotFill, PENS[dot.Color], dot.Center, 3, 3);
}
}
private void DrawCurves(DrawingContext dc, double top, double bottom) {
foreach (var line in data.Paths) {
var last = line.Points[0];
var size = line.Points.Count;
if (line.Points[size - 1].Y < top) continue;
if (last.Y > bottom) continue;
var geo = new StreamGeometry();
var pen = PENS[line.Color];
using (var ctx = geo.Open()) {
var started = false;
var ended = false;
for (int i = 1; i < size; i++) {
var cur = line.Points[i];
if (cur.Y < top) {
last = cur;
continue;
}
if (!started) {
ctx.BeginFigure(last, false, false);
started = true;
}
if (cur.Y > bottom) {
cur.Y = bottom;
ended = true;
}
if (cur.X > last.X) {
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur, true, false);
} else if (cur.X < last.X) {
if (i < size - 1) {
var midY = (last.Y + cur.Y) / 2;
var midX = (last.X + cur.X) / 2;
ctx.PolyQuadraticBezierTo(new Point[] {
new Point(last.X, midY),
new Point(midX, midY),
new Point(cur.X, midY),
cur}, true, false);
} else {
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur, true, false);
}
} else {
ctx.LineTo(cur, true, false);
}
if (ended) break;
last = cur;
}
}
geo.Freeze();
dc.DrawGeometry(null, pen, geo);
}
foreach (var link in data.Links) {
if (link.End.Y < top) continue;
if (link.Start.Y > bottom) break;
var geo = new StreamGeometry();
using (var ctx = geo.Open()) {
ctx.BeginFigure(link.Start, false, false);
ctx.QuadraticBezierTo(link.Control, link.End, true, false);
}
geo.Freeze();
dc.DrawGeometry(null, PENS[link.Color], geo);
}
}
}
}

View file

@ -1,85 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 支持部分高亮的文本组件
/// </summary>
public class HighlightableTextBlock : TextBlock {
private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(60, 0, 0, 0));
private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(128, 0, 255, 0));
private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(128, 255, 0, 0));
public static readonly DependencyProperty DataProperty = DependencyProperty.Register(
"Data",
typeof(Models.TextChanges.Line),
typeof(HighlightableTextBlock),
new PropertyMetadata(null, OnContentChanged));
public Models.TextChanges.Line Data {
get { return (Models.TextChanges.Line)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var txt = d as HighlightableTextBlock;
if (txt == null) return;
txt.Inlines.Clear();
txt.Text = null;
txt.Background = Brushes.Transparent;
txt.FontStyle = FontStyles.Normal;
if (txt.Data == null) return;
Brush highlightBrush = Brushes.Transparent;
switch (txt.Data.Mode) {
case Models.TextChanges.LineMode.None:
txt.Background = BG_EMPTY;
break;
case Models.TextChanges.LineMode.Indicator:
txt.FontStyle = FontStyles.Italic;
break;
case Models.TextChanges.LineMode.Added:
txt.Background = BG_ADDED;
highlightBrush = HL_ADDED;
break;
case Models.TextChanges.LineMode.Deleted:
txt.Background = BG_DELETED;
highlightBrush = HL_DELETED;
break;
default:
break;
}
txt.SetResourceReference(ForegroundProperty, txt.Data.Mode == Models.TextChanges.LineMode.Indicator ? "Brush.FG2" : "Brush.FG1");
if (txt.Data.Highlights == null || txt.Data.Highlights.Count == 0) {
txt.Text = txt.Data.Content;
return;
}
var started = 0;
foreach (var highlight in txt.Data.Highlights) {
if (started < highlight.Start) {
txt.Inlines.Add(new Run(txt.Data.Content.Substring(started, highlight.Start - started)));
}
txt.Inlines.Add(new Run() {
Background = highlightBrush,
Text = txt.Data.Content.Substring(highlight.Start, highlight.Count),
});
started = highlight.Start + highlight.Count;
}
if (started < txt.Data.Content.Length) {
txt.Inlines.Add(new Run(txt.Data.Content.Substring(started)));
}
}
}
}

View file

@ -1,46 +0,0 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 简化只有一个Icon的Button
/// </summary>
public class IconButton : Button {
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
"Icon",
typeof(Geometry),
typeof(IconButton),
new PropertyMetadata(null));
public Geometry Icon {
get { return (Geometry)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconSizeProperty = DependencyProperty.Register(
"IconSize",
typeof(double),
typeof(IconButton),
new PropertyMetadata(14.0));
public double IconSize {
get { return (double)GetValue(IconSizeProperty); }
set { SetValue(IconSizeProperty, value); }
}
public static readonly DependencyProperty HoverBackgroundProperty = DependencyProperty.Register(
"HoverBackground",
typeof(Brush),
typeof(IconButton),
new PropertyMetadata(Brushes.Transparent));
public Brush HoverBackground {
get { return (Brush)GetValue(HoverBackgroundProperty); }
set { SetValue(HoverBackgroundProperty, value); }
}
}
}

View file

@ -1,51 +0,0 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SourceGit.Views.Controls {
/// <summary>
/// 加载中图标
/// </summary>
public class Loading : UserControl {
private Path icon = null;
public static readonly DependencyProperty IsAnimatingProperty = DependencyProperty.Register(
"IsAnimating",
typeof(bool),
typeof(Loading),
new PropertyMetadata(false, OnIsAnimatingChanged));
public bool IsAnimating {
get { return (bool)GetValue(IsAnimatingProperty); }
set { SetValue(IsAnimatingProperty, value); }
}
public Loading() {
icon = new Path();
icon.Data = FindResource("Icon.Loading") as Geometry;
icon.RenderTransformOrigin = new Point(.5, .5);
icon.RenderTransform = new RotateTransform(0);
icon.Width = double.NaN;
icon.Height = double.NaN;
AddChild(icon);
}
private static void OnIsAnimatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var loading = d as Loading;
if (loading == null) return;
if (loading.IsAnimating) {
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.icon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
} else {
loading.icon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
}
}
}
}

View file

@ -1,66 +0,0 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 用于方便切换子页面的组件
/// </summary>
public class PageContainer : Grid {
private Dictionary<string, UIElement> pages;
private string front;
public PageContainer() {
pages = new Dictionary<string, UIElement>();
front = null;
Loaded += OnLoaded;
}
public void Add(string id, UIElement view) {
view.Visibility = Visibility.Collapsed;
pages.Add(id, view);
Children.Add(view);
}
public UIElement Get(string id) {
if (pages.ContainsKey(id)) return pages[id];
return null;
}
public void Goto(string id) {
if (!pages.ContainsKey(id)) return;
if (!string.IsNullOrEmpty(front)) {
if (front == id) return;
pages[front].Visibility = Visibility.Collapsed;
}
front = id;
pages[front].Visibility = Visibility.Visible;
}
public void Remove(string id) {
if (!pages.ContainsKey(id)) return;
if (front == id) front = null;
Children.Remove(pages[id]);
pages.Remove(id);
}
private void OnLoaded(object sender, RoutedEventArgs e) {
foreach (var child in Children) {
var elem = child as UIElement;
var id = elem.Uid;
if (string.IsNullOrEmpty(id)) continue;
pages.Add(id, elem);
front = id;
}
if (!string.IsNullOrEmpty(front)) {
pages[front].Visibility = Visibility.Visible;
}
}
}
}

View file

@ -1,102 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 可显示弹出面板的容器接口
/// </summary>
public interface IPopupContainer {
void Show(PopupWidget widget);
void ShowAndStart(PopupWidget widget);
void UpdateProgress(string message);
void ClosePopups(bool unlock);
}
/// <summary>
/// 可弹出面板
/// </summary>
public class PopupWidget : UserControl {
private static Dictionary<string, IPopupContainer> containers = new Dictionary<string, IPopupContainer>();
private static string currentContainer = null;
private IPopupContainer mine = null;
/// <summary>
/// 注册一个弹出容器
/// </summary>
/// <param name="id">页面ID</param>
/// <param name="container">容器实例</param>
public static void RegisterContainer(string id, IPopupContainer container) {
if (containers.ContainsKey(id)) containers[id] = container;
else containers.Add(id, container);
}
/// <summary>
/// 删除一个弹出容器
/// </summary>
/// <param name="id">容器ID</param>
public static void UnregisterContainer(string id) {
if (containers.ContainsKey(id)) containers.Remove(id);
}
/// <summary>
/// 设置当前的弹出容器
/// </summary>
/// <param name="id">容器ID</param>
public static void SetCurrentContainer(string id) {
if (containers.ContainsKey(id)) currentContainer = id;
}
/// <summary>
/// 构造函数
/// </summary>
public PopupWidget() {
Height = double.NaN;
Padding = new Thickness(1);
}
/// <summary>
/// 显示
/// </summary>
public void Show() {
if (string.IsNullOrEmpty(currentContainer) || !containers.ContainsKey(currentContainer)) return;
mine = containers[currentContainer];
mine.Show(this);
}
/// <summary>
/// 显示并直接点击开始
/// </summary>
public void ShowAndStart() {
if (string.IsNullOrEmpty(currentContainer) || !containers.ContainsKey(currentContainer)) return;
mine = containers[currentContainer];
mine.ShowAndStart(this);
}
/// <summary>
/// 窗体标题
/// </summary>
/// <returns>返回具体的标题</returns>
public virtual string GetTitle() {
return "TITLE";
}
/// <summary>
/// 点击确定时的回调,由程序自己
/// </summary>
/// <returns>返回一个任务任务预期返回类型为bool表示是否关闭Popup</returns>
public virtual Task<bool> Start() {
return null;
}
/// <summary>
/// 更新进度显示
/// </summary>
/// <param name="message"></param>
protected void UpdateProgress(string message) {
mine?.UpdateProgress(message);
}
}
}

View file

@ -1,92 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SourceGit.Views.Controls {
/// <summary>
/// 扩展默认TextBox
/// </summary>
public class TextEdit : TextBox {
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(
"Placeholder",
typeof(string),
typeof(TextEdit),
new PropertyMetadata(""));
public string Placeholder {
get { return (string)GetValue(PlaceholderProperty); }
set { SetValue(PlaceholderProperty, value); }
}
public static readonly DependencyProperty PlaceholderVisibilityProperty = DependencyProperty.Register(
"PlaceholderVisibility",
typeof(Visibility),
typeof(TextEdit),
new PropertyMetadata(Visibility.Visible));
public Visibility PlaceholderVisibility {
get { return (Visibility)GetValue(PlaceholderVisibilityProperty); }
set { SetValue(PlaceholderVisibilityProperty, value); }
}
public TextEdit() {
TextChanged += OnTextChanged;
SelectionChanged += OnSelectionChanged;
}
protected override void OnMouseWheel(MouseWheelEventArgs e) {
base.OnMouseWheel(e);
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) {
if (e.Delta > 0) {
LineLeft();
} else {
LineRight();
}
} else {
if (e.Delta > 0) {
LineUp();
} else {
LineDown();
}
}
}
private void OnTextChanged(object sender, TextChangedEventArgs e) {
PlaceholderVisibility = string.IsNullOrEmpty(Text) ? Visibility.Visible : Visibility.Collapsed;
}
private void OnSelectionChanged(object sender, RoutedEventArgs e) {
if (!IsFocused) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && SelectionLength > 0) {
var p = Mouse.GetPosition(this);
if (p.X <= 8) {
LineLeft();
} else if (p.X >= ActualWidth - 8) {
LineRight();
}
if (p.Y <= 8) {
LineUp();
} else if (p.Y >= ActualHeight - 8) {
LineDown();
}
} else {
var rect = GetRectFromCharacterIndex(CaretIndex);
if (rect.Left <= 0) {
ScrollToHorizontalOffset(HorizontalOffset + rect.Left);
} else if (rect.Right >= ActualWidth) {
ScrollToHorizontalOffset(HorizontalOffset + rect.Right);
}
if (rect.Top <= 0) {
ScrollToVerticalOffset(VerticalOffset + rect.Top);
} else if (rect.Bottom >= ActualHeight) {
ScrollToVerticalOffset(VerticalOffset + rect.Bottom);
}
}
}
}
}

View file

@ -1,246 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 树
/// </summary>
public class Tree : TreeView {
public static readonly DependencyProperty MultiSelectionProperty = DependencyProperty.Register(
"MultiSelection",
typeof(bool),
typeof(Tree),
new PropertyMetadata(false));
public bool MultiSelection {
get { return (bool)GetValue(MultiSelectionProperty); }
set { SetValue(MultiSelectionProperty, value); }
}
public static readonly DependencyProperty IndentProperty = DependencyProperty.Register(
"Indent",
typeof(double),
typeof(TreeItem),
new PropertyMetadata(16.0));
public double Indent {
get { return (double)GetValue(IndentProperty); }
set { SetValue(IndentProperty, value); }
}
public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(
"SelectionChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(Tree));
public event RoutedEventHandler SelectionChanged {
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
public List<object> Selected {
get;
set;
} = new List<object>();
public TreeItem FindItem(DependencyObject elem) {
if (elem == null) return null;
if (elem is TreeItem) return elem as TreeItem;
if (elem is Tree) return null;
return FindItem(VisualTreeHelper.GetParent(elem));
}
public void SelectAll() {
SelectAllChildren(this);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
public void UnselectAll() {
if (Selected.Count == 0) return;
UnselectAllChildren(this);
Selected.Clear();
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
public void Select(object dataContext) {
if (Selected.Count == 1 && Selected[0] == dataContext) return;
var item = FindItemByDataContext(this, dataContext);
if (item != null) {
AddSelected(item, true);
item.BringIntoView();
}
}
protected override DependencyObject GetContainerForItemOverride() {
return new TreeItem(0, Indent);
}
protected override bool IsItemItsOwnContainerOverride(object item) {
return item is TreeItem;
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
base.OnItemsSourceChanged(oldValue, newValue);
if (Selected.Count > 0) {
Selected.Clear();
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
}
protected override void OnPreviewKeyDown(KeyEventArgs e) {
base.OnPreviewKeyDown(e);
if (MultiSelection && e.Key == Key.A && Keyboard.Modifiers == ModifierKeys.Control) {
SelectAll();
e.Handled = true;
}
}
protected override void OnPreviewMouseDown(MouseButtonEventArgs e) {
base.OnPreviewMouseDown(e);
var hit = VisualTreeHelper.HitTest(this, e.GetPosition(this));
if (hit == null || hit.VisualHit == null) return;
var item = FindItem(hit.VisualHit);
if (item == null) return;
if (!MultiSelection) {
if (item.IsChecked) return;
AddSelected(item, true);
return;
}
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
if (item.IsChecked) {
RemoveSelected(item);
} else {
AddSelected(item, false);
}
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && Selected.Count > 0) {
var last = FindItemByDataContext(this, Selected.Last());
if (last == item) return;
var lastPos = last.PointToScreen(new Point(0, 0));
var curPos = item.PointToScreen(new Point(0, 0));
if (lastPos.Y > curPos.Y) {
SelectRange(this, item, last);
} else {
SelectRange(this, last, item);
}
AddSelected(item, false);
} else if (e.RightButton == MouseButtonState.Pressed) {
if (item.IsChecked) return;
AddSelected(item, true);
} else {
if (item.IsChecked && Selected.Count == 1) return;
AddSelected(item, true);
}
}
private TreeItem FindItemByDataContext(ItemsControl control, object data) {
if (control == null) return null;
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (control.Items[i] == data) return child;
var found = FindItemByDataContext(child, data);
if (found != null) return found;
}
return null;
}
private void AddSelected(TreeItem item, bool removeOthers) {
if (!item.IsVisible) return;
if (removeOthers && Selected.Count > 0) {
UnselectAllChildren(this);
Selected.Clear();
}
item.IsChecked = true;
Selected.Add(item.DataContext);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void RemoveSelected(TreeItem item) {
item.IsChecked = false;
Selected.Remove(item.DataContext);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void SelectAllChildren(ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
child.IsChecked = true;
Selected.Add(control.Items[i]);
SelectAllChildren(child);
}
}
private void UnselectAllChildren(ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (child.IsChecked) child.IsChecked = false;
UnselectAllChildren(child);
}
}
private int SelectRange(ItemsControl control, TreeItem from, TreeItem to, int matches = 0) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (matches == 1) {
if (child == to) return 2;
Selected.Add(control.Items[i]);
child.IsChecked = true;
if (TryEndRangeSelection(child, to)) return 2;
} else if (child == from) {
matches = 1;
if (TryEndRangeSelection(child, to)) return 2;
} else {
matches = SelectRange(child, from, to, matches);
if (matches == 2) return 2;
}
}
return matches;
}
private bool TryEndRangeSelection(ItemsControl control, TreeItem end) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (child == end) {
return true;
} else {
Selected.Add(control.Items[i]);
child.IsChecked = true;
var ended = TryEndRangeSelection(child, end);
if (ended) return true;
}
}
return false;
}
}
}

View file

@ -1,40 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 树节点
/// </summary>
public class TreeItem : TreeViewItem {
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(
"IsChecked",
typeof(bool),
typeof(TreeItem),
new PropertyMetadata(false));
public bool IsChecked {
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
private int depth = 0;
private double indent = 16;
public TreeItem(int depth, double indent) {
this.depth = depth;
this.indent = indent;
Padding = new Thickness(indent * depth, 0, 0, 0);
}
protected override DependencyObject GetContainerForItemOverride() {
return new TreeItem(depth + 1, indent);
}
protected override bool IsItemItsOwnContainerOverride(object item) {
return item is TreeItem;
}
}
}

View file

@ -1,97 +0,0 @@
using System;
using System.Windows;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 项目使用的窗体基类
/// </summary>
public class Window : System.Windows.Window {
[StructLayout(LayoutKind.Sequential)]
private struct OSVERSIONINFOEX {
public int Size;
public int Major;
public int Minor;
public int Build;
public int Platform;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string CSDVersion;
public ushort ServicePackMajor;
public ushort ServicePackMinor;
public short SuiteMask;
public byte ProductType;
public byte Reserved;
}
[DllImport("ntdll.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int RtlGetVersion(ref OSVERSIONINFOEX version);
[DllImport("dwmapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern long DwmSetWindowAttribute(IntPtr hwnd,
uint attribute,
ref uint pvAttribute,
uint cbAttribute);
public static readonly DependencyProperty IsMaximizedProperty = DependencyProperty.Register(
"IsMaximized",
typeof(bool),
typeof(Window),
new PropertyMetadata(false, OnIsMaximizedChanged));
public bool IsMaximized {
get { return (bool)GetValue(IsMaximizedProperty); }
set { SetValue(IsMaximizedProperty, value); }
}
public Window() {
Style = FindResource("Style.Window") as Style;
Loaded += OnWindowLoaded;
}
private void OnWindowLoaded(object sender, RoutedEventArgs e) {
OnStateChanged(null);
// Windows 11 需要特殊处理一下边框使得其与Window 10下表现一致
OSVERSIONINFOEX version = new OSVERSIONINFOEX() { Size = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
if (RtlGetVersion(ref version) == 0 && version.Major >= 10 && version.Build >= 22000) {
Models.Theme.Changed += UpdateBorderColor;
Unloaded += (_, __) => Models.Theme.Changed -= UpdateBorderColor;
UpdateBorderColor();
}
}
private void UpdateBorderColor() {
IntPtr hWnd = new WindowInteropHelper(GetWindow(this)).EnsureHandle();
Color color = (BorderBrush as SolidColorBrush).Color;
uint preference = ((uint)color.B << 16) | ((uint)color.G << 8) | (uint)color.R;
DwmSetWindowAttribute(hWnd, 34, ref preference, sizeof(uint));
}
protected override void OnStateChanged(EventArgs e) {
if (WindowState == WindowState.Maximized) {
if (!IsMaximized) IsMaximized = true;
BorderThickness = new Thickness(0);
Padding = new Thickness((SystemParameters.MaximizedPrimaryScreenWidth - SystemParameters.WorkArea.Width) / 2);
} else {
if (IsMaximized) IsMaximized = false;
BorderThickness = new Thickness(1);
Padding = new Thickness(0);
}
}
private static void OnIsMaximizedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Window w = d as Window;
if (w != null) {
if (w.IsMaximized) {
SystemCommands.MaximizeWindow(w);
} else if (w.WindowState != WindowState.Minimized) {
SystemCommands.RestoreWindow(w);
}
}
}
}
}

View file

@ -1,18 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
public class BoolToCollapsed : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -1,19 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
public class BranchToName : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var b = value as Models.Branch;
if (b == null) return "";
return string.IsNullOrEmpty(b.Remote) ? b.Name : $"{b.Remote}/{b.Name}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -1,20 +0,0 @@
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
public class FontFamiliesToName : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is string s)
return s.Split(',').ElementAt(0);
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return value;
}
}
}

View file

@ -1,40 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Views.Converters {
public class IntToBookmarkBrush : IValueConverter {
public static readonly Brush[] COLORS = new Brush[] {
Brushes.Transparent,
Brushes.Red,
Brushes.Orange,
Brushes.Gold,
Brushes.ForestGreen,
Brushes.DarkCyan,
Brushes.DeepSkyBlue,
Brushes.Purple,
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var index = (int)value;
return COLORS[index];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public class IntToBookmarkStrokeBrush : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var index = (int)value;
return index == 0 ? App.Current.FindResource("Brush.FG1") : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -1,17 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
public class InverseBool : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return !(bool)value;
}
}
}

View file

@ -1,21 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
/// <summary>
/// 将路径转换为纯文件名
/// </summary>
public class PureFileName : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return Path.GetFileName(value as string);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -1,21 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
/// <summary>
/// 将路径转换为纯目录
/// </summary>
public class PureFolderName : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return Path.GetDirectoryName(value as string);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -1,22 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Views.Converters {
/// <summary>
/// 将当前窗口的状态转换为标题栏高度
/// </summary>
public class WindowStateToTitleBarHeight : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
WindowState state = (WindowState)value;
return state == WindowState.Normal ? 36 : 30;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,74 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.CreateBranch"
x:DataType="vm:CreateBranch">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.CreateBranch}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CreateBranch.BasedOn}"/>
<ContentControl Grid.Column="1" Content="{Binding BasedOn}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" FontFamily="{StaticResource JetBrainsMono}" Foreground="DarkOrange" Margin="8,0,0,0"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Subject}}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Tag">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Tag}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" Margin="8,0,0,0"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CreateBranch.Name}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CreateBranch.LocalChanges}"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}"
GroupName="LocalChanges"
IsChecked="{Binding AutoStash, Mode=TwoWay}"/>
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.Discard}"
GroupName="LocalChanges"
Margin="8,0,0,0"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1"
Content="{DynamicResource Text.CreateBranch.Checkout}"
IsChecked="{Binding CheckoutAfterCreated, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class CreateBranch : UserControl {
public CreateBranch() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,17 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CreateGroup"
x:DataType="vm:CreateGroup">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock Classes="bold" FontSize="18" Text="{DynamicResource Text.Welcome.AddRootFolder}"/>
<Grid Margin="8,16,0,0" Height="28" ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="8,0" Text="{DynamicResource Text.Name}"/>
<TextBox Grid.Column="1" CornerRadius="3" Text="{Binding Name, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class CreateGroup : UserControl {
public CreateGroup() {
InitializeComponent();
}
}
}

62
src/Views/CreateTag.axaml Normal file
View file

@ -0,0 +1,62 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.CreateTag"
x:DataType="vm:CreateTag">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.CreateTag}"/>
<Grid Margin="0,16,8,0" RowDefinitions="32,32,64" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CreateTag.BasedOn}"/>
<ContentControl Grid.Column="1" Content="{Binding BasedOn}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" FontFamily="{StaticResource JetBrainsMono}" Foreground="DarkOrange" Margin="8,0,0,0"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Subject}}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.CreateTag.Name}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Text="{Binding TagName, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,6,8,0"
Text="{DynamicResource Text.CreateTag.Message}"/>
<TextBox Grid.Row="2" Grid.Column="1"
Height="56"
AcceptsReturn="True" AcceptsTab="False"
VerticalAlignment="Center" VerticalContentAlignment="Top"
CornerRadius="2"
Watermark="{DynamicResource Text.CreateTag.Message.Placeholder}"
Text="{Binding Message, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class CreateTag : UserControl {
public CreateTag() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,20 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteBranch"
x:DataType="vm:DeleteBranch">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteBranch}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,16,0,0">
<TextBlock Text="{DynamicResource Text.DeleteBranch.Branch}"/>
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Target, Converter={x:Static c:BranchConverters.ToName}}"/>
</StackPanel>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DeleteBranch : UserControl {
public DeleteBranch() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,19 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteRemote"
x:DataType="vm:DeleteRemote">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteRemote}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,16,0,0">
<TextBlock Text="{DynamicResource Text.DeleteRemote.Remote}"/>
<Path Width="14" Height="14" Margin="8,6,8,0" Data="{StaticResource Icons.Remote}"/>
<TextBlock Text="{Binding Remote.Name}"/>
</StackPanel>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DeleteRemote : UserControl {
public DeleteRemote() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,44 @@
<UserControl 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:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteRepositoryNode"
x:DataType="vm:DeleteRepositoryNode">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteRepositoryNode.TitleForGroup}"
IsVisible="{Binding !Node.IsRepository}"/>
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteRepositoryNode.TitleForRepository}"
IsVisible="{Binding Node.IsRepository}"/>
<Grid Margin="0,16,8,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
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}}"
StrokeThickness="{Binding Node.Bookmark, Converter={x:Static c:BookmarkConverters.ToStrokeThickness}}"
Stroke="{DynamicResource Brush.FG1}"
HorizontalAlignment="Left" VerticalAlignment="Center"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding Node.IsRepository}"/>
<Path Width="12" Height="12" Margin="0,0,8,0"
Fill="{DynamicResource Brush.FG1}"
HorizontalAlignment="Left" VerticalAlignment="Center"
Data="{StaticResource Icons.Folder}"
IsVisible="{Binding !Node.IsRepository}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Node.Name}"/>
<TextBlock Margin="8,0" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Node.Id}" IsVisible="{Binding Node.IsRepository}"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DeleteRepositoryNode : UserControl {
public DeleteRepositoryNode() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,26 @@
<UserControl 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:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteSubmodule"
x:DataType="vm:DeleteSubmodule">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteSubmodule}"/>
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" Height="28" HorizontalAlignment="Center">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.DeleteSubmodule.Path}"/>
<Path Width="12" Height="12"
HorizontalAlignment="Left" VerticalAlignment="Center"
Data="{StaticResource Icons.Submodule}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Submodule}" Margin="8,0,0,0"/>
</StackPanel>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DeleteSubmodule : UserControl {
public DeleteSubmodule() {
InitializeComponent();
}
}
}

29
src/Views/DeleteTag.axaml Normal file
View file

@ -0,0 +1,29 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteTag"
x:DataType="vm:DeleteTag">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.DeleteTag}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.DeleteTag.Tag}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Tag}"/>
<TextBlock Text="{Binding Target.Name}" Margin="8,0,0,0"/>
</StackPanel>
<CheckBox Grid.Row="1" Grid.Column="1"
Content="{DynamicResource Text.DeleteTag.WithRemote}"
IsChecked="{Binding ShouldPushToRemote, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DeleteTag : UserControl {
public DeleteTag() {
InitializeComponent();
}
}
}

120
src/Views/DiffView.axaml Normal file
View file

@ -0,0 +1,120 @@
<UserControl 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="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DiffView"
x:DataType="vm:DiffContext">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="{DynamicResource Brush.Window}">
<Grid RowDefinitions="26,*">
<!-- Toolbar -->
<Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border2}">
<Grid ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" IsVisible="{Binding IsOrgFilePathVisible}" VerticalAlignment="Center">
<Path Width="12" Height="12" Data="{StaticResource Icons.File}" Margin="8,0,0,0"/>
<TextBlock Margin="4,0,0,0" Text="{Binding OrgFilePath, Converter={x:Static c:PathConverters.TruncateIfTooLong}}" FontSize="11"/>
<TextBlock Margin="8,0,0,0" Text="→"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.File}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="1" Margin="4,0,0,0" Text="{Binding FilePath, Converter={x:Static c:PathConverters.TruncateIfTooLong}}" FontSize="11"/>
<Path Grid.Column="2" Classes="rotating" Width="10" Height="10" Margin="8,0" Data="{DynamicResource Icons.Loading}" IsVisible="{Binding IsLoading}"/>
</StackPanel>
<StackPanel Grid.Column="2" Margin="32,0,0,0" Orientation="Horizontal" IsVisible="{Binding IsTextDiff}" VerticalAlignment="Center">
<ToggleButton x:Name="toggleDisplayMode"
Classes="layout_direction"
Width="32" Height="26"
Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseCombinedTextDiff, Mode=TwoWay}"
ToolTip.Tip="{DynamicResource Text.Diff.Mode}"/>
<Button Classes="icon_button" Width="32" Command="{Binding OpenExternalMergeTool}" ToolTip.Tip="{DynamicResource Text.Diff.UseMerger}">
<Path Width="14" Height="14" Stretch="Uniform" Data="{StaticResource Icons.OpenWith}"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- Same Page -->
<Border Grid.Row="1" Background="{DynamicResource Brush.Window}" IsVisible="{Binding IsNoChange}">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Path Width="64" Height="64" Data="{StaticResource Icons.Check}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0"
Text="{DynamicResource Text.Diff.NoChange}"
FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource Brush.FG2}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Diff Contents -->
<ContentControl Grid.Row="1" Content="{Binding Content}">
<ContentControl.DataTemplates>
<!-- Binary Diff -->
<DataTemplate DataType="m:BinaryDiff">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.Binary}"
Margin="0,0,0,32"
FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource Brush.FG2}"
HorizontalAlignment="Center"/>
<Path Width="64" Height="64" Data="{StaticResource Icons.Binary}" Fill="{DynamicResource Brush.FG2}"/>
<Grid Margin="0,16,0,0" HorizontalAlignment="Center" RowDefinitions="32,32" ColumnDefinitions="Auto,Auto,Auto" TextElement.FontFamily="{StaticResource JetBrainsMono}">
<Border Grid.Row="0" Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding OldSize}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="16" Margin="8,0"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" FontSize="16"/>
<Border Grid.Row="1" Grid.Column="0" Height="16" Background="{DynamicResource Brush.Accent1}" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding NewSize}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="16" Margin="8,0"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" FontSize="16"/>
</Grid>
</StackPanel>
</DataTemplate>
<!-- LFS Diff -->
<DataTemplate DataType="m:LFSDiff">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.LFS}"
Margin="0,0,0,32"
FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource Brush.FG2}"
HorizontalAlignment="Center"/>
<Path Width="64" Height="64" Data="{StaticResource Icons.LFS}" Fill="{DynamicResource Brush.FG2}"/>
<Grid Margin="0,16,0,0" HorizontalAlignment="Center" RowDefinitions="32,32" ColumnDefinitions="Auto,Auto,Auto" TextElement.FontFamily="{StaticResource JetBrainsMono}">
<Border Grid.Row="0" Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Old.Size}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="16" Margin="8,0"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" FontSize="16"/>
<Border Grid.Row="1" Grid.Column="0" Height="16" Background="{DynamicResource Brush.Accent1}" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding New.Size}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="16" Margin="8,0"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" FontSize="16"/>
</Grid>
</StackPanel>
</DataTemplate>
<!-- Text Diff -->
<DataTemplate DataType="m:TextDiff">
<v:TextDiffView TextDiff="{Binding}" UseCombined="{Binding #toggleDisplayMode.IsChecked}"/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</Border>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DiffView : UserControl {
public DiffView() {
InitializeComponent();
}
}
}

45
src/Views/Discard.axaml Normal file
View file

@ -0,0 +1,45 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.Discard"
x:DataType="vm:Discard">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Discard}"/>
<Grid Margin="0,16,0,8" RowDefinitions="32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,8,0" HorizontalAlignment="Right" Text="{DynamicResource Text.Discard.Changes}"/>
<ContentControl Grid.Row="0" Grid.Column="1" Content="{Binding Mode}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:DiscardModeAll">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,2,0,0" Data="{StaticResource Icons.Folder.Open}"/>
<TextBlock Text="{DynamicResource Text.Discard.All}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="vm:DiscardModeSingle">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.File}"/>
<TextBlock Text="{Binding File}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="vm:DiscardModeMulti">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.File}"/>
<TextBlock Text="{Binding Count, Converter={x:Static c:StringConverters.FormatByResourceKey}, ConverterParameter='Discard.Total'}" Margin="4,0,0,0"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Text="{DynamicResource Text.Discard.Warning}" Foreground="{DynamicResource Brush.FG2}"/>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class Discard : UserControl {
public Discard() {
InitializeComponent();
}
}
}

29
src/Views/DropStash.axaml Normal file
View file

@ -0,0 +1,29 @@
<UserControl 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:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.DropStash"
x:DataType="vm:DropStash">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.StashDropConfirm}"/>
<Grid Margin="0,16,8,0" Height="28" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.StashDropConfirm.Label}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,0,8,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Data="{StaticResource Icons.Stashes}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Stash.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Stash.Message}" Margin="8,0,0,0"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class DropStash : UserControl {
public DropStash() {
InitializeComponent();
}
}
}

View file

@ -0,0 +1,57 @@
<UserControl 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:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.EditRemote"
x:DataType="vm:EditRemote">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.Remote.AddTitle}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,Auto" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Remote.Name}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.Remote.Name.Placeholder}"
Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Remote.URL}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
VerticalAlignment="Center"
CornerRadius="2"
Watermark="{DynamicResource Text.Remote.URL.Placeholder}"
Text="{Binding Url, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.SSHKey}"
IsVisible="{Binding UseSSH}"/>
<Grid Grid.Row="2" Grid.Column="2" Height="32" ColumnDefinitions="*,Auto" IsVisible="{Binding UseSSH}">
<TextBox Grid.Column="0"
x:Name="txtSSHKey"
Height="26"
CornerRadius="3"
Watermark="{DynamicResource Text.SSHKey.Placeholder}"
Text="{Binding SSHKey, Mode=TwoWay}"/>
<Button Grid.Column="1" Classes="icon_button" Width="32" Height="32" Margin="4,0,0,0" Click="SelectSSHKey">
<Path Data="{StaticResource Icons.Folder.Open}"/>
</Button>
</Grid>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views {
public partial class EditRemote : UserControl {
public EditRemote() {
InitializeComponent();
}
private async void SelectSSHKey(object sender, RoutedEventArgs e) {
var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] };
var toplevel = TopLevel.GetTopLevel(this);
var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1) {
txtSSHKey.Text = selected[0].Path.LocalPath;
}
e.Handled = true;
}
}
}

View file

@ -1,77 +0,0 @@
<controls:Window
x:Class="SourceGit.Views.EditRepositoryDisplayName"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:validations="clr-namespace:SourceGit.Views.Validations"
mc:Ignorable="d"
Title="{DynamicResource Text.Welcome.Rename}"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Width="500" SizeToContent="Height">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
<RowDefinition Height="48"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Icon -->
<Path Grid.Column="0" Margin="6,0" Width="12" Height="12" Data="{StaticResource Icon.Preference}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Welcome.Rename}"/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="OnCancel"
Width="28"
IconSize="10"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/>
<Grid Grid.Row="2" Margin="16,24,16,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="0,0,8,0" Text="{DynamicResource Text.Welcome.NewName}"/>
<controls:TextEdit Grid.Column="1" x:Name="txtName" Height="24">
<controls:TextEdit.Text>
<Binding ElementName="me" Path="NewName" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<validations:Required/>
</Binding.ValidationRules>
</Binding>
</controls:TextEdit.Text>
</controls:TextEdit>
</Grid>
<StackPanel Grid.Row="3" Height="32" Margin="16,0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Click="OnSure" Width="80" Content="{DynamicResource Text.Sure}" BorderBrush="{DynamicResource Brush.FG1}" Background="{DynamicResource Brush.Accent1}" FontWeight="Bold"/>
<Button Click="OnCancel" Width="80" Margin="8,0,0,0" Content="{DynamicResource Text.Cancel}" FontWeight="Bold"/>
</StackPanel>
</Grid>
</controls:Window>

View file

@ -1,35 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views {
/// <summary>
/// 修改仓库显示名称
/// </summary>
public partial class EditRepositoryDisplayName : Controls.Window {
private Models.Repository repository = null;
public string NewName {
get;
set;
}
public EditRepositoryDisplayName(Models.Repository repository) {
this.repository = repository;
NewName = repository.Name;
InitializeComponent();
}
private void OnSure(object s, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
repository.Name = NewName;
DialogResult = true;
Close();
}
private void OnCancel(object s, RoutedEventArgs e) {
DialogResult = false;
Close();
}
}
}

View file

@ -0,0 +1,54 @@
<UserControl 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:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.EditRepositoryNode"
x:DataType="vm:EditRepositoryNode">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.EditRepositoryNode.TitleForGroup}"
IsVisible="{Binding !IsRepository}"/>
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.EditRepositoryNode.TitleForRepository}"
IsVisible="{Binding IsRepository}"/>
<Grid Height="28" Margin="8,16,0,0" ColumnDefinitions="120,*" >
<TextBlock Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Text="{DynamicResource Text.EditRepositoryNode.Target}"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding Name, Mode=OneTime}" IsVisible="{Binding !IsRepository}"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding Id, Mode=TwoWay}" IsVisible="{Binding IsRepository}"/>
</Grid>
<Grid Height="28" Margin="8,4,0,0" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.EditRepositoryNode.Name}"/>
<TextBox Grid.Column="1" CornerRadius="3" Text="{Binding Name, Mode=TwoWay}"/>
</Grid>
<Grid Height="28" Margin="8,4,0,0" ColumnDefinitions="120,*" IsVisible="{Binding IsRepository}">
<TextBlock Grid.Column="0" HorizontalAlignment="Right" Margin="0,0,8,0" Text="{DynamicResource Text.EditRepositoryNode.Bookmark}"/>
<ComboBox Grid.Column="1"
BorderThickness="0"
Height="28"
ItemsSource="{x:Static m:Bookmarks.Supported}"
SelectedIndex="{Binding Bookmark, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Border Height="20" VerticalAlignment="Center">
<Path Width="12" Height="12"
Fill="{Binding, Converter={x:Static c:BookmarkConverters.ToBrush}}"
StrokeThickness="{Binding, Converter={x:Static c:BookmarkConverters.ToStrokeThickness}}"
Stroke="{DynamicResource Brush.FG1}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="{StaticResource Icons.Bookmark}"/>
</Border>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,10 @@
using Avalonia.Controls;
namespace SourceGit.Views {
public partial class EditRepositoryNode : UserControl {
public EditRepositoryNode() {
InitializeComponent();
}
}
}

12
src/Views/Extensions.cs Normal file
View file

@ -0,0 +1,12 @@
using Avalonia.Markup.Xaml;
using System;
namespace SourceGit.Views {
public class IntExtension : MarkupExtension {
public int Value { get; set; }
public IntExtension(int value) { this.Value = value; }
public override object ProvideValue(IServiceProvider serviceProvider) {
return Value;
}
}
}

Some files were not shown because too many files have changed in this diff Show more