mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
3d0916b17c
@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Linux.DryIoc;
|
using Artemis.UI.Linux.DryIoc;
|
||||||
using Artemis.UI.Linux.Providers.Input;
|
using Artemis.UI.Linux.Providers.Input;
|
||||||
@ -26,15 +27,11 @@ public class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
if (Design.IsDesignMode)
|
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ArtemisBootstrapper.Initialize();
|
|
||||||
|
|
||||||
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args);
|
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args ?? Array.Empty<string>());
|
||||||
|
ArtemisBootstrapper.Initialize();
|
||||||
RegisterProviders();
|
RegisterProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
174
src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs
Normal file
174
src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
39
src/Artemis.UI.Shared/Controls/Pagination/PaginationPage.cs
Normal file
39
src/Artemis.UI.Shared/Controls/Pagination/PaginationPage.cs
Normal file
@ -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>
|
||||||
@ -50,8 +50,8 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
Stream? stream = ConfigurationIcon.GetIconStream();
|
Stream? stream = ConfigurationIcon?.GetIconStream();
|
||||||
if (stream == null)
|
if (stream == null || ConfigurationIcon == null)
|
||||||
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
|
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
|
||||||
else
|
else
|
||||||
LoadFromBitmap(ConfigurationIcon, stream);
|
LoadFromBitmap(ConfigurationIcon, stream);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
@ -26,6 +27,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
|||||||
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
|
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
|
||||||
.Register<ReactiveAppWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
|
.Register<ReactiveAppWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
|
||||||
|
|
||||||
|
private bool _micaEnabled;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}" /> class.
|
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -33,23 +36,33 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
|||||||
{
|
{
|
||||||
// This WhenActivated block calls ViewModel's WhenActivated
|
// This WhenActivated block calls ViewModel's WhenActivated
|
||||||
// block if the ViewModel implements IActivatableViewModel.
|
// 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(DataContextProperty).Subscribe(OnDataContextChanged);
|
||||||
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
|
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private void ToggleMica(bool enable)
|
||||||
protected override void OnOpened(EventArgs e)
|
|
||||||
{
|
{
|
||||||
// TODO: Move to a style and remove opacity on focus loss
|
if (enable == _micaEnabled)
|
||||||
base.OnOpened(e);
|
|
||||||
|
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TransparencyBackgroundFallback = Brushes.Transparent;
|
if (enable)
|
||||||
TransparencyLevelHint = new[] {WindowTransparencyLevel.Mica};
|
{
|
||||||
TryEnableMicaEffect();
|
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)
|
private void OnDataContextChanged(object? value)
|
||||||
@ -67,32 +80,6 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel> whe
|
|||||||
else if (DataContext != value) DataContext = value;
|
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>
|
/// <summary>
|
||||||
/// The ViewModel.
|
/// The ViewModel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -62,7 +62,15 @@ public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRouta
|
|||||||
|
|
||||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
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)
|
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
@ -72,7 +73,15 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
|
|||||||
|
|
||||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
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)
|
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
|||||||
25
src/Artemis.UI.Shared/Routing/Route/IRouterRegistration.cs
Normal file
25
src/Artemis.UI.Shared/Routing/Route/IRouterRegistration.cs
Normal file
@ -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;
|
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a contract for parsing route parameters.
|
||||||
|
/// </summary>
|
||||||
public interface IRouteParameterParser
|
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);
|
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);
|
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;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a route at a certain path.
|
||||||
|
/// </summary>
|
||||||
public class Route
|
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)
|
public Route(string path)
|
||||||
{
|
{
|
||||||
Path = path;
|
Path = path;
|
||||||
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
|
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the path of the route.
|
||||||
|
/// </summary>
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of segments that makes up the path.
|
||||||
|
/// </summary>
|
||||||
public List<RouteSegment> Segments { get; }
|
public List<RouteSegment> Segments { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
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
|
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)
|
public RouteRegistration(string path)
|
||||||
{
|
{
|
||||||
Route = new Route(path);
|
Route = new Route(path);
|
||||||
@ -18,6 +24,9 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
|
|||||||
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
|
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the route associated with this registration.
|
||||||
|
/// </summary>
|
||||||
public Route Route { get; }
|
public Route Route { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -25,12 +34,4 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<IRouterRegistration> Children { get; set; } = new();
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using DryIoc;
|
using DryIoc;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|||||||
@ -4,10 +4,17 @@ using Artemis.UI.Shared.Routing.ParameterParsers;
|
|||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a segment of a route.
|
||||||
|
/// </summary>
|
||||||
public partial class RouteSegment
|
public partial class RouteSegment
|
||||||
{
|
{
|
||||||
private readonly IRouteParameterParser? _parameterParser;
|
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)
|
public RouteSegment(string segment)
|
||||||
{
|
{
|
||||||
Segment = segment;
|
Segment = segment;
|
||||||
@ -21,10 +28,26 @@ public partial class RouteSegment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the segment value.
|
||||||
|
/// </summary>
|
||||||
public string Segment { get; }
|
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; }
|
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; }
|
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)
|
public bool IsMatch(string value)
|
||||||
{
|
{
|
||||||
if (_parameterParser == null)
|
if (_parameterParser == null)
|
||||||
@ -32,6 +55,11 @@ public partial class RouteSegment
|
|||||||
return _parameterParser.IsMatch(this, value);
|
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)
|
public object? GetParameter(string value)
|
||||||
{
|
{
|
||||||
if (_parameterParser == null)
|
if (_parameterParser == null)
|
||||||
@ -48,13 +76,20 @@ public partial class RouteSegment
|
|||||||
|
|
||||||
private IRouteParameterParser GetParameterParser(string parameterType)
|
private IRouteParameterParser GetParameterParser(string parameterType)
|
||||||
{
|
{
|
||||||
if (parameterType == "guid")
|
return parameterType switch
|
||||||
return new GuidParameterParser();
|
{
|
||||||
|
"guid" => new GuidParameterParser(),
|
||||||
|
"int" => new IntParameterParser(),
|
||||||
|
_ => new StringParameterParser()
|
||||||
|
};
|
||||||
|
|
||||||
// Default to a string parser which just returns the segment as is
|
// 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+)\}")]
|
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
|
||||||
private static partial Regex ParameterRegex();
|
private static partial Regex ParameterRegex();
|
||||||
}
|
}
|
||||||
@ -70,7 +70,18 @@ internal class Navigation
|
|||||||
|
|
||||||
// Only change the screen if it wasn't reused
|
// Only change the screen if it wasn't reused
|
||||||
if (!ReferenceEquals(host.InternalScreen, screen))
|
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))
|
if (CancelIfRequested(args, "ChangeScreen", screen))
|
||||||
return;
|
return;
|
||||||
@ -86,15 +97,24 @@ internal class Navigation
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Cancel();
|
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))
|
if (CancelIfRequested(args, "OnNavigating", screen))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolution.Child != null && screen is IRoutableScreen childScreen)
|
if (screen is IRoutableScreen childScreen)
|
||||||
await NavigateResolution(resolution.Child, args, 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;
|
Completed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<Styles.Resources>
|
<Styles.Resources>
|
||||||
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
|
<ResourceDictionary>
|
||||||
<VisualBrush.Visual>
|
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,12,12">
|
||||||
<Canvas Width="12" Height="12">
|
<VisualBrush.Visual>
|
||||||
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
|
<Canvas Width="12" Height="12">
|
||||||
<Rectangle Width="6" Height="6" Canvas.Left="6" />
|
<Rectangle Width="6" Height="6" Fill="Black" Opacity="0.15" />
|
||||||
<Rectangle Width="6" Height="6" Canvas.Top="6" />
|
<Rectangle Width="6" Height="6" Canvas.Left="6" />
|
||||||
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
|
<Rectangle Width="6" Height="6" Canvas.Top="6" />
|
||||||
</Canvas>
|
<Rectangle Width="6" Height="6" Canvas.Left="6" Canvas.Top="6" Fill="Black" Opacity="0.15" />
|
||||||
</VisualBrush.Visual>
|
</Canvas>
|
||||||
</VisualBrush>
|
</VisualBrush.Visual>
|
||||||
|
</VisualBrush>
|
||||||
|
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<MergeResourceInclude Source="/Controls/Pagination/PaginationStyles.axaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
|
|
||||||
|
|
||||||
<!-- Custom controls -->
|
<!-- Custom controls -->
|
||||||
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
|
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
|
||||||
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
|
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
|
||||||
|
|||||||
@ -106,7 +106,7 @@
|
|||||||
<gradientPicker:GradientPickerColorStop ColorStop="{CompiledBinding}"
|
<gradientPicker:GradientPickerColorStop ColorStop="{CompiledBinding}"
|
||||||
PositionReference="{CompiledBinding $parent[Border]}"
|
PositionReference="{CompiledBinding $parent[Border]}"
|
||||||
Classes="gradient-handle"
|
Classes="gradient-handle"
|
||||||
GradientPicker="{CompiledBinding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
|
GradientPicker="{CompiledBinding $parent[gradientPicker:GradientPicker]}">
|
||||||
</gradientPicker:GradientPickerColorStop>
|
</gradientPicker:GradientPickerColorStop>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
@ -207,7 +207,7 @@
|
|||||||
<Button Name="DeleteButton"
|
<Button Name="DeleteButton"
|
||||||
Grid.Column="3"
|
Grid.Column="3"
|
||||||
Classes="icon-button"
|
Classes="icon-button"
|
||||||
Command="{CompiledBinding DeleteStop, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}"
|
Command="{CompiledBinding $parent[gradientPicker:GradientPicker].DeleteStop}"
|
||||||
CommandParameter="{CompiledBinding}">
|
CommandParameter="{CompiledBinding}">
|
||||||
<avalonia:MaterialIcon Kind="Close" />
|
<avalonia:MaterialIcon Kind="Close" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -13,9 +13,15 @@
|
|||||||
|
|
||||||
<!-- Add Styles Here -->
|
<!-- Add Styles Here -->
|
||||||
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
|
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
|
||||||
<Setter Property="MinHeight" Value="35" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="MinHeight" Value="{DynamicResource NavigationViewItemOnLeftMinHeight}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ListBox.sidebar-listbox ContentPresenter">
|
<!-- <Style Selector="ListBox.sidebar-listbox ContentPresenter"> -->
|
||||||
<Setter Property="Margin" Value="0" />
|
<!-- <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>
|
</Style>
|
||||||
</Styles>
|
</Styles>
|
||||||
@ -10,6 +10,15 @@
|
|||||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||||
<TextBlock Classes="subtitle">This is a subtitle</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>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
@ -50,4 +59,26 @@
|
|||||||
<Setter Property="FontWeight" Value="Medium" />
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
<Setter Property="Margin" Value="0 25 0 5" />
|
<Setter Property="Margin" Value="0 25 0 5" />
|
||||||
</Style>
|
</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>
|
</Styles>
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive.Concurrency;
|
using System.Reactive.Concurrency;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Input.Platform;
|
using Avalonia.Input.Platform;
|
||||||
using IContainer = DryIoc.IContainer;
|
using Avalonia.Threading;
|
||||||
|
using DryIoc;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared;
|
namespace Artemis.UI.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static UI helpers.
|
/// Static UI helpers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UI
|
public static class UI
|
||||||
{
|
{
|
||||||
|
private static readonly BehaviorSubject<bool> MicaEnabledSubject = new(false);
|
||||||
|
|
||||||
|
public static EventLoopScheduler BackgroundScheduler = new(ts => new Thread(ts));
|
||||||
|
|
||||||
static UI()
|
static UI()
|
||||||
{
|
{
|
||||||
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
|
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
|
||||||
}
|
MicaEnabled = MicaEnabledSubject.AsObservable();
|
||||||
|
|
||||||
public static EventLoopScheduler BackgroundScheduler = new EventLoopScheduler(ts => new Thread(ts));
|
|
||||||
|
|
||||||
internal static void ClearCache()
|
|
||||||
{
|
|
||||||
DeviceVisualizer.BitmapCache.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -40,4 +40,24 @@ public static class UI
|
|||||||
/// Gets a boolean indicating whether hotkeys are to be disabled.
|
/// Gets a boolean indicating whether hotkeys are to be disabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IObservable<bool> KeyBindingsEnabled { get; }
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -43,8 +43,8 @@ public class App : Application
|
|||||||
{
|
{
|
||||||
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown)
|
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args);
|
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args ?? Array.Empty<string>());
|
||||||
ArtemisBootstrapper.Initialize();
|
ArtemisBootstrapper.Initialize();
|
||||||
RegisterProviders(_container!);
|
RegisterProviders(_container!);
|
||||||
}
|
}
|
||||||
@ -52,11 +52,14 @@ public class App : Application
|
|||||||
private void RegisterProviders(IContainer container)
|
private void RegisterProviders(IContainer container)
|
||||||
{
|
{
|
||||||
IInputService inputService = container.Resolve<IInputService>();
|
IInputService inputService = container.Resolve<IInputService>();
|
||||||
inputService.AddInputProvider(container.Resolve<InputProvider>(serviceKey: WindowsInputProvider.Id));
|
inputService.AddInputProvider(container.Resolve<InputProvider>(WindowsInputProvider.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool FocusExistingInstance()
|
private bool FocusExistingInstance()
|
||||||
{
|
{
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
return false;
|
||||||
|
|
||||||
_artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535f", out bool createdNew);
|
_artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535f", out bool createdNew);
|
||||||
return !createdNew && RemoteFocus();
|
return !createdNew && RemoteFocus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
|
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
|
||||||
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
|
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
|
||||||
<ProjectReference Include="..\Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj" />
|
<ProjectReference Include="..\Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj" />
|
||||||
|
<ProjectReference Include="..\Artemis.WebClient.Workshop\Artemis.WebClient.Workshop.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ using Artemis.UI.Shared.DryIoc;
|
|||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.VisualScripting.DryIoc;
|
using Artemis.VisualScripting.DryIoc;
|
||||||
using Artemis.WebClient.Updating.DryIoc;
|
using Artemis.WebClient.Updating.DryIoc;
|
||||||
|
using Artemis.WebClient.Workshop.DryIoc;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
@ -45,6 +46,7 @@ public static class ArtemisBootstrapper
|
|||||||
_container.RegisterUI();
|
_container.RegisterUI();
|
||||||
_container.RegisterSharedUI();
|
_container.RegisterSharedUI();
|
||||||
_container.RegisterUpdatingClient();
|
_container.RegisterUpdatingClient();
|
||||||
|
_container.RegisterWorkshopClient();
|
||||||
_container.RegisterNoStringEvaluating();
|
_container.RegisterNoStringEvaluating();
|
||||||
configureServices?.Invoke(_container);
|
configureServices?.Invoke(_container);
|
||||||
|
|
||||||
|
|||||||
BIN
src/Artemis.UI/Assets/Images/avatar-placeholder.png
Normal file
BIN
src/Artemis.UI/Assets/Images/avatar-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
BIN
src/Artemis.UI/Assets/Images/workshop-banner.jpg
Normal file
BIN
src/Artemis.UI/Assets/Images/workshop-banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
@ -30,6 +31,8 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.WebClient.Updating;
|
using Artemis.WebClient.Updating;
|
||||||
using DryIoc;
|
using DryIoc;
|
||||||
|
using DynamicData;
|
||||||
|
using Material.Icons;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.DryIoc.Factories;
|
namespace Artemis.UI.DryIoc.Factories;
|
||||||
@ -137,7 +140,7 @@ public class SidebarVmFactory : ISidebarVmFactory
|
|||||||
{
|
{
|
||||||
_container = container;
|
_container = container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory)
|
public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory)
|
||||||
{
|
{
|
||||||
return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory });
|
return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory });
|
||||||
|
|||||||
@ -10,12 +10,12 @@
|
|||||||
Icon="/Assets/Images/Logo/application.ico"
|
Icon="/Assets/Images/Logo/application.ico"
|
||||||
Title="Artemis 2.0"
|
Title="Artemis 2.0"
|
||||||
MinWidth="600"
|
MinWidth="600"
|
||||||
MinHeight="400"
|
MinHeight="400"
|
||||||
PointerReleased="InputElement_OnPointerReleased">
|
PointerReleased="InputElement_OnPointerReleased">
|
||||||
<windowing:AppWindow.Styles>
|
<windowing:AppWindow.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
<Style Selector="Border#TitleBarContainer">
|
<Style Selector="Border#TitleBarContainer">
|
||||||
<Setter Property="Height" Value="40"></Setter>
|
<Setter Property="MinHeight" Value="40"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="windowing|AppWindow:windows Border#TitleBarContainer">
|
<Style Selector="windowing|AppWindow:windows Border#TitleBarContainer">
|
||||||
<Setter Property="Margin" Value="0 0 138 0"></Setter>
|
<Setter Property="Margin" Value="0 0 138 0"></Setter>
|
||||||
|
|||||||
@ -5,6 +5,8 @@ using Artemis.UI.Screens.Settings;
|
|||||||
using Artemis.UI.Screens.Settings.Updating;
|
using Artemis.UI.Screens.Settings.Updating;
|
||||||
using Artemis.UI.Screens.SurfaceEditor;
|
using Artemis.UI.Screens.SurfaceEditor;
|
||||||
using Artemis.UI.Screens.Workshop;
|
using Artemis.UI.Screens.Workshop;
|
||||||
|
using Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
using Artemis.UI.Screens.Workshop.Profile;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
namespace Artemis.UI.Routing;
|
namespace Artemis.UI.Routing;
|
||||||
@ -14,7 +16,18 @@ public static class Routes
|
|||||||
public static List<IRouterRegistration> ArtemisRoutes = new()
|
public static List<IRouterRegistration> ArtemisRoutes = new()
|
||||||
{
|
{
|
||||||
new RouteRegistration<HomeViewModel>("home"),
|
new RouteRegistration<HomeViewModel>("home"),
|
||||||
new RouteRegistration<WorkshopViewModel>("workshop"),
|
#if DEBUG
|
||||||
|
new RouteRegistration<WorkshopViewModel>("workshop")
|
||||||
|
{
|
||||||
|
Children = new List<IRouterRegistration>()
|
||||||
|
{
|
||||||
|
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
|
||||||
|
new RouteRegistration<ProfileDetailsViewModel>("profiles/{entryId:guid}"),
|
||||||
|
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
|
||||||
|
new RouteRegistration<LayoutDetailsViewModel>("layouts/{entryId:guid}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
#endif
|
||||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||||
new RouteRegistration<SettingsViewModel>("settings")
|
new RouteRegistration<SettingsViewModel>("settings")
|
||||||
{
|
{
|
||||||
@ -25,7 +38,7 @@ public static class Routes
|
|||||||
new RouteRegistration<DevicesTabViewModel>("devices"),
|
new RouteRegistration<DevicesTabViewModel>("devices"),
|
||||||
new RouteRegistration<ReleasesTabViewModel>("releases")
|
new RouteRegistration<ReleasesTabViewModel>("releases")
|
||||||
{
|
{
|
||||||
Children = new List<IRouterRegistration>()
|
Children = new List<IRouterRegistration>
|
||||||
{
|
{
|
||||||
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,20 +15,17 @@
|
|||||||
Height="200"
|
Height="200"
|
||||||
Stretch="UniformToFill"
|
Stretch="UniformToFill"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
||||||
|
|
||||||
<!-- TODO: Replace with a shadow when available -->
|
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock Grid.Row="0"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Foreground="Black"
|
Foreground="White"
|
||||||
FontSize="32"
|
FontSize="32"
|
||||||
Margin="32"
|
Margin="30"
|
||||||
Text=" Welcome to Artemis, the unified RGB platform." />
|
Text=" Welcome to Artemis, the unified RGB platform.">
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock.Effect>
|
||||||
TextWrapping="Wrap"
|
<DropShadowEffect Color="Black" OffsetX="2" OffsetY="2" BlurRadius="5"></DropShadowEffect>
|
||||||
Foreground="White"
|
</TextBlock.Effect>
|
||||||
FontSize="32"
|
</TextBlock>
|
||||||
Margin="30"
|
|
||||||
Text=" Welcome to Artemis, the unified RGB platform." />
|
|
||||||
|
|
||||||
<Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom" ColumnDefinitions="*,*" RowDefinitions="*,*">
|
<Grid Grid.Row="1" MaxWidth="840" Margin="30" VerticalAlignment="Bottom" ColumnDefinitions="*,*" RowDefinitions="*,*">
|
||||||
<Border Classes="card" Margin="8" Grid.ColumnSpan="2" ClipToBounds="True">
|
<Border Classes="card" Margin="8" Grid.ColumnSpan="2" ClipToBounds="True">
|
||||||
|
|||||||
@ -20,11 +20,11 @@ namespace Artemis.UI.Screens.Root;
|
|||||||
|
|
||||||
public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowProvider
|
public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowProvider
|
||||||
{
|
{
|
||||||
private readonly IRouter _router;
|
|
||||||
private readonly ICoreService _coreService;
|
private readonly ICoreService _coreService;
|
||||||
private readonly IDebugService _debugService;
|
private readonly IDebugService _debugService;
|
||||||
private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel;
|
private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel;
|
||||||
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
||||||
|
private readonly IRouter _router;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
@ -41,6 +41,7 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
SidebarViewModel sidebarViewModel,
|
SidebarViewModel sidebarViewModel,
|
||||||
DefaultTitleBarViewModel defaultTitleBarViewModel)
|
DefaultTitleBarViewModel defaultTitleBarViewModel)
|
||||||
{
|
{
|
||||||
|
Shared.UI.SetMicaEnabled(settingsService.GetSetting("UI.EnableMica", true).Value);
|
||||||
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
||||||
SidebarViewModel = sidebarViewModel;
|
SidebarViewModel = sidebarViewModel;
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
|
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
|
||||||
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
|
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
|
||||||
this.WhenAnyValue(vm => vm.Screen).Subscribe(UpdateTitleBarViewModel);
|
this.WhenAnyValue(vm => vm.Screen).Subscribe(UpdateTitleBarViewModel);
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
if (_updateService.Initialize())
|
if (_updateService.Initialize())
|
||||||
@ -85,6 +86,8 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
set => RaiseAndSetIfChanged(ref _titleBarViewModel, value);
|
set => RaiseAndSetIfChanged(ref _titleBarViewModel, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
|
||||||
|
|
||||||
public void GoBack()
|
public void GoBack()
|
||||||
{
|
{
|
||||||
_router.GoBack();
|
_router.GoBack();
|
||||||
@ -95,8 +98,6 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
_router.GoForward();
|
_router.GoForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
|
|
||||||
|
|
||||||
private void UpdateTitleBarViewModel(IMainScreenViewModel? viewModel)
|
private void UpdateTitleBarViewModel(IMainScreenViewModel? viewModel)
|
||||||
{
|
{
|
||||||
if (viewModel?.TitleBarViewModel != null)
|
if (viewModel?.TitleBarViewModel != null)
|
||||||
|
|||||||
@ -56,8 +56,8 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.RowSpan="3"
|
Grid.RowSpan="3"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="75"
|
Height="40"
|
||||||
Width="75"
|
Width="40"
|
||||||
Margin="0 0 15 0"
|
Margin="0 0 15 0"
|
||||||
IsVisible="{CompiledBinding RobertProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
IsVisible="{CompiledBinding RobertProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||||
|
|||||||
@ -44,6 +44,19 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock>Enable Mica effect</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle" TextWrapping="Wrap">
|
||||||
|
The Mica effect is the semi-transparent effect used by the application window, the colors are based on your wallpaper.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<ToggleSwitch IsChecked="{CompiledBinding EnableMica.Value}" OnContent="Yes" OffContent="No" MinWidth="0" Margin="0 -10" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0">
|
<StackPanel Grid.Column="0">
|
||||||
|
|||||||
@ -30,9 +30,9 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
private readonly IAutoRunProvider? _autoRunProvider;
|
private readonly IAutoRunProvider? _autoRunProvider;
|
||||||
private readonly IDebugService _debugService;
|
private readonly IDebugService _debugService;
|
||||||
private readonly PluginSetting<LayerBrushReference> _defaultLayerBrushDescriptor;
|
private readonly PluginSetting<LayerBrushReference> _defaultLayerBrushDescriptor;
|
||||||
|
private readonly INotificationService _notificationService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
private readonly INotificationService _notificationService;
|
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private bool _startupWizardOpen;
|
private bool _startupWizardOpen;
|
||||||
|
|
||||||
@ -74,12 +74,14 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
UIAutoRun.SettingChanged += UIAutoRunOnSettingChanged;
|
UIAutoRun.SettingChanged += UIAutoRunOnSettingChanged;
|
||||||
UIAutoRunDelay.SettingChanged += UIAutoRunDelayOnSettingChanged;
|
UIAutoRunDelay.SettingChanged += UIAutoRunDelayOnSettingChanged;
|
||||||
|
EnableMica.SettingChanged += EnableMicaOnSettingChanged;
|
||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(ApplyAutoRun);
|
Dispatcher.UIThread.InvokeAsync(ApplyAutoRun);
|
||||||
Disposable.Create(() =>
|
Disposable.Create(() =>
|
||||||
{
|
{
|
||||||
UIAutoRun.SettingChanged -= UIAutoRunOnSettingChanged;
|
UIAutoRun.SettingChanged -= UIAutoRunOnSettingChanged;
|
||||||
UIAutoRunDelay.SettingChanged -= UIAutoRunDelayOnSettingChanged;
|
UIAutoRunDelay.SettingChanged -= UIAutoRunDelayOnSettingChanged;
|
||||||
|
EnableMica.SettingChanged -= EnableMicaOnSettingChanged;
|
||||||
|
|
||||||
_settingsService.SaveAllSettings();
|
_settingsService.SaveAllSettings();
|
||||||
}).DisposeWith(d);
|
}).DisposeWith(d);
|
||||||
@ -146,6 +148,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
|
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
|
||||||
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
||||||
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
||||||
|
public PluginSetting<bool> EnableMica => _settingsService.GetSetting("UI.EnableMica", true);
|
||||||
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
||||||
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
||||||
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
||||||
@ -238,4 +241,9 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
_windowService.ShowExceptionDialog("Failed to apply auto-run", exception);
|
_windowService.ShowExceptionDialog("Failed to apply auto-run", exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnableMicaOnSettingChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Shared.UI.SetMicaEnabled(EnableMica.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +99,6 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
|
<Style Selector="ListBox.sidebar-listbox ListBoxItem">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
|
||||||
<Setter Property="(i:Interaction.Behaviors)">
|
<Setter Property="(i:Interaction.Behaviors)">
|
||||||
<i:BehaviorCollectionTemplate>
|
<i:BehaviorCollectionTemplate>
|
||||||
<i:BehaviorCollection>
|
<i:BehaviorCollection>
|
||||||
|
|||||||
@ -76,8 +76,8 @@
|
|||||||
x:Name="ProfileIcon"
|
x:Name="ProfileIcon"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"
|
ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"
|
||||||
Width="20"
|
Width="22"
|
||||||
Height="20"
|
Height="22"
|
||||||
Margin="0 0 5 0">
|
Margin="0 0 5 0">
|
||||||
<shared:ProfileConfigurationIcon.Transitions>
|
<shared:ProfileConfigurationIcon.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:vm="clr-namespace:Artemis.UI.Screens.Sidebar;assembly=Artemis.UI"
|
xmlns:vm="clr-namespace:Artemis.UI.Screens.Sidebar;assembly=Artemis.UI"
|
||||||
x:DataType="vm:SidebarScreenViewModel"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarScreenView">
|
x:Class="Artemis.UI.Screens.Sidebar.SidebarScreenView"
|
||||||
<StackPanel Orientation="Horizontal">
|
x:DataType="vm:SidebarScreenViewModel">
|
||||||
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Width="16" Height="16" />
|
<StackPanel Orientation="Horizontal" Height="34" Background="Transparent" PointerPressed="InputElement_OnPointerPressed" DoubleTapped="InputElement_OnDoubleTapped" >
|
||||||
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="{CompiledBinding DisplayName}" />
|
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Width="18" Height="18" />
|
||||||
|
<TextBlock Margin="10 0" VerticalAlignment="Center" FontSize="13" Text="{CompiledBinding DisplayName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,13 +1,29 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Sidebar;
|
namespace Artemis.UI.Screens.Sidebar;
|
||||||
|
|
||||||
public partial class SidebarScreenView : UserControl
|
public partial class SidebarScreenView : ReactiveUserControl<SidebarScreenViewModel>
|
||||||
{
|
{
|
||||||
public SidebarScreenView()
|
public SidebarScreenView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel != null)
|
||||||
|
ViewModel.IsExpanded = !ViewModel.IsExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputElement_OnDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel != null)
|
||||||
|
ViewModel.IsExpanded = !ViewModel.IsExpanded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Material.Icons;
|
using Material.Icons;
|
||||||
|
|
||||||
@ -6,20 +8,57 @@ namespace Artemis.UI.Screens.Sidebar;
|
|||||||
|
|
||||||
public class SidebarScreenViewModel : ViewModelBase
|
public class SidebarScreenViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public SidebarScreenViewModel(MaterialIconKind icon, string displayName, string path)
|
private bool _isExpanded;
|
||||||
|
|
||||||
|
public SidebarScreenViewModel(MaterialIconKind icon, string displayName, string path, string? rootPath = null, ObservableCollection<SidebarScreenViewModel>? screens = null)
|
||||||
{
|
{
|
||||||
Icon = icon;
|
Icon = icon;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
RootPath = rootPath ?? path;
|
||||||
DisplayName = displayName;
|
DisplayName = displayName;
|
||||||
|
Screens = screens ?? new ObservableCollection<SidebarScreenViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MaterialIconKind Icon { get; }
|
public MaterialIconKind Icon { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
public string RootPath { get; }
|
||||||
|
|
||||||
|
public ObservableCollection<SidebarScreenViewModel> Screens { get; }
|
||||||
|
|
||||||
|
public bool IsExpanded
|
||||||
|
{
|
||||||
|
get => _isExpanded;
|
||||||
|
set => RaiseAndSetIfChanged(ref _isExpanded, value);
|
||||||
|
}
|
||||||
|
|
||||||
public bool Matches(string? path)
|
public bool Matches(string? path)
|
||||||
{
|
{
|
||||||
if (path == null)
|
if (path == null)
|
||||||
return false;
|
return false;
|
||||||
return path.StartsWith(Path, StringComparison.InvariantCultureIgnoreCase);
|
return path.StartsWith(RootPath, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SidebarScreenViewModel? GetMatch(string path)
|
||||||
|
{
|
||||||
|
foreach (SidebarScreenViewModel sidebarScreenViewModel in Screens)
|
||||||
|
{
|
||||||
|
SidebarScreenViewModel? match = sidebarScreenViewModel.GetMatch(path);
|
||||||
|
if (match != null)
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Screens.FirstOrDefault(s => s.Matches(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExpandIfRequired(SidebarScreenViewModel selected)
|
||||||
|
{
|
||||||
|
if (selected == this)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Screens.Contains(selected))
|
||||||
|
IsExpanded = true;
|
||||||
|
|
||||||
|
foreach (SidebarScreenViewModel sidebarScreenViewModel in Screens)
|
||||||
|
sidebarScreenViewModel.ExpandIfRequired(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
|
xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
|
||||||
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarView"
|
x:Class="Artemis.UI.Screens.Sidebar.SidebarView"
|
||||||
@ -20,11 +20,23 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Built-in screens -->
|
<!-- Built-in screens -->
|
||||||
<ListBox Classes="sidebar-listbox"
|
<TreeView Grid.Row="1"
|
||||||
Grid.Row="1"
|
Margin="10 2"
|
||||||
Margin="10 2"
|
ItemsSource="{CompiledBinding SidebarScreen.Screens}"
|
||||||
ItemsSource="{CompiledBinding SidebarScreens}"
|
SelectedItem="{CompiledBinding SelectedScreen}"
|
||||||
SelectedItem="{CompiledBinding SelectedSidebarScreen}" />
|
ItemContainerTheme="{StaticResource MenuTreeViewItem}">
|
||||||
|
<TreeView.Styles>
|
||||||
|
<Style Selector="TreeViewItem">
|
||||||
|
<Setter Property="IsExpanded" Value="{CompiledBinding IsExpanded, Mode=TwoWay, DataType=sidebar:SidebarScreenViewModel}" />
|
||||||
|
</Style>
|
||||||
|
</TreeView.Styles>
|
||||||
|
<TreeView.ItemTemplate>
|
||||||
|
<TreeDataTemplate ItemsSource="{CompiledBinding Screens}">
|
||||||
|
<ContentControl Content="{CompiledBinding}" />
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</TreeView.ItemTemplate>
|
||||||
|
</TreeView>
|
||||||
|
|
||||||
<Border Grid.Row="2" Margin="8" Height="1" Background="{DynamicResource ButtonBorderBrush}"></Border>
|
<Border Grid.Row="2" Margin="8" Height="1" Background="{DynamicResource ButtonBorderBrush}"></Border>
|
||||||
|
|
||||||
<!-- Categories -->
|
<!-- Categories -->
|
||||||
@ -41,51 +53,51 @@
|
|||||||
<!-- Bottom buttons -->
|
<!-- Bottom buttons -->
|
||||||
<Border Grid.Row="4" Margin="8" Height="1" Background="{DynamicResource ButtonBorderBrush}"></Border>
|
<Border Grid.Row="4" Margin="8" Height="1" Background="{DynamicResource ButtonBorderBrush}"></Border>
|
||||||
<WrapPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Left" Margin="5 0 5 5">
|
<WrapPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Left" Margin="5 0 5 5">
|
||||||
<controls:HyperlinkButton Classes="icon-button"
|
<ui:HyperlinkButton Classes="icon-button"
|
||||||
Width="44"
|
Width="44"
|
||||||
Height="44"
|
Height="44"
|
||||||
ToolTip.Tip="View website"
|
ToolTip.Tip="View website"
|
||||||
ToolTip.Placement="Top"
|
ToolTip.Placement="Top"
|
||||||
ToolTip.VerticalOffset="-5"
|
ToolTip.VerticalOffset="-5"
|
||||||
NavigateUri="https://artemis-rgb.com?mtm_campaign=artemis&mtm_kwd=sidebar">
|
NavigateUri="https://artemis-rgb.com?mtm_campaign=artemis&mtm_kwd=sidebar">
|
||||||
<avalonia:MaterialIcon Kind="Web" Width="20" Height="20" />
|
<avalonia:MaterialIcon Kind="Web" Width="20" Height="20" />
|
||||||
</controls:HyperlinkButton>
|
</ui:HyperlinkButton>
|
||||||
<controls:HyperlinkButton Classes="icon-button"
|
<ui:HyperlinkButton Classes="icon-button"
|
||||||
Width="44"
|
Width="44"
|
||||||
Height="44"
|
Height="44"
|
||||||
ToolTip.Tip="View GitHub repository"
|
ToolTip.Tip="View GitHub repository"
|
||||||
ToolTip.Placement="Top"
|
ToolTip.Placement="Top"
|
||||||
ToolTip.VerticalOffset="-5"
|
ToolTip.VerticalOffset="-5"
|
||||||
NavigateUri="https://github.com/Artemis-RGB/Artemis">
|
NavigateUri="https://github.com/Artemis-RGB/Artemis">
|
||||||
<avalonia:MaterialIcon Kind="Github" Width="20" Height="20" />
|
<avalonia:MaterialIcon Kind="Github" Width="20" Height="20" />
|
||||||
</controls:HyperlinkButton>
|
</ui:HyperlinkButton>
|
||||||
<controls:HyperlinkButton Classes="icon-button"
|
<ui:HyperlinkButton Classes="icon-button"
|
||||||
Width="44"
|
Width="44"
|
||||||
Height="44"
|
Height="44"
|
||||||
ToolTip.Tip="View Wiki"
|
ToolTip.Tip="View Wiki"
|
||||||
ToolTip.Placement="Top"
|
ToolTip.Placement="Top"
|
||||||
ToolTip.VerticalOffset="-5"
|
ToolTip.VerticalOffset="-5"
|
||||||
NavigateUri="https://wiki.artemis-rgb.com?mtm_campaign=artemis&mtm_kwd=sidebar">
|
NavigateUri="https://wiki.artemis-rgb.com?mtm_campaign=artemis&mtm_kwd=sidebar">
|
||||||
<avalonia:MaterialIcon Kind="BookOpenOutline" Width="20" Height="20" />
|
<avalonia:MaterialIcon Kind="BookOpenOutline" Width="20" Height="20" />
|
||||||
</controls:HyperlinkButton>
|
</ui:HyperlinkButton>
|
||||||
<controls:HyperlinkButton Classes="icon-button"
|
<ui:HyperlinkButton Classes="icon-button"
|
||||||
Width="44"
|
Width="44"
|
||||||
Height="44"
|
Height="44"
|
||||||
ToolTip.Tip="Join our Discord"
|
ToolTip.Tip="Join our Discord"
|
||||||
ToolTip.Placement="Top"
|
ToolTip.Placement="Top"
|
||||||
ToolTip.VerticalOffset="-5"
|
ToolTip.VerticalOffset="-5"
|
||||||
NavigateUri="https://discord.gg/S3MVaC9">
|
NavigateUri="https://discord.gg/S3MVaC9">
|
||||||
<avalonia:MaterialIcon Kind="Chat" Width="20" Height="20" />
|
<avalonia:MaterialIcon Kind="Chat" Width="20" Height="20" />
|
||||||
</controls:HyperlinkButton>
|
</ui:HyperlinkButton>
|
||||||
<controls:HyperlinkButton Classes="icon-button"
|
<ui:HyperlinkButton Classes="icon-button"
|
||||||
Width="44"
|
Width="44"
|
||||||
Height="44"
|
Height="44"
|
||||||
ToolTip.Tip="View donation options"
|
ToolTip.Tip="View donation options"
|
||||||
ToolTip.Placement="Top"
|
ToolTip.Placement="Top"
|
||||||
ToolTip.VerticalOffset="-5"
|
ToolTip.VerticalOffset="-5"
|
||||||
NavigateUri="https://wiki.artemis-rgb.com/en/donating?mtm_campaign=artemis&mtm_kwd=sidebar">
|
NavigateUri="https://wiki.artemis-rgb.com/en/donating?mtm_campaign=artemis&mtm_kwd=sidebar">
|
||||||
<avalonia:MaterialIcon Kind="Gift" Width="20" Height="20" />
|
<avalonia:MaterialIcon Kind="Gift" Width="20" Height="20" />
|
||||||
</controls:HyperlinkButton>
|
</ui:HyperlinkButton>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
@ -23,34 +24,40 @@ namespace Artemis.UI.Screens.Sidebar;
|
|||||||
|
|
||||||
public class SidebarViewModel : ActivatableViewModelBase
|
public class SidebarViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
|
public const string ROOT_SCREEN = "root";
|
||||||
|
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private SidebarScreenViewModel? _selectedSidebarScreen;
|
|
||||||
private ReadOnlyObservableCollection<SidebarCategoryViewModel> _sidebarCategories = new(new ObservableCollection<SidebarCategoryViewModel>());
|
private ReadOnlyObservableCollection<SidebarCategoryViewModel> _sidebarCategories = new(new ObservableCollection<SidebarCategoryViewModel>());
|
||||||
|
private SidebarScreenViewModel? _selectedScreen;
|
||||||
|
|
||||||
public SidebarViewModel(IRouter router, IProfileService profileService, IWindowService windowService, ISidebarVmFactory sidebarVmFactory)
|
public SidebarViewModel(IRouter router, IProfileService profileService, IWindowService windowService, ISidebarVmFactory sidebarVmFactory)
|
||||||
{
|
{
|
||||||
_router = router;
|
_router = router;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
|
|
||||||
SidebarScreens = new ObservableCollection<SidebarScreenViewModel>
|
SidebarScreen = new SidebarScreenViewModel(MaterialIconKind.Abacus, ROOT_SCREEN, "", null, new ObservableCollection<SidebarScreenViewModel>()
|
||||||
{
|
{
|
||||||
new(MaterialIconKind.Home, "Home", "home"),
|
new(MaterialIconKind.HomeOutline, "Home", "home"),
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
new(MaterialIconKind.TestTube, "Workshop", "workshop"),
|
new(MaterialIconKind.TestTube, "Workshop", "workshop", null, new ObservableCollection<SidebarScreenViewModel>
|
||||||
#endif
|
{
|
||||||
|
new(MaterialIconKind.FolderVideo, "Profiles", "workshop/profiles/1", "workshop/profiles"),
|
||||||
|
new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/layouts/1", "workshop/layouts"),
|
||||||
|
}),
|
||||||
|
#endif
|
||||||
new(MaterialIconKind.Devices, "Surface Editor", "surface-editor"),
|
new(MaterialIconKind.Devices, "Surface Editor", "surface-editor"),
|
||||||
new(MaterialIconKind.Cog, "Settings", "settings")
|
new(MaterialIconKind.SettingsOutline, "Settings", "settings")
|
||||||
};
|
});
|
||||||
|
|
||||||
AddCategory = ReactiveCommand.CreateFromTask(ExecuteAddCategory);
|
AddCategory = ReactiveCommand.CreateFromTask(ExecuteAddCategory);
|
||||||
|
this.WhenAnyValue(vm => vm.SelectedScreen).WhereNotNull().Subscribe(NavigateToScreen);
|
||||||
|
this.WhenAnyValue(vm => vm.SelectedScreen).WhereNotNull().Subscribe(s => SidebarScreen.ExpandIfRequired(s));
|
||||||
|
|
||||||
SourceList<ProfileCategory> profileCategories = new();
|
SourceList<ProfileCategory> profileCategories = new();
|
||||||
|
|
||||||
this.WhenAnyValue(vm => vm.SelectedSidebarScreen).WhereNotNull().Subscribe(NavigateToScreen);
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
_router.CurrentPath.WhereNotNull().Subscribe(r => SelectedSidebarScreen = SidebarScreens.FirstOrDefault(s => s.Matches(r))).DisposeWith(d);
|
_router.CurrentPath.WhereNotNull().Subscribe(r => SelectedScreen = SidebarScreen.GetMatch(r)).DisposeWith(d);
|
||||||
|
|
||||||
Observable.FromEventPattern<ProfileCategoryEventArgs>(x => profileService.ProfileCategoryAdded += x, x => profileService.ProfileCategoryAdded -= x)
|
Observable.FromEventPattern<ProfileCategoryEventArgs>(x => profileService.ProfileCategoryAdded += x, x => profileService.ProfileCategoryAdded -= x)
|
||||||
.Subscribe(e => profileCategories.Add(e.EventArgs.ProfileCategory))
|
.Subscribe(e => profileCategories.Add(e.EventArgs.ProfileCategory))
|
||||||
@ -75,11 +82,17 @@ public class SidebarViewModel : ActivatableViewModelBase
|
|||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|
||||||
SidebarCategories = categoryViewModels;
|
SidebarCategories = categoryViewModels;
|
||||||
SelectedSidebarScreen = SidebarScreens.First();
|
|
||||||
});
|
});
|
||||||
|
SelectedScreen = SidebarScreen.Screens.First();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<SidebarScreenViewModel> SidebarScreens { get; }
|
public SidebarScreenViewModel SidebarScreen { get; }
|
||||||
|
|
||||||
|
public SidebarScreenViewModel? SelectedScreen
|
||||||
|
{
|
||||||
|
get => _selectedScreen;
|
||||||
|
set => RaiseAndSetIfChanged(ref _selectedScreen, value);
|
||||||
|
}
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<SidebarCategoryViewModel> SidebarCategories
|
public ReadOnlyObservableCollection<SidebarCategoryViewModel> SidebarCategories
|
||||||
{
|
{
|
||||||
@ -87,12 +100,6 @@ public class SidebarViewModel : ActivatableViewModelBase
|
|||||||
set => RaiseAndSetIfChanged(ref _sidebarCategories, value);
|
set => RaiseAndSetIfChanged(ref _sidebarCategories, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SidebarScreenViewModel? SelectedSidebarScreen
|
|
||||||
{
|
|
||||||
get => _selectedSidebarScreen;
|
|
||||||
set => RaiseAndSetIfChanged(ref _selectedSidebarScreen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> AddCategory { get; }
|
public ReactiveCommand<Unit, Unit> AddCategory { get; }
|
||||||
|
|
||||||
private async Task ExecuteAddCategory()
|
private async Task ExecuteAddCategory()
|
||||||
@ -112,7 +119,7 @@ public class SidebarViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _router.Navigate(sidebarScreenViewModel.Path, new RouterNavigationOptions {IgnoreOnPartialMatch = true});
|
await _router.Navigate(sidebarScreenViewModel.Path);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
<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:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Categories.CategoriesView"
|
||||||
|
x:DataType="categories:CategoriesViewModel">
|
||||||
|
<ItemsRepeater ItemsSource="{CompiledBinding Categories}">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate DataType="categories:CategoryViewModel">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5" Background="Transparent" Cursor="Hand" PointerReleased="InputElement_OnPointerReleased">
|
||||||
|
<CheckBox IsChecked="{CompiledBinding IsSelected}" Padding="1 5 1 0"/>
|
||||||
|
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" />
|
||||||
|
<TextBlock Text="{CompiledBinding Name}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Categories;
|
||||||
|
|
||||||
|
public partial class CategoriesView : ReactiveUserControl<CategoriesViewModel>
|
||||||
|
{
|
||||||
|
public CategoriesView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.InitialPressMouseButton == MouseButton.Left && sender is IDataContextProvider p && p.DataContext is CategoryViewModel categoryViewModel)
|
||||||
|
categoryViewModel.IsSelected = !categoryViewModel.IsSelected;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Extensions;
|
||||||
|
using DynamicData;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using ReactiveUI;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Categories;
|
||||||
|
|
||||||
|
public class CategoriesViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private ObservableAsPropertyHelper<IReadOnlyList<EntryFilterInput>?>? _categoryFilters;
|
||||||
|
|
||||||
|
public CategoriesViewModel(IWorkshopClient client)
|
||||||
|
{
|
||||||
|
client.GetCategories
|
||||||
|
.Watch(ExecutionStrategy.CacheFirst)
|
||||||
|
.SelectOperationResult(c => c.Categories)
|
||||||
|
.ToObservableChangeSet(c => c.Id)
|
||||||
|
.Transform(c => new CategoryViewModel(c))
|
||||||
|
.Bind(out ReadOnlyObservableCollection<CategoryViewModel> categoryViewModels)
|
||||||
|
.Subscribe();
|
||||||
|
|
||||||
|
Categories = categoryViewModels;
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_categoryFilters = Categories.ToObservableChangeSet()
|
||||||
|
.AutoRefresh(c => c.IsSelected)
|
||||||
|
.Filter(e => e.IsSelected)
|
||||||
|
.Select(_ => CreateFilter())
|
||||||
|
.ToProperty(this, vm => vm.CategoryFilters)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObservableCollection<CategoryViewModel> Categories { get; }
|
||||||
|
public IReadOnlyList<EntryFilterInput>? CategoryFilters => _categoryFilters?.Value;
|
||||||
|
|
||||||
|
private IReadOnlyList<EntryFilterInput>? CreateFilter()
|
||||||
|
{
|
||||||
|
List<int?> categories = Categories.Where(c => c.IsSelected).Select(c => (int?) c.Id).ToList();
|
||||||
|
if (!categories.Any())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
List<EntryFilterInput> categoryFilters = new();
|
||||||
|
foreach (int? category in categories)
|
||||||
|
{
|
||||||
|
categoryFilters.Add(new EntryFilterInput
|
||||||
|
{
|
||||||
|
Categories = new ListFilterInputTypeOfCategoryFilterInput {Some = new CategoryFilterInput {Id = new IntOperationFilterInput {Eq = category}}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryFilters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Material.Icons;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Categories;
|
||||||
|
|
||||||
|
public class CategoryViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private bool _isSelected;
|
||||||
|
|
||||||
|
public CategoryViewModel(IGetCategories_Categories category)
|
||||||
|
{
|
||||||
|
Id = category.Id;
|
||||||
|
Name = category.Name;
|
||||||
|
if (Enum.TryParse(typeof(MaterialIconKind), category.Icon, out object? icon))
|
||||||
|
Icon = icon as MaterialIconKind? ?? MaterialIconKind.QuestionMarkCircle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Id { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public MaterialIconKind Icon { get; }
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set => RaiseAndSetIfChanged(ref _isSelected, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<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:currentUser="clr-namespace:Artemis.UI.Screens.Workshop.CurrentUser"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.CurrentUser.CurrentUserView"
|
||||||
|
x:DataType="currentUser:CurrentUserViewModel">
|
||||||
|
|
||||||
|
<Panel Name="Container" IsVisible="{CompiledBinding !Loading}">
|
||||||
|
<!-- Signed out -->
|
||||||
|
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding Name, Converter={x:Static StringConverters.IsNullOrEmpty}}">
|
||||||
|
<Ellipse.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuItem Header="Login" Command="{CompiledBinding Login}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="Login" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Ellipse.ContextFlyout>
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="/Assets/Images/avatar-placeholder.png" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
|
||||||
|
<!-- Signed in -->
|
||||||
|
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Name="UserMenu">
|
||||||
|
<Ellipse.ContextFlyout>
|
||||||
|
<Flyout>
|
||||||
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*,*" MinWidth="300">
|
||||||
|
<Ellipse Grid.Column="0" Grid.RowSpan="3" Height="50" Width="50" Margin="0 0 8 0" VerticalAlignment="Top">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="{CompiledBinding Avatar}" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Name}" Margin="0 4 0 0"></TextBlock>
|
||||||
|
<TextBlock Grid.Column="1" Grid.Row="1" Text="{CompiledBinding Email}"></TextBlock>
|
||||||
|
<controls:HyperlinkButton
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="-8 0 0 0"
|
||||||
|
Padding="6 4"
|
||||||
|
Click="Button_OnClick">
|
||||||
|
Sign out
|
||||||
|
</controls:HyperlinkButton>
|
||||||
|
</Grid>
|
||||||
|
</Flyout>
|
||||||
|
</Ellipse.ContextFlyout>
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="{CompiledBinding Avatar}" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public partial class CurrentUserView : ReactiveUserControl<CurrentUserViewModel>
|
||||||
|
{
|
||||||
|
public CurrentUserView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
UserMenu.ContextFlyout?.Hide();
|
||||||
|
ViewModel?.Logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Flurl.Http;
|
||||||
|
using ReactiveUI;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public class CurrentUserViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private bool _loading = true;
|
||||||
|
private Bitmap? _avatar;
|
||||||
|
private string? _email;
|
||||||
|
private string? _name;
|
||||||
|
private string? _userId;
|
||||||
|
|
||||||
|
public CurrentUserViewModel(ILogger logger, IAuthenticationService authenticationService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
||||||
|
|
||||||
|
this.WhenActivated(d => ReactiveCommand.CreateFromTask(ExecuteAutoLogin).Execute().Subscribe().DisposeWith(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Loading
|
||||||
|
{
|
||||||
|
get => _loading;
|
||||||
|
set => RaiseAndSetIfChanged(ref _loading, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => RaiseAndSetIfChanged(ref _userId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set => RaiseAndSetIfChanged(ref _name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => RaiseAndSetIfChanged(ref _email, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap? Avatar
|
||||||
|
{
|
||||||
|
get => _avatar;
|
||||||
|
set => RaiseAndSetIfChanged(ref _avatar, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Login { get; }
|
||||||
|
|
||||||
|
public void Logout()
|
||||||
|
{
|
||||||
|
_authenticationService.Logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteLogin(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _authenticationService.Login();
|
||||||
|
await LoadCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteAutoLogin(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _authenticationService.AutoLogin();
|
||||||
|
await LoadCurrentUser();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warning(e, "Failed to load the current user");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCurrentUser()
|
||||||
|
{
|
||||||
|
if (!_authenticationService.IsLoggedIn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UserId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
|
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||||
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
|
|
||||||
|
if (UserId != null)
|
||||||
|
await LoadAvatar(UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAvatar(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Avatar = new Bitmap(await $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}".GetStreamAsync());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml
Normal file
71
src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||||
|
xmlns:entries1="clr-namespace:Artemis.UI.Screens.Workshop.Entries"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="110"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Entries.EntryListView"
|
||||||
|
x:DataType="entries1:EntryListViewModel">
|
||||||
|
<Button MinHeight="110"
|
||||||
|
MaxHeight="140"
|
||||||
|
Padding="16"
|
||||||
|
CornerRadius="8"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
Command="{CompiledBinding NavigateToEntry}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<!-- Icon -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
CornerRadius="12"
|
||||||
|
Background="{StaticResource ControlStrokeColorOnAccentDefault}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0 0 10 0"
|
||||||
|
Width="80"
|
||||||
|
Height="80">
|
||||||
|
<avalonia:MaterialIcon Kind="HandOkay" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Width="70" Height="70" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis" >
|
||||||
|
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
|
<Run Classes="subtitle">by</Run>
|
||||||
|
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Classes="subtitle"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}"></TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"></StackPanel>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||||
|
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Info -->
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, StringFormat={}{0:g}, FallbackValue=01-01-1337}" />
|
||||||
|
<TextBlock TextAlignment="Right">
|
||||||
|
<avalonia:MaterialIcon Kind="Downloads" />
|
||||||
|
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
|
||||||
|
<Run>downloads</Run>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Entries;
|
||||||
|
|
||||||
|
public partial class EntryListView : ReactiveUserControl<EntryListViewModel>
|
||||||
|
{
|
||||||
|
public EntryListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Entries;
|
||||||
|
|
||||||
|
public class EntryListViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IRouter _router;
|
||||||
|
|
||||||
|
public EntryListViewModel(IGetEntries_Entries_Items entry, IRouter router)
|
||||||
|
{
|
||||||
|
_router = router;
|
||||||
|
Entry = entry;
|
||||||
|
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IGetEntries_Entries_Items Entry { get; }
|
||||||
|
public ReactiveCommand<Unit,Unit> NavigateToEntry { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteNavigateToEntry()
|
||||||
|
{
|
||||||
|
switch (Entry.EntryType)
|
||||||
|
{
|
||||||
|
case EntryType.Layout:
|
||||||
|
await _router.Navigate($"workshop/layouts/{Entry.Id}");
|
||||||
|
break;
|
||||||
|
case EntryType.Profile:
|
||||||
|
await _router.Navigate($"workshop/profiles/{Entry.Id}");
|
||||||
|
break;
|
||||||
|
case EntryType.Plugin:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
Normal file
78
src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:home="clr-namespace:Artemis.UI.Screens.Workshop.Home"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Home.WorkshopHomeView"
|
||||||
|
x:DataType="home:WorkshopHomeViewModel">
|
||||||
|
<Border Classes="router-container">
|
||||||
|
<Grid RowDefinitions="200,*,*">
|
||||||
|
<Image Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Source="/Assets/Images/workshop-banner.jpg"
|
||||||
|
Height="200"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||||
|
<Image.OpacityMask>
|
||||||
|
<LinearGradientBrush StartPoint="0%,70%" EndPoint="0%,100%">
|
||||||
|
<GradientStops>
|
||||||
|
<GradientStop Color="Black" Offset="0"></GradientStop>
|
||||||
|
<GradientStop Color="Transparent" Offset="100"></GradientStop>
|
||||||
|
</GradientStops>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Image.OpacityMask>
|
||||||
|
</Image>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="White"
|
||||||
|
FontSize="32"
|
||||||
|
Margin="30"
|
||||||
|
Text="Welcome to the Artemis Workshop!">
|
||||||
|
<TextBlock.Effect>
|
||||||
|
<DropShadowEffect Color="Black" OffsetX="2" OffsetY="2" BlurRadius="5"></DropShadowEffect>
|
||||||
|
</TextBlock.Effect>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<StackPanel Margin="30 -75 30 0" Grid.Row="1">
|
||||||
|
<StackPanel Spacing="10" Orientation="Horizontal" VerticalAlignment="Top">
|
||||||
|
<Button Width="150" Height="180" Command="{CompiledBinding AddSubmission}" VerticalContentAlignment="Top">
|
||||||
|
<StackPanel>
|
||||||
|
<avalonia:MaterialIcon Kind="CloudUpload" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Add submission</TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.8">Upload your own creations to the workshop!</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/profiles/1" VerticalContentAlignment="Top">
|
||||||
|
<StackPanel>
|
||||||
|
<avalonia:MaterialIcon Kind="FolderVideo" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Profiles</TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.8">Browse new profiles created by other users.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/layouts/1" VerticalContentAlignment="Top">
|
||||||
|
<StackPanel>
|
||||||
|
<avalonia:MaterialIcon Kind="KeyboardVariant" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Layouts</TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.8">Layouts make your devices look great in the editor.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="h4" Margin="0 15 0 5">Featured submissions</TextBlock>
|
||||||
|
<TextBlock>Not yet implemented, here we'll show submissions we think are worth some extra attention.</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Classes="h4" Margin="0 15 0 5">Recently updated</TextBlock>
|
||||||
|
<TextBlock>Not yet implemented, here we'll a few of the most recent uploads/updates to the workshop.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Home;
|
||||||
|
|
||||||
|
public partial class WorkshopHomeView : ReactiveUserControl<WorkshopHomeViewModel>
|
||||||
|
{
|
||||||
|
public WorkshopHomeView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.SubmissionWizard;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Home;
|
||||||
|
|
||||||
|
public class WorkshopHomeViewModel : ActivatableViewModelBase, IWorkshopViewModel
|
||||||
|
{
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
|
||||||
|
public WorkshopHomeViewModel(IRouter router, IWindowService windowService)
|
||||||
|
{
|
||||||
|
_windowService = windowService;
|
||||||
|
AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission);
|
||||||
|
Navigate = ReactiveCommand.CreateFromTask<string>(async r => await router.Navigate(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> AddSubmission { get; }
|
||||||
|
public ReactiveCommand<string, Unit> Navigate { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteAddSubmission(CancellationToken arg)
|
||||||
|
{
|
||||||
|
await _windowService.ShowDialogAsync<SubmissionWizardViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType => null;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<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:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
||||||
|
x:DataType="layout:LayoutDetailsViewModel">
|
||||||
|
<Border Classes="router-container">
|
||||||
|
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*" Margin="10">
|
||||||
|
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" >
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.Name, FallbackValue=Layout}" Classes="h3 no-margin"/>
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.Author, FallbackValue=Author}" Classes="subtitle" Margin="0 0 0 5"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Classes="card-condensed" Grid.Row="1" Grid.Column="0" Margin="0 0 10 0">
|
||||||
|
<TextBlock>Side panel</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="card-condensed" Grid.Row="1" Grid.Column="1">
|
||||||
|
<TextBlock>Layout details panel</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
|
||||||
|
public partial class LayoutDetailsView : ReactiveUserControl<LayoutDetailsViewModel>
|
||||||
|
{
|
||||||
|
public LayoutDetailsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
|
||||||
|
public class LayoutDetailsViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopDetailParameters>, IWorkshopViewModel
|
||||||
|
{
|
||||||
|
private readonly IWorkshopClient _client;
|
||||||
|
private IGetEntryById_Entry? _entry;
|
||||||
|
|
||||||
|
public LayoutDetailsViewModel(IWorkshopClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType => null;
|
||||||
|
|
||||||
|
public IGetEntryById_Entry? Entry
|
||||||
|
{
|
||||||
|
get => _entry;
|
||||||
|
set => RaiseAndSetIfChanged(ref _entry, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await GetEntry(parameters.EntryId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetEntry(Guid entryId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
|
if (result.IsErrorResult())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Entry = result.Data?.Entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
Normal file
25
src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListView"
|
||||||
|
x:DataType="layout:LayoutListViewModel">
|
||||||
|
<Border Classes="router-container">
|
||||||
|
<Grid ColumnDefinitions="300,*" Margin="10">
|
||||||
|
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="h3">Categories</TextBlock>
|
||||||
|
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="card-condensed" Grid.Column="1">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text="Layout list main panel, page: " /><Run Text="{CompiledBinding Page}"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
|
||||||
|
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
|
||||||
|
{
|
||||||
|
public LayoutListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Categories;
|
||||||
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
|
||||||
|
public class LayoutListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IWorkshopViewModel
|
||||||
|
{
|
||||||
|
private int _page;
|
||||||
|
|
||||||
|
public LayoutListViewModel(CategoriesViewModel categoriesViewModel)
|
||||||
|
{
|
||||||
|
CategoriesViewModel = categoriesViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CategoriesViewModel CategoriesViewModel { get; }
|
||||||
|
|
||||||
|
public int Page
|
||||||
|
{
|
||||||
|
get => _page;
|
||||||
|
set => RaiseAndSetIfChanged(ref _page, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Page = Math.Max(1, parameters.Page);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType => WebClient.Workshop.EntryType.Layout;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
|
||||||
|
public class WorkshopDetailParameters
|
||||||
|
{
|
||||||
|
public Guid EntryId { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
|
||||||
|
public class WorkshopListParameters
|
||||||
|
{
|
||||||
|
public int Page { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
||||||
|
x:DataType="profile:ProfileDetailsViewModel">
|
||||||
|
<Border Classes="router-container">
|
||||||
|
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*" Margin="10">
|
||||||
|
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" >
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.Name, FallbackValue=Profile}" Classes="h3 no-margin"/>
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.Author, FallbackValue=Author}" Classes="subtitle" Margin="0 0 0 5"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Classes="card-condensed" Grid.Row="1" Grid.Column="0" Margin="0 0 10 0">
|
||||||
|
<TextBlock>Side panel</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="card-condensed" Grid.Row="1" Grid.Column="1">
|
||||||
|
<TextBlock>Profile details panel</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
|
public partial class ProfileDetailsView : ReactiveUserControl<ProfileDetailsViewModel>
|
||||||
|
{
|
||||||
|
public ProfileDetailsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
|
public class ProfileDetailsViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopDetailParameters>, IWorkshopViewModel
|
||||||
|
{
|
||||||
|
private readonly IWorkshopClient _client;
|
||||||
|
private IGetEntryById_Entry? _entry;
|
||||||
|
|
||||||
|
public ProfileDetailsViewModel(IWorkshopClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType => null;
|
||||||
|
|
||||||
|
public IGetEntryById_Entry? Entry
|
||||||
|
{
|
||||||
|
get => _entry;
|
||||||
|
set => RaiseAndSetIfChanged(ref _entry, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await GetEntry(parameters.EntryId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetEntry(Guid entryId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
|
if (result.IsErrorResult())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Entry = result.Data?.Entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||||
|
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileListView"
|
||||||
|
x:DataType="profile:ProfileListViewModel">
|
||||||
|
<Border Classes="router-container">
|
||||||
|
<Grid ColumnDefinitions="300,*" Margin="10" RowDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Grid.RowSpan="2" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||||
|
<TextBlock Classes="card-title" Margin="0 0 0 5">
|
||||||
|
Categories
|
||||||
|
</TextBlock>
|
||||||
|
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
|
||||||
|
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 26 20 0" IsVisible="{CompiledBinding IsLoading}" IsIndeterminate="True"/>
|
||||||
|
<ScrollViewer Grid.Column="1" Grid.Row="0" Margin="0 26 0 0">
|
||||||
|
<ItemsRepeater ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<pagination:Pagination Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0 20 0 10"
|
||||||
|
IsVisible="{CompiledBinding ShowPagination}"
|
||||||
|
Value="{CompiledBinding Page}"
|
||||||
|
Maximum="{CompiledBinding TotalPages}"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
|
public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
|
||||||
|
{
|
||||||
|
public ProfileListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
Normal file
146
src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Categories;
|
||||||
|
using Artemis.UI.Screens.Workshop.Entries;
|
||||||
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using ReactiveUI;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
|
public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IWorkshopViewModel
|
||||||
|
{
|
||||||
|
private readonly IRouter _router;
|
||||||
|
private readonly INotificationService _notificationService;
|
||||||
|
private readonly IWorkshopClient _workshopClient;
|
||||||
|
private readonly ObservableAsPropertyHelper<bool> _showPagination;
|
||||||
|
private readonly ObservableAsPropertyHelper<bool> _isLoading;
|
||||||
|
private List<EntryListViewModel>? _entries;
|
||||||
|
private int _page;
|
||||||
|
private int _loadedPage = -1;
|
||||||
|
private int _totalPages = 1;
|
||||||
|
private int _entriesPerPage = 10;
|
||||||
|
|
||||||
|
public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel, INotificationService notificationService)
|
||||||
|
{
|
||||||
|
_workshopClient = workshopClient;
|
||||||
|
_router = router;
|
||||||
|
_notificationService = notificationService;
|
||||||
|
_showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination);
|
||||||
|
_isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading);
|
||||||
|
|
||||||
|
CategoriesViewModel = categoriesViewModel;
|
||||||
|
|
||||||
|
// Respond to page changes
|
||||||
|
this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => _router.Navigate($"workshop/profiles/{p}")));
|
||||||
|
// Respond to filter changes
|
||||||
|
this.WhenActivated(d => CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ =>
|
||||||
|
{
|
||||||
|
// Reset to page one, will trigger a query
|
||||||
|
if (Page != 1)
|
||||||
|
Page = 1;
|
||||||
|
// If already at page one, force a query
|
||||||
|
else
|
||||||
|
Task.Run(() => Query(CancellationToken.None));
|
||||||
|
}).DisposeWith(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowPagination => _showPagination.Value;
|
||||||
|
public bool IsLoading => _isLoading.Value;
|
||||||
|
|
||||||
|
public CategoriesViewModel CategoriesViewModel { get; }
|
||||||
|
|
||||||
|
public List<EntryListViewModel>? Entries
|
||||||
|
{
|
||||||
|
get => _entries;
|
||||||
|
set => RaiseAndSetIfChanged(ref _entries, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Page
|
||||||
|
{
|
||||||
|
get => _page;
|
||||||
|
set => RaiseAndSetIfChanged(ref _page, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int LoadedPage
|
||||||
|
{
|
||||||
|
get => _loadedPage;
|
||||||
|
set => RaiseAndSetIfChanged(ref _loadedPage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TotalPages
|
||||||
|
{
|
||||||
|
get => _totalPages;
|
||||||
|
set => RaiseAndSetIfChanged(ref _totalPages, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int EntriesPerPage
|
||||||
|
{
|
||||||
|
get => _entriesPerPage;
|
||||||
|
set => RaiseAndSetIfChanged(ref _entriesPerPage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Page = Math.Max(1, parameters.Page);
|
||||||
|
|
||||||
|
// Throttle page changes
|
||||||
|
await Task.Delay(200, cancellationToken);
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
await Query(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Query(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EntryFilterInput filter = GetFilter();
|
||||||
|
IOperationResult<IGetEntriesResult> entries = await _workshopClient.GetEntries.ExecuteAsync(filter, EntriesPerPage * (Page - 1), EntriesPerPage, cancellationToken);
|
||||||
|
entries.EnsureNoErrors();
|
||||||
|
|
||||||
|
if (entries.Data?.Entries?.Items != null)
|
||||||
|
{
|
||||||
|
Entries = entries.Data.Entries.Items.Select(n => new EntryListViewModel(n, _router)).ToList();
|
||||||
|
TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TotalPages = 1;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_notificationService.CreateNotification()
|
||||||
|
.WithTitle("Failed to load entries")
|
||||||
|
.WithMessage(e.Message)
|
||||||
|
.WithSeverity(NotificationSeverity.Error)
|
||||||
|
.Show();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LoadedPage = Page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntryFilterInput GetFilter()
|
||||||
|
{
|
||||||
|
EntryFilterInput filter = new()
|
||||||
|
{
|
||||||
|
EntryType = new EntryTypeOperationFilterInput {Eq = WebClient.Workshop.EntryType.Profile},
|
||||||
|
And = CategoriesViewModel.CategoryFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType => null;
|
||||||
|
}
|
||||||
57
src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml
Normal file
57
src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml
Normal 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:search="clr-namespace:Artemis.UI.Screens.Workshop.Search"
|
||||||
|
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
|
||||||
|
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Search.SearchView"
|
||||||
|
x:DataType="search:SearchViewModel">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="SearchViewStyles.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Panel>
|
||||||
|
<AutoCompleteBox Name="SearchBox"
|
||||||
|
MaxWidth="500"
|
||||||
|
Watermark="Search"
|
||||||
|
Margin="0 5"
|
||||||
|
ValueMemberBinding="{CompiledBinding Name, DataType=workshop:ISearchEntries_SearchEntries}"
|
||||||
|
AsyncPopulator="{CompiledBinding SearchAsync}"
|
||||||
|
SelectedItem="{CompiledBinding SelectedEntry}"
|
||||||
|
FilterMode="None"
|
||||||
|
windowing:AppWindow.AllowInteractionInTitleBar="True">
|
||||||
|
<AutoCompleteBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="workshop:ISearchEntries_SearchEntries">
|
||||||
|
<Panel>
|
||||||
|
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{CompiledBinding Name}" />
|
||||||
|
<TextBlock Text="{CompiledBinding Summary}" Foreground="{DynamicResource TextFillColorSecondary}" />
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding Categories}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5"></StackPanel>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Classes="category">
|
||||||
|
<TextBlock Text="{CompiledBinding Name}"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</AutoCompleteBox.ItemTemplate>
|
||||||
|
</AutoCompleteBox>
|
||||||
|
<ContentControl HorizontalAlignment="Right"
|
||||||
|
Width="28"
|
||||||
|
Height="28"
|
||||||
|
Margin="0 0 50 0"
|
||||||
|
Content="{CompiledBinding CurrentUserViewModel}"
|
||||||
|
windowing:AppWindow.AllowInteractionInTitleBar="True"/>
|
||||||
|
</Panel>
|
||||||
|
</UserControl>
|
||||||
18
src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs
Normal file
18
src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Search;
|
||||||
|
|
||||||
|
public partial class SearchView : UserControl
|
||||||
|
{
|
||||||
|
public SearchView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Artemis.UI/Screens/Workshop/Search/SearchViewModel.cs
Normal file
67
src/Artemis.UI/Screens/Workshop/Search/SearchViewModel.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using ReactiveUI;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Search;
|
||||||
|
|
||||||
|
public class SearchViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public CurrentUserViewModel CurrentUserViewModel { get; }
|
||||||
|
private readonly IRouter _router;
|
||||||
|
private readonly IWorkshopClient _workshopClient;
|
||||||
|
private EntryType? _entryType;
|
||||||
|
private ISearchEntries_SearchEntries? _selectedEntry;
|
||||||
|
|
||||||
|
public SearchViewModel(IWorkshopClient workshopClient, IRouter router, CurrentUserViewModel currentUserViewModel)
|
||||||
|
{
|
||||||
|
CurrentUserViewModel = currentUserViewModel;
|
||||||
|
_workshopClient = workshopClient;
|
||||||
|
_router = router;
|
||||||
|
SearchAsync = ExecuteSearchAsync;
|
||||||
|
|
||||||
|
this.WhenAnyValue(vm => vm.SelectedEntry).WhereNotNull().Subscribe(NavigateToEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<string?, CancellationToken, Task<IEnumerable<object>>> SearchAsync { get; }
|
||||||
|
|
||||||
|
public ISearchEntries_SearchEntries? SelectedEntry
|
||||||
|
{
|
||||||
|
get => _selectedEntry;
|
||||||
|
set => RaiseAndSetIfChanged(ref _selectedEntry, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType? EntryType
|
||||||
|
{
|
||||||
|
get => _entryType;
|
||||||
|
set => RaiseAndSetIfChanged(ref _entryType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToEntry(ISearchEntries_SearchEntries entry)
|
||||||
|
{
|
||||||
|
string? url = null;
|
||||||
|
if (entry.EntryType == WebClient.Workshop.EntryType.Profile)
|
||||||
|
url = $"workshop/profiles/{entry.Id}";
|
||||||
|
if (entry.EntryType == WebClient.Workshop.EntryType.Layout)
|
||||||
|
url = $"workshop/layouts/{entry.Id}";
|
||||||
|
|
||||||
|
if (url != null)
|
||||||
|
Task.Run(() => _router.Navigate(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<object>> ExecuteSearchAsync(string? input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return new List<object>();
|
||||||
|
|
||||||
|
IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(input, EntryType, cancellationToken);
|
||||||
|
return results.Data?.SearchEntries.Cast<object>() ?? new List<object>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="10">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<Border Classes="category">
|
||||||
|
<TextBlock Text="Media"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="category">
|
||||||
|
<TextBlock Text="Audio"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="category">
|
||||||
|
<TextBlock Text="Interaction"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<!-- Add Styles Here -->
|
||||||
|
<Style Selector="Border.category">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlSolidFillColorDefaultBrush}" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
<Setter Property="Padding" Value="6 1"></Setter>
|
||||||
|
<Setter Property="TextBlock.FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<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"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock>Welcome to the Workshop Submission Wizard 🧙</TextBlock>
|
||||||
|
<TextBlock>Here we'll take you, step by step, through the process of uploading your submission to the workshop.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||||
|
|
||||||
|
public partial class WelcomeStepView : ReactiveUserControl<WelcomeStepViewModel>
|
||||||
|
{
|
||||||
|
public WelcomeStepView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||||
|
|
||||||
|
public class WelcomeStepViewModel : SubmissionViewModel
|
||||||
|
{
|
||||||
|
#region Overrides of SubmissionViewModel
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override ReactiveCommand<Unit, Unit> Continue { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
|
||||||
|
|
||||||
|
public WelcomeStepViewModel()
|
||||||
|
{
|
||||||
|
ShowGoBack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:submissionWizard="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:ui="clr-namespace:Artemis.UI"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.SubmissionWizardView"
|
||||||
|
x:DataType="submissionWizard:SubmissionWizardViewModel"
|
||||||
|
Icon="/Assets/Images/Logo/application.ico"
|
||||||
|
Title="Artemis | Workshop submission wizard"
|
||||||
|
Width="1000"
|
||||||
|
Height="735"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto" Margin="0 0 0 15">
|
||||||
|
<ContentControl Grid.Column="0" Grid.RowSpan="2" Width="65" Height="65" VerticalAlignment="Center" Margin="0 0 20 0" Content="{CompiledBinding CurrentUserViewModel}"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="{CompiledBinding CurrentUserViewModel.Name, FallbackValue=Not logged in}"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Orientation="Horizontal">
|
||||||
|
<controls:HyperlinkButton Classes="icon-button" ToolTip.Tip="View Wiki" NavigateUri="https://wiki.artemis-rgb.com?mtm_campaign=artemis&mtm_kwd=workshop-wizard">
|
||||||
|
<avalonia:MaterialIcon Kind="BookOpenOutline" />
|
||||||
|
</controls:HyperlinkButton>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Classes="subtitle"
|
||||||
|
Text="New workshop submission" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Classes="card" Grid.Row="1" Grid.Column="0">
|
||||||
|
<controls:Frame Name="Frame" IsNavigationStackEnabled="False" CacheSize="0">
|
||||||
|
<controls:Frame.NavigationPageFactory>
|
||||||
|
<ui:PageFactory/>
|
||||||
|
</controls:Frame.NavigationPageFactory>
|
||||||
|
</controls:Frame>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Margin="0 15 0 0">
|
||||||
|
<Button Command="{CompiledBinding Screen.GoBack}" IsVisible="{CompiledBinding Screen.ShowGoBack}">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80">
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Window>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
|
||||||
|
|
||||||
|
public partial class SubmissionWizardView : ReactiveAppWindow<SubmissionWizardViewModel>
|
||||||
|
{
|
||||||
|
public SubmissionWizardView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Navigate(SubmissionViewModel viewModel)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(() => Frame.NavigateFromObject(viewModel));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
|
||||||
|
|
||||||
|
public class SubmissionWizardViewModel : DialogViewModelBase<bool>
|
||||||
|
{
|
||||||
|
private SubmissionViewModel _screen;
|
||||||
|
|
||||||
|
public SubmissionWizardViewModel(CurrentUserViewModel currentUserViewModel)
|
||||||
|
{
|
||||||
|
_screen = new WelcomeStepViewModel();
|
||||||
|
CurrentUserViewModel = currentUserViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrentUserViewModel CurrentUserViewModel { get; }
|
||||||
|
|
||||||
|
public SubmissionViewModel Screen
|
||||||
|
{
|
||||||
|
get => _screen;
|
||||||
|
set => RaiseAndSetIfChanged(ref _screen, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class SubmissionViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private bool _showFinish;
|
||||||
|
private bool _showGoBack = true;
|
||||||
|
|
||||||
|
public abstract ReactiveCommand<Unit, Unit> Continue { get; }
|
||||||
|
public abstract ReactiveCommand<Unit, Unit> GoBack { get; }
|
||||||
|
|
||||||
|
public bool ShowGoBack
|
||||||
|
{
|
||||||
|
get => _showGoBack;
|
||||||
|
set => RaiseAndSetIfChanged(ref _showGoBack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowFinish
|
||||||
|
{
|
||||||
|
get => _showFinish;
|
||||||
|
set => RaiseAndSetIfChanged(ref _showFinish, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,76 +2,15 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:builders="clr-namespace:Artemis.UI.Shared.Services.Builders;assembly=Artemis.UI.Shared"
|
|
||||||
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
|
||||||
xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared"
|
|
||||||
xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop"
|
xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop"
|
||||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
xmlns:ui="clr-namespace:Artemis.UI"
|
||||||
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
|
|
||||||
xmlns:materialIconPicker="clr-namespace:Artemis.UI.Shared.MaterialIconPicker;assembly=Artemis.UI.Shared"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800"
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
|
x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
|
||||||
x:DataType="workshop:WorkshopViewModel">
|
x:DataType="workshop:WorkshopViewModel">
|
||||||
<Border Classes="router-container">
|
<controls:Frame Name="WorkshopFrame" IsNavigationStackEnabled="False" CacheSize="0">
|
||||||
<ScrollViewer>
|
<controls:Frame.NavigationPageFactory>
|
||||||
<StackPanel Margin="12" Spacing="5">
|
<ui:PageFactory/>
|
||||||
<Border Classes="card">
|
</controls:Frame.NavigationPageFactory>
|
||||||
<StackPanel Spacing="5">
|
</controls:Frame>
|
||||||
<TextBlock Classes="h4">Navigation test</TextBlock>
|
|
||||||
<TextBox Text="{CompiledBinding NavigationPath}" Watermark="Enter a navigation path"></TextBox>
|
|
||||||
<Button Command="{CompiledBinding TestNavigation}">
|
|
||||||
Navigate
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<TextBlock Classes="h4">Notification tests</TextBlock>
|
|
||||||
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Informational}">
|
|
||||||
Notification test (default)
|
|
||||||
</Button>
|
|
||||||
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Warning}">
|
|
||||||
Notification test (warning)
|
|
||||||
</Button>
|
|
||||||
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Error}">
|
|
||||||
Notification test (error)
|
|
||||||
</Button>
|
|
||||||
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Success}">
|
|
||||||
Notification test (success)
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<shared:HotkeyBox Watermark="Some hotkey" Width="250" HorizontalAlignment="Left" />
|
|
||||||
|
|
||||||
<controls1:NumberBox
|
|
||||||
attachedProperties:NumberBoxAssist.PrefixText="$"
|
|
||||||
attachedProperties:NumberBoxAssist.SuffixText="%">
|
|
||||||
</controls1:NumberBox>
|
|
||||||
|
|
||||||
<TextBox
|
|
||||||
attachedProperties:TextBoxAssist.PrefixText="$"
|
|
||||||
attachedProperties:TextBoxAssist.SuffixText="%">
|
|
||||||
</TextBox>
|
|
||||||
|
|
||||||
<TextBlock Classes="h4" Text="{CompiledBinding TestValue}"/>
|
|
||||||
<controls1:NumberBox Value="{CompiledBinding TestValue}"/>
|
|
||||||
<controls:DraggableNumberBox Value="{CompiledBinding TestValue}"/>
|
|
||||||
<controls:DraggableNumberBox Value="{CompiledBinding TestValue}" Classes="condensed"/>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
|
||||||
<Border Classes="card" Cursor="{CompiledBinding Cursor}">
|
|
||||||
<TextBlock Text="{CompiledBinding SelectedCursor}"></TextBlock>
|
|
||||||
</Border>
|
|
||||||
<shared:EnumComboBox Value="{CompiledBinding SelectedCursor}"></shared:EnumComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button Command="{CompiledBinding CreateRandomGradient}">
|
|
||||||
Create random gradient
|
|
||||||
</Button>
|
|
||||||
<gradientPicker:GradientPickerButton ColorGradient="{CompiledBinding ColorGradient}" IsCompact="True" />
|
|
||||||
|
|
||||||
<materialIconPicker:MaterialIconPickerButton Name="IconPicker" Value="Abc"/>
|
|
||||||
<TextBlock Text="{CompiledBinding #IconPicker.Value}"></TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,5 +1,8 @@
|
|||||||
using Avalonia.Markup.Xaml;
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop;
|
namespace Artemis.UI.Screens.Workshop;
|
||||||
|
|
||||||
@ -8,6 +11,6 @@ public partial class WorkshopView : ReactiveUserControl<WorkshopViewModel>
|
|||||||
public WorkshopView()
|
public WorkshopView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(vm => WorkshopFrame.NavigateFromObject(vm ?? ViewModel?.HomeViewModel)).DisposeWith(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,82 +1,38 @@
|
|||||||
using System.Reactive;
|
using System;
|
||||||
using System.Reactive.Linq;
|
using System.Threading;
|
||||||
using Artemis.Core;
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Home;
|
||||||
|
using Artemis.UI.Screens.Workshop.Search;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using ReactiveUI;
|
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop;
|
namespace Artemis.UI.Screens.Workshop;
|
||||||
|
|
||||||
public class WorkshopViewModel : ActivatableViewModelBase, IMainScreenViewModel
|
public class WorkshopViewModel : RoutableScreen<IWorkshopViewModel>, IMainScreenViewModel
|
||||||
{
|
{
|
||||||
private readonly ObservableAsPropertyHelper<Cursor> _cursor;
|
private readonly SearchViewModel _searchViewModel;
|
||||||
private readonly INotificationService _notificationService;
|
|
||||||
|
|
||||||
private ColorGradient _colorGradient = new()
|
public WorkshopViewModel(SearchViewModel searchViewModel, WorkshopHomeViewModel homeViewModel)
|
||||||
{
|
{
|
||||||
new ColorGradientStop(new SKColor(0xFFFF6D00), 0f),
|
_searchViewModel = searchViewModel;
|
||||||
new ColorGradientStop(new SKColor(0xFFFE6806), 0.2f),
|
|
||||||
new ColorGradientStop(new SKColor(0xFFEF1788), 0.4f),
|
TitleBarViewModel = searchViewModel;
|
||||||
new ColorGradientStop(new SKColor(0xFFEF1788), 0.6f),
|
HomeViewModel = homeViewModel;
|
||||||
new ColorGradientStop(new SKColor(0xFF00FCCC), 0.8f),
|
|
||||||
new ColorGradientStop(new SKColor(0xFF00FCCC), 1f)
|
|
||||||
};
|
|
||||||
|
|
||||||
private StandardCursorType _selectedCursor;
|
|
||||||
private double _testValue;
|
|
||||||
private string? _navigationPath;
|
|
||||||
|
|
||||||
public WorkshopViewModel(INotificationService notificationService, IRouter router)
|
|
||||||
{
|
|
||||||
_notificationService = notificationService;
|
|
||||||
_cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor);
|
|
||||||
|
|
||||||
DisplayName = "Workshop";
|
|
||||||
ShowNotification = ReactiveCommand.Create<NotificationSeverity>(ExecuteShowNotification);
|
|
||||||
TestNavigation = ReactiveCommand.CreateFromTask(async () => await router.Navigate(NavigationPath!), this.WhenAnyValue(vm => vm.NavigationPath).Select(p => !string.IsNullOrWhiteSpace(p)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewModelBase? TitleBarViewModel => null;
|
public ViewModelBase TitleBarViewModel { get; }
|
||||||
public ReactiveCommand<NotificationSeverity, Unit> ShowNotification { get; set; }
|
public WorkshopHomeViewModel HomeViewModel { get; }
|
||||||
public ReactiveCommand<Unit, Unit> TestNavigation { get; set; }
|
|
||||||
|
|
||||||
public StandardCursorType SelectedCursor
|
/// <inheritdoc />
|
||||||
|
public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
get => _selectedCursor;
|
_searchViewModel.EntryType = Screen?.EntryType;
|
||||||
set => RaiseAndSetIfChanged(ref _selectedCursor, value);
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor Cursor => _cursor.Value;
|
public interface IWorkshopViewModel
|
||||||
|
{
|
||||||
public string? NavigationPath
|
public EntryType? EntryType { get; }
|
||||||
{
|
|
||||||
get => _navigationPath;
|
|
||||||
set => RaiseAndSetIfChanged(ref _navigationPath, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ColorGradient ColorGradient
|
|
||||||
{
|
|
||||||
get => _colorGradient;
|
|
||||||
set => RaiseAndSetIfChanged(ref _colorGradient, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double TestValue
|
|
||||||
{
|
|
||||||
get => _testValue;
|
|
||||||
set => RaiseAndSetIfChanged(ref _testValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateRandomGradient()
|
|
||||||
{
|
|
||||||
ColorGradient = ColorGradient.GetRandom(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteShowNotification(NotificationSeverity severity)
|
|
||||||
{
|
|
||||||
_notificationService.CreateNotification().WithTitle("Test title").WithMessage("Test message").WithSeverity(severity).Show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -8,4 +8,12 @@
|
|||||||
|
|
||||||
<!-- <FluentTheme Mode="Dark"></FluentTheme> -->
|
<!-- <FluentTheme Mode="Dark"></FluentTheme> -->
|
||||||
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
|
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
|
||||||
|
|
||||||
|
<Styles.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<MergeResourceInclude Source="TreeView.axaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
||||||
121
src/Artemis.UI/Styles/TreeView.axaml
Normal file
121
src/Artemis.UI/Styles/TreeView.axaml
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
x:CompileBindings="True">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="30" MinWidth="350">
|
||||||
|
<TreeView ItemContainerTheme="{StaticResource MenuTreeViewItem}">
|
||||||
|
<TreeViewItem>
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="Home" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Home" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
</TreeViewItem>
|
||||||
|
<TreeViewItem IsExpanded="True">
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="TestTube" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Workshop" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
<TreeViewItem>
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="FolderVideo" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Profiles" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
</TreeViewItem>
|
||||||
|
<TreeViewItem>
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="KeyboardVariant" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Layouts" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
</TreeViewItem>
|
||||||
|
</TreeViewItem>
|
||||||
|
<TreeViewItem>
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="Devices" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Surface Editor" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
</TreeViewItem>
|
||||||
|
<TreeViewItem>
|
||||||
|
<TreeViewItem.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="Cog" Width="16" Height="16" />
|
||||||
|
<TextBlock FontSize="12" Margin="10 0" VerticalAlignment="Center" Text="Settings" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeViewItem.Header>
|
||||||
|
</TreeViewItem>
|
||||||
|
</TreeView>
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<x:Double x:Key="TreeViewItemIndent">31</x:Double>
|
||||||
|
<x:Double x:Key="TreeViewItemExpandCollapseChevronSize">12</x:Double>
|
||||||
|
<Thickness x:Key="TreeViewItemExpandCollapseChevronMargin">12, 0, 12, 0</Thickness>
|
||||||
|
<converters:MarginMultiplierConverter Indent="{StaticResource TreeViewItemIndent}"
|
||||||
|
Left="True"
|
||||||
|
x:Key="TreeViewItemLeftMarginConverter" />
|
||||||
|
|
||||||
|
<ControlTheme TargetType="TreeViewItem" x:Key="MenuTreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<StackPanel>
|
||||||
|
<Border Name="PART_LayoutRoot"
|
||||||
|
Classes="TreeViewItemLayoutRoot"
|
||||||
|
Focusable="True"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
MinHeight="{DynamicResource NavigationViewItemOnLeftMinHeight}"
|
||||||
|
CornerRadius="{DynamicResource ControlCornerRadius}"
|
||||||
|
TemplatedControl.IsTemplateFocusTarget="True"
|
||||||
|
Margin="2">
|
||||||
|
<Panel>
|
||||||
|
<Grid Name="PART_Header"
|
||||||
|
ColumnDefinitions="12, *, Auto"
|
||||||
|
Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
|
||||||
|
<Rectangle Name="SelectionIndicator"
|
||||||
|
Width="3"
|
||||||
|
Height="16"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
RadiusX="2"
|
||||||
|
RadiusY="2"
|
||||||
|
IsVisible="False"
|
||||||
|
Fill="{DynamicResource TreeViewItemSelectionIndicatorForeground}" />
|
||||||
|
|
||||||
|
<ContentPresenter Name="PART_HeaderPresenter"
|
||||||
|
Grid.Column="1"
|
||||||
|
Focusable="False"
|
||||||
|
Content="{TemplateBinding Header}"
|
||||||
|
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||||
|
Margin="{TemplateBinding Padding}" />
|
||||||
|
<Panel Name="PART_ExpandCollapseChevronContainer" Grid.Column="2">
|
||||||
|
<ToggleButton Name="PART_ExpandCollapseChevron"
|
||||||
|
Theme="{StaticResource TreeViewChevronButton}"
|
||||||
|
Focusable="False"
|
||||||
|
Margin="{StaticResource TreeViewItemExpandCollapseChevronMargin}"
|
||||||
|
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
|
||||||
|
</Panel>
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
<ItemsPresenter Name="PART_ItemsPresenter"
|
||||||
|
IsVisible="{TemplateBinding IsExpanded}"
|
||||||
|
ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Untitled GraphQL Schema",
|
|
||||||
"schemaPath": "schema.graphql",
|
|
||||||
"extensions": {
|
|
||||||
"endpoints": {
|
|
||||||
"Default GraphQL Endpoint": {
|
|
||||||
"url": "https://updating.artemis-rgb.com/graphql",
|
|
||||||
"headers": {
|
|
||||||
"user-agent": "JS GraphQL"
|
|
||||||
},
|
|
||||||
"introspect": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,4 +14,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
<PackageReference Include="StrawberryShake.Server" Version="13.0.5" />
|
<PackageReference Include="StrawberryShake.Server" Version="13.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove=".graphqlconfig" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
8
src/Artemis.WebClient.Updating/graphql.config.yml
Normal file
8
src/Artemis.WebClient.Updating/graphql.config.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
schema: schema.graphql
|
||||||
|
extensions:
|
||||||
|
endpoints:
|
||||||
|
Default GraphQL Endpoint:
|
||||||
|
url: https://updating.artemis-rgb.com/graphql
|
||||||
|
headers:
|
||||||
|
user-agent: JS GraphQL
|
||||||
|
introspect: true
|
||||||
@ -1,13 +0,0 @@
|
|||||||
scalar _KeyFieldSet
|
|
||||||
|
|
||||||
directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT
|
|
||||||
|
|
||||||
directive @serializationType(name: String!) on SCALAR
|
|
||||||
|
|
||||||
directive @runtimeType(name: String!) on SCALAR
|
|
||||||
|
|
||||||
directive @enumValue(value: String!) on ENUM_VALUE
|
|
||||||
|
|
||||||
directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE
|
|
||||||
|
|
||||||
extend schema @key(fields: "id")
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# This file was generated based on ".graphqlconfig". Do not edit manually.
|
# This file was generated. Do not edit manually.
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
@ -107,10 +107,11 @@ type Release {
|
|||||||
|
|
||||||
type ReleaseStatistic {
|
type ReleaseStatistic {
|
||||||
count: Int!
|
count: Int!
|
||||||
|
date: Date!
|
||||||
lastReportedUsage: DateTime!
|
lastReportedUsage: DateTime!
|
||||||
linuxCount: Int!
|
linuxCount: Int!
|
||||||
osxCount: Int!
|
osxCount: Int!
|
||||||
releaseId: UUID!
|
release: Release
|
||||||
windowsCount: Int!
|
windowsCount: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +145,9 @@ enum SortEnumType {
|
|||||||
DESC
|
DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"The `Date` scalar represents an ISO-8601 compliant date type."
|
||||||
|
scalar Date
|
||||||
|
|
||||||
"The `DateTime` scalar represents an ISO-8601 compliant date time type."
|
"The `DateTime` scalar represents an ISO-8601 compliant date time type."
|
||||||
scalar DateTime
|
scalar DateTime
|
||||||
|
|
||||||
@ -176,6 +180,21 @@ input BooleanOperationFilterInput {
|
|||||||
neq: Boolean
|
neq: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input DateOperationFilterInput {
|
||||||
|
eq: Date
|
||||||
|
gt: Date
|
||||||
|
gte: Date
|
||||||
|
in: [Date]
|
||||||
|
lt: Date
|
||||||
|
lte: Date
|
||||||
|
neq: Date
|
||||||
|
ngt: Date
|
||||||
|
ngte: Date
|
||||||
|
nin: [Date]
|
||||||
|
nlt: Date
|
||||||
|
nlte: Date
|
||||||
|
}
|
||||||
|
|
||||||
input DateTimeOperationFilterInput {
|
input DateTimeOperationFilterInput {
|
||||||
eq: DateTime
|
eq: DateTime
|
||||||
gt: DateTime
|
gt: DateTime
|
||||||
@ -265,20 +284,22 @@ input ReleaseSortInput {
|
|||||||
input ReleaseStatisticFilterInput {
|
input ReleaseStatisticFilterInput {
|
||||||
and: [ReleaseStatisticFilterInput!]
|
and: [ReleaseStatisticFilterInput!]
|
||||||
count: IntOperationFilterInput
|
count: IntOperationFilterInput
|
||||||
|
date: DateOperationFilterInput
|
||||||
lastReportedUsage: DateTimeOperationFilterInput
|
lastReportedUsage: DateTimeOperationFilterInput
|
||||||
linuxCount: IntOperationFilterInput
|
linuxCount: IntOperationFilterInput
|
||||||
or: [ReleaseStatisticFilterInput!]
|
or: [ReleaseStatisticFilterInput!]
|
||||||
osxCount: IntOperationFilterInput
|
osxCount: IntOperationFilterInput
|
||||||
releaseId: UuidOperationFilterInput
|
release: ReleaseFilterInput
|
||||||
windowsCount: IntOperationFilterInput
|
windowsCount: IntOperationFilterInput
|
||||||
}
|
}
|
||||||
|
|
||||||
input ReleaseStatisticSortInput {
|
input ReleaseStatisticSortInput {
|
||||||
count: SortEnumType
|
count: SortEnumType
|
||||||
|
date: SortEnumType
|
||||||
lastReportedUsage: SortEnumType
|
lastReportedUsage: SortEnumType
|
||||||
linuxCount: SortEnumType
|
linuxCount: SortEnumType
|
||||||
osxCount: SortEnumType
|
osxCount: SortEnumType
|
||||||
releaseId: SortEnumType
|
release: ReleaseSortInput
|
||||||
windowsCount: SortEnumType
|
windowsCount: SortEnumType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/Artemis.WebClient.Workshop/.config/dotnet-tools.json
Normal file
12
src/Artemis.WebClient.Workshop/.config/dotnet-tools.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"strawberryshake.tools": {
|
||||||
|
"version": "13.0.0-rc.4",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-graphql"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Artemis.WebClient.Workshop/.graphqlrc.json
Normal file
22
src/Artemis.WebClient.Workshop/.graphqlrc.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schema": "schema.graphql",
|
||||||
|
"documents": "**/*.graphql",
|
||||||
|
"extensions": {
|
||||||
|
"strawberryShake": {
|
||||||
|
"name": "WorkshopClient",
|
||||||
|
"namespace": "Artemis.WebClient.Workshop",
|
||||||
|
"url": "https://workshop.artemis-rgb.com/graphql/",
|
||||||
|
"emitGeneratedCode": false,
|
||||||
|
"records": {
|
||||||
|
"inputs": false,
|
||||||
|
"entities": false
|
||||||
|
},
|
||||||
|
"transportProfiles": [
|
||||||
|
{
|
||||||
|
"default": "Http",
|
||||||
|
"subscription": "WebSocket"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<Import Project="..\Artemis.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
|
||||||
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
|
<PackageReference Include="IdentityModel" Version="6.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
|
<PackageReference Include="ReactiveUI" Version="18.4.26" />
|
||||||
|
<PackageReference Include="StrawberryShake.Server" Version="13.0.5" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.31.0" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove=".graphqlconfig" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<GraphQL Update="Queries\SearchEntries.graphql">
|
||||||
|
<Generator>MSBuild:GenerateGraphQLCode</Generator>
|
||||||
|
</GraphQL>
|
||||||
|
<GraphQL Update="Queries\GetCategories.graphql">
|
||||||
|
<Generator>MSBuild:GenerateGraphQLCode</Generator>
|
||||||
|
</GraphQL>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
38
src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs
Normal file
38
src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Artemis.WebClient.Workshop.Repositories;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using DryIoc;
|
||||||
|
using DryIoc.Microsoft.DependencyInjection;
|
||||||
|
using IdentityModel.Client;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.DryIoc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class ContainerExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the updating client into the container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="container">The builder building the current container</param>
|
||||||
|
public static void RegisterWorkshopClient(this IContainer container)
|
||||||
|
{
|
||||||
|
ServiceCollection serviceCollection = new();
|
||||||
|
serviceCollection
|
||||||
|
.AddHttpClient()
|
||||||
|
.AddWorkshopClient()
|
||||||
|
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton<IDiscoveryCache>(r =>
|
||||||
|
{
|
||||||
|
IHttpClientFactory factory = r.GetRequiredService<IHttpClientFactory>();
|
||||||
|
return new DiscoveryCache(WorkshopConstants.AUTHORITY_URL, () => factory.CreateClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
container.WithDependencyInjectionAdapter(serviceCollection);
|
||||||
|
|
||||||
|
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
|
||||||
|
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Artemis.WebClient.Workshop.Entities;
|
||||||
|
|
||||||
|
public class RefreshTokenEntity
|
||||||
|
{
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An exception thrown when a web client related error occurs
|
||||||
|
/// </summary>
|
||||||
|
public class ArtemisWebClientException : Exception
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException(string? message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException(string? message, Exception? innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
using System.Reactive.Linq;
|
||||||
|
using ReactiveUI;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Extensions;
|
||||||
|
|
||||||
|
public static class ReactiveExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Projects the data of the provided operation result into a new observable sequence if the result is successfull and
|
||||||
|
/// contains data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">A sequence of operation results to invoke a transform function on.</param>
|
||||||
|
/// <param name="selector">A transform function to apply to the data of each source element.</param>
|
||||||
|
/// <typeparam name="TSource">The type of data contained in the operation result.</typeparam>
|
||||||
|
/// <typeparam name="TResult">The type of data to project from the result.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// An observable sequence whose elements are the result of invoking the transform function on each element of
|
||||||
|
/// source.
|
||||||
|
/// </returns>
|
||||||
|
public static IObservable<TResult> SelectOperationResult<TSource, TResult>(this IObservable<IOperationResult<TSource>> source, Func<TSource, TResult?> selector) where TSource : class
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.Where(s => !s.Errors.Any())
|
||||||
|
.Select(s => s.Data)
|
||||||
|
.WhereNotNull()
|
||||||
|
.Select(selector)
|
||||||
|
.WhereNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
query GetCategories {
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql
Normal file
18
src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
query GetEntries($filter: EntryFilterInput $skip: Int $take: Int) {
|
||||||
|
entries(where: $filter skip: $skip take: $take) {
|
||||||
|
totalCount
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
author
|
||||||
|
name
|
||||||
|
summary
|
||||||
|
entryType
|
||||||
|
downloads
|
||||||
|
createdAt
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
query GetEntryById($id: UUID!) {
|
||||||
|
entry(id: $id) {
|
||||||
|
author
|
||||||
|
name
|
||||||
|
entryType
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user