mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 03:48:56 +00:00
Merge branch 'feature/workshop' into development
This commit is contained in:
commit
3030312df9
108 files changed
+3309
-365
No files matched your search
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pagination control that can be used to switch between pages.
|
||||
/// </summary>
|
||||
[TemplatePart("PART_PreviousButton", typeof(Button))]
|
||||
[TemplatePart("PART_NextButton", typeof(Button))]
|
||||
[TemplatePart("PART_PagesView", typeof(StackPanel))]
|
||||
public partial class Pagination : TemplatedControl
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Pagination()
|
||||
{
|
||||
PropertyChanged += OnPropertyChanged;
|
||||
}
|
||||
|
||||
public Button? PreviousButton { get; set; }
|
||||
public Button? NextButton { get; set; }
|
||||
public StackPanel? PagesView { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
if (PreviousButton != null)
|
||||
PreviousButton.Click -= PreviousButtonOnClick;
|
||||
if (NextButton != null)
|
||||
NextButton.Click -= NextButtonOnClick;
|
||||
|
||||
PreviousButton = e.NameScope.Find<Button>("PART_PreviousButton");
|
||||
NextButton = e.NameScope.Find<Button>("PART_NextButton");
|
||||
PagesView = e.NameScope.Find<StackPanel>("PART_PagesView");
|
||||
|
||||
if (PreviousButton != null)
|
||||
PreviousButton.Click += PreviousButtonOnClick;
|
||||
if (NextButton != null)
|
||||
NextButton.Click += NextButtonOnClick;
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property == ValueProperty || e.Property == MaximumProperty)
|
||||
Update();
|
||||
}
|
||||
|
||||
private void NextButtonOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Value < Maximum)
|
||||
Value++;
|
||||
}
|
||||
|
||||
private void PreviousButtonOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Value > 1)
|
||||
Value--;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (PagesView == null)
|
||||
return;
|
||||
|
||||
List<int> pages = GetPages(Value, Maximum);
|
||||
|
||||
// Remove extra children
|
||||
while (PagesView.Children.Count > pages.Count)
|
||||
{
|
||||
PagesView.Children.RemoveAt(PagesView.Children.Count - 1);
|
||||
}
|
||||
|
||||
if (PagesView.Children.Count > pages.Count)
|
||||
PagesView.Children.RemoveRange(0, PagesView.Children.Count - pages.Count);
|
||||
|
||||
// Add/modify children
|
||||
for (int i = 0; i < pages.Count; i++)
|
||||
{
|
||||
int page = pages[i];
|
||||
|
||||
// -1 indicates an ellipsis (...)
|
||||
if (page == -1)
|
||||
{
|
||||
if (PagesView.Children.ElementAtOrDefault(i) is not PaginationEllipsis)
|
||||
{
|
||||
if (PagesView.Children.Count - 1 >= i)
|
||||
PagesView.Children[i] = new PaginationEllipsis();
|
||||
else
|
||||
PagesView.Children.Add(new PaginationEllipsis());
|
||||
}
|
||||
}
|
||||
// Anything else indicates a regular page
|
||||
else
|
||||
{
|
||||
if (PagesView.Children.ElementAtOrDefault(i) is PaginationPage paginationPage)
|
||||
{
|
||||
paginationPage.Page = page;
|
||||
paginationPage.Command = ReactiveCommand.Create(() => Value = page);
|
||||
continue;
|
||||
}
|
||||
|
||||
paginationPage = new PaginationPage {Page = page, Command = ReactiveCommand.Create(() => Value = page)};
|
||||
if (PagesView.Children.Count - 1 >= i)
|
||||
PagesView.Children[i] = paginationPage;
|
||||
else
|
||||
PagesView.Children.Add(paginationPage);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Control child in PagesView.Children)
|
||||
{
|
||||
if (child is PaginationPage paginationPage)
|
||||
((IPseudoClasses) paginationPage.Classes).Set(":selected", paginationPage.Page == Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void PaginationPageOnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (sender is PaginationPage paginationPage)
|
||||
Value = paginationPage.Page;
|
||||
}
|
||||
|
||||
private static List<int> GetPages(int currentPage, int pageCount)
|
||||
{
|
||||
// Determine the delta based on how close to the edge the current page is
|
||||
int delta;
|
||||
if (pageCount <= 7)
|
||||
delta = 7;
|
||||
else
|
||||
delta = currentPage > 4 && currentPage < pageCount - 3 ? 2 : 4;
|
||||
|
||||
int start = currentPage - delta / 2;
|
||||
int end = currentPage + delta / 2;
|
||||
|
||||
if (start - 1 == 1 || end + 1 == pageCount)
|
||||
{
|
||||
start += 1;
|
||||
end += 1;
|
||||
}
|
||||
|
||||
// Determine start and end numbers based on how close to the edge the current page is
|
||||
start = currentPage > delta ? Math.Min(start, pageCount - delta) : 1;
|
||||
end = currentPage > delta ? Math.Min(end, pageCount) : Math.Min(pageCount, delta + 1);
|
||||
|
||||
// Start with the pages neighbouring the current page
|
||||
List<int> paginationItems = Enumerable.Range(start, end - start + 1).ToList();
|
||||
|
||||
// If not starting at the first page, add the first page and an ellipsis (-1)
|
||||
if (paginationItems.First() != 1)
|
||||
{
|
||||
paginationItems.Insert(0, 1);
|
||||
paginationItems.Insert(1, -1);
|
||||
}
|
||||
|
||||
// If not ending at the last page, add an ellipsis (-1) and the last page
|
||||
if (paginationItems.Last() < pageCount)
|
||||
{
|
||||
paginationItems.Add(-1);
|
||||
paginationItems.Add(pageCount);
|
||||
}
|
||||
|
||||
return paginationItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace Artemis.UI.Shared.Pagination;
|
||||
|
||||
public partial class Pagination : TemplatedControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Value" /> property
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> ValueProperty =
|
||||
AvaloniaProperty.Register<Pagination, int>(nameof(Value), 1, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Maximum" /> property
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> MaximumProperty =
|
||||
AvaloniaProperty.Register<Pagination, int>(nameof(Maximum), 10, coerce: (_, v) => Math.Max(1, v));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric value of a NumberBox.
|
||||
/// </summary>
|
||||
public int Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numerical maximum for Value.
|
||||
/// </summary>
|
||||
public int Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace Artemis.UI.Shared.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pagination ellipsis control that indicates a gap in pagination.
|
||||
/// </summary>
|
||||
public class PaginationEllipsis : TemplatedControl
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace Artemis.UI.Shared.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pagination page control that indicates a page in pagination.
|
||||
/// </summary>
|
||||
public class PaginationPage : TemplatedControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Page" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> PageProperty = AvaloniaProperty.Register<PaginationPage, int>(nameof(Page));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the page that is being represented.
|
||||
/// </summary>
|
||||
public int Page
|
||||
{
|
||||
get => GetValue(PageProperty);
|
||||
set => SetValue(PageProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Command" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<PaginationPage, ICommand>(nameof(Command));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the command to invoke when the page is clicked.
|
||||
/// </summary>
|
||||
public ICommand Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination"
|
||||
x:CompileBindings="True">
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="30" Width="400">
|
||||
<StackPanel Spacing="20">
|
||||
<pagination:Pagination Value="{CompiledBinding Value, ElementName=Numeric, Mode=TwoWay}" HorizontalAlignment="Center"/>
|
||||
<pagination:Pagination Value="{CompiledBinding Value, ElementName=Numeric, Mode=TwoWay}" Maximum="999" HorizontalAlignment="Center"/>
|
||||
<NumericUpDown Name="Numeric" Value="1" Width="120" HorizontalAlignment="Center"></NumericUpDown>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<ControlTheme x:Key="{x:Type pagination:Pagination}" TargetType="pagination:Pagination">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<Button Name="PART_PreviousButton" Theme="{StaticResource TransparentButton}" Width="32" Height="30">
|
||||
<ui:SymbolIcon Symbol="ChevronLeft" />
|
||||
</Button>
|
||||
<StackPanel Name="PART_PagesView" Orientation="Horizontal" Spacing="2"></StackPanel>
|
||||
<Button Name="PART_NextButton" Theme="{StaticResource TransparentButton}" Width="32" Height="30">
|
||||
<ui:SymbolIcon Symbol="ChevronRight" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type pagination:PaginationPage}" TargetType="pagination:PaginationPage">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel>
|
||||
<Button Theme="{StaticResource TransparentButton}"
|
||||
MinWidth="32"
|
||||
Padding="6 5"
|
||||
Content="{TemplateBinding Page}"
|
||||
Command="{TemplateBinding Command}"/>
|
||||
<Rectangle Name="SelectionIndicator"
|
||||
Width="16"
|
||||
Height="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
RadiusX="2"
|
||||
RadiusY="2"
|
||||
IsVisible="False"
|
||||
RenderTransform="scaleX(0)"
|
||||
Fill="{DynamicResource TreeViewItemSelectionIndicatorForeground}">
|
||||
<Rectangle.Transitions>
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Duration="00:00:00.167"
|
||||
Property="RenderTransform"
|
||||
Easing="0,0 0,1" />
|
||||
</Transitions>
|
||||
</Rectangle.Transitions>
|
||||
</Rectangle>
|
||||
</Panel>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:selected">
|
||||
<Style Selector="^ /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="RenderTransform" Value="scaleX(1)" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
|
||||
<ControlTheme x:Key="{x:Type pagination:PaginationEllipsis}" TargetType="pagination:PaginationEllipsis">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<TextBlock VerticalAlignment="Bottom" Margin="11 5">...</TextBlock>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@@ -26,6 +27,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
||||
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
|
||||
.Register<ReactiveAppWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
|
||||
|
||||
private bool _micaEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}" /> class.
|
||||
/// </summary>
|
||||
@@ -33,23 +36,33 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
||||
{
|
||||
// This WhenActivated block calls ViewModel's WhenActivated
|
||||
// block if the ViewModel implements IActivatableViewModel.
|
||||
this.WhenActivated(disposables => { });
|
||||
this.WhenActivated(disposables => UI.MicaEnabled.Subscribe(ToggleMica).DisposeWith(disposables));
|
||||
this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged);
|
||||
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnOpened(EventArgs e)
|
||||
private void ToggleMica(bool enable)
|
||||
{
|
||||
// TODO: Move to a style and remove opacity on focus loss
|
||||
base.OnOpened(e);
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
|
||||
if (enable == _micaEnabled)
|
||||
return;
|
||||
|
||||
TransparencyBackgroundFallback = Brushes.Transparent;
|
||||
TransparencyLevelHint = new[] {WindowTransparencyLevel.Mica};
|
||||
TryEnableMicaEffect();
|
||||
if (enable)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
|
||||
return;
|
||||
|
||||
// TransparencyBackgroundFallback = Brushes.Transparent;
|
||||
TransparencyLevelHint = new[] {WindowTransparencyLevel.Mica};
|
||||
Background = new SolidColorBrush(new Color(80, 0,0,0));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearValue(TransparencyLevelHintProperty);
|
||||
ClearValue(BackgroundProperty);
|
||||
}
|
||||
|
||||
_micaEnabled = enable;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? value)
|
||||
@@ -67,32 +80,6 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
||||
else if (DataContext != value) DataContext = value;
|
||||
}
|
||||
|
||||
private void TryEnableMicaEffect()
|
||||
{
|
||||
// The background colors for the Mica brush are still based around SolidBackgroundFillColorBase resource
|
||||
// BUT since we can't control the actual Mica brush color, we have to use the window background to create
|
||||
// the same effect. However, we can't use SolidBackgroundFillColorBase directly since its opaque, and if
|
||||
// we set the opacity the color become lighter than we want. So we take the normal color, darken it and
|
||||
// apply the opacity until we get the roughly the correct color
|
||||
// NOTE that the effect still doesn't look right, but it suffices. Ideally we need access to the Mica
|
||||
// CompositionBrush to properly change the color but I don't know if we can do that or not
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Dark, out object? value) ? (Color) value : new Color2(32, 32, 32);
|
||||
color = color.LightenPercent(-0.5f);
|
||||
|
||||
Background = new ImmutableSolidColorBrush(color, 0.78);
|
||||
}
|
||||
else if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
// Similar effect here
|
||||
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Light, out object? value) ? (Color) value : new Color2(243, 243, 243);
|
||||
color = color.LightenPercent(0.5f);
|
||||
|
||||
Background = new ImmutableSolidColorBrush(color, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ViewModel.
|
||||
/// </summary>
|
||||
|
||||
@@ -62,7 +62,15 @@ public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRouta
|
||||
|
||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
||||
{
|
||||
Screen = screen as TScreen;
|
||||
if (screen == null)
|
||||
{
|
||||
Screen = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen is not TScreen typedScreen)
|
||||
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
|
||||
Screen = typedScreen;
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
@@ -72,7 +73,15 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
|
||||
|
||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
||||
{
|
||||
Screen = screen as TScreen;
|
||||
if (screen == null)
|
||||
{
|
||||
Screen = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen is not TScreen typedScreen)
|
||||
throw new ArtemisRoutingException($"Provided screen is not assignable to {typeof(TScreen).FullName}");
|
||||
Screen = typedScreen;
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a registration for a route.
|
||||
/// </summary>
|
||||
public interface IRouterRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the route associated with this registration.
|
||||
/// </summary>
|
||||
Route Route { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the view model associated with the route.
|
||||
/// </summary>
|
||||
Type ViewModel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the child registrations of this route.
|
||||
/// </summary>
|
||||
List<IRouterRegistration> Children { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a contract for parsing route parameters.
|
||||
/// </summary>
|
||||
public interface IRouteParameterParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the given segment matches the provided source.
|
||||
/// </summary>
|
||||
/// <param name="segment">The route segment to match.</param>
|
||||
/// <param name="source">The source value to match against the route segment.</param>
|
||||
/// <returns><see langword="true"/> if the segment matches the source; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsMatch(RouteSegment segment, string source);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value from the provided source value.
|
||||
/// </summary>
|
||||
/// <param name="segment">The route segment containing the parameter information.</param>
|
||||
/// <param name="source">The source value from which to extract the parameter value.</param>
|
||||
/// <returns>The extracted parameter value.</returns>
|
||||
object GetValue(RouteSegment segment, string source);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
internal class IntParameterParser : IRouteParameterParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsMatch(RouteSegment segment, string source)
|
||||
{
|
||||
return int.TryParse(source, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object GetValue(RouteSegment segment, string source)
|
||||
{
|
||||
return int.Parse(source);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,29 @@ using System.Linq;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a route at a certain path.
|
||||
/// </summary>
|
||||
public class Route
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Route" /> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the route.</param>
|
||||
public Route(string path)
|
||||
{
|
||||
Path = path;
|
||||
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the route.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of segments that makes up the path.
|
||||
/// </summary>
|
||||
public List<RouteSegment> Segments { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a registration for a route and its associated view model.
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam>
|
||||
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the route.</param>
|
||||
public RouteRegistration(string path)
|
||||
{
|
||||
Route = new Route(path);
|
||||
@@ -18,6 +24,9 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
|
||||
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route associated with this registration.
|
||||
/// </summary>
|
||||
public Route Route { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -25,12 +34,4 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<IRouterRegistration> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
public interface IRouterRegistration
|
||||
{
|
||||
Route Route { get; }
|
||||
Type ViewModel { get; }
|
||||
List<IRouterRegistration> Children { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DryIoc;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
@@ -4,10 +4,17 @@ using Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a segment of a route.
|
||||
/// </summary>
|
||||
public partial class RouteSegment
|
||||
{
|
||||
private readonly IRouteParameterParser? _parameterParser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segment">The segment value.</param>
|
||||
public RouteSegment(string segment)
|
||||
{
|
||||
Segment = segment;
|
||||
@@ -21,10 +28,26 @@ public partial class RouteSegment
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment value.
|
||||
/// </summary>
|
||||
public string Segment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter name if the segment is a parameterized segment; otherwise <see langword="null"/>.
|
||||
/// </summary>
|
||||
public string? Parameter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the parameter if the segment is a parameterized segment; otherwise <see langword="null"/>.
|
||||
/// </summary>
|
||||
public string? ParameterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the segment matches the provided value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to compare with the segment.</param>
|
||||
/// <returns><see langword="true"/> if the segment matches the value; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsMatch(string value)
|
||||
{
|
||||
if (_parameterParser == null)
|
||||
@@ -32,6 +55,11 @@ public partial class RouteSegment
|
||||
return _parameterParser.IsMatch(this, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value from the provided value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value from which to extract the parameter value.</param>
|
||||
/// <returns>The extracted parameter value.</returns>
|
||||
public object? GetParameter(string value)
|
||||
{
|
||||
if (_parameterParser == null)
|
||||
@@ -48,13 +76,20 @@ public partial class RouteSegment
|
||||
|
||||
private IRouteParameterParser GetParameterParser(string parameterType)
|
||||
{
|
||||
if (parameterType == "guid")
|
||||
return new GuidParameterParser();
|
||||
return parameterType switch
|
||||
{
|
||||
"guid" => new GuidParameterParser(),
|
||||
"int" => new IntParameterParser(),
|
||||
_ => new StringParameterParser()
|
||||
};
|
||||
|
||||
// Default to a string parser which just returns the segment as is
|
||||
return new StringParameterParser();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the regular expression used to identify parameterized segments in the route.
|
||||
/// </summary>
|
||||
/// <returns>The regular expression pattern.</returns>
|
||||
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
|
||||
private static partial Regex ParameterRegex();
|
||||
}
|
||||
@@ -70,7 +70,18 @@ internal class Navigation
|
||||
|
||||
// Only change the screen if it wasn't reused
|
||||
if (!ReferenceEquals(host.InternalScreen, screen))
|
||||
host.InternalChangeScreen(screen);
|
||||
{
|
||||
try
|
||||
{
|
||||
host.InternalChangeScreen(screen);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Cancel();
|
||||
if (e is not TaskCanceledException)
|
||||
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
|
||||
}
|
||||
}
|
||||
|
||||
if (CancelIfRequested(args, "ChangeScreen", screen))
|
||||
return;
|
||||
@@ -86,15 +97,24 @@ internal class Navigation
|
||||
catch (Exception e)
|
||||
{
|
||||
Cancel();
|
||||
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
|
||||
if (e is not TaskCanceledException)
|
||||
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
|
||||
}
|
||||
|
||||
if (CancelIfRequested(args, "OnNavigating", screen))
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution.Child != null && screen is IRoutableScreen childScreen)
|
||||
await NavigateResolution(resolution.Child, args, childScreen);
|
||||
if (screen is IRoutableScreen childScreen)
|
||||
{
|
||||
// Navigate the child too
|
||||
if (resolution.Child != null)
|
||||
await NavigateResolution(resolution.Child, args, childScreen);
|
||||
// Make sure there is no child
|
||||
else if (childScreen.InternalScreen != null)
|
||||
childScreen.InternalChangeScreen(null);
|
||||
}
|
||||
|
||||
|
||||
Completed = true;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Styles.Resources>
|
||||
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
|
||||
<VisualBrush.Visual>
|
||||
<Canvas Width="12" Height="12">
|
||||
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Left="6" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Top="6" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
|
||||
</Canvas>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
<ResourceDictionary>
|
||||
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
|
||||
<VisualBrush.Visual>
|
||||
<Canvas Width="12" Height="12">
|
||||
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Left="6" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Top="6" />
|
||||
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
|
||||
</Canvas>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<MergeResourceInclude Source="/Controls/Pagination/PaginationStyles.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
|
||||
|
||||
<!-- Custom controls -->
|
||||
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
|
||||
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<gradientPicker:GradientPickerColorStop ColorStop="{CompiledBinding}"
|
||||
PositionReference="{CompiledBinding $parent[Border]}"
|
||||
Classes="gradient-handle"
|
||||
GradientPicker="{CompiledBinding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
|
||||
GradientPicker="{CompiledBinding $parent[gradientPicker:GradientPicker]}">
|
||||
</gradientPicker:GradientPickerColorStop>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -207,7 +207,7 @@
|
||||
<Button Name="DeleteButton"
|
||||
Grid.Column="3"
|
||||
Classes="icon-button"
|
||||
Command="{CompiledBinding DeleteStop, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}"
|
||||
Command="{CompiledBinding $parent[gradientPicker:GradientPicker].DeleteStop}"
|
||||
CommandParameter="{CompiledBinding}">
|
||||
<avalonia:MaterialIcon Kind="Close" />
|
||||
</Button>
|
||||
|
||||
@@ -13,9 +13,15 @@
|
||||
|
||||
<!-- Add Styles Here -->
|
||||
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
|
||||
<Setter Property="MinHeight" Value="35" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="{DynamicResource NavigationViewItemOnLeftMinHeight}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.sidebar-listbox ContentPresenter">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<!-- <Style Selector="ListBox.sidebar-listbox ContentPresenter"> -->
|
||||
<!-- <Setter Property="Margin" Value="0" /> -->
|
||||
<!-- </Style> -->
|
||||
|
||||
<Style Selector="ListBox.sidebar-listbox ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="MinHeight" Value="{DynamicResource NavigationViewItemOnLeftMinHeight}" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -10,6 +10,15 @@
|
||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||
<TextBlock Classes="subtitle">This is a subtitle</TextBlock>
|
||||
<TextBlock>
|
||||
<Run Classes="h1">This is heading 1</Run>
|
||||
<Run Classes="h2">This is heading 2</Run>
|
||||
<Run Classes="h3">This is heading 3</Run>
|
||||
<Run Classes="h4">This is heading 4</Run>
|
||||
<Run Classes="h5">This is heading 5</Run>
|
||||
<Run Classes="h6">This is heading 6</Run>
|
||||
<Run Classes="subtitle">This is a subtitle</Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
@@ -50,4 +59,26 @@
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="Margin" Value="0 25 0 5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Run.h1">
|
||||
<Setter Property="FontSize" Value="64" />
|
||||
</Style>
|
||||
<Style Selector="Run.h2">
|
||||
<Setter Property="FontSize" Value="48" />
|
||||
</Style>
|
||||
<Style Selector="Run.h3">
|
||||
<Setter Property="FontSize" Value="32" />
|
||||
</Style>
|
||||
<Style Selector="Run.h4">
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
</Style>
|
||||
<Style Selector="Run.h5">
|
||||
<Setter Property="FontSize" Value="18" />
|
||||
</Style>
|
||||
<Style Selector="Run.h6">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
</Style>
|
||||
<Style Selector="Run.subtitle">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -1,29 +1,29 @@
|
||||
using System;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using IContainer = DryIoc.IContainer;
|
||||
using Avalonia.Threading;
|
||||
using DryIoc;
|
||||
|
||||
namespace Artemis.UI.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Static UI helpers.
|
||||
/// Static UI helpers.
|
||||
/// </summary>
|
||||
public static class UI
|
||||
{
|
||||
private static readonly BehaviorSubject<bool> MicaEnabledSubject = new(false);
|
||||
|
||||
public static EventLoopScheduler BackgroundScheduler = new(ts => new Thread(ts));
|
||||
|
||||
static UI()
|
||||
{
|
||||
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
|
||||
}
|
||||
|
||||
public static EventLoopScheduler BackgroundScheduler = new EventLoopScheduler(ts => new Thread(ts));
|
||||
|
||||
internal static void ClearCache()
|
||||
{
|
||||
DeviceVisualizer.BitmapCache.Clear();
|
||||
MicaEnabled = MicaEnabledSubject.AsObservable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,4 +40,24 @@ public static class UI
|
||||
/// Gets a boolean indicating whether hotkeys are to be disabled.
|
||||
/// </summary>
|
||||
public static IObservable<bool> KeyBindingsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the Mica effect should be enabled.
|
||||
/// </summary>
|
||||
public static IObservable<bool> MicaEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes whether Mica should be enabled.
|
||||
/// </summary>
|
||||
/// <param name="enabled"></param>
|
||||
public static void SetMicaEnabled(bool enabled)
|
||||
{
|
||||
if (MicaEnabledSubject.Value != enabled)
|
||||
Dispatcher.UIThread.Invoke(() => MicaEnabledSubject.OnNext(enabled));
|
||||
}
|
||||
|
||||
internal static void ClearCache()
|
||||
{
|
||||
DeviceVisualizer.BitmapCache.Clear();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user