diff --git a/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs b/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs index b09afb676..b524f739a 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs @@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen { bool RecycleScreen { get; } IRoutableScreen? InternalScreen { get; } + IRoutableScreen? InternalDefaultScreen { get; } void InternalChangeScreen(IRoutableScreen? screen); } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs index c61c27820..ca5208f19 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs @@ -25,7 +25,13 @@ public bool RecycleScreen protected set => RaiseAndSetIfChanged(ref _recycleScreen, value); } + /// + /// Gets the screen to show when no other screen is active. + /// + public virtual TScreen? DefaultScreen { get; } + IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen; + IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen; void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen) { diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs index 89773e8d4..1dfef6b76 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs @@ -27,7 +27,13 @@ public bool RecycleScreen protected set => RaiseAndSetIfChanged(ref _recycleScreen, value); } + /// + /// Gets the screen to show when no other screen is active. + /// + public virtual TScreen? DefaultScreen { get; } + IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen; + IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen; void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen) { diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs index bbe71fe2a..52e8a5e48 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs @@ -13,6 +13,11 @@ namespace Artemis.UI.Shared.Routing; /// The type of parameters the screen expects. It must have a parameterless constructor. public abstract class RoutableScreen : RoutableScreen, IRoutableScreen where TParam : new() { + /// + /// Gets or sets the parameter source of the screen. + /// + protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment; + /// /// Called while navigating to this screen. /// @@ -26,15 +31,16 @@ public virtual Task OnNavigating(TParam parameters, NavigationArguments args, Ca { return Task.CompletedTask; } - + async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken) { Func activator = GetParameterActivator(); - if (args.SegmentParameters.Length != _parameterPropertyCount) - throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}."); + object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters; + if (routeParameters.Length != _parameterPropertyCount) + throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {routeParameters.Length}."); - TParam parameters = activator(args.SegmentParameters); + TParam parameters = activator(routeParameters); await OnNavigating(args, cancellationToken); await OnNavigating(parameters, args, cancellationToken); } @@ -97,4 +103,20 @@ private static Func GetParameterActivator() } #endregion +} + +/// +/// Enum representing the source of parameters in the RoutableScreen class. +/// +public enum ParameterSource +{ + /// + /// Represents the source where parameters are obtained from the segment of the route. + /// + Segment, + + /// + /// Represents the source where parameters are obtained from the entire route. + /// + Route } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs b/src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs index c62437d03..22246b3f0 100644 --- a/src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs +++ b/src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs @@ -18,6 +18,17 @@ public RouteRegistration(string path) Route = new Route(path); } + /// + /// Initializes a new instance of the class. + /// + /// The path of the route. + /// The children of the route. + public RouteRegistration(string path, List children) + { + Route = new Route(path); + Children = children; + } + /// public override string ToString() { diff --git a/src/Artemis.UI.Shared/Routing/Router/IRouter.cs b/src/Artemis.UI.Shared/Routing/Router/IRouter.cs index e23be6eae..2901fdbaf 100644 --- a/src/Artemis.UI.Shared/Routing/Router/IRouter.cs +++ b/src/Artemis.UI.Shared/Routing/Router/IRouter.cs @@ -45,6 +45,12 @@ public interface IRouter /// /// A task containing a boolean value which indicates whether there was a forward path to go back to. Task GoForward(); + + /// + /// Asynchronously navigates upwards to the parent route. + /// + /// + Task GoUp(); /// /// Clears the navigation history. diff --git a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs index 0d0f6083c..1df871d34 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs @@ -109,12 +109,11 @@ private async Task NavigateResolution(RouteResolution resolution, NavigationArgu // 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); + // Without a resolution, navigate to the default screen (which may be null) + else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen) + childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen); } - Completed = true; } diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs index 39e067ec5..269d51fce 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Router.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Artemis.Core; @@ -72,7 +73,13 @@ private bool PathEquals(string path, RouterNavigationOptions options) /// public async Task Navigate(string path, RouterNavigationOptions? options = null) { - path = path.ToLower().Trim(' ', '/', '\\'); + if (path.StartsWith('/') && _currentRouteSubject.Value != null) + path = _currentRouteSubject.Value + path; + if (path.StartsWith("../") && _currentRouteSubject.Value != null) + path = NavigateUp(_currentRouteSubject.Value, path); + else + path = path.ToLower().Trim(' ', '/', '\\'); + options ??= new RouterNavigationOptions(); // Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself @@ -161,6 +168,28 @@ public async Task GoForward() return true; } + /// + public async Task GoUp() + { + string? currentPath = _currentRouteSubject.Value; + + // Keep removing segments until we find a parent route that resolves + while (currentPath != null && currentPath.Contains('/')) + { + string parentPath = currentPath[..currentPath.LastIndexOf('/')]; + RouteResolution resolution = Resolve(parentPath); + if (resolution.Success) + { + await Navigate(parentPath, new RouterNavigationOptions {AddToHistory = false}); + return true; + } + + currentPath = parentPath; + } + + return false; + } + /// public void ClearHistory() { @@ -194,6 +223,24 @@ public void Dispose() _logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace); } + + + private string NavigateUp(string current, string path) + { + string[] pathParts = current.Split('/'); + string[] navigateParts = path.Split('/'); + int upCount = navigateParts.TakeWhile(part => part == "..").Count(); + + if (upCount >= pathParts.Length) + { + throw new InvalidOperationException("Cannot navigate up beyond the root"); + } + + IEnumerable remainingCurrentPathParts = pathParts.Take(pathParts.Length - upCount); + IEnumerable remainingNavigatePathParts = navigateParts.Skip(upCount); + + return string.Join("/", remainingCurrentPathParts.Concat(remainingNavigatePathParts)); + } private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e) { diff --git a/src/Artemis.UI.Shared/Styles/Skeleton.axaml b/src/Artemis.UI.Shared/Styles/Skeleton.axaml index 2596c3fe6..0483a9763 100644 --- a/src/Artemis.UI.Shared/Styles/Skeleton.axaml +++ b/src/Artemis.UI.Shared/Styles/Skeleton.axaml @@ -8,6 +8,7 @@ + TitleTextBlockStyle This is heading 1 This is heading 2 This is heading 3 @@ -22,6 +23,7 @@ + @@ -39,6 +41,7 @@ + TitleTextBlockStyle This is heading 1 This is heading 2 This is heading 3 @@ -51,6 +54,7 @@ + @@ -68,6 +72,7 @@ + TitleTextBlockStyle This is heading 1 This is heading 2 This is heading 3 @@ -125,6 +130,11 @@ + + + + + + + + + Categories + + + + + + + + + + + + + + + + + + + + + + + + + + + Looks like your current filters gave no results + + Modify or clear your filters to view other entries + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs similarity index 51% rename from src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs index a003f47bb..e4a15ed77 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs @@ -1,38 +1,30 @@ -using System; -using System.Reactive.Disposables; using System.Threading; -using Artemis.UI.Shared.Routing; using Avalonia.Controls; using Avalonia.ReactiveUI; -using Avalonia.Threading; using ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; +namespace Artemis.UI.Screens.Workshop.Entries.List; -public partial class LayoutListView : ReactiveUserControl +public partial class EntryListView : ReactiveUserControl { - public LayoutListView() + public EntryListView() { InitializeComponent(); EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch(); - - this.WhenActivated(d => - { - UpdateEntriesPerFetch(); - ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); - }); + + this.WhenActivated(_ => UpdateEntriesPerFetch()); } private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) { + if (ViewModel == null) + return; + // When near the bottom of EntriesScrollViewer, call FetchMore on the view model if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100) - ViewModel?.FetchMore(CancellationToken.None); - } + ViewModel.FetchMore(CancellationToken.None); - private void Navigate(RoutableScreen viewModel) - { - Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle); + ViewModel.ScrollOffset = EntriesScrollViewer.Offset; } private void UpdateEntriesPerFetch() diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 92322920d..f335c9128 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; +using Artemis.UI.Extensions; using Artemis.UI.Screens.Workshop.Categories; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; @@ -15,29 +16,28 @@ using PropertyChanged.SourceGenerator; using ReactiveUI; using StrawberryShake; +using Vector = Avalonia.Vector; namespace Artemis.UI.Screens.Workshop.Entries.List; -public abstract partial class EntryListViewModel : RoutableHostScreen +public partial class EntryListViewModel : RoutableScreen { private readonly SourceList _entries = new(); private readonly INotificationService _notificationService; private readonly IWorkshopClient _workshopClient; - private readonly string _route; private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo; [Notify] private bool _initializing = true; [Notify] private bool _fetchingMore; [Notify] private int _entriesPerFetch; + [Notify] private Vector _scrollOffset; - protected EntryListViewModel(string route, - IWorkshopClient workshopClient, + protected EntryListViewModel(IWorkshopClient workshopClient, CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) { - _route = route; _workshopClient = workshopClient; _notificationService = notificationService; @@ -50,37 +50,31 @@ protected EntryListViewModel(string route, .Subscribe(); Entries = entries; + // Respond to filter query input changes this.WhenActivated(d => { - // Respond to filter query input changes InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d); }); + + // Load entries when the view model is first activated + this.WhenActivatedAsync(async _ => + { + if (_entries.Count == 0) + { + await Task.Delay(250); + await FetchMore(CancellationToken.None); + Initializing = false; + } + }); } public CategoriesViewModel CategoriesViewModel { get; } public EntryListInputViewModel InputViewModel { get; } + public EntryType? EntryType { get; set; } public ReadOnlyObservableCollection Entries { get; } - - public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken) - { - if (_entries.Count == 0) - { - await Task.Delay(250, cancellationToken); - await FetchMore(cancellationToken); - Initializing = false; - } - } - - public override Task OnClosing(NavigationArguments args) - { - // Clear search if not navigating to a child - if (!args.Path.StartsWith(_route)) - InputViewModel.ClearLastSearch(); - return base.OnClosing(args); - } - + public async Task FetchMore(CancellationToken cancellationToken) { if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage) @@ -119,12 +113,19 @@ public async Task FetchMore(CancellationToken cancellationToken) } } - protected virtual EntryFilterInput GetFilter() + private EntryFilterInput GetFilter() { - return new EntryFilterInput {And = CategoriesViewModel.CategoryFilters}; + return new EntryFilterInput + { + And = + [ + new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}}, + ..CategoriesViewModel.CategoryFilters ?? [] + ] + }; } - protected virtual IReadOnlyList GetSort() + private IReadOnlyList GetSort() { // Sort by created at if (InputViewModel.SortBy == 1) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml deleted file mode 100644 index 1c0c4182c..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view other device layouts - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs deleted file mode 100644 index 11c97846c..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Artemis.UI.Screens.Workshop.Categories; -using Artemis.UI.Screens.Workshop.Entries.List; -using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; -using Artemis.WebClient.Workshop; - -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; - -public class LayoutListViewModel : List.EntryListViewModel -{ - public LayoutListViewModel(IWorkshopClient workshopClient, - CategoriesViewModel categoriesViewModel, - EntryListInputViewModel entryListInputViewModel, - INotificationService notificationService, - Func getEntryListViewModel) - : base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) - { - entryListInputViewModel.SearchWatermark = "Search layouts"; - } - - protected override EntryFilterInput GetFilter() - { - return new EntryFilterInput - { - And = new[] - { - new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}}, - new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}}, - base.GetFilter() - } - }; - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml deleted file mode 100644 index 7b6cb1f0c..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view other plugins - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs deleted file mode 100644 index 8cfaa1696..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading; -using Artemis.UI.Shared.Routing; -using Avalonia.Controls; -using Avalonia.ReactiveUI; -using Avalonia.Threading; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; - -public partial class PluginListView : ReactiveUserControl -{ - public PluginListView() - { - InitializeComponent(); - EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch(); - - this.WhenActivated(d => - { - UpdateEntriesPerFetch(); - ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); - }); - } - - private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) - { - // When near the bottom of EntriesScrollViewer, call FetchMore on the view model - if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100) - ViewModel?.FetchMore(CancellationToken.None); - } - - private void Navigate(RoutableScreen viewModel) - { - Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle); - } - - private void UpdateEntriesPerFetch() - { - if (ViewModel != null) - ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs deleted file mode 100644 index c7ea484a6..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Artemis.UI.Screens.Workshop.Categories; -using Artemis.UI.Screens.Workshop.Entries.List; -using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; -using Artemis.WebClient.Workshop; - -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; - -public class PluginListViewModel : EntryListViewModel -{ - public PluginListViewModel(IWorkshopClient workshopClient, - CategoriesViewModel categoriesViewModel, - EntryListInputViewModel entryListInputViewModel, - INotificationService notificationService, - Func getEntryListViewModel) - : base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) - { - entryListInputViewModel.SearchWatermark = "Search plugins"; - } - - protected override EntryFilterInput GetFilter() - { - return new EntryFilterInput - { - And = new[] - { - new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}}, - base.GetFilter() - } - }; - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml deleted file mode 100644 index 03028d4b8..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view some awesome profiles - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs deleted file mode 100644 index b25ba45f3..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading; -using Artemis.UI.Shared.Routing; -using Avalonia.Controls; -using Avalonia.ReactiveUI; -using Avalonia.Threading; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; - -public partial class ProfileListView : ReactiveUserControl -{ - public ProfileListView() - { - InitializeComponent(); - EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch(); - - this.WhenActivated(d => - { - UpdateEntriesPerFetch(); - ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); - }); - } - - private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) - { - // When near the bottom of EntriesScrollViewer, call FetchMore on the view model - if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100) - ViewModel?.FetchMore(CancellationToken.None); - } - - private void Navigate(RoutableScreen viewModel) - { - Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle); - } - - private void UpdateEntriesPerFetch() - { - if (ViewModel != null) - ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs deleted file mode 100644 index 09ed5410b..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Artemis.UI.Screens.Workshop.Categories; -using Artemis.UI.Screens.Workshop.Entries.List; -using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; -using Artemis.WebClient.Workshop; - -namespace Artemis.UI.Screens.Workshop.Entries.Tabs; - -public class ProfileListViewModel : List.EntryListViewModel -{ - public ProfileListViewModel(IWorkshopClient workshopClient, - CategoriesViewModel categoriesViewModel, - EntryListInputViewModel entryListInputViewModel, - INotificationService notificationService, - Func getEntryListViewModel) - : base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) - { - entryListInputViewModel.SearchWatermark = "Search profiles"; - } - - protected override EntryFilterInput GetFilter() - { - return new EntryFilterInput - { - And = new[] - { - new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Profile}}, - base.GetFilter() - } - }; - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml new file mode 100644 index 000000000..1a328d64a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + Created + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs new file mode 100644 index 000000000..ffc325a79 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseItemView : ReactiveUserControl +{ + public EntryReleaseItemView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs new file mode 100644 index 000000000..bafa65507 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.UI.Shared; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseItemViewModel : ActivatableViewModelBase +{ + private readonly IWorkshopService _workshopService; + private readonly IEntryDetails _entry; + [Notify] private bool _isCurrentVersion; + + public EntryReleaseItemViewModel(IWorkshopService workshopService, IEntryDetails entry, IRelease release) + { + _workshopService = workshopService; + _entry = entry; + + Release = release; + UpdateIsCurrentVersion(); + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => _workshopService.OnInstalledEntrySaved += x, x => _workshopService.OnInstalledEntrySaved -= x) + .Subscribe(_ => UpdateIsCurrentVersion()) + .DisposeWith(d); + }); + } + + public IRelease Release { get; } + + private void UpdateIsCurrentVersion() + { + IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml new file mode 100644 index 000000000..62a991f55 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + Release info + + + + + + + + + + + + + + + + + + + + Version + + + + + + Release date + + + + + + File size + + + + + + + + + + Release notes + + + + There are no release notes for this release. + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs new file mode 100644 index 000000000..ea7532c70 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseView : ReactiveUserControl +{ + public EntryReleaseView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs new file mode 100644 index 000000000..32b1de42a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using StrawberryShake; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseViewModel : RoutableScreen +{ + private readonly IWorkshopClient _client; + private readonly IRouter _router; + private readonly INotificationService _notificationService; + private readonly IWindowService _windowService; + private readonly IWorkshopService _workshopService; + private readonly EntryInstallationHandlerFactory _factory; + private readonly Progress _progress = new(); + + [Notify] private IGetReleaseById_Release? _release; + [Notify] private float _installProgress; + [Notify] private bool _installationInProgress; + [Notify] private bool _isCurrentVersion; + + private CancellationTokenSource? _cts; + + public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, IWorkshopService workshopService, + EntryInstallationHandlerFactory factory) + { + _client = client; + _router = router; + _notificationService = notificationService; + _windowService = windowService; + _workshopService = workshopService; + _factory = factory; + _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; + } + + public async Task Close() + { + await _router.GoUp(); + } + + public async Task Install() + { + if (Release == null) + return; + + _cts = new CancellationTokenSource(); + InstallProgress = 0; + InstallationInProgress = true; + try + { + IEntryInstallationHandler handler = _factory.CreateHandler(Release.Entry.EntryType); + EntryInstallResult result = await handler.InstallAsync(Release.Entry, Release, _progress, _cts.Token); + if (result.IsSuccess) + { + _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show(); + IsCurrentVersion = true; + InstallationInProgress = false; + await Manage(); + } + else if (!_cts.IsCancellationRequested) + _notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show(); + } + catch (Exception e) + { + InstallationInProgress = false; + _windowService.ShowExceptionDialog("Failed to install workshop entry", e); + } + } + + public async Task Manage() + { + if (Release?.Entry.EntryType != EntryType.Profile) + await _router.Navigate("../../manage"); + } + + public async Task Reinstall() + { + if (await _windowService.ShowConfirmContentDialog("Reinstall entry", "Are you sure you want to reinstall this entry?")) + await Install(); + } + + public void Cancel() + { + _cts?.Cancel(); + } + + /// + public override async Task OnNavigating(ReleaseDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + IOperationResult result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken); + Release = result.Data?.Release; + IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id; + } + + #region Overrides of RoutableScreen + + /// + public override Task OnClosing(NavigationArguments args) + { + if (!InstallationInProgress) + return Task.CompletedTask; + + args.Cancel(); + _notificationService.CreateNotification().WithMessage("Please wait for the installation to finish").Show(); + return Task.CompletedTask; + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml new file mode 100644 index 000000000..0e0346d83 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml @@ -0,0 +1,13 @@ + + Releases + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs new file mode 100644 index 000000000..39b603187 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleasesView : ReactiveUserControl +{ + public EntryReleasesView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs new file mode 100644 index 000000000..19eab58ea --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Extensions; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleasesViewModel : ActivatableViewModelBase +{ + private readonly IRouter _router; + [Notify] private EntryReleaseItemViewModel? _selectedRelease; + + public EntryReleasesViewModel(IEntryDetails entry, IRouter router, Func getEntryReleaseItemViewModel) + { + _router = router; + + Entry = entry; + Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList(); + + this.WhenActivated(d => + { + router.CurrentPath.Subscribe(p => + SelectedRelease = p != null && p.StartsWith(Entry.GetEntryPath()) && float.TryParse(p.Split('/').Last(), out float releaseId) + ? Releases.FirstOrDefault(r => r.Release.Id == releaseId) + : null) + .DisposeWith(d); + + this.WhenAnyValue(vm => vm.SelectedRelease) + .WhereNotNull() + .Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}")) + .DisposeWith(d); + }); + } + + public IEntryDetails Entry { get; } + public List Releases { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml new file mode 100644 index 000000000..e4cf60508 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs new file mode 100644 index 000000000..5276e422f --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutDescriptionView : ReactiveUserControl +{ + public LayoutDescriptionView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs new file mode 100644 index 000000000..3fb8f678e --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs @@ -0,0 +1,10 @@ +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutDescriptionViewModel : RoutableScreen +{ + [Notify] private IEntryDetails? _entry; +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml index 3dd205871..7f021d50f 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml @@ -3,7 +3,8 @@ 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" - xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:ui="clr-namespace:Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView" x:DataType="layout:LayoutDetailsViewModel"> @@ -12,21 +13,17 @@ - + - - - - - - - - - + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs index 57e93b1e1..c97e73859 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs @@ -1,4 +1,7 @@ +using System; +using System.Reactive.Disposables; using Avalonia.ReactiveUI; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Layout; @@ -7,5 +10,9 @@ public partial class LayoutDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs index 9663f5ad4..7379c1217 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs @@ -1,87 +1,64 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; -using Artemis.UI.Screens.Workshop.Layout.Dialogs; +using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Models; -using Artemis.WebClient.Workshop.Services; using PropertyChanged.SourceGenerator; using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Layout; -public partial class LayoutDetailsViewModel : RoutableScreen +public partial class LayoutDetailsViewModel : RoutableHostScreen { private readonly IWorkshopClient _client; - private readonly IDeviceService _deviceService; - private readonly IWindowService _windowService; - private readonly Func _getEntryInfoViewModel; + private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel; private readonly Func _getEntryReleasesViewModel; private readonly Func _getEntryImagesViewModel; [Notify] private IEntryDetails? _entry; - [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; public LayoutDetailsViewModel(IWorkshopClient client, - IDeviceService deviceService, - IWindowService windowService, - Func getEntryInfoViewModel, + LayoutDescriptionViewModel layoutDescriptionViewModel, + EntryInfoViewModel entryInfoViewModel, Func getEntryReleasesViewModel, Func getEntryImagesViewModel) { _client = client; - _deviceService = deviceService; - _windowService = windowService; - _getEntryInfoViewModel = getEntryInfoViewModel; + _layoutDescriptionViewModel = layoutDescriptionViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; + + RecycleScreen = false; + EntryInfoViewModel = entryInfoViewModel; } + public override RoutableScreen DefaultScreen => _layoutDescriptionViewModel; + public EntryInfoViewModel EntryInfoViewModel { get; } + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { - await GetEntry(parameters.EntryId, cancellationToken); + if (Entry?.Id != parameters.EntryId) + await GetEntry(parameters.EntryId, cancellationToken); } private async Task GetEntry(long entryId, CancellationToken cancellationToken) { + Task grace = Task.Delay(300, cancellationToken); IOperationResult result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); if (result.IsErrorResult()) return; + + // Let the UI settle to avoid lag when deep linking + await grace; Entry = result.Data?.Entry; - EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; + EntryInfoViewModel.SetEntry(Entry); EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; - - if (EntryReleasesViewModel != null) - EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished; - } - - private async Task OnInstallationFinished(InstalledEntry installedEntry) - { - // Find compatible devices - ArtemisLayout layout = new(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml")); - List devices = _deviceService.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == layout.RgbLayout.Type).ToList(); - - // If any are found, offer to apply - if (devices.Any()) - { - await _windowService.CreateContentDialog() - .WithTitle("Apply layout to devices") - .WithViewModel(out DeviceSelectionDialogViewModel vm, devices, installedEntry) - .WithCloseButtonText(null) - .HavingPrimaryButton(b => b.WithText("Continue").WithCommand(vm.Apply)) - .ShowAsync(); - } + _layoutDescriptionViewModel.Entry = Entry; } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml new file mode 100644 index 000000000..6c1b6e8ae --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs new file mode 100644 index 000000000..cb60c4707 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs @@ -0,0 +1,20 @@ +using System; +using System.Reactive.Disposables; +using Artemis.UI.Shared.Routing; +using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutListView : ReactiveUserControl +{ + public LayoutListView() + { + InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs new file mode 100644 index 000000000..a6165a585 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs @@ -0,0 +1,17 @@ +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public class LayoutListViewModel : RoutableHostScreen +{ + private readonly EntryListViewModel _entryListViewModel; + public override RoutableScreen DefaultScreen => _entryListViewModel; + + public LayoutListViewModel(EntryListViewModel entryListViewModel) + { + _entryListViewModel = entryListViewModel; + _entryListViewModel.EntryType = EntryType.Layout; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml new file mode 100644 index 000000000..af6b25fc2 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml @@ -0,0 +1,46 @@ + + + + + + Manage layout + + + + + + This layout is made for devices of type + . + Unfortunately, none were detected. + + + + Select the devices on which you would like to apply the downloaded layout. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs new file mode 100644 index 000000000..bc26260c9 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutManageView : ReactiveUserControl +{ + public LayoutManageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs new file mode 100644 index 000000000..9ce907d57 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs @@ -0,0 +1,103 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Screens.SurfaceEditor; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Providers; +using Artemis.WebClient.Workshop.Services; +using Avalonia.Threading; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutManageViewModel : RoutableScreen +{ + private readonly ISurfaceVmFactory _surfaceVmFactory; + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private readonly IDeviceService _deviceService; + private readonly WorkshopLayoutProvider _layoutProvider; + private readonly IWindowService _windowService; + [Notify] private ArtemisLayout? _layout; + [Notify] private InstalledEntry? _entry; + [Notify] private ObservableCollection? _devices; + + public LayoutManageViewModel(ISurfaceVmFactory surfaceVmFactory, + IRouter router, + IWorkshopService workshopService, + IDeviceService deviceService, + WorkshopLayoutProvider layoutProvider, + IWindowService windowService) + { + _surfaceVmFactory = surfaceVmFactory; + _router = router; + _workshopService = workshopService; + _deviceService = deviceService; + _layoutProvider = layoutProvider; + _windowService = windowService; + Apply = ReactiveCommand.Create(ExecuteApply); + ParameterSource = ParameterSource.Route; + } + + public ReactiveCommand Apply { get; } + + public async Task Close() + { + await _router.GoUp(); + } + + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId); + if (installedEntry == null) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Entry not found", "The entry you're trying to manage could not be found.", "Go back", null); + await Close(); + }); + return; + } + + Layout = new ArtemisLayout(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml")); + if (!Layout.IsValid) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid layout", "The layout of the entry you're trying to manage is invalid.", "Go back", null); + await Close(); + }); + return; + } + + Entry = installedEntry; + Devices = new ObservableCollection(_deviceService.Devices + .Where(d => d.RgbDevice.DeviceInfo.DeviceType == Layout.RgbLayout.Type) + .Select(_surfaceVmFactory.ListDeviceViewModel)); + } + + private void ExecuteApply() + { + if (Devices == null) + return; + + foreach (ListDeviceViewModel listDeviceViewModel in Devices.Where(d => d.IsSelected)) + { + _layoutProvider.ConfigureDevice(listDeviceViewModel.Device, Entry); + _deviceService.SaveDevice(listDeviceViewModel.Device); + _deviceService.LoadDeviceLayout(listDeviceViewModel.Device); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml deleted file mode 100644 index dbb99d606..000000000 --- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - Management - - - - - - downloads - - - - - Created - - - - - - - - - - - - - View workshop page - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs deleted file mode 100644 index 729f02a2b..000000000 --- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Library; - -public partial class SubmissionDetailView : ReactiveUserControl -{ - public SubmissionDetailView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml new file mode 100644 index 000000000..538b24b83 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs new file mode 100644 index 000000000..77a1f67ec --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Library; + +public partial class SubmissionDetailsView : ReactiveUserControl +{ + public SubmissionDetailsView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs similarity index 82% rename from src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs rename to src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs index 839511a5d..7829b7261 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs @@ -8,8 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Image; -using Artemis.UI.Screens.Workshop.Parameters; -using Artemis.UI.Screens.Workshop.SubmissionWizard; +using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; @@ -25,7 +24,7 @@ namespace Artemis.UI.Screens.Workshop.Library; -public partial class SubmissionDetailViewModel : RoutableScreen +public partial class SubmissionDetailsViewModel : RoutableScreen { private readonly IWorkshopClient _client; private readonly IWindowService _windowService; @@ -40,7 +39,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen vm.HasChanges)); SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges)); } - + public ObservableCollection Images { get; } = new(); - public ReactiveCommand CreateRelease { get; } - public ReactiveCommand DeleteSubmission { get; } - public ReactiveCommand ViewWorkshopPage { get; } public ReactiveCommand AddImage { get; } public ReactiveCommand SaveChanges { get; } public ReactiveCommand DiscardChanges { get; } - public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + public async Task SetEntry(IGetSubmittedEntryById_Entry? entry, CancellationToken cancellationToken) { - IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken); - if (result.IsErrorResult()) - return; - - Entry = result.Data?.Entry; + Entry = entry; await ApplyDetailsFromEntry(cancellationToken); - ApplyImagesFromEntry(); } - public override async Task OnClosing(NavigationArguments args) + public async Task OnClosing(NavigationArguments args) { if (!HasChanges) return; @@ -91,6 +79,8 @@ public override async Task OnClosing(NavigationArguments args) bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?"); if (!confirmed) args.Cancel(); + else + await ExecuteDiscardChanges(); } private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken) @@ -106,6 +96,7 @@ private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken) if (Entry == null) { EntrySpecificationsViewModel = null; + ApplyImagesFromEntry(); return; } @@ -188,7 +179,6 @@ private void UpdateHasChanges() private async Task ExecuteDiscardChanges() { await ApplyDetailsFromEntry(CancellationToken.None); - ApplyImagesFromEntry(); } private async Task ExecuteSaveChanges(CancellationToken cancellationToken) @@ -243,30 +233,7 @@ private async Task ExecuteSaveChanges(CancellationToken cancellationToken) HasChanges = false; await _router.Reload(); } - - private async Task ExecuteCreateRelease(CancellationToken cancellationToken) - { - if (Entry != null) - await _windowService.ShowDialogAsync(Entry); - } - - private async Task ExecuteDeleteSubmission(CancellationToken cancellationToken) - { - if (Entry == null) - return; - - bool confirmed = await _windowService.ShowConfirmContentDialog( - "Delete submission?", - "You cannot undo this by yourself.\r\n" + - "Users that have already downloaded your submission will keep it."); - if (!confirmed) - return; - - IOperationResult result = await _client.RemoveEntry.ExecuteAsync(Entry.Id, cancellationToken); - result.EnsureNoErrors(); - await _router.Navigate("workshop/library/submissions"); - } - + private async Task ExecuteAddImage(CancellationToken arg) { string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync(); @@ -297,12 +264,6 @@ private async Task ExecuteAddImage(CancellationToken arg) } } - private async Task ExecuteViewWorkshopPage() - { - if (Entry != null) - await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType); - } - private void InputChanged(object? sender, EventArgs e) { UpdateHasChanges(); diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml new file mode 100644 index 000000000..8c7710dc1 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml @@ -0,0 +1,85 @@ + + + + + + + + + + + Management + + + + + + downloads + + + + + Created + + + + + + + + + + + + + + Releases + + + + + + + + + + Created + + + + + + + + + + View workshop page + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs new file mode 100644 index 000000000..3e14dacb8 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Library; + +public partial class SubmissionManagementView : ReactiveUserControl +{ + public SubmissionManagementView() + { + InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs new file mode 100644 index 000000000..2f7a67147 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Screens.Workshop.SubmissionWizard; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; +using StrawberryShake; + +namespace Artemis.UI.Screens.Workshop.Library; + +public partial class SubmissionManagementViewModel : RoutableHostScreen +{ + private readonly IWorkshopClient _client; + private readonly IWindowService _windowService; + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private readonly SubmissionDetailsViewModel _detailsViewModel; + + [Notify] private IGetSubmittedEntryById_Entry? _entry; + [Notify] private List? _releases; + [Notify] private IGetSubmittedEntryById_Entry_Releases? _selectedRelease; + + public SubmissionManagementViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, IWorkshopService workshopService, SubmissionDetailsViewModel detailsViewModel) + { + _detailsViewModel = detailsViewModel; + _client = client; + _router = router; + _windowService = windowService; + _workshopService = workshopService; + + RecycleScreen = false; + + this.WhenActivated(d => + { + this.WhenAnyValue(vm => vm.SelectedRelease) + .WhereNotNull() + .Subscribe(r => _router.Navigate($"workshop/library/submissions/{Entry?.Id}/releases/{r.Id}")) + .DisposeWith(d); + }); + } + + public override RoutableScreen DefaultScreen => _detailsViewModel; + + public async Task ViewWorkshopPage() + { + if (Entry != null) + await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType); + } + + public async Task CreateRelease() + { + if (Entry != null) + await _windowService.ShowDialogAsync(Entry); + } + + public async Task DeleteSubmission() + { + if (Entry == null) + return; + + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Delete submission?", + "You cannot undo this by yourself.\r\n" + + "Users that have already downloaded your submission will keep it."); + if (!confirmed) + return; + + IOperationResult result = await _client.RemoveEntry.ExecuteAsync(Entry.Id); + result.EnsureNoErrors(); + await _router.Navigate("workshop/library/submissions"); + } + + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + // If there is a 2nd parameter, it's a release ID + SelectedRelease = args.RouteParameters.Length > 1 ? Releases?.FirstOrDefault(r => r.Id == (long) args.RouteParameters[1]) : null; + + // OnNavigating may just be getting called to update the selected release + if (Entry?.Id == parameters.EntryId) + { + // Reapply the entry when closing a release, this is mainly because the entry icon probably got disposed + if (SelectedRelease == null) + await _detailsViewModel.SetEntry(Entry, cancellationToken); + + // No need to reload the entry since it's the same + return; + } + + IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken); + if (result.IsErrorResult()) + return; + + Entry = result.Data?.Entry; + Releases = Entry?.Releases.OrderByDescending(r => r.CreatedAt).ToList(); + + await _detailsViewModel.SetEntry(Entry, cancellationToken); + } + + public override async Task OnClosing(NavigationArguments args) + { + await _detailsViewModel.OnClosing(args); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml new file mode 100644 index 000000000..2040309d0 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + Release management + + + + + + + Version + + + + + + Release date + + + + + + File size + + + + + + + + + + Release notes + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs new file mode 100644 index 000000000..de58a90f6 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Library; + +public partial class SubmissionReleaseView : ReactiveUserControl +{ + public SubmissionReleaseView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs new file mode 100644 index 000000000..025d2ce5a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs @@ -0,0 +1,110 @@ +using System; +using System.Reactive; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.WebClient.Workshop; +using Avalonia.Layout; +using AvaloniaEdit.Document; +using PropertyChanged.SourceGenerator; +using ReactiveUI; +using StrawberryShake; + +namespace Artemis.UI.Screens.Workshop.Library; + +public partial class SubmissionReleaseViewModel : RoutableScreen +{ + private readonly IWorkshopClient _client; + private readonly IRouter _router; + private readonly IWindowService _windowService; + private readonly INotificationService _notificationService; + + [Notify] private IGetReleaseById_Release? _release; + [Notify] private string? _changelog; + [Notify] private bool _hasChanges; + + public SubmissionReleaseViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, INotificationService notificationService) + { + _client = client; + _router = router; + _windowService = windowService; + _notificationService = notificationService; + this.WhenAnyValue(vm => vm.Changelog, vm => vm.Release, (current, release) => current != release?.Changelog).Subscribe(hasChanges => HasChanges = hasChanges); + + Discard = ReactiveCommand.Create(ExecuteDiscard, this.WhenAnyValue(vm => vm.HasChanges)); + Save = ReactiveCommand.CreateFromTask(ExecuteSave, this.WhenAnyValue(vm => vm.HasChanges)); + } + + public ReactiveCommand Discard { get; set; } + public ReactiveCommand Save { get; set; } + + public override async Task OnNavigating(ReleaseDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + IOperationResult result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken); + Release = result.Data?.Release; + Changelog = Release?.Changelog; + } + + public override async Task OnClosing(NavigationArguments args) + { + if (!HasChanges) + return; + + bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?"); + if (!confirmed) + args.Cancel(); + } + + public async Task DeleteRelease() + { + if (Release == null) + return; + + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Delete release?", + "This cannot be undone.\r\n" + + "Users that have already downloaded this release will keep it."); + if (!confirmed) + return; + + await _client.RemoveRelease.ExecuteAsync(Release.Id); + _notificationService.CreateNotification() + .WithTitle("Deleted release.") + .WithSeverity(NotificationSeverity.Success) + .WithHorizontalPosition(HorizontalAlignment.Left) + .Show(); + + HasChanges = false; + await Close(); + } + + public async Task Close() + { + await _router.GoUp(); + } + + private async Task ExecuteSave(CancellationToken cancellationToken) + { + if (Release == null) + return; + + await _client.UpdateRelease.ExecuteAsync(new UpdateReleaseInput {Id = Release.Id, Changelog = Changelog}, cancellationToken); + _notificationService.CreateNotification() + .WithTitle("Saved changelog.") + .WithSeverity(NotificationSeverity.Success) + .WithHorizontalPosition(HorizontalAlignment.Left) + .Show(); + + HasChanges = false; + } + + private void ExecuteDiscard() + { + Changelog = Release?.Changelog; + HasChanges = false; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs index 7758b5729..5fa303000 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs @@ -18,9 +18,9 @@ public WorkshopLibraryView() private void Navigate(ViewModelBase viewModel) { - Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel)); + TabFrame.NavigateFromObject(viewModel); } - + private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e) { ViewModel?.GoBack(); diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs index 413914803..af02fa985 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs @@ -53,7 +53,7 @@ public override async Task OnNavigating(NavigationArguments args, CancellationTo public void GoBack() { if (ViewingDetails) - _router.GoBack(); + _router.Navigate("workshop/library/submissions"); else _router.Navigate("workshop"); } diff --git a/src/Artemis.UI/Screens/Workshop/Parameters/ReleaseDetailParameters.cs b/src/Artemis.UI/Screens/Workshop/Parameters/ReleaseDetailParameters.cs new file mode 100644 index 000000000..252095dad --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Parameters/ReleaseDetailParameters.cs @@ -0,0 +1,6 @@ +namespace Artemis.UI.Screens.Workshop.Parameters; + +public class ReleaseDetailParameters +{ + public long ReleaseId { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml deleted file mode 100644 index 3c0336a37..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Plugin features - - - - - diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs deleted file mode 100644 index 19329553e..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs; - -public partial class PluginDialogView : ReactiveUserControl -{ - public PluginDialogView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs deleted file mode 100644 index 863417326..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive.Linq; -using Artemis.Core; -using Artemis.UI.DryIoc.Factories; -using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.Plugins.Features; -using Artemis.UI.Shared; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs; - -public class PluginDialogViewModel : ContentDialogViewModelBase -{ - public PluginDialogViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory) - { - PluginViewModel = settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => {}, Observable.Empty())); - PluginFeatures = new ObservableCollection(plugin.Features.Select(f => settingsVmFactory.PluginFeatureViewModel(f, false))); - } - - public PluginViewModel PluginViewModel { get; } - public ObservableCollection PluginFeatures { get; } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml new file mode 100644 index 000000000..24c91abf8 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + Used by these profiles + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs new file mode 100644 index 000000000..bffef29b2 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginDescriptionView : ReactiveUserControl +{ + public PluginDescriptionView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs new file mode 100644 index 000000000..0cae2bbcd --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginDescriptionViewModel : RoutableScreen +{ + [Notify] private IEntryDetails? _entry; + [Notify] private List? _dependants; + private readonly IWorkshopClient _client; + private readonly Func _getEntryListViewModel; + + public PluginDescriptionViewModel(IWorkshopClient client, Func getEntryListViewModel) + { + _client = client; + _getEntryListViewModel = getEntryListViewModel; + } + + public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken) + { + Entry = entry; + + if (entry != null) + { + IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, 0, 25, cancellationToken)).Data?.Entries?.Items; + Dependants = dependants != null && dependants.Any() ? dependants.Select(_getEntryListViewModel).OrderByDescending(d => d.Entry.Downloads).Take(10).ToList() : null; + } + else + { + Dependants = null; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml index dcbb4f5d1..9b99c42c3 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml @@ -2,9 +2,10 @@ 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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:ui="clr-namespace:Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView" x:DataType="plugins:PluginDetailsViewModel"> @@ -15,47 +16,44 @@ - - Admin required - - + + + + + + + + + + + Admin required + + - Supported platforms - - - - + Supported platforms + + + + + - + + - + - - - - - - - - - - - - Used by these profiles - - - - - - - + + + + + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs index c50ffbe89..fe248c549 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs @@ -1,4 +1,7 @@ +using System; +using System.Reactive.Disposables; using Avalonia.ReactiveUI; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Plugins; @@ -7,5 +10,9 @@ public partial class PluginDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs index 64b556639..802698aab 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs @@ -1,104 +1,68 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Parameters; -using Artemis.UI.Screens.Workshop.Plugins.Dialogs; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Models; using PropertyChanged.SourceGenerator; using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Plugins; -public partial class PluginDetailsViewModel : RoutableScreen +public partial class PluginDetailsViewModel : RoutableHostScreen { private readonly IWorkshopClient _client; - private readonly IWindowService _windowService; - private readonly IPluginManagementService _pluginManagementService; - private readonly Func _getEntryInfoViewModel; + private readonly PluginDescriptionViewModel _pluginDescriptionViewModel; private readonly Func _getEntryReleasesViewModel; private readonly Func _getEntryImagesViewModel; - private readonly Func _getEntryListViewModel; [Notify] private IGetPluginEntryById_Entry? _entry; - [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private ReadOnlyObservableCollection? _dependants; - + public PluginDetailsViewModel(IWorkshopClient client, - IWindowService windowService, - IPluginManagementService pluginManagementService, - Func getEntryInfoViewModel, + PluginDescriptionViewModel pluginDescriptionViewModel, + EntryInfoViewModel entryInfoViewModel, Func getEntryReleasesViewModel, - Func getEntryImagesViewModel, - Func getEntryListViewModel) + Func getEntryImagesViewModel) { _client = client; - _windowService = windowService; - _pluginManagementService = pluginManagementService; - _getEntryInfoViewModel = getEntryInfoViewModel; + _pluginDescriptionViewModel = pluginDescriptionViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; - _getEntryListViewModel = getEntryListViewModel; + + EntryInfoViewModel = entryInfoViewModel; + RecycleScreen = false; } + public override RoutableScreen DefaultScreen => _pluginDescriptionViewModel; + public EntryInfoViewModel EntryInfoViewModel { get; } + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { - await GetEntry(parameters.EntryId, cancellationToken); + if (Entry?.Id != parameters.EntryId) + await GetEntry(parameters.EntryId, cancellationToken); } private async Task GetEntry(long entryId, CancellationToken cancellationToken) { + Task grace = Task.Delay(300, cancellationToken); IOperationResult result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken); if (result.IsErrorResult()) return; + // Let the UI settle to avoid lag when deep linking + await grace; + Entry = result.Data?.Entry; - EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; + EntryInfoViewModel.SetEntry(Entry); EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; - if (EntryReleasesViewModel != null) - { - EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted; - EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished; - } - - IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items; - Dependants = dependants != null && dependants.Any() - ? new ReadOnlyObservableCollection(new ObservableCollection(dependants.Select(_getEntryListViewModel))) - : null; - } - - private async Task OnInstallationStarted(IEntryDetails entryDetails) - { - bool confirm = await _windowService.ShowConfirmContentDialog( - "Installing plugin", - $"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" + - "Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!", - "I trust this plugin, install it" - ); - - return !confirm; - } - - private async Task OnInstallationFinished(InstalledEntry installedEntry) - { - if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId)) - return; - Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId); - if (plugin == null) - return; - - await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync(); + await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml new file mode 100644 index 000000000..da72fd3fe --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs new file mode 100644 index 000000000..f5d9d54c1 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginListView : ReactiveUserControl +{ + public PluginListView() + { + InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs new file mode 100644 index 000000000..7401c2659 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs @@ -0,0 +1,17 @@ +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public class PluginListViewModel : RoutableHostScreen +{ + private readonly EntryListViewModel _entryListViewModel; + public override RoutableScreen DefaultScreen => _entryListViewModel; + + public PluginListViewModel(EntryListViewModel entryListViewModel) + { + _entryListViewModel = entryListViewModel; + _entryListViewModel.EntryType = EntryType.Plugin; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml new file mode 100644 index 000000000..c6fe8c059 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml @@ -0,0 +1,33 @@ + + + + + + Manage plugin + + + + + + + + + + Plugin features + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs new file mode 100644 index 000000000..d5136c54e --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginManageView : ReactiveUserControl +{ + public PluginManageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs new file mode 100644 index 000000000..9bd558217 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.Plugins.Features; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using Avalonia.Threading; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginManageViewModel : RoutableScreen +{ + private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + [Notify] private PluginViewModel? _pluginViewModel; + [Notify] private ObservableCollection? _pluginFeatures; + + public PluginManageViewModel(ISettingsVmFactory settingsVmFactory, IRouter router, IWorkshopService workshopService, IPluginManagementService pluginManagementService, IWindowService windowService) + { + _settingsVmFactory = settingsVmFactory; + _router = router; + _workshopService = workshopService; + _pluginManagementService = pluginManagementService; + _windowService = windowService; + ParameterSource = ParameterSource.Route; + } + + public async Task Close() + { + await _router.GoUp(); + } + + /// + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId); + if (installedEntry == null || !installedEntry.TryGetMetadata("PluginId", out Guid pluginId)) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null); + await Close(); + }); + return; + } + + Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId); + if (plugin == null) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null); + await Close(); + }); + return; + } + + PluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); + PluginFeatures = new ObservableCollection(plugin.Features.Select(f => _settingsVmFactory.PluginFeatureViewModel(f, false))); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml new file mode 100644 index 000000000..5b29dc430 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + Required plugins + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs new file mode 100644 index 000000000..5fed996a6 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public partial class ProfileDescriptionView : ReactiveUserControl +{ + public ProfileDescriptionView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs new file mode 100644 index 000000000..fd16c6cac --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public partial class ProfileDescriptionViewModel : RoutableScreen +{ + private readonly IWorkshopClient _client; + private readonly Func _getEntryListViewModel; + [Notify] private IEntryDetails? _entry; + [Notify] private List? _dependencies; + + public ProfileDescriptionViewModel(IWorkshopClient client, Func getEntryListViewModel) + { + _client = client; + _getEntryListViewModel = getEntryListViewModel; + } + + public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken) + { + Entry = entry; + + if (entry != null) + { + IReadOnlyList? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entry.Id, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies; + Dependencies = dependencies != null && dependencies.Any() ? dependencies.Select(_getEntryListViewModel).ToList() : null; + } + else + { + Dependencies = null; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml index 499a5b978..b6d9a52b0 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml @@ -3,7 +3,8 @@ 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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:ui="clr-namespace:Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView" x:DataType="profile:ProfileDetailsViewModel"> @@ -12,34 +13,19 @@ - + - - - - - - - - - - - - Required plugins - - - - - - - + + + + + - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs index 1150bd94c..38b60575c 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs @@ -1,4 +1,7 @@ +using System; +using System.Reactive.Disposables; using Avalonia.ReactiveUI; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Profile; @@ -7,5 +10,9 @@ public partial class ProfileDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs index 8aadc53d4..e9b8480a2 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Entries.Details; -using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; @@ -14,52 +14,56 @@ namespace Artemis.UI.Screens.Workshop.Profile; -public partial class ProfileDetailsViewModel : RoutableScreen +public partial class ProfileDetailsViewModel : RoutableHostScreen { private readonly IWorkshopClient _client; - private readonly Func _getEntryInfoViewModel; + private readonly ProfileDescriptionViewModel _profileDescriptionViewModel; private readonly Func _getEntryReleasesViewModel; private readonly Func _getEntryImagesViewModel; - private readonly Func _getEntryListViewModel; [Notify] private IEntryDetails? _entry; - [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; - [Notify] private ReadOnlyObservableCollection? _dependencies; public ProfileDetailsViewModel(IWorkshopClient client, - Func getEntryInfoViewModel, + ProfileDescriptionViewModel profileDescriptionViewModel, + EntryInfoViewModel entryInfoViewModel, Func getEntryReleasesViewModel, - Func getEntryImagesViewModel, - Func getEntryListViewModel) + Func getEntryImagesViewModel) { _client = client; - _getEntryInfoViewModel = getEntryInfoViewModel; + _profileDescriptionViewModel = profileDescriptionViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; - _getEntryListViewModel = getEntryListViewModel; + + EntryInfoViewModel = entryInfoViewModel; + RecycleScreen = false; } + public override RoutableScreen DefaultScreen => _profileDescriptionViewModel; + public EntryInfoViewModel EntryInfoViewModel { get; } + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { - await GetEntry(parameters.EntryId, cancellationToken); + if (Entry?.Id != parameters.EntryId) + await GetEntry(parameters.EntryId, cancellationToken); } private async Task GetEntry(long entryId, CancellationToken cancellationToken) { + Task grace = Task.Delay(300, cancellationToken); IOperationResult result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); if (result.IsErrorResult()) return; + // Let the UI settle to avoid lag when deep linking + await grace; + Entry = result.Data?.Entry; - EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; + EntryInfoViewModel.SetEntry(Entry); EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; - - IReadOnlyList? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entryId, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies; - Dependencies = dependencies != null && dependencies.Any() - ? new ReadOnlyObservableCollection(new ObservableCollection(dependencies.Select(_getEntryListViewModel))) - : null; + + await _profileDescriptionViewModel.SetEntry(Entry, cancellationToken); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml new file mode 100644 index 000000000..70ca1997a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs new file mode 100644 index 000000000..1d7abe1ce --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public partial class ProfileListView : ReactiveUserControl +{ + public ProfileListView() + { + InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs new file mode 100644 index 000000000..75faa19a1 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs @@ -0,0 +1,17 @@ +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public class ProfileListViewModel : RoutableHostScreen +{ + private readonly EntryListViewModel _entryListViewModel; + public override RoutableScreen DefaultScreen => _entryListViewModel; + + public ProfileListViewModel(EntryListViewModel entryListViewModel) + { + _entryListViewModel = entryListViewModel; + _entryListViewModel.EntryType = EntryType.Profile; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index c92f6290b..aaf688ef6 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -38,6 +38,7 @@ public SubmissionWizardState(IWorkshopWizardViewModel wizardViewModel, IContaine public List Images { get; set; } = new(); public IEntrySource? EntrySource { get; set; } + public string? Changelog { get; set; } public void ChangeScreen() where TSubmissionViewModel : SubmissionViewModel { diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs index 50bbc4ace..ca6667b66 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs @@ -8,7 +8,7 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard; -public partial class ReleaseWizardView: ReactiveAppWindow +public partial class ReleaseWizardView : ReactiveAppWindow { public ReleaseWizardView() { @@ -25,7 +25,7 @@ private void Navigate(SubmissionViewModel viewModel) { try { - Dispatcher.UIThread.Invoke(() => Frame.NavigateFromObject(viewModel)); + Frame.NavigateFromObject(viewModel); } catch (Exception e) { diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml new file mode 100644 index 000000000..ff6ddd67d --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml @@ -0,0 +1,27 @@ + + + + + + + + + Changelog + + If you want to inform your users what has changed in this release, you can provide a changelog. This is optional but recommended. + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs new file mode 100644 index 000000000..2733746f7 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; + +public partial class ChangelogStepView : ReactiveUserControl +{ + public ChangelogStepView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs new file mode 100644 index 000000000..e2e97795f --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs @@ -0,0 +1,40 @@ +using System.Reactive.Disposables; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; +using Artemis.WebClient.Workshop; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; + +public partial class ChangelogStepViewModel : SubmissionViewModel +{ + [Notify] private string? _changelog; + + public ChangelogStepViewModel() + { + GoBack = ReactiveCommand.Create(ExecuteGoBack); + Continue = ReactiveCommand.Create(ExecuteContinue); + ContinueText = "Submit"; + + this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog); + } + + private void ExecuteContinue() + { + State.Changelog = Changelog; + State.ChangeScreen(); + } + + private void ExecuteGoBack() + { + State.Changelog = Changelog; + if (State.EntryType == EntryType.Layout) + State.ChangeScreen(); + else if (State.EntryType == EntryType.Plugin) + State.ChangeScreen(); + else if (State.EntryType == EntryType.Profile) + State.ChangeScreen(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs index 23c3b293e..3ebe3b748 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs @@ -98,6 +98,6 @@ private void ExecuteContinue() if (State.EntryId == null) State.ChangeScreen(); else - State.ChangeScreen(); + State.ChangeScreen(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs index 2d8cc45d9..4008c5f57 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs @@ -78,6 +78,6 @@ private void ExecuteContinue() if (State.EntryId == null) State.ChangeScreen(); else - State.ChangeScreen(); + State.ChangeScreen(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs index 0e3556a96..87d7872dc 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs @@ -64,6 +64,6 @@ private void ExecuteContinue() if (State.EntryId == null) State.ChangeScreen(); else - State.ChangeScreen(); + State.ChangeScreen(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs index 81e464311..c205e24aa 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs @@ -70,7 +70,7 @@ private async Task ExecuteUpload(CancellationToken cancellationToken) // Create a release for the new entry IEntryUploadHandler uploadHandler = _entryUploadHandlerFactory.CreateHandler(State.EntryType); - EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, cancellationToken); + EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, State.Changelog, cancellationToken); if (!uploadResult.IsSuccess) throw new ArtemisWorkshopException(uploadResult.Message); diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs index 6c7ece92f..377bc17cb 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs @@ -25,7 +25,7 @@ private void Navigate(SubmissionViewModel viewModel) { try { - Dispatcher.UIThread.Invoke(() => Frame.NavigateFromObject(viewModel)); + Frame.NavigateFromObject(viewModel); } catch (Exception e) { diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj index 3e348ce4e..a51abe913 100644 --- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj +++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj @@ -48,6 +48,9 @@ MSBuild:GenerateGraphQLCode + + MSBuild:GenerateGraphQLCode + diff --git a/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs b/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs new file mode 100644 index 000000000..d901b1865 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs @@ -0,0 +1,9 @@ +namespace Artemis.WebClient.Workshop.Extensions; + +public static class EntryExtensions +{ + public static string GetEntryPath(this IEntryDetails entry) + { + return $"workshop/entries/{entry.EntryType.ToString().ToLower()}s/details/{entry.Id}"; + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs index aacd0d580..555ed11c4 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs @@ -6,6 +6,6 @@ namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; public interface IEntryInstallationHandler { - Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken); + Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken); Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs index 3014950f7..4e8b469bd 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -25,7 +25,7 @@ public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWor _defaultLayoutProvider = defaultLayoutProvider; } - public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) + public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken) { using MemoryStream stream = new(); diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs index 592b13037..476b658dc 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs @@ -22,7 +22,7 @@ public PluginEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWor _pluginManagementService = pluginManagementService; } - public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) + public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken) { // Ensure there is an installed entry InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id); diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index 1219f84d6..1264c43b7 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -20,7 +20,7 @@ public ProfileEntryInstallationHandler(IHttpClientFactory httpClientFactory, IPr _workshopService = workshopService; } - public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) + public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken) { using MemoryStream stream = new(); diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs index c0bc829be..a25dc689b 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs @@ -1,8 +1,6 @@ -using Artemis.UI.Shared.Utilities; - -namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; +namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; public interface IEntryUploadHandler { - Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken); + Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs index 38e2c9789..0ed8edfc6 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs @@ -18,7 +18,7 @@ public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory) } /// - public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken) + public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken) { if (entrySource is not LayoutEntrySource source) throw new InvalidOperationException("Can only create releases for layouts"); @@ -62,6 +62,8 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour MultipartFormDataContent content = new(); StreamContent streamContent = new(archiveStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); + if (!string.IsNullOrWhiteSpace(changelog)) + content.Add(new StringContent(changelog), "Changelog"); content.Add(streamContent, "file", "file.zip"); // Submit diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs index f8c51034c..926f8485b 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs @@ -14,7 +14,7 @@ public PluginEntryUploadHandler(IHttpClientFactory httpClientFactory) } /// - public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken) + public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken) { if (entrySource is not PluginEntrySource source) throw new InvalidOperationException("Can only create releases for plugins"); @@ -27,6 +27,8 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour MultipartFormDataContent content = new(); StreamContent streamContent = new(fileStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); + if (!string.IsNullOrWhiteSpace(changelog)) + content.Add(new StringContent(changelog), "Changelog"); content.Add(streamContent, "file", "file.zip"); // Submit diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs index 3dbbd68e9..381bd0199 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs @@ -17,7 +17,7 @@ public ProfileEntryUploadHandler(IHttpClientFactory httpClientFactory, IProfileS } /// - public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken) + public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken) { if (entrySource is not ProfileEntrySource source) throw new InvalidOperationException("Can only create releases for profile configurations"); @@ -32,6 +32,8 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour StreamContent streamContent = new(archiveStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); content.Add(JsonContent.Create(source.Dependencies.Select(d => new {PluginId = d.Plugin.Guid, FeatureId = d.Id}).ToList()), "ReleaseDependencies"); + if (!string.IsNullOrWhiteSpace(changelog)) + content.Add(new StringContent(changelog), "Changelog"); content.Add(streamContent, "file", "file.zip"); // Submit diff --git a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs index ce90444b0..401e5495e 100644 --- a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs +++ b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs @@ -16,7 +16,7 @@ internal InstalledEntry(EntryEntity entity) Load(); } - public InstalledEntry(IEntryDetails entry, IRelease release) + public InstalledEntry(IEntrySummary entry, IRelease release) { Entity = new EntryEntity(); diff --git a/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql index fe53ec3e5..1a667c53b 100644 --- a/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql +++ b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql @@ -3,3 +3,16 @@ mutation UpdateEntry ($input: UpdateEntryInput!) { id } } + + +mutation UpdateRelease($input: UpdateReleaseInput!) { + updateRelease(input: $input) { + id + } +} + +mutation RemoveRelease($input: Long!) { + removeRelease(id: $input) { + id + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index 0325293c5..ea41aa36a 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -54,12 +54,12 @@ fragment entryDetails on Entry { categories { ...category } - latestRelease { - ...release - } images { ...image } + releases { + ...release + } } fragment release on Release { diff --git a/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql new file mode 100644 index 000000000..84ee9604a --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql @@ -0,0 +1,9 @@ +query GetReleaseById($id: Long!) { + release(id: $id) { + ...release + changelog + entry { + ...entrySummary + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql index 1bcd98fd6..6aece2ca4 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql @@ -14,5 +14,10 @@ query GetSubmittedEntryById($id: Long!) { images { ...image } + releases { + id + version + createdAt + } } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index ea61b3b6d..304f4ef05 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -20,4 +20,6 @@ public interface IWorkshopService void Initialize(); public record WorkshopStatus(bool IsReachable, string Message); + + event EventHandler? OnInstalledEntrySaved; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 95b90ccc5..715a5a1bb 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -172,6 +172,8 @@ public void SaveInstalledEntry(InstalledEntry entry) { entry.Save(); _entryRepository.Save(entry.Entity); + + OnInstalledEntrySaved?.Invoke(this, entry); } /// @@ -231,4 +233,6 @@ private void RemoveOrphanedDirectory(string directory) _logger.Warning(e, "Failed to remove orphaned workshop entry at {Directory}", directory); } } + + public event EventHandler? OnInstalledEntrySaved; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index 442923f3d..f26e43af8 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -100,8 +100,10 @@ type Mutation { addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo removeEntry(id: Long!): Entry removeLayoutInfo(id: Long!): LayoutInfo! + removeRelease(id: Long!): Release! updateEntry(input: UpdateEntryInput!): Entry updateEntryImage(input: UpdateEntryImageInput!): Image + updateRelease(input: UpdateReleaseInput!): Release } "Information about pagination in a connection." @@ -158,6 +160,7 @@ type Query { entry(id: Long!): Entry pluginInfo(pluginGuid: UUID!): PluginInfo pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment + release(id: Long!): Release searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]! searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo @@ -165,6 +168,7 @@ type Query { } type Release { + changelog: String createdAt: DateTime! dependencies: [Entry!]! downloadSize: Long! @@ -498,6 +502,7 @@ input RGBDeviceTypeOperationFilterInput { input ReleaseFilterInput { and: [ReleaseFilterInput!] + changelog: StringOperationFilterInput createdAt: DateTimeOperationFilterInput dependencies: ListFilterInputTypeOfEntryFilterInput downloadSize: LongOperationFilterInput @@ -511,6 +516,7 @@ input ReleaseFilterInput { } input ReleaseSortInput { + changelog: SortEnumType createdAt: SortEnumType downloadSize: SortEnumType downloads: SortEnumType @@ -558,6 +564,11 @@ input UpdateEntryInput { tags: [String!]! } +input UpdateReleaseInput { + changelog: String + id: Long! +} + input UuidOperationFilterInput { eq: UUID gt: UUID