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/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs index 39e067ec5..3f1263a29 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Router.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs @@ -161,6 +161,28 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable 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() { diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml index 34258aae0..cdb9738aa 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml @@ -124,7 +124,7 @@ - + File size - + - Release info + + + Release info + + + + + + + + + + + + - - - Release date - - - - + Version + + + Release date + + + - + File size Release notes - + + There are no release notes for this release. + + diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs index d92912c5c..a44cc6e8d 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs @@ -1,8 +1,13 @@ +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 PropertyChanged.SourceGenerator; using StrawberryShake; @@ -11,20 +16,84 @@ namespace Artemis.UI.Screens.Workshop.EntryReleases; public partial class EntryReleaseViewModel : RoutableScreen { private readonly IWorkshopClient _client; - [Notify] private IGetReleaseById_Release? _release; + private readonly IRouter _router; + private readonly INotificationService _notificationService; + private readonly IWindowService _windowService; + private readonly EntryInstallationHandlerFactory _factory; + private readonly Progress _progress = new(); - public EntryReleaseViewModel(IWorkshopClient client) + [Notify] private IGetReleaseById_Release? _release; + [Notify] private float _installProgress; + [Notify] private bool _installationInProgress; + + private CancellationTokenSource? _cts; + + public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, EntryInstallationHandlerFactory factory) { _client = client; + _router = router; + _notificationService = notificationService; + _windowService = windowService; + _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(); + else if (!_cts.IsCancellationRequested) + _notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show(); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Failed to install workshop entry", e); + } + finally + { + InstallationInProgress = false; + } + } + + 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); - if (result.IsErrorResult()) - return; - Release = result.Data?.Release; } + + #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 index 899065ec7..cf62f0e5b 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml @@ -2,78 +2,38 @@ 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:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop" + xmlns:entryReleases="clr-namespace:Artemis.UI.Screens.Workshop.EntryReleases" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryReleasesView" - x:DataType="details:EntryReleasesViewModel"> + x:Class="Artemis.UI.Screens.Workshop.EntryReleases.EntryReleasesView" + x:DataType="entryReleases:EntryReleasesViewModel"> + Releases - - - - - - - - - + + - + + + + + + Created + + + + - - - + + \ 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 index 523edc7e5..39b603187 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs @@ -1,10 +1,9 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries.Details; +namespace Artemis.UI.Screens.Workshop.EntryReleases; -public partial class EntryReleasesView : UserControl +public partial class EntryReleasesView : ReactiveUserControl { public EntryReleasesView() { diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs index 4ec497e37..c21b841dc 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs @@ -2,51 +2,49 @@ using System.Collections.Generic; using System.Linq; using System.Reactive; -using System.Threading; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Shared; 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.Models; -using Humanizer; +using PropertyChanged.SourceGenerator; using ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries.Details; +namespace Artemis.UI.Screens.Workshop.EntryReleases; -public class EntryReleasesViewModel : ViewModelBase +public partial class EntryReleasesViewModel : ActivatableViewModelBase { - private readonly EntryInstallationHandlerFactory _factory; - private readonly IWindowService _windowService; - private readonly INotificationService _notificationService; private readonly IRouter _router; + [Notify] private IRelease? _selectedRelease; - public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService, IRouter router) + public EntryReleasesViewModel(IEntryDetails entry, IRouter router) { - _factory = factory; - _windowService = windowService; - _notificationService = notificationService; _router = router; Entry = entry; - LatestRelease = Entry.Releases.MaxBy(r => r.CreatedAt); - OtherReleases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Skip(1).Take(4).Cast().ToList(); - - DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); - OnInstallationStarted = Confirm; + Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Cast().ToList(); NavigateToRelease = ReactiveCommand.CreateFromTask(ExecuteNavigateToRelease); + + this.WhenActivated(d => + { + router.CurrentPath.Subscribe(p => SelectedRelease = p != null && p.Contains("releases") && float.TryParse(p.Split('/').Last(), out float releaseId) + ? Releases.FirstOrDefault(r => r.Id == releaseId) + : null) + .DisposeWith(d); + + this.WhenAnyValue(vm => vm.SelectedRelease) + .WhereNotNull() + .Subscribe(s => ExecuteNavigateToRelease(s)) + .DisposeWith(d); + }); } public IEntryDetails Entry { get; } - public IRelease? LatestRelease { get; } - public List OtherReleases { get; } + public List Releases { get; } - public ReactiveCommand DownloadLatestRelease { get; } public ReactiveCommand NavigateToRelease { get; } - + public Func> OnInstallationStarted { get; set; } public Func? OnInstallationFinished { get; set; } @@ -67,39 +65,4 @@ public class EntryReleasesViewModel : ViewModelBase throw new ArgumentOutOfRangeException(nameof(Entry.EntryType)); } } - - private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken) - { - if (LatestRelease == null) - return; - - if (await OnInstallationStarted(Entry, LatestRelease)) - return; - - IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); - EntryInstallResult result = await installationHandler.InstallAsync(Entry, LatestRelease, new Progress(), cancellationToken); - if (result.IsSuccess && result.Entry != null) - { - if (OnInstallationFinished != null) - await OnInstallationFinished(result.Entry); - _notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show(); - } - else - { - _notificationService.CreateNotification() - .WithTitle($"Failed to install {Entry.EntryType.Humanize(LetterCasing.LowerCase)}") - .WithMessage(result.Message) - .WithSeverity(NotificationSeverity.Error).Show(); - } - } - - private async Task Confirm(IEntryDetails entryDetails, IRelease release) - { - bool confirm = await _windowService.ShowConfirmContentDialog( - "Install latest release", - $"Are you sure you want to download and install version {release.Version} of {entryDetails.Name}?" - ); - - return !confirm; - } } \ 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 69853e0f9..c03896bf8 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; +using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Layout.Dialogs; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; @@ -40,13 +41,15 @@ public partial class LayoutDetailsViewModel : RoutableHostScreen getEntryReleasesViewModel, Func getEntryImagesViewModel) { - LayoutDescriptionViewModel = layoutDescriptionViewModel; _client = client; _deviceService = deviceService; _windowService = windowService; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; + + LayoutDescriptionViewModel = layoutDescriptionViewModel; + RecycleScreen = false; } public LayoutDescriptionViewModel LayoutDescriptionViewModel { get; } diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs index b2c222fbb..a1b6e0939 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs @@ -8,6 +8,7 @@ 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; @@ -41,13 +42,15 @@ public partial class PluginDetailsViewModel : RoutableHostScreen getEntryReleasesViewModel, Func getEntryImagesViewModel) { - PluginDescriptionViewModel = pluginDescriptionViewModel; _client = client; _windowService = windowService; _pluginManagementService = pluginManagementService; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; + + PluginDescriptionViewModel = pluginDescriptionViewModel; + RecycleScreen = false; } public PluginDescriptionViewModel PluginDescriptionViewModel { get; } diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs index 1c26abb8d..bd08e49d9 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Entries.Details; +using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; @@ -31,12 +32,13 @@ public partial class ProfileDetailsViewModel : RoutableHostScreen getEntryReleasesViewModel, Func getEntryImagesViewModel) { - ProfileDescriptionViewModel = profileDescriptionViewModel; - _client = client; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; + + ProfileDescriptionViewModel = profileDescriptionViewModel; + RecycleScreen = false; } public ProfileDescriptionViewModel ProfileDescriptionViewModel { get; } 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 class LayoutEntryInstallationHandler : IEntryInstallationHandler _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 class PluginEntryInstallationHandler : IEntryInstallationHandler _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 class ProfileEntryInstallationHandler : IEntryInstallationHandler _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/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 @@ public class InstalledEntry Load(); } - public InstalledEntry(IEntryDetails entry, IRelease release) + public InstalledEntry(IEntrySummary entry, IRelease release) { Entity = new EntryEntity(); diff --git a/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql index 4b9aaa912..84ee9604a 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql @@ -2,5 +2,8 @@ query GetReleaseById($id: Long!) { release(id: $id) { ...release changelog + entry { + ...entrySummary + } } } \ No newline at end of file