1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Added workshop home, changed VM routing structure

This commit is contained in:
Robert 2023-07-08 21:44:39 +02:00
parent 99a365be0b
commit 65f81ab768
34 changed files with 667 additions and 75 deletions

View File

@ -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)

View File

@ -70,7 +70,17 @@ 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();
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
}
if (CancelIfRequested(args, "ChangeScreen", screen)) if (CancelIfRequested(args, "ChangeScreen", screen))
return; return;
@ -93,8 +103,16 @@ internal class Navigation
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;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -15,7 +15,7 @@
<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>

View File

@ -16,15 +16,17 @@ 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")
new RouteRegistration<WorkshopViewModel>("workshop"), {
new RouteRegistration<ProfileListViewModel>("workshop/profiles/{page:int}"), Children = new List<IRouterRegistration>()
new RouteRegistration<ProfileDetailsViewModel>("workshop/profiles/{entryId:guid}"), {
new RouteRegistration<LayoutListViewModel>("workshop/layouts/{page:int}"), new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
new RouteRegistration<LayoutDetailsViewModel>("workshop/layouts/{entryId:guid}"), new RouteRegistration<ProfileDetailsViewModel>("profiles/{entryId:guid}"),
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
new RouteRegistration<LayoutDetailsViewModel>("layouts/{entryId:guid}")
}
},
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"), new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
new RouteRegistration<SettingsViewModel>("settings") new RouteRegistration<SettingsViewModel>("settings")
{ {
Children = new List<IRouterRegistration> Children = new List<IRouterRegistration>

View File

@ -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">

View File

@ -7,7 +7,7 @@
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"
x:DataType="vm:SidebarScreenViewModel"> x:DataType="vm:SidebarScreenViewModel">
<StackPanel Orientation="Horizontal" Background="Transparent" PointerReleased="InputElement_OnPointerReleased" DoubleTapped="InputElement_OnDoubleTapped"> <StackPanel Orientation="Horizontal" Height="34" Background="Transparent" PointerPressed="InputElement_OnPointerPressed" DoubleTapped="InputElement_OnDoubleTapped" >
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Width="18" Height="18" /> <avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Width="18" Height="18" />
<TextBlock Margin="10 0" VerticalAlignment="Center" FontSize="13" Text="{CompiledBinding DisplayName}" /> <TextBlock Margin="10 0" VerticalAlignment="Center" FontSize="13" Text="{CompiledBinding DisplayName}" />
</StackPanel> </StackPanel>

View File

@ -1,6 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Sidebar; namespace Artemis.UI.Screens.Sidebar;
@ -14,12 +12,18 @@ public partial class SidebarScreenView : ReactiveUserControl<SidebarScreenViewMo
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e) private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
// if (ViewModel != null) if (ViewModel != null)
// ViewModel.IsExpanded = !ViewModel.IsExpanded; ViewModel.IsExpanded = !ViewModel.IsExpanded;
} }
private void InputElement_OnDoubleTapped(object? sender, TappedEventArgs e) private void InputElement_OnDoubleTapped(object? sender, TappedEventArgs e)
{ {
e.Handled = true; e.Handled = true;
} }
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel != null)
ViewModel.IsExpanded = !ViewModel.IsExpanded;
}
} }

View File

@ -2,11 +2,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Avalonia.Threading;
using Material.Icons; using Material.Icons;
using ReactiveUI;
namespace Artemis.UI.Screens.Sidebar; namespace Artemis.UI.Screens.Sidebar;
@ -22,7 +18,7 @@ public class SidebarScreenViewModel : ViewModelBase
DisplayName = displayName; DisplayName = displayName;
Screens = screens ?? new ObservableCollection<SidebarScreenViewModel>(); 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 string RootPath { get; }
@ -56,15 +52,12 @@ public class SidebarScreenViewModel : ViewModelBase
public void ExpandIfRequired(SidebarScreenViewModel selected) public void ExpandIfRequired(SidebarScreenViewModel selected)
{ {
if (selected == this && Screens.Any()) if (selected == this)
{
IsExpanded = true;
return; return;
}
if (Screens.Contains(selected)) if (Screens.Contains(selected))
IsExpanded = true; IsExpanded = true;
foreach (SidebarScreenViewModel sidebarScreenViewModel in Screens) foreach (SidebarScreenViewModel sidebarScreenViewModel in Screens)
sidebarScreenViewModel.ExpandIfRequired(selected); sidebarScreenViewModel.ExpandIfRequired(selected);
} }

View File

@ -0,0 +1,8 @@
<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.Categories.CategoriesView">
Welcome to Avalonia!
</UserControl>

View File

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
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);
}
}

View File

@ -0,0 +1,14 @@
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Categories;
public class CategoriesViewModel : ActivatableViewModelBase
{
private readonly IWorkshopClient _client;
public CategoriesViewModel(IWorkshopClient client)
{
_client = client;
}
}

View File

@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:home="clr-namespace:Artemis.UI.Screens.Workshop.Home"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
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 Grid.Row="1" Margin="30 -75 30 0" Spacing="10" Orientation="Horizontal" VerticalAlignment="Top">
<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>
</Grid>
</Border>
</UserControl>

View File

@ -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);
}
}

View File

@ -0,0 +1,20 @@
using System.Reactive;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Home;
public class WorkshopHomeViewModel : ActivatableViewModelBase, IWorkshopViewModel
{
public WorkshopHomeViewModel(IRouter router)
{
Navigate = ReactiveCommand.CreateFromTask<string>(async r => await router.Navigate(r));
}
public ReactiveCommand<string, Unit> Navigate { get; set; }
public EntryType? EntryType => null;
}

View File

@ -2,20 +2,24 @@
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"> x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
x:DataType="layout:LayoutDetailsViewModel">
<Border Classes="router-container"> <Border Classes="router-container">
<Grid ColumnDefinitions="300,*" Margin="10"> <Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*" Margin="10">
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0"> <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> <TextBlock>Side panel</TextBlock>
</Border> </Border>
<Border Classes="card-condensed" Grid.Column="1"> <Border Classes="card-condensed" Grid.Row="1" Grid.Column="1">
<TextBlock>Main panel</TextBlock> <TextBlock>Layout details panel</TextBlock>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>

View File

@ -1,8 +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;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Layout; namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutDetailsViewModel : ActivatableViewModelBase 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;
}
} }

View File

@ -1,15 +1,23 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Parameters;
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.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Layout; namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IMainScreenViewModel public class LayoutListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IWorkshopViewModel
{ {
private int _page; private int _page;
/// <inheritdoc />
public LayoutListViewModel()
{
}
public int Page public int Page
{ {
get => _page; get => _page;
@ -22,5 +30,5 @@ public class LayoutListViewModel : RoutableScreen<ActivatableViewModelBase, Work
return Task.CompletedTask; return Task.CompletedTask;
} }
public ViewModelBase? TitleBarViewModel => null; public EntryType? EntryType => WebClient.Workshop.EntryType.Layout;
} }

View File

@ -0,0 +1,8 @@
using System;
namespace Artemis.UI.Screens.Workshop.Parameters;
public class WorkshopDetailParameters
{
public Guid EntryId { get; set; }
}

View File

@ -1,4 +1,4 @@
namespace Artemis.UI.Screens.Workshop; namespace Artemis.UI.Screens.Workshop.Parameters;
public class WorkshopListParameters public class WorkshopListParameters
{ {

View File

@ -2,7 +2,24 @@
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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"> x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
Welcome to Avalonia! x:DataType="profile:ProfileDetailsViewModel">
</UserControl> <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>

View File

@ -1,8 +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;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Profile; namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileDetailsViewModel : ActivatableViewModelBase 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;
}
} }

View File

@ -1,15 +1,24 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Parameters;
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.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Profile; namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IMainScreenViewModel public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IWorkshopViewModel
{ {
private int _page; private int _page;
/// <inheritdoc />
public ProfileListViewModel()
{
}
public int Page public int Page
{ {
get => _page; get => _page;
@ -21,6 +30,6 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
Page = Math.Max(1, parameters.Page); Page = Math.Max(1, parameters.Page);
return Task.CompletedTask; return Task.CompletedTask;
} }
public ViewModelBase? TitleBarViewModel => null; public EntryType? EntryType => null;
} }

View File

@ -0,0 +1,55 @@
<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_Entries_Nodes}"
AsyncPopulator="{CompiledBinding SearchAsync}"
SelectedItem="{CompiledBinding SelectedEntry}"
FilterMode="None"
windowing:AppWindow.AllowInteractionInTitleBar="True">
<AutoCompleteBox.ItemTemplate>
<DataTemplate x:DataType="workshop:ISearchEntries_Entries_Nodes">
<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"
Margin="0 0 50 0"
Content="{CompiledBinding CurrentUserViewModel}"
windowing:AppWindow.AllowInteractionInTitleBar="True"/>
</Panel>
</UserControl>

View 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);
}
}

View File

@ -0,0 +1,80 @@
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_Entries_Nodes? _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_Entries_Nodes? SelectedEntry
{
get => _selectedEntry;
set => RaiseAndSetIfChanged(ref _selectedEntry, value);
}
public EntryType? EntryType
{
get => _entryType;
set => RaiseAndSetIfChanged(ref _entryType, value);
}
private void NavigateToEntry(ISearchEntries_Entries_Nodes 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>();
EntryFilterInput filter;
if (EntryType != null)
filter = new EntryFilterInput
{
And = new[]
{
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}},
new EntryFilterInput {Name = new StringOperationFilterInput {Contains = input}}
}
};
else
filter = new EntryFilterInput {Name = new StringOperationFilterInput {Contains = input}};
IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(filter, cancellationToken);
return results.Data?.Entries?.Nodes?.Cast<object>() ?? new List<object>();
}
}

View File

@ -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>

View File

@ -8,7 +8,9 @@
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">
<TextBlock>Workshop overview</TextBlock> <controls:Frame.NavigationPageFactory>
</Border> <ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</UserControl> </UserControl>

View File

@ -1,4 +1,8 @@
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;
@ -7,5 +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));
} }
} }

View File

@ -1,13 +1,38 @@
using Artemis.UI.Shared; using System;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Search;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop; namespace Artemis.UI.Screens.Workshop;
public class WorkshopViewModel : RoutableScreen<object>, IMainScreenViewModel public class WorkshopViewModel : RoutableScreen<IWorkshopViewModel>, IMainScreenViewModel
{ {
public WorkshopViewModel() private readonly SearchViewModel _searchViewModel;
public WorkshopViewModel(SearchViewModel searchViewModel, WorkshopHomeViewModel homeViewModel)
{ {
_searchViewModel = searchViewModel;
TitleBarViewModel = searchViewModel;
HomeViewModel = homeViewModel;
} }
public ViewModelBase? TitleBarViewModel => null; public ViewModelBase TitleBarViewModel { get; }
public WorkshopHomeViewModel HomeViewModel { get; }
/// <inheritdoc />
public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
_searchViewModel.EntryType = Screen?.EntryType;
return Task.CompletedTask;
}
}
public interface IWorkshopViewModel
{
public EntryType? EntryType { get; }
} }

View File

@ -22,4 +22,13 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" /> <ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<GraphQL Update="Queries\SearchEntries.graphql">
<Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL>
<GraphQL Update="Queries\GetCategories.graphql">
<Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL>
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,7 @@
query GetCategories {
categories {
id
name
icon
}
}

View File

@ -0,0 +1,18 @@
query SearchEntries($filter: EntryFilterInput) {
entries(
first: 10
where: $filter
) {
nodes {
id
name
summary
entryType
categories {
id
name
icon
}
}
}
}

View File

@ -5,6 +5,12 @@ schema {
mutation: Mutation mutation: Mutation
} }
type Category {
icon: String!
id: Int!
name: String!
}
"A connection to a list of items." "A connection to a list of items."
type EntriesConnection { type EntriesConnection {
"A list of edges." "A list of edges."
@ -27,14 +33,18 @@ type EntriesEdge {
type Entry { type Entry {
author: UUID! author: UUID!
categories: [Category!]!
createdAt: DateTime!
description: String! description: String!
downloads: Long!
entryType: EntryType! entryType: EntryType!
icon: Image icon: Image
id: UUID! id: UUID!
images: [Image!]! images: [Image!]!
name: String! name: String!
releases: [Release!]! releases: [Release!]!
tags: [String!]! summary: String!
tags: [Tag!]!
} }
type Image { type Image {
@ -59,6 +69,7 @@ type PageInfo {
} }
type Query { type Query {
categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]!
entries( entries(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
after: String, after: String,
@ -78,11 +89,17 @@ type Release {
createdAt: DateTime! createdAt: DateTime!
downloadSize: Long! downloadSize: Long!
downloads: Long! downloads: Long!
entry: Entry!
id: UUID! id: UUID!
md5Hash: String md5Hash: String
version: String! version: String!
} }
type Tag {
id: Int!
name: String!
}
enum EntryType { enum EntryType {
LAYOUT LAYOUT
PLUGIN PLUGIN
@ -102,6 +119,20 @@ scalar Long
scalar UUID scalar UUID
input CategoryFilterInput {
and: [CategoryFilterInput!]
icon: StringOperationFilterInput
id: IntOperationFilterInput
name: StringOperationFilterInput
or: [CategoryFilterInput!]
}
input CategorySortInput {
icon: SortEnumType
id: SortEnumType
name: SortEnumType
}
input DateTimeOperationFilterInput { input DateTimeOperationFilterInput {
eq: DateTime eq: DateTime
gt: DateTime gt: DateTime
@ -120,7 +151,10 @@ input DateTimeOperationFilterInput {
input EntryFilterInput { input EntryFilterInput {
and: [EntryFilterInput!] and: [EntryFilterInput!]
author: UuidOperationFilterInput author: UuidOperationFilterInput
categories: ListFilterInputTypeOfCategoryFilterInput
createdAt: DateTimeOperationFilterInput
description: StringOperationFilterInput description: StringOperationFilterInput
downloads: LongOperationFilterInput
entryType: EntryTypeOperationFilterInput entryType: EntryTypeOperationFilterInput
icon: ImageFilterInput icon: ImageFilterInput
id: UuidOperationFilterInput id: UuidOperationFilterInput
@ -128,7 +162,8 @@ input EntryFilterInput {
name: StringOperationFilterInput name: StringOperationFilterInput
or: [EntryFilterInput!] or: [EntryFilterInput!]
releases: ListFilterInputTypeOfReleaseFilterInput releases: ListFilterInputTypeOfReleaseFilterInput
tags: ListStringOperationFilterInput summary: StringOperationFilterInput
tags: ListFilterInputTypeOfTagFilterInput
} }
input EntryInput { input EntryInput {
@ -140,11 +175,14 @@ input EntryInput {
input EntrySortInput { input EntrySortInput {
author: SortEnumType author: SortEnumType
createdAt: SortEnumType
description: SortEnumType description: SortEnumType
downloads: SortEnumType
entryType: SortEnumType entryType: SortEnumType
icon: ImageSortInput icon: ImageSortInput
id: SortEnumType id: SortEnumType
name: SortEnumType name: SortEnumType
summary: SortEnumType
} }
input EntryTypeOperationFilterInput { input EntryTypeOperationFilterInput {
@ -166,6 +204,28 @@ input ImageSortInput {
mimeType: SortEnumType mimeType: SortEnumType
} }
input IntOperationFilterInput {
eq: Int
gt: Int
gte: Int
in: [Int]
lt: Int
lte: Int
neq: Int
ngt: Int
ngte: Int
nin: [Int]
nlt: Int
nlte: Int
}
input ListFilterInputTypeOfCategoryFilterInput {
all: CategoryFilterInput
any: Boolean
none: CategoryFilterInput
some: CategoryFilterInput
}
input ListFilterInputTypeOfImageFilterInput { input ListFilterInputTypeOfImageFilterInput {
all: ImageFilterInput all: ImageFilterInput
any: Boolean any: Boolean
@ -180,11 +240,11 @@ input ListFilterInputTypeOfReleaseFilterInput {
some: ReleaseFilterInput some: ReleaseFilterInput
} }
input ListStringOperationFilterInput { input ListFilterInputTypeOfTagFilterInput {
all: StringOperationFilterInput all: TagFilterInput
any: Boolean any: Boolean
none: StringOperationFilterInput none: TagFilterInput
some: StringOperationFilterInput some: TagFilterInput
} }
input LongOperationFilterInput { input LongOperationFilterInput {
@ -207,6 +267,7 @@ input ReleaseFilterInput {
createdAt: DateTimeOperationFilterInput createdAt: DateTimeOperationFilterInput
downloadSize: LongOperationFilterInput downloadSize: LongOperationFilterInput
downloads: LongOperationFilterInput downloads: LongOperationFilterInput
entry: EntryFilterInput
id: UuidOperationFilterInput id: UuidOperationFilterInput
md5Hash: StringOperationFilterInput md5Hash: StringOperationFilterInput
or: [ReleaseFilterInput!] or: [ReleaseFilterInput!]
@ -228,6 +289,13 @@ input StringOperationFilterInput {
startsWith: String startsWith: String
} }
input TagFilterInput {
and: [TagFilterInput!]
id: IntOperationFilterInput
name: StringOperationFilterInput
or: [TagFilterInput!]
}
input UuidOperationFilterInput { input UuidOperationFilterInput {
eq: UUID eq: UUID
gt: UUID gt: UUID