From 671c587df61e598772dd918154845389ef29728d Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 26 Aug 2023 20:47:48 +0200 Subject: [PATCH] Profiles - Added workshop installing --- .../Storage/Interfaces/IProfileService.cs | 3 +- .../Services/Storage/ProfileService.cs | 22 +++++-- .../Extensions/HttpClientExtensions.cs | 14 ++--- .../Extensions/ZipArchiveExtensions.cs | 5 +- src/Artemis.UI/Routing/Routes.cs | 2 + .../Sidebar/SidebarCategoryViewModel.cs | 12 ++-- .../Workshop/Home/WorkshopHomeView.axaml | 15 ++--- .../Workshop/Home/WorkshopHomeViewModel.cs | 26 ++++++++- .../Workshop/Home/WorkshopOfflineView.axaml | 36 ++++++++++++ .../Home/WorkshopOfflineView.axaml.cs | 14 +++++ .../Workshop/Home/WorkshopOfflineViewModel.cs | 57 +++++++++++++++++++ .../Workshop/Profile/ProfileDetailsView.axaml | 21 ++++--- .../Profile/ProfileDetailsViewModel.cs | 35 ++++++++++-- .../Steps/UploadStepView.axaml | 40 +++++++------ .../Steps/UploadStepViewModel.cs | 33 +++++++++-- .../Services/Updating/ReleaseInstaller.cs | 16 +++--- .../DownloadHandlers/EntryUploadResult.cs | 28 +++++++++ .../DownloadHandlers/IEntryDownloadHandler.cs | 7 +++ .../ProfileEntryDownloadHandler.cs | 51 +++++++++++++++++ .../DryIoc/ContainerExtensions.cs | 2 + .../Services/IWorkshopService.cs | 35 +++++++++++- 21 files changed, 401 insertions(+), 73 deletions(-) rename src/{Artemis.UI => Artemis.UI.Shared}/Extensions/HttpClientExtensions.cs (78%) create mode 100644 src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineViewModel.cs create mode 100644 src/Artemis.WebClient.Workshop/DownloadHandlers/EntryUploadResult.cs create mode 100644 src/Artemis.WebClient.Workshop/DownloadHandlers/IEntryDownloadHandler.cs create mode 100644 src/Artemis.WebClient.Workshop/DownloadHandlers/Implementations/ProfileEntryDownloadHandler.cs diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 4d2907af4..1744e5335 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -76,8 +76,9 @@ public interface IProfileService : IArtemisService /// Creates a new profile category and saves it to persistent storage. /// /// The name of the new profile category, must be unique. + /// A boolean indicating whether or not to add the category to the top. /// The newly created profile category. - ProfileCategory CreateProfileCategory(string name); + ProfileCategory CreateProfileCategory(string name, bool addToTop = false); /// /// Permanently deletes the provided profile category. diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 159e2ebf5..3a09a8c06 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -286,12 +286,26 @@ internal class ProfileService : IProfileService } /// - public ProfileCategory CreateProfileCategory(string name) + public ProfileCategory CreateProfileCategory(string name, bool addToTop = false) { ProfileCategory profileCategory; lock (_profileRepository) { - profileCategory = new ProfileCategory(name, _profileCategories.Count + 1); + if (addToTop) + { + profileCategory = new ProfileCategory(name, 1); + foreach (ProfileCategory category in _profileCategories) + { + category.Order++; + category.Save(); + _profileCategoryRepository.Save(category.Entity); + } + } + else + { + profileCategory = new ProfileCategory(name, _profileCategories.Count + 1); + } + _profileCategories.Add(profileCategory); SaveProfileCategory(profileCategory); } @@ -370,7 +384,7 @@ internal class ProfileService : IProfileService profile.ProfileEntity.IsFreshImport = false; _profileRepository.Save(profile.ProfileEntity); - + // If the provided profile is external (cloned or from the workshop?) but it is loaded locally too, reload the local instance // A bit dodge but it ensures local instances always represent the latest stored version ProfileConfiguration? localInstance = ProfileConfigurations.FirstOrDefault(p => p.Profile != null && p.Profile != profile && p.ProfileId == profile.ProfileEntity.Id); @@ -450,7 +464,7 @@ internal class ProfileService : IProfileService List modules = _pluginManagementService.GetFeaturesOfType(); profileConfiguration.LoadModules(modules); SaveProfileCategory(category); - + return profileConfiguration; } diff --git a/src/Artemis.UI/Extensions/HttpClientExtensions.cs b/src/Artemis.UI.Shared/Extensions/HttpClientExtensions.cs similarity index 78% rename from src/Artemis.UI/Extensions/HttpClientExtensions.cs rename to src/Artemis.UI.Shared/Extensions/HttpClientExtensions.cs index 50af33443..d9adda42f 100644 --- a/src/Artemis.UI/Extensions/HttpClientExtensions.cs +++ b/src/Artemis.UI.Shared/Extensions/HttpClientExtensions.cs @@ -3,12 +3,13 @@ using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Artemis.UI.Shared.Utilities; -namespace Artemis.UI.Extensions +namespace Artemis.UI.Shared.Extensions { public static class HttpClientProgressExtensions { - public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress? progress, CancellationToken cancellationToken) + public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress? progress, CancellationToken cancellationToken) { using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); @@ -23,13 +24,10 @@ namespace Artemis.UI.Extensions } // Such progress and contentLength much reporting Wow! - Progress progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value))); - await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken); - - float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f; + await download.CopyToAsync(destination, 81920, progress, contentLength, cancellationToken); } - static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress, CancellationToken cancellationToken) + static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress, long? contentLength, CancellationToken cancellationToken) { if (bufferSize < 0) throw new ArgumentOutOfRangeException(nameof(bufferSize)); @@ -49,7 +47,7 @@ namespace Artemis.UI.Extensions { await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; - progress?.Report(totalBytesRead); + progress?.Report(new StreamProgress(totalBytesRead, contentLength ?? totalBytesRead)); } } } diff --git a/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs index 0d1fc507d..ad1900caa 100644 --- a/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs +++ b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.IO.Compression; using System.Threading; +using Artemis.UI.Shared.Utilities; namespace Artemis.UI.Extensions; @@ -16,7 +17,7 @@ public static class ZipArchiveExtensions /// A boolean indicating whether to override existing files /// The progress to report to. /// A cancellation token - public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress progress, CancellationToken cancellationToken) + public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress progress, CancellationToken cancellationToken) { if (source == null) throw new ArgumentNullException(nameof(source)); @@ -28,7 +29,7 @@ public static class ZipArchiveExtensions { ZipArchiveEntry entry = source.Entries[index]; entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); - progress.Report((index + 1f) / source.Entries.Count * 100f); + progress.Report(new StreamProgress(index + 1, source.Entries.Count)); cancellationToken.ThrowIfCancellationRequested(); } } diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index d2709888a..dbdf17053 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -5,6 +5,7 @@ using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Settings.Updating; using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.Workshop; +using Artemis.UI.Screens.Workshop.Home; using Artemis.UI.Screens.Workshop.Layout; using Artemis.UI.Screens.Workshop.Profile; using Artemis.UI.Shared.Routing; @@ -21,6 +22,7 @@ public static class Routes { Children = new List() { + new RouteRegistration("offline/{message:string}"), new RouteRegistration("profiles/{page:int}"), new RouteRegistration("profiles/{entryId:guid}"), new RouteRegistration("layouts/{page:int}"), diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index 5fd46c94c..8911989d7 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -77,16 +77,16 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase Observable.FromEventPattern(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x) .Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration))) .DisposeWith(d); + + profileConfigurations.Edit(updater => + { + updater.Clear(); + updater.AddRange(profileCategory.ProfileConfigurations); + }); _isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d); _isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d); }); - - profileConfigurations.Edit(updater => - { - foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) - updater.Add(profileConfiguration); - }); } public ReactiveCommand ImportProfile { get; } diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index d0717860f..0695a7554 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -9,6 +9,8 @@ x:DataType="home:WorkshopHomeViewModel"> + + - + - + - + - + - + Featured submissions Not yet implemented, here we'll show submissions we think are worth some extra attention. - + Recently updated Not yet implemented, here we'll a few of the most recent uploads/updates to the workshop. - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs index dfaf0a8f2..45663f0d7 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.SubmissionWizard; @@ -6,6 +7,8 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; +using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Home; @@ -13,21 +16,38 @@ namespace Artemis.UI.Screens.Workshop.Home; public class WorkshopHomeViewModel : ActivatableViewModelBase, IWorkshopViewModel { private readonly IWindowService _windowService; + private readonly IWorkshopService _workshopService; + private bool _workshopReachable; - public WorkshopHomeViewModel(IRouter router, IWindowService windowService) + public WorkshopHomeViewModel(IRouter router, IWindowService windowService, IWorkshopService workshopService) { _windowService = windowService; - AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission); - Navigate = ReactiveCommand.CreateFromTask(async r => await router.Navigate(r)); + _workshopService = workshopService; + + AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable)); + Navigate = ReactiveCommand.CreateFromTask(async r => await router.Navigate(r), this.WhenAnyValue(vm => vm.WorkshopReachable)); + + this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(ValidateWorkshopStatus)); } public ReactiveCommand AddSubmission { get; } public ReactiveCommand Navigate { get; } + public bool WorkshopReachable + { + get => _workshopReachable; + private set => RaiseAndSetIfChanged(ref _workshopReachable, value); + } + private async Task ExecuteAddSubmission(CancellationToken arg) { await _windowService.ShowDialogAsync(); } + private async Task ValidateWorkshopStatus() + { + WorkshopReachable = await _workshopService.ValidateWorkshopStatus(); + } + public EntryType? EntryType => null; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml new file mode 100644 index 000000000..b700c5375 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + Could not reach the workshop + + + + Please ensure you are connected to the internet. + If this keeps occuring, hit us up on Discord + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml.cs new file mode 100644 index 000000000..a7d5eebdb --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Home; + +public partial class WorkshopOfflineView : ReactiveUserControl +{ + public WorkshopOfflineView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineViewModel.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineViewModel.cs new file mode 100644 index 000000000..5ee84b8d4 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopOfflineViewModel.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Home; + +public class WorkshopOfflineViewModel : RoutableScreen, IWorkshopViewModel +{ + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private string _message; + + /// + public WorkshopOfflineViewModel(IWorkshopService workshopService, IRouter router) + { + _workshopService = workshopService; + _router = router; + + Retry = ReactiveCommand.CreateFromTask(ExecuteRetry); + } + + public ReactiveCommand Retry { get; } + + public string Message + { + get => _message; + set => RaiseAndSetIfChanged(ref _message, value); + } + + public override Task OnNavigating(WorkshopOfflineParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + Message = parameters.Message; + return base.OnNavigating(parameters, args, cancellationToken); + } + + private async Task ExecuteRetry(CancellationToken cancellationToken) + { + IWorkshopService.WorkshopStatus status = await _workshopService.GetWorkshopStatus(); + if (status.IsReachable) + await _router.Navigate("workshop"); + + Message = status.Message; + } + + public EntryType? EntryType => null; +} + +public class WorkshopOfflineParameters +{ + public string Message { get; set; } +} \ 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 2adad018e..00ffc76c9 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml @@ -86,29 +86,36 @@ Latest release - diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs index ed92162f8..f15ca7cf6 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs @@ -1,11 +1,18 @@ using System; +using System.Reactive; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; +using Artemis.Core; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.DownloadHandlers; +using Artemis.WebClient.Workshop.DownloadHandlers.Implementations; using ReactiveUI; using StrawberryShake; @@ -14,18 +21,24 @@ namespace Artemis.UI.Screens.Workshop.Profile; public class ProfileDetailsViewModel : RoutableScreen, IWorkshopViewModel { private readonly IWorkshopClient _client; + private readonly ProfileEntryDownloadHandler _downloadHandler; + private readonly INotificationService _notificationService; private readonly ObservableAsPropertyHelper _updatedAt; private IGetEntryById_Entry? _entry; - public ProfileDetailsViewModel(IWorkshopClient client) + public ProfileDetailsViewModel(IWorkshopClient client, ProfileEntryDownloadHandler downloadHandler, INotificationService notificationService) { _client = client; + _downloadHandler = downloadHandler; + _notificationService = notificationService; _updatedAt = this.WhenAnyValue(vm => vm.Entry).Select(e => e?.LatestRelease?.CreatedAt ?? e?.CreatedAt).ToProperty(this, vm => vm.UpdatedAt); + + DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); } - public DateTimeOffset? UpdatedAt => _updatedAt.Value; + public ReactiveCommand DownloadLatestRelease { get; } - public EntryType? EntryType => null; + public DateTimeOffset? UpdatedAt => _updatedAt.Value; public IGetEntryById_Entry? Entry { @@ -43,7 +56,21 @@ public class ProfileDetailsViewModel : RoutableScreen result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); if (result.IsErrorResult()) return; - + Entry = result.Data?.Entry; } + + private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken) + { + if (Entry?.LatestRelease == null) + return; + + EntryInstallResult result = await _downloadHandler.InstallProfileAsync(Entry.LatestRelease.Id, new Progress(), cancellationToken); + if (result.IsSuccess) + _notificationService.CreateNotification().WithTitle("Profile installed").WithSeverity(NotificationSeverity.Success).Show(); + else + _notificationService.CreateNotification().WithTitle("Failed to install profile").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show(); + } + + public EntryType? EntryType => null; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml index 89a7ec328..e3b9e23c3 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml @@ -3,10 +3,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps" - mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="900" + mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="900" x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.UploadStepView" x:DataType="steps:UploadStepViewModel"> - + - + + Uploading your submission... - - Wooo, the final step, that was pretty easy, right!? - - - - - + + + + + All done! Hit finish to view your submission. - + + + 😢 + + Unfortunately something went wrong while uploading your submission. + + Hit finish to view your submission, from there you can try to upload a new release. + If this keeps occuring, hit us up on Discord + + - + \ 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 05ef0355a..14e56958a 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs @@ -28,8 +28,10 @@ public class UploadStepViewModel : SubmissionViewModel private readonly ObservableAsPropertyHelper _progressPercentage; private readonly ObservableAsPropertyHelper _progressIndeterminate; - private bool _finished; private Guid? _entryId; + private bool _finished; + private bool _succeeded; + private bool _failed; /// public UploadStepViewModel(IWorkshopClient workshopClient, IWorkshopService workshopService, EntryUploadHandlerFactory entryUploadHandlerFactory, IWindowService windowService, IRouter router) @@ -43,14 +45,14 @@ public class UploadStepViewModel : SubmissionViewModel ShowGoBack = false; ContinueText = "Finish"; Continue = ReactiveCommand.CreateFromTask(ExecuteContinue, this.WhenAnyValue(vm => vm.Finished)); - + _progressPercentage = Observable.FromEventPattern(x => _progress.ProgressChanged += x, x => _progress.ProgressChanged -= x) .Select(e => e.EventArgs.ProgressPercentage) .ToProperty(this, vm => vm.ProgressPercentage); _progressIndeterminate = Observable.FromEventPattern(x => _progress.ProgressChanged += x, x => _progress.ProgressChanged -= x) .Select(e => e.EventArgs.ProgressPercentage == 0) .ToProperty(this, vm => vm.ProgressIndeterminate); - + this.WhenActivated(d => Observable.FromAsync(ExecuteUpload).Subscribe().DisposeWith(d)); } @@ -69,6 +71,18 @@ public class UploadStepViewModel : SubmissionViewModel set => RaiseAndSetIfChanged(ref _finished, value); } + public bool Succeeded + { + get => _succeeded; + set => RaiseAndSetIfChanged(ref _succeeded, value); + } + + public bool Failed + { + get => _failed; + set => RaiseAndSetIfChanged(ref _failed, value); + } + public async Task ExecuteUpload(CancellationToken cancellationToken) { IOperationResult result = await _workshopClient.AddEntry.ExecuteAsync(new CreateEntryInput @@ -85,12 +99,13 @@ public class UploadStepViewModel : SubmissionViewModel if (result.IsErrorResult() || entryId == null) { await _windowService.ShowConfirmContentDialog("Failed to create workshop entry", result.Errors.ToString() ?? "Not even an error message", "Close", null); + State.ChangeScreen(); return; } if (cancellationToken.IsCancellationRequested) return; - + // Upload image if (State.Icon != null) await _workshopService.SetEntryIcon(entryId.Value, _progress, State.Icon, cancellationToken); @@ -113,21 +128,27 @@ public class UploadStepViewModel : SubmissionViewModel } _entryId = entryId; - Finished = true; + Succeeded = true; } catch (Exception e) { // Something went wrong when creating a release :c // We'll keep the workshop entry so that the user can make changes and try again + Failed = true; + } + finally + { + Finished = true; } } private async Task ExecuteContinue() { + State.Finish(); + if (_entryId == null) return; - State.Finish(); switch (State.EntryType) { case EntryType.Layout: diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs index 04ca4f74f..91a9a2c9c 100644 --- a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Exceptions; using Artemis.UI.Extensions; +using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Updating; using Octodiff.Core; using Octodiff.Diagnostics; @@ -32,7 +34,7 @@ public class ReleaseInstaller : CorePropertyChanged private IGetReleaseById_PublishedRelease _release = null!; private IGetReleaseById_PublishedRelease_Artifacts _artifact = null!; - private Progress _stepProgress = new(); + private Progress _stepProgress = new(); private string _status = string.Empty; private float _floatProgress; @@ -69,9 +71,7 @@ public class ReleaseInstaller : CorePropertyChanged public async Task InstallAsync(CancellationToken cancellationToken) { - _stepProgress = new Progress(); - - ((IProgress) _progress).Report(0); + _stepProgress = new Progress(); Status = "Retrieving details"; _logger.Information("Retrieving details for release {ReleaseId}", _releaseId); @@ -99,7 +99,7 @@ public class ReleaseInstaller : CorePropertyChanged { // 10 - 50% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; - void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(10f + e * 0.4f); + void StepProgressOnProgressChanged(object? sender, StreamProgress e) => ((IProgress) _progress).Report(10f + e.ProgressPercentage * 0.4f); Status = "Downloading..."; await using MemoryStream stream = new(); @@ -113,7 +113,7 @@ public class ReleaseInstaller : CorePropertyChanged { // 50 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; - void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(50f + e * 0.1f); + void StepProgressOnProgressChanged(object? sender, StreamProgress e) => ((IProgress) _progress).Report(50f + e.ProgressPercentage * 0.1f); Status = "Patching..."; await using FileStream newFileStream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); @@ -139,7 +139,7 @@ public class ReleaseInstaller : CorePropertyChanged { // 10 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; - void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(10f + e * 0.5f); + void StepProgressOnProgressChanged(object? sender, StreamProgress e) => ((IProgress) _progress).Report(10f + e.ProgressPercentage * 0.5f); Status = "Downloading..."; await using FileStream stream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); @@ -155,7 +155,7 @@ public class ReleaseInstaller : CorePropertyChanged { // 60 - 100% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; - void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(60f + e * 0.4f); + void StepProgressOnProgressChanged(object? sender, StreamProgress e) => ((IProgress) _progress).Report(60f + e.ProgressPercentage * 0.4f); Status = "Extracting..."; // Ensure the directory is empty diff --git a/src/Artemis.WebClient.Workshop/DownloadHandlers/EntryUploadResult.cs b/src/Artemis.WebClient.Workshop/DownloadHandlers/EntryUploadResult.cs new file mode 100644 index 000000000..d1162bee8 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/DownloadHandlers/EntryUploadResult.cs @@ -0,0 +1,28 @@ +using Artemis.Web.Workshop.Entities; + +namespace Artemis.WebClient.Workshop.DownloadHandlers; + +public class EntryInstallResult +{ + public bool IsSuccess { get; set; } + public string? Message { get; set; } + public T? Result { get; set; } + + public static EntryInstallResult FromFailure(string? message) + { + return new EntryInstallResult + { + IsSuccess = false, + Message = message + }; + } + + public static EntryInstallResult FromSuccess(T installationResult) + { + return new EntryInstallResult + { + IsSuccess = true, + Result = installationResult + }; + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/DownloadHandlers/IEntryDownloadHandler.cs b/src/Artemis.WebClient.Workshop/DownloadHandlers/IEntryDownloadHandler.cs new file mode 100644 index 000000000..8d477e7ff --- /dev/null +++ b/src/Artemis.WebClient.Workshop/DownloadHandlers/IEntryDownloadHandler.cs @@ -0,0 +1,7 @@ +using Artemis.UI.Shared.Utilities; + +namespace Artemis.WebClient.Workshop.DownloadHandlers; + +public interface IEntryDownloadHandler +{ +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/DownloadHandlers/Implementations/ProfileEntryDownloadHandler.cs b/src/Artemis.WebClient.Workshop/DownloadHandlers/Implementations/ProfileEntryDownloadHandler.cs new file mode 100644 index 000000000..496072c9c --- /dev/null +++ b/src/Artemis.WebClient.Workshop/DownloadHandlers/Implementations/ProfileEntryDownloadHandler.cs @@ -0,0 +1,51 @@ +using System.IO.Compression; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Utilities; +using Newtonsoft.Json; + +namespace Artemis.WebClient.Workshop.DownloadHandlers.Implementations; + +public class ProfileEntryDownloadHandler : IEntryDownloadHandler +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IProfileService _profileService; + + public ProfileEntryDownloadHandler(IHttpClientFactory httpClientFactory, IProfileService profileService) + { + _httpClientFactory = httpClientFactory; + _profileService = profileService; + } + + public async Task> InstallProfileAsync(Guid releaseId, Progress progress, CancellationToken cancellationToken) + { + try + { + HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); + using MemoryStream stream = new(); + await client.DownloadDataAsync($"releases/download/{releaseId}", stream, progress, cancellationToken); + + using ZipArchive zipArchive = new(stream, ZipArchiveMode.Read); + List profiles = zipArchive.Entries.Where(e => e.Name.EndsWith("json", StringComparison.InvariantCultureIgnoreCase)).ToList(); + ZipArchiveEntry userProfileEntry = profiles.First(); + ProfileConfigurationExportModel profile = await GetProfile(userProfileEntry); + + ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true); + ProfileConfiguration profileConfiguration = _profileService.ImportProfile(category, profile, true, true, null); + return EntryInstallResult.FromSuccess(profileConfiguration); + } + catch (Exception e) + { + return EntryInstallResult.FromFailure(e.Message); + } + } + + private async Task GetProfile(ZipArchiveEntry userProfileEntry) + { + await using Stream stream = userProfileEntry.Open(); + using StreamReader reader = new(stream); + + return JsonConvert.DeserializeObject(await reader.ReadToEndAsync(), IProfileService.ExportSettings)!; + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs index 05f1ecb21..e98f8c8e7 100644 --- a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Artemis.WebClient.Workshop.DownloadHandlers; using Artemis.WebClient.Workshop.Extensions; using Artemis.WebClient.Workshop.Repositories; using Artemis.WebClient.Workshop.Services; @@ -48,5 +49,6 @@ public static class ContainerExtensions container.Register(Reuse.Transient); container.RegisterMany(workshopAssembly, type => type.IsAssignableTo(), Reuse.Transient); + container.RegisterMany(workshopAssembly, type => type.IsAssignableTo(), Reuse.Transient); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs index f81e9d0ee..c79bde1e1 100644 --- a/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs @@ -1,4 +1,6 @@ +using System.Net; using System.Net.Http.Headers; +using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop.UploadHandlers; @@ -11,11 +13,13 @@ public class WorkshopService : IWorkshopService { private readonly Dictionary _entryIconCache = new(); private readonly IHttpClientFactory _httpClientFactory; + private readonly IRouter _router; private readonly SemaphoreSlim _iconCacheLock = new(1); - public WorkshopService(IHttpClientFactory httpClientFactory, IMainWindowService mainWindowService) + public WorkshopService(IHttpClientFactory httpClientFactory, IMainWindowService mainWindowService, IRouter router) { _httpClientFactory = httpClientFactory; + _router = router; mainWindowService.MainWindowClosed += (_, _) => Dispatcher.UIThread.InvokeAsync(async () => { await Task.Delay(1000); @@ -43,6 +47,31 @@ public class WorkshopService : IWorkshopService return ImageUploadResult.FromSuccess(); } + /// + public async Task GetWorkshopStatus() + { + try + { + // Don't use the workshop client which adds auth headers + HttpClient client = _httpClientFactory.CreateClient(); + HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, WorkshopConstants.WORKSHOP_URL + "/status")); + return new IWorkshopService.WorkshopStatus(response.IsSuccessStatusCode, response.StatusCode.ToString()); + } + catch (HttpRequestException e) + { + return new IWorkshopService.WorkshopStatus(false, e.Message); + } + } + + /// + public async Task ValidateWorkshopStatus() + { + IWorkshopService.WorkshopStatus status = await GetWorkshopStatus(); + if (!status.IsReachable) + await _router.Navigate($"workshop/offline/{status.Message}"); + return status.IsReachable; + } + private void ClearCache() { try @@ -63,4 +92,8 @@ public class WorkshopService : IWorkshopService public interface IWorkshopService { Task SetEntryIcon(Guid entryId, Progress progress, Stream icon, CancellationToken cancellationToken); + Task GetWorkshopStatus(); + Task ValidateWorkshopStatus(); + + public record WorkshopStatus(bool IsReachable, string Message); } \ No newline at end of file