1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Workshop - Added installed entries and update profiles when reinstalling them

This commit is contained in:
Robert 2023-09-06 20:39:43 +02:00
parent fcde1d4ecc
commit c69be2836e
17 changed files with 327 additions and 58 deletions

View File

@ -98,6 +98,7 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// TODO: Look into this, it doesn't seem to make sense
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;

View File

@ -127,7 +127,7 @@ public interface IProfileService : IArtemisService
Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Imports the provided base64 encoded GZIPed JSON as a profile configuration.
/// Imports the provided ZIP archive stream as a profile configuration.
/// </summary>
/// <param name="archiveStream">The zip archive containing the profile to import.</param>
/// <param name="category">The <see cref="ProfileCategory" /> in which to import the profile.</param>
@ -137,8 +137,17 @@ public interface IProfileService : IArtemisService
/// any changes are made to it.
/// </param>
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</param>
/// <param name="targetIndex">The index at which to import the profile into the category.</param>
/// <returns>The resulting profile configuration.</returns>
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported");
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0);
/// <summary>
/// Imports the provided ZIP archive stream into the provided profile configuration
/// </summary>
/// <param name="archiveStream">The zip archive containing the profile to import.</param>
/// <param name="profileConfiguration">The profile configuration to overwrite.</param>
/// <returns>The resulting profile configuration.</returns>
Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration);
/// <summary>
/// Adapts a given profile to the currently active devices.
@ -176,4 +185,5 @@ public interface IProfileService : IArtemisService
/// Occurs whenever a profile category is removed.
/// </summary>
public event EventHandler<ProfileCategoryEventArgs>? ProfileCategoryRemoved;
}

View File

@ -438,7 +438,7 @@ internal class ProfileService : IProfileService
}
/// <inheritdoc />
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix)
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0)
{
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true);
@ -500,7 +500,7 @@ internal class ProfileService : IProfileService
}
profileConfiguration.Entity.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, 0);
category.AddProfileConfiguration(profileConfiguration, targetIndex);
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
profileConfiguration.LoadModules(modules);
@ -509,6 +509,17 @@ internal class ProfileService : IProfileService
return profileConfiguration;
}
/// <inheritdoc />
public async Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration)
{
ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration.Order + 1);
DeleteProfile(profileConfiguration);
SaveProfileCategory(imported.Category);
return imported;
}
/// <inheritdoc />
public void AdaptProfile(Profile profile)
{

View File

@ -0,0 +1,21 @@
using System;
namespace Artemis.Storage.Entities.Workshop;
public class EntryEntity
{
public Guid Id { get; set; }
public Guid EntryId { get; set; }
public int EntryType { get; set; }
public string Author { get; set; }
public string Name { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public Guid ReleaseId { get; set; }
public string ReleaseVersion { get; set; }
public DateTimeOffset InstalledAt { get; set; }
public string LocalReference { get; set; }
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Workshop;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
internal class EntryRepository : IEntryRepository
{
private readonly LiteRepository _repository;
public EntryRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.Id);
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.EntryId);
}
public void Add(EntryEntity entryEntity)
{
_repository.Insert(entryEntity);
}
public void Remove(EntryEntity entryEntity)
{
_repository.Delete<EntryEntity>(entryEntity.Id);
}
public EntryEntity Get(Guid id)
{
return _repository.FirstOrDefault<EntryEntity>(s => s.Id == id);
}
public EntryEntity GetByEntryId(Guid entryId)
{
return _repository.FirstOrDefault<EntryEntity>(s => s.EntryId == entryId);
}
public List<EntryEntity> GetAll()
{
return _repository.Query<EntryEntity>().ToList();
}
public void Save(EntryEntity entryEntity)
{
_repository.Upsert(entryEntity);
}
public void Save(IEnumerable<EntryEntity> entryEntities)
{
_repository.Upsert(entryEntities);
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Workshop;
namespace Artemis.Storage.Repositories.Interfaces;
public interface IEntryRepository : IRepository
{
void Add(EntryEntity entryEntity);
void Remove(EntryEntity entryEntity);
EntryEntity Get(Guid id);
EntryEntity GetByEntryId(Guid entryId);
List<EntryEntity> GetAll();
void Save(EntryEntity entryEntity);
void Save(IEnumerable<EntryEntity> entryEntities);
}

View File

@ -20,16 +20,16 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly ProfileEntryDownloadHandler _downloadHandler;
private readonly ProfileEntryInstallationHandler _installationHandler;
private readonly INotificationService _notificationService;
private readonly IWindowService _windowService;
private readonly ObservableAsPropertyHelper<DateTimeOffset?> _updatedAt;
private IGetEntryById_Entry? _entry;
public ProfileDetailsViewModel(IWorkshopClient client, ProfileEntryDownloadHandler downloadHandler, INotificationService notificationService, IWindowService windowService)
public ProfileDetailsViewModel(IWorkshopClient client, ProfileEntryInstallationHandler installationHandler, INotificationService notificationService, IWindowService windowService)
{
_client = client;
_downloadHandler = downloadHandler;
_installationHandler = installationHandler;
_notificationService = notificationService;
_windowService = windowService;
_updatedAt = this.WhenAnyValue(vm => vm.Entry).Select(e => e?.LatestRelease?.CreatedAt ?? e?.CreatedAt).ToProperty(this, vm => vm.UpdatedAt);
@ -70,7 +70,7 @@ public class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
if (!confirm)
return;
EntryInstallResult<ProfileConfiguration> result = await _downloadHandler.InstallProfileAsync(Entry.LatestRelease.Id, new Progress<StreamProgress>(), cancellationToken);
EntryInstallResult<ProfileConfiguration> result = await _installationHandler.InstallProfileAsync(Entry, Entry.LatestRelease.Id, new Progress<StreamProgress>(), cancellationToken);
if (result.IsSuccess)
_notificationService.CreateNotification().WithTitle("Profile installed").WithSeverity(NotificationSeverity.Success).Show();
else

View File

@ -5,6 +5,7 @@
xmlns:search="clr-namespace:Artemis.UI.Screens.Workshop.Search"
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Search.SearchView"
x:DataType="search:SearchViewModel">
@ -30,12 +31,16 @@
<ContentControl HorizontalAlignment="Right"
Width="28"
Height="28"
Margin="0 0 50 0"
Margin="0 0 75 0"
Content="{CompiledBinding CurrentUserViewModel}"
windowing:AppWindow.AllowInteractionInTitleBar="True" />
<Border VerticalAlignment="Top" CornerRadius="4 4 0 0" ClipToBounds="True" MaxWidth="500">
<ProgressBar IsIndeterminate="True" VerticalAlignment="Top" IsVisible="{CompiledBinding IsLoading}"></ProgressBar>
</Border>
<Button Classes="title-bar-button" Command="{CompiledBinding ShowDebugger}" Margin="0 -5 0 0" VerticalAlignment="Top" HorizontalAlignment="Right" windowing:AppWindow.AllowInteractionInTitleBar="True" >
<avalonia:MaterialIcon Kind="Bug"></avalonia:MaterialIcon>
</Button>
</Panel>
</UserControl>

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
@ -18,15 +19,17 @@ public class SearchViewModel : ViewModelBase
{
private readonly ILogger _logger;
private readonly IRouter _router;
private readonly IDebugService _debugService;
private readonly IWorkshopClient _workshopClient;
private bool _isLoading;
private SearchResultViewModel? _selectedEntry;
public SearchViewModel(ILogger logger, IWorkshopClient workshopClient, IRouter router, CurrentUserViewModel currentUserViewModel)
public SearchViewModel(ILogger logger, IWorkshopClient workshopClient, IRouter router, CurrentUserViewModel currentUserViewModel, IDebugService debugService)
{
_logger = logger;
_workshopClient = workshopClient;
_router = router;
_debugService = debugService;
CurrentUserViewModel = currentUserViewModel;
SearchAsync = ExecuteSearchAsync;
@ -49,6 +52,11 @@ public class SearchViewModel : ViewModelBase
set => RaiseAndSetIfChanged(ref _isLoading, value);
}
public void ShowDebugger()
{
_debugService.ShowDebugger();
}
private void NavigateToEntry(SearchResultViewModel searchResult)
{
string? url = null;

View File

@ -2,6 +2,6 @@
namespace Artemis.WebClient.Workshop.DownloadHandlers;
public interface IEntryDownloadHandler
public interface IEntryInstallationHandler
{
}

View File

@ -1,36 +0,0 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Utilities;
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<EntryInstallResult<ProfileConfiguration>> InstallProfileAsync(Guid releaseId, Progress<StreamProgress> 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);
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true);
ProfileConfiguration profileConfiguration = await _profileService.ImportProfile(stream, category, true, true, null);
return EntryInstallResult<ProfileConfiguration>.FromSuccess(profileConfiguration);
}
catch (Exception e)
{
return EntryInstallResult<ProfileConfiguration>.FromFailure(e.Message);
}
}
}

View File

@ -0,0 +1,73 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.DownloadHandlers.Implementations;
public class ProfileEntryInstallationHandler : IEntryInstallationHandler
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IProfileService _profileService;
private readonly IWorkshopService _workshopService;
public ProfileEntryInstallationHandler(IHttpClientFactory httpClientFactory, IProfileService profileService, IWorkshopService workshopService)
{
_httpClientFactory = httpClientFactory;
_profileService = profileService;
_workshopService = workshopService;
}
public async Task<EntryInstallResult<ProfileConfiguration>> InstallProfileAsync(IGetEntryById_Entry entry, Guid releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{
using MemoryStream stream = new();
// Download the provided release
try
{
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
await client.DownloadDataAsync($"releases/download/{releaseId}", stream, progress, cancellationToken);
}
catch (Exception e)
{
return EntryInstallResult<ProfileConfiguration>.FromFailure(e.Message);
}
// Find existing installation to potentially replace the profile
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry);
if (installedEntry != null && Guid.TryParse(installedEntry.LocalReference, out Guid profileId))
{
ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId);
if (existing != null)
{
ProfileConfiguration overwritten = await _profileService.OverwriteProfile(stream, existing);
installedEntry.LocalReference = overwritten.ProfileId.ToString();
// Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry);
return EntryInstallResult<ProfileConfiguration>.FromSuccess(overwritten);
}
}
// Ensure there is an installed entry
installedEntry ??= _workshopService.CreateInstalledEntry(entry);
// Add the profile as a fresh import
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true);
ProfileConfiguration imported = await _profileService.ImportProfile(stream, category, true, true, null);
installedEntry.LocalReference = imported.ProfileId.ToString();
// Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry);
return EntryInstallResult<ProfileConfiguration>.FromSuccess(imported);
}
private void UpdateRelease(Guid releaseId, InstalledEntry installedEntry)
{
installedEntry.ReleaseId = releaseId;
installedEntry.ReleaseVersion = "TODO";
installedEntry.InstalledAt = DateTimeOffset.UtcNow;
_workshopService.SaveInstalledEntry(installedEntry);
}
}

View File

@ -49,6 +49,6 @@ public static class ContainerExtensions
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryDownloadHandler>(), Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryInstallationHandler>(), Reuse.Transient);
}
}

View File

@ -0,0 +1,72 @@
using Artemis.Storage.Entities.Workshop;
namespace Artemis.WebClient.Workshop.Services;
public class InstalledEntry
{
internal InstalledEntry(EntryEntity entity)
{
Entity = entity;
Load();
}
public InstalledEntry(IGetEntryById_Entry entry)
{
Entity = new EntryEntity();
EntryId = entry.Id;
EntryType = entry.EntryType;
Author = entry.Author;
Name = entry.Name;
Summary = entry.Summary;
}
public Guid EntryId { get; set; }
public EntryType EntryType { get; set; }
public string Author { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public Guid ReleaseId { get; set; }
public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; }
public string? LocalReference { get; set; }
internal EntryEntity Entity { get; }
internal void Load()
{
EntryId = Entity.EntryId;
EntryType = (EntryType) Entity.EntryType;
Author = Entity.Author;
Name = Entity.Name;
Summary = Entity.Summary;
ReleaseId = Entity.ReleaseId;
ReleaseVersion = Entity.ReleaseVersion;
InstalledAt = Entity.InstalledAt;
LocalReference = Entity.LocalReference;
}
internal void Save()
{
Entity.EntryId = EntryId;
Entity.EntryType = (int) EntryType;
Entity.Author = Author;
Entity.Name = Name;
Entity.Summary = Summary;
Entity.ReleaseId = ReleaseId;
Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt;
Entity.LocalReference = LocalReference;
}
}

View File

@ -0,0 +1,18 @@
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.UploadHandlers;
namespace Artemis.WebClient.Workshop.Services;
public interface IWorkshopService
{
Task<Stream?> GetEntryIcon(Guid entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(Guid entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(Guid entryId, EntryType entryType);
InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry);
InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry);
void SaveInstalledEntry(InstalledEntry entry);
public record WorkshopStatus(bool IsReachable, string Message);
}

View File

@ -1,4 +1,6 @@
using System.Net.Http.Headers;
using Artemis.Storage.Entities.Workshop;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.UploadHandlers;
@ -9,11 +11,13 @@ public class WorkshopService : IWorkshopService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IRouter _router;
private readonly IEntryRepository _entryRepository;
public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router)
public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
{
_httpClientFactory = httpClientFactory;
_router = router;
_entryRepository = entryRepository;
}
public async Task<Stream?> GetEntryIcon(Guid entryId, CancellationToken cancellationToken)
@ -98,15 +102,27 @@ public class WorkshopService : IWorkshopService
throw new ArgumentOutOfRangeException(nameof(entryType));
}
}
}
public interface IWorkshopService
{
Task<Stream?> GetEntryIcon(Guid entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(Guid entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(Guid entryId, EntryType entryType);
/// <inheritdoc />
public InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry)
{
EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id);
if (entity == null)
return null;
public record WorkshopStatus(bool IsReachable, string Message);
return new InstalledEntry(entity);
}
/// <inheritdoc />
public InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry)
{
return new InstalledEntry(entry);
}
/// <inheritdoc />
public void SaveInstalledEntry(InstalledEntry entry)
{
entry.Save();
_entryRepository.Save(entry.Entity);
}
}