mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-11-01 13:13:21 -07:00
optimize: new way to update corner radius of branch tree node to improve performance (#137)
This commit is contained in:
parent
476f9265e1
commit
8e3a8f4c06
5 changed files with 128 additions and 208 deletions
|
@ -1,99 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Data.Converters;
|
|
||||||
|
|
||||||
namespace SourceGit.Converters
|
|
||||||
{
|
|
||||||
public static class BranchTreeNodeConverters
|
|
||||||
{
|
|
||||||
public static readonly CornerRadius DEFAULT = new CornerRadius(4);
|
|
||||||
|
|
||||||
public static readonly FuncMultiValueConverter<object, CornerRadius> ToCornerRadius =
|
|
||||||
new FuncMultiValueConverter<object, CornerRadius>(v =>
|
|
||||||
{
|
|
||||||
if (v == null)
|
|
||||||
return DEFAULT;
|
|
||||||
|
|
||||||
var array = new List<object>();
|
|
||||||
array.AddRange(v);
|
|
||||||
if (array.Count != 2)
|
|
||||||
return DEFAULT;
|
|
||||||
|
|
||||||
var item = array[1] as TreeViewItem;
|
|
||||||
if (item == null || !item.IsSelected)
|
|
||||||
return DEFAULT;
|
|
||||||
|
|
||||||
var prev = GetPrevTreeViewItem(item);
|
|
||||||
var next = GetNextTreeViewItem(item, true);
|
|
||||||
|
|
||||||
double top = 4, bottom = 4;
|
|
||||||
if (prev != null && prev.IsSelected)
|
|
||||||
top = 0;
|
|
||||||
if (next != null && next.IsSelected)
|
|
||||||
bottom = 0;
|
|
||||||
|
|
||||||
return new CornerRadius(top, bottom);
|
|
||||||
});
|
|
||||||
|
|
||||||
private static TreeViewItem GetPrevTreeViewItem(TreeViewItem item)
|
|
||||||
{
|
|
||||||
if (item.Parent is TreeView tree)
|
|
||||||
{
|
|
||||||
var idx = tree.IndexFromContainer(item);
|
|
||||||
if (idx == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var prev = tree.ContainerFromIndex(idx - 1) as TreeViewItem;
|
|
||||||
if (prev != null && prev.IsExpanded && prev.ItemCount > 0)
|
|
||||||
return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem;
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
else if (item.Parent is TreeViewItem parentItem)
|
|
||||||
{
|
|
||||||
var idx = parentItem.IndexFromContainer(item);
|
|
||||||
if (idx == 0)
|
|
||||||
return parentItem;
|
|
||||||
|
|
||||||
var prev = parentItem.ContainerFromIndex(idx - 1) as TreeViewItem;
|
|
||||||
if (prev != null && prev.IsExpanded && prev.ItemCount > 0)
|
|
||||||
return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem;
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TreeViewItem GetNextTreeViewItem(TreeViewItem item, bool intoSelf = false)
|
|
||||||
{
|
|
||||||
if (intoSelf && item.IsExpanded && item.ItemCount > 0)
|
|
||||||
return item.ContainerFromIndex(0) as TreeViewItem;
|
|
||||||
|
|
||||||
if (item.Parent is TreeView tree)
|
|
||||||
{
|
|
||||||
var idx = tree.IndexFromContainer(item);
|
|
||||||
if (idx == tree.ItemCount - 1)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return tree.ContainerFromIndex(idx + 1) as TreeViewItem;
|
|
||||||
}
|
|
||||||
else if (item.Parent is TreeViewItem parentItem)
|
|
||||||
{
|
|
||||||
var idx = parentItem.IndexFromContainer(item);
|
|
||||||
if (idx == parentItem.ItemCount - 1)
|
|
||||||
return GetNextTreeViewItem(parentItem);
|
|
||||||
|
|
||||||
return parentItem.ContainerFromIndex(idx + 1) as TreeViewItem;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace SourceGit.ViewModels
|
namespace SourceGit.ViewModels
|
||||||
{
|
{
|
||||||
public enum BranchTreeNodeType
|
public enum BranchTreeNodeType
|
||||||
|
@ -12,8 +16,10 @@ namespace SourceGit.ViewModels
|
||||||
Branch,
|
Branch,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BranchTreeNode
|
public class BranchTreeNode : ObservableObject
|
||||||
{
|
{
|
||||||
|
public const double DEFAULT_CORNER = 4.0;
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public BranchTreeNodeType Type { get; set; }
|
public BranchTreeNodeType Type { get; set; }
|
||||||
public object Backend { get; set; }
|
public object Backend { get; set; }
|
||||||
|
@ -51,6 +57,43 @@ namespace SourceGit.ViewModels
|
||||||
get => IsBranch && (Backend as Models.Branch).IsCurrent;
|
get => IsBranch && (Backend as Models.Branch).IsCurrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set => SetProperty(ref _isSelected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CornerRadius CornerRadius
|
||||||
|
{
|
||||||
|
get => _cornerRadius;
|
||||||
|
set => SetProperty(ref _cornerRadius, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCornerRadius(ref BranchTreeNode prev)
|
||||||
|
{
|
||||||
|
if (_isSelected && prev != null && prev.IsSelected)
|
||||||
|
{
|
||||||
|
var prevTop = prev.CornerRadius.TopLeft;
|
||||||
|
prev.CornerRadius = new CornerRadius(prevTop, 0);
|
||||||
|
CornerRadius = new CornerRadius(0, DEFAULT_CORNER);
|
||||||
|
}
|
||||||
|
else if (CornerRadius.TopLeft != DEFAULT_CORNER ||
|
||||||
|
CornerRadius.BottomLeft != DEFAULT_CORNER)
|
||||||
|
{
|
||||||
|
CornerRadius = new CornerRadius(DEFAULT_CORNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = this;
|
||||||
|
|
||||||
|
if (!IsBranch && IsExpanded)
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
child.UpdateCornerRadius(ref prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool _isSelected = false;
|
||||||
|
private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER);
|
||||||
|
|
||||||
public class Builder
|
public class Builder
|
||||||
{
|
{
|
||||||
public List<BranchTreeNode> Locals => _locals;
|
public List<BranchTreeNode> Locals => _locals;
|
||||||
|
|
|
@ -14,75 +14,69 @@
|
||||||
Classes="bold"
|
Classes="bold"
|
||||||
Text="{DynamicResource Text.DeleteMultiBranch}" />
|
Text="{DynamicResource Text.DeleteMultiBranch}" />
|
||||||
|
|
||||||
<Grid Margin="0,16,8,0" RowDefinitions="Auto,Auto" ColumnDefinitions="120,*">
|
<Border Margin="4,16,0,0"
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
Background="{DynamicResource Brush.Contents}"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Top"
|
BorderThickness="1"
|
||||||
Text="{DynamicResource Text.DeleteMultiBranch.Targets}" />
|
BorderBrush="{DynamicResource Brush.Border1}"
|
||||||
<Border Grid.Row="0" Grid.Column="1"
|
CornerRadius="4"
|
||||||
Background="{DynamicResource Brush.Contents}"
|
Padding="4">
|
||||||
BorderThickness="1"
|
<DataGrid MaxHeight="200"
|
||||||
BorderBrush="{DynamicResource Brush.Border1}"
|
Background="Transparent"
|
||||||
CornerRadius="4"
|
BorderThickness="0"
|
||||||
Padding="4">
|
ItemsSource="{Binding Targets}"
|
||||||
<DataGrid MaxHeight="200"
|
SelectionMode="Single"
|
||||||
Background="Transparent"
|
CanUserReorderColumns="False"
|
||||||
BorderThickness="0"
|
CanUserResizeColumns="False"
|
||||||
ItemsSource="{Binding Targets}"
|
CanUserSortColumns="False"
|
||||||
SelectionMode="Single"
|
IsReadOnly="True"
|
||||||
CanUserReorderColumns="False"
|
HeadersVisibility="None"
|
||||||
CanUserResizeColumns="False"
|
Focusable="False"
|
||||||
CanUserSortColumns="False"
|
RowHeight="26"
|
||||||
IsReadOnly="True"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
HeadersVisibility="None"
|
VerticalScrollBarVisibility="Auto">
|
||||||
Focusable="False"
|
<DataGrid.Styles>
|
||||||
RowHeight="26"
|
<Style Selector="DataGridRow">
|
||||||
HorizontalScrollBarVisibility="Auto"
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
VerticalScrollBarVisibility="Auto">
|
</Style>
|
||||||
<DataGrid.Styles>
|
|
||||||
<Style Selector="DataGridRow">
|
|
||||||
<Setter Property="CornerRadius" Value="4" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="DataGridRow /template/ Border#RowBorder">
|
<Style Selector="DataGridRow /template/ Border#RowBorder">
|
||||||
<Setter Property="ClipToBounds" Value="True" />
|
<Setter Property="ClipToBounds" Value="True" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
|
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||||
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
|
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
|
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
|
||||||
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
|
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
|
||||||
</Style>
|
</Style>
|
||||||
</DataGrid.Styles>
|
</DataGrid.Styles>
|
||||||
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTemplateColumn Header="ICON">
|
<DataGridTemplateColumn Header="ICON">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Path Width="10" Height="10" Margin="4,0,8,0" Data="{StaticResource Icons.Branch}" />
|
<Path Width="10" Height="10" Margin="4,0,8,0" Data="{StaticResource Icons.Branch}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
<DataGridTemplateColumn Width="*" Header="NAME">
|
<DataGridTemplateColumn Width="*" Header="NAME">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" ClipToBounds="True"
|
<TextBlock Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" ClipToBounds="True"
|
||||||
Classes="monospace" />
|
Classes="monospace" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
<TextBlock Margin="4,8,0,0"
|
||||||
Margin="0,8,0,0"
|
Text="{DynamicResource Text.DeleteMultiBranch.Tip}"
|
||||||
Text="{DynamicResource Text.DeleteMultiBranch.Tip}"
|
TextWrapping="Wrap"
|
||||||
TextWrapping="Wrap"
|
Foreground="{DynamicResource Brush.FG2}"
|
||||||
Foreground="{DynamicResource Brush.FG2}"
|
FontStyle="Italic" />
|
||||||
FontStyle="Italic" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
@ -208,14 +208,8 @@
|
||||||
<TreeView.Styles>
|
<TreeView.Styles>
|
||||||
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
|
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
|
||||||
<Setter Property="CornerRadius">
|
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
|
||||||
<Setter.Value>
|
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
|
||||||
<MultiBinding Converter="{x:Static c:BranchTreeNodeConverters.ToCornerRadius}">
|
|
||||||
<Binding Path="$parent[v:Repository].RefereshLocalBranchSelectionToken"/>
|
|
||||||
<Binding Path="$self"/>
|
|
||||||
</MultiBinding>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
|
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
|
||||||
|
@ -278,14 +272,8 @@
|
||||||
<TreeView.Styles>
|
<TreeView.Styles>
|
||||||
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
|
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
|
||||||
<Setter Property="CornerRadius">
|
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
|
||||||
<Setter.Value>
|
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
|
||||||
<MultiBinding Converter="{x:Static c:BranchTreeNodeConverters.ToCornerRadius}">
|
|
||||||
<Binding Path="$parent[v:Repository].RefereshRemoteBranchSelectionToken"/>
|
|
||||||
<Binding Path="$self"/>
|
|
||||||
</MultiBinding>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
|
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
|
||||||
|
|
|
@ -58,24 +58,6 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
public partial class Repository : UserControl
|
public partial class Repository : UserControl
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<ulong> RefereshLocalBranchSelectionTokenProperty =
|
|
||||||
AvaloniaProperty.Register<Repository, ulong>(nameof(RefereshLocalBranchSelectionToken), 0);
|
|
||||||
|
|
||||||
public ulong RefereshLocalBranchSelectionToken
|
|
||||||
{
|
|
||||||
get => GetValue(RefereshLocalBranchSelectionTokenProperty);
|
|
||||||
set => SetValue(RefereshLocalBranchSelectionTokenProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<ulong> RefereshRemoteBranchSelectionTokenProperty =
|
|
||||||
AvaloniaProperty.Register<Repository, ulong>(nameof(RefereshRemoteBranchSelectionToken), 0);
|
|
||||||
|
|
||||||
public ulong RefereshRemoteBranchSelectionToken
|
|
||||||
{
|
|
||||||
get => GetValue(RefereshRemoteBranchSelectionTokenProperty);
|
|
||||||
set => SetValue(RefereshRemoteBranchSelectionTokenProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repository()
|
public Repository()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
@ -93,18 +75,19 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is TreeView tree && tree.SelectedItem != null)
|
if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo)
|
||||||
{
|
{
|
||||||
remoteBranchTree.UnselectAll();
|
remoteBranchTree.UnselectAll();
|
||||||
tagsList.SelectedItem = null;
|
tagsList.SelectedItem = null;
|
||||||
|
|
||||||
var next = RefereshLocalBranchSelectionToken + 1;
|
ViewModels.BranchTreeNode prev = null;
|
||||||
SetCurrentValue(RefereshLocalBranchSelectionTokenProperty, next);
|
foreach (var node in repo.LocalBranchTrees)
|
||||||
|
node.UpdateCornerRadius(ref prev);
|
||||||
|
|
||||||
if (tree.SelectedItems.Count == 1)
|
if (tree.SelectedItems.Count == 1)
|
||||||
{
|
{
|
||||||
var node = tree.SelectedItem as ViewModels.BranchTreeNode;
|
var node = tree.SelectedItem as ViewModels.BranchTreeNode;
|
||||||
if (node.IsBranch && DataContext is ViewModels.Repository repo)
|
if (node.IsBranch)
|
||||||
repo.NavigateToCommit((node.Backend as Models.Branch).Head);
|
repo.NavigateToCommit((node.Backend as Models.Branch).Head);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,18 +95,19 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is TreeView tree && tree.SelectedItem != null)
|
if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo)
|
||||||
{
|
{
|
||||||
localBranchTree.UnselectAll();
|
localBranchTree.UnselectAll();
|
||||||
tagsList.SelectedItem = null;
|
tagsList.SelectedItem = null;
|
||||||
|
|
||||||
var next = RefereshRemoteBranchSelectionToken + 1;
|
ViewModels.BranchTreeNode prev = null;
|
||||||
SetCurrentValue(RefereshRemoteBranchSelectionTokenProperty, next);
|
foreach (var node in repo.RemoteBranchTrees)
|
||||||
|
node.UpdateCornerRadius(ref prev);
|
||||||
|
|
||||||
if (tree.SelectedItems.Count == 1)
|
if (tree.SelectedItems.Count == 1)
|
||||||
{
|
{
|
||||||
var node = tree.SelectedItem as ViewModels.BranchTreeNode;
|
var node = tree.SelectedItem as ViewModels.BranchTreeNode;
|
||||||
if (node.IsBranch && DataContext is ViewModels.Repository repo)
|
if (node.IsBranch)
|
||||||
repo.NavigateToCommit((node.Backend as Models.Branch).Head);
|
repo.NavigateToCommit((node.Backend as Models.Branch).Head);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,6 +188,11 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
var repo = DataContext as ViewModels.Repository;
|
var repo = DataContext as ViewModels.Repository;
|
||||||
var tree = sender as TreeView;
|
var tree = sender as TreeView;
|
||||||
|
if (tree.SelectedItems.Count == 0)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var branches = new List<Models.Branch>();
|
var branches = new List<Models.Branch>();
|
||||||
foreach (var item in tree.SelectedItems)
|
foreach (var item in tree.SelectedItems)
|
||||||
|
@ -243,6 +232,11 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
var repo = DataContext as ViewModels.Repository;
|
var repo = DataContext as ViewModels.Repository;
|
||||||
var tree = sender as TreeView;
|
var tree = sender as TreeView;
|
||||||
|
if (tree.SelectedItems.Count == 0)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tree.SelectedItems.Count == 1)
|
if (tree.SelectedItems.Count == 1)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue