diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTScreenTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTScreenTParam.cs index 38afcd3d7..ac62129dc 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTScreenTParam.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTScreenTParam.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using Avalonia.Platform; namespace Artemis.UI.Shared.Routing; @@ -72,7 +73,15 @@ public abstract class RoutableScreen : ActivatableViewModelBase void IRoutableScreen.InternalChangeScreen(object? screen) { - Screen = screen as TScreen; + if (screen == null) + { + Screen = null; + return; + } + + if (screen is not TScreen typedScreen) + throw new ArtemisRoutingException($"Provided screen is not assignable to {typeof(TScreen).FullName}"); + Screen = typedScreen; } async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken) diff --git a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs index 69d3ed15b..ac171100f 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs @@ -70,7 +70,17 @@ internal class Navigation // Only change the screen if it wasn't reused if (!ReferenceEquals(host.InternalScreen, screen)) - host.InternalChangeScreen(screen); + { + try + { + host.InternalChangeScreen(screen); + } + catch (Exception e) + { + Cancel(); + _logger.Error(e, "Failed to navigate to {Path}", resolution.Path); + } + } if (CancelIfRequested(args, "ChangeScreen", screen)) return; @@ -93,8 +103,16 @@ internal class Navigation return; } - if (resolution.Child != null && screen is IRoutableScreen childScreen) - await NavigateResolution(resolution.Child, args, childScreen); + if (screen is IRoutableScreen childScreen) + { + // Navigate the child too + if (resolution.Child != null) + await NavigateResolution(resolution.Child, args, childScreen); + // Make sure there is no child + else if (childScreen.InternalScreen != null) + childScreen.InternalChangeScreen(null); + } + Completed = true; } diff --git a/src/Artemis.UI/Assets/Images/workshop-banner.jpg b/src/Artemis.UI/Assets/Images/workshop-banner.jpg new file mode 100644 index 000000000..da259f5f5 Binary files /dev/null and b/src/Artemis.UI/Assets/Images/workshop-banner.jpg differ diff --git a/src/Artemis.UI/MainWindow.axaml b/src/Artemis.UI/MainWindow.axaml index d735ae109..bff2c07c9 100644 --- a/src/Artemis.UI/MainWindow.axaml +++ b/src/Artemis.UI/MainWindow.axaml @@ -15,7 +15,7 @@ + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml index cb672cb99..39cd6ca6c 100644 --- a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -8,7 +8,9 @@ mc:Ignorable="d" d:DesignWidth="800" x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> - - Workshop overview - + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml.cs b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml.cs index 76fcc9e13..c013afa8e 100644 --- a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml.cs @@ -1,4 +1,8 @@ +using System; +using System.Reactive.Disposables; using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop; @@ -7,5 +11,6 @@ public partial class WorkshopView : ReactiveUserControl public WorkshopView() { InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(vm => WorkshopFrame.NavigateFromObject(vm ?? ViewModel?.HomeViewModel)).DisposeWith(d)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 86d0c6bdc..f6b53a013 100644 --- a/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -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.WebClient.Workshop; namespace Artemis.UI.Screens.Workshop; -public class WorkshopViewModel : RoutableScreen, IMainScreenViewModel +public class WorkshopViewModel : RoutableScreen, 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; } + + /// + public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken) + { + _searchViewModel.EntryType = Screen?.EntryType; + return Task.CompletedTask; + } +} + +public interface IWorkshopViewModel +{ + public EntryType? EntryType { get; } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj index ecacce222..09befbe14 100644 --- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj +++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj @@ -22,4 +22,13 @@ + + + + MSBuild:GenerateGraphQLCode + + + MSBuild:GenerateGraphQLCode + + diff --git a/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql b/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql new file mode 100644 index 000000000..d39be92f2 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql @@ -0,0 +1,7 @@ +query GetCategories { + categories { + id + name + icon + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/SearchEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/SearchEntries.graphql new file mode 100644 index 000000000..b2a03cf3f --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/SearchEntries.graphql @@ -0,0 +1,18 @@ +query SearchEntries($filter: EntryFilterInput) { + entries( + first: 10 + where: $filter + ) { + nodes { + id + name + summary + entryType + categories { + id + name + icon + } + } + } +} diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index daa53bde8..93e31079c 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -5,6 +5,12 @@ schema { mutation: Mutation } +type Category { + icon: String! + id: Int! + name: String! +} + "A connection to a list of items." type EntriesConnection { "A list of edges." @@ -27,14 +33,18 @@ type EntriesEdge { type Entry { author: UUID! + categories: [Category!]! + createdAt: DateTime! description: String! + downloads: Long! entryType: EntryType! icon: Image id: UUID! images: [Image!]! name: String! releases: [Release!]! - tags: [String!]! + summary: String! + tags: [Tag!]! } type Image { @@ -59,6 +69,7 @@ type PageInfo { } type Query { + categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]! entries( "Returns the elements in the list that come after the specified cursor." after: String, @@ -78,11 +89,17 @@ type Release { createdAt: DateTime! downloadSize: Long! downloads: Long! + entry: Entry! id: UUID! md5Hash: String version: String! } +type Tag { + id: Int! + name: String! +} + enum EntryType { LAYOUT PLUGIN @@ -102,6 +119,20 @@ scalar Long scalar UUID +input CategoryFilterInput { + and: [CategoryFilterInput!] + icon: StringOperationFilterInput + id: IntOperationFilterInput + name: StringOperationFilterInput + or: [CategoryFilterInput!] +} + +input CategorySortInput { + icon: SortEnumType + id: SortEnumType + name: SortEnumType +} + input DateTimeOperationFilterInput { eq: DateTime gt: DateTime @@ -120,7 +151,10 @@ input DateTimeOperationFilterInput { input EntryFilterInput { and: [EntryFilterInput!] author: UuidOperationFilterInput + categories: ListFilterInputTypeOfCategoryFilterInput + createdAt: DateTimeOperationFilterInput description: StringOperationFilterInput + downloads: LongOperationFilterInput entryType: EntryTypeOperationFilterInput icon: ImageFilterInput id: UuidOperationFilterInput @@ -128,7 +162,8 @@ input EntryFilterInput { name: StringOperationFilterInput or: [EntryFilterInput!] releases: ListFilterInputTypeOfReleaseFilterInput - tags: ListStringOperationFilterInput + summary: StringOperationFilterInput + tags: ListFilterInputTypeOfTagFilterInput } input EntryInput { @@ -140,11 +175,14 @@ input EntryInput { input EntrySortInput { author: SortEnumType + createdAt: SortEnumType description: SortEnumType + downloads: SortEnumType entryType: SortEnumType icon: ImageSortInput id: SortEnumType name: SortEnumType + summary: SortEnumType } input EntryTypeOperationFilterInput { @@ -166,6 +204,28 @@ input ImageSortInput { 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 { all: ImageFilterInput any: Boolean @@ -180,11 +240,11 @@ input ListFilterInputTypeOfReleaseFilterInput { some: ReleaseFilterInput } -input ListStringOperationFilterInput { - all: StringOperationFilterInput +input ListFilterInputTypeOfTagFilterInput { + all: TagFilterInput any: Boolean - none: StringOperationFilterInput - some: StringOperationFilterInput + none: TagFilterInput + some: TagFilterInput } input LongOperationFilterInput { @@ -207,6 +267,7 @@ input ReleaseFilterInput { createdAt: DateTimeOperationFilterInput downloadSize: LongOperationFilterInput downloads: LongOperationFilterInput + entry: EntryFilterInput id: UuidOperationFilterInput md5Hash: StringOperationFilterInput or: [ReleaseFilterInput!] @@ -228,6 +289,13 @@ input StringOperationFilterInput { startsWith: String } +input TagFilterInput { + and: [TagFilterInput!] + id: IntOperationFilterInput + name: StringOperationFilterInput + or: [TagFilterInput!] +} + input UuidOperationFilterInput { eq: UUID gt: UUID