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

Added layout archive generation

This commit is contained in:
RobertBeekman 2023-11-02 22:29:42 +01:00
parent 59fe1df40f
commit 6b4e84c95a
18 changed files with 224 additions and 68 deletions

View File

@ -47,7 +47,6 @@
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RGB.NET.Core" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Presets" Version="$(RGBDotNetVersion)" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
@ -65,4 +64,10 @@
<None Include="Resources/intro-profile.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="DefaultLayouts/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -64,7 +64,12 @@ public class ArtemisLedLayout
private void ApplyLogicalLayout(LayoutCustomLedDataLogicalLayout logicalLayout)
{
string? layoutDirectory = Path.GetDirectoryName(DeviceLayout.FilePath);
LogicalName = logicalLayout.Name;
Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute);
if (layoutDirectory != null && logicalLayout.Image != null)
Image = new Uri(Path.Combine(layoutDirectory, logicalLayout.Image!), UriKind.Absolute);
else
Image = null;
}
}

View File

@ -29,4 +29,9 @@
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -44,7 +44,6 @@
<PackageReference Include="ReactiveUI" Version="19.5.1" />
<PackageReference Include="ReactiveUI.Validation" Version="3.1.7" />
<PackageReference Include="RGB.NET.Core" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
<PackageReference Include="Splat.DryIoc" Version="14.7.1" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
@ -53,4 +52,10 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,9 +1,7 @@
using System.IO;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using System.Windows.Input;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using PropertyChanged.SourceGenerator;

View File

@ -9,6 +9,7 @@ using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Layout.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
@ -24,7 +25,7 @@ public partial class LayoutInfoViewModel : ValidatableViewModelBase
[Notify] private Guid _deviceProviderId;
[Notify] private string? _deviceProviderIdInput;
[Notify] private ICommand? _remove;
public LayoutInfoViewModel(ArtemisLayout layout,
IDeviceService deviceService,
IWindowService windowService,
@ -65,4 +66,14 @@ public partial class LayoutInfoViewModel : ValidatableViewModelBase
if (deviceProvider != null)
DeviceProviderId = deviceProvider.Plugin.Guid;
}
public LayoutInfo ToLayoutInfo()
{
return new LayoutInfo
{
Model = Model ?? string.Empty,
Vendor = Vendor ?? string.Empty,
DeviceProviderId = DeviceProviderId
};
}
}

View File

@ -11,7 +11,6 @@ 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.Handlers.InstallationHandlers.Implementations;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;

View File

@ -14,6 +14,7 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class ImagesStepViewModel : SubmissionViewModel
{
private const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
private readonly IWindowService _windowService;
private readonly SourceList<Stream> _imageStreams;
@ -60,7 +61,14 @@ public class ImagesStepViewModel : SubmissionViewModel
if (_imageStreams.Items.Any(i => i is FileStream fs && fs.Name == path))
continue;
FileStream stream = new(path, FileMode.Open);
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
if (stream.Length > MAX_FILE_SIZE)
{
await _windowService.ShowConfirmContentDialog("File too big", $"File {path} exceeds maximum file size of 10 MB", "Skip file", null);
await stream.DisposeAsync();
continue;
}
_imageStreams.Add(stream);
State.Images.Add(stream);
}

View File

@ -85,40 +85,15 @@ public partial class LayoutInfoStepViewModel : SubmissionViewModel
return;
layoutEntrySource.PhysicalLayout = PhysicalLayout;
// layoutEntrySource.LayoutInfo = new List<LayoutInfo>(LayoutInfo.Select(i => i.ToLayoutInfo()));
layoutEntrySource.LayoutInfo = new List<LayoutInfo>(LayoutInfo.Select(i => i.ToLayoutInfo()));
if (string.IsNullOrWhiteSpace(State.Name))
State.Name = layoutEntrySource.Layout.RgbLayout.Name ?? "";
if (string.IsNullOrWhiteSpace(State.Summary))
{
State.Summary = !string.IsNullOrWhiteSpace(layoutEntrySource.Layout.RgbLayout.Vendor)
? $"{layoutEntrySource.Layout.RgbLayout.Vendor} {layoutEntrySource.Layout.RgbLayout.Type} device layout"
: $"{layoutEntrySource.Layout.RgbLayout.Type} device layout";
}
if (string.IsNullOrWhiteSpace(State.Description))
{
State.Description = $@"### Layout properties
**Name**
{layoutEntrySource.Layout.RgbLayout.Name ?? "N/A"}
**Description**
{layoutEntrySource.Layout.RgbLayout.Description ?? "N/A"}
**Author**
{layoutEntrySource.Layout.RgbLayout.Author ?? "N/A"}
**Type**
{layoutEntrySource.Layout.RgbLayout.Type}
**Vendor**
{layoutEntrySource.Layout.RgbLayout.Vendor ?? "N/A"}
**Model**
{layoutEntrySource.Layout.RgbLayout.Model ?? "N/A"}
**Shape**
{layoutEntrySource.Layout.RgbLayout.Shape}
**Width**
{layoutEntrySource.Layout.RgbLayout.Width}mm
**Height**
{layoutEntrySource.Layout.RgbLayout.Height}mm";
}
State.Categories = new List<long> {8}; // Device category, yes this could change but why would it
if (State.EntryId == null)
State.ChangeScreen<SpecificationsStepViewModel>();

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services;
@ -60,15 +61,23 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
if (selected == null || selected.Length != 1)
return;
ArtemisLayout layout = new(selected[0], LayoutSource.User);
if (!layout.IsValid)
try
{
await _windowService.ShowConfirmContentDialog("Invalid layout file", "The selected file does not appear to be a valid RGB.NET layout file", cancel: null);
return;
}
ArtemisLayout layout = new(selected[0], LayoutSource.User);
if (!layout.IsValid)
{
await _windowService.ShowConfirmContentDialog("Failed to load layout", "The selected file does not appear to be a valid RGB.NET layout file.", "Close", null);
return;
}
SelectedDevice = null;
Layout = layout;
SelectedDevice = null;
Layout = layout;
}
catch (Exception e)
{
await _windowService.ShowConfirmContentDialog("Failed to load layout", "The selected file does not appear to be a valid RGB.NET layout file.\r\n" + e.Message, "Close", null);
throw;
}
}
private void CreatePreviewDevice(ArtemisLayout? layout)
@ -87,25 +96,69 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
{
if (Layout == null)
return;
if (!await ValidateLayout(Layout))
return;
State.EntrySource = new LayoutEntrySource(Layout);
await Dispatcher.UIThread.InvokeAsync(SetDeviceImages, DispatcherPriority.Background);
State.ChangeScreen<LayoutInfoStepViewModel>();
}
private async Task<bool> ValidateLayout(ArtemisLayout? layout)
{
if (layout == null)
return true;
string? layoutPath = Path.GetDirectoryName(layout.FilePath);
if (layoutPath == null)
throw new ArtemisUIException($"Could not determine directory of {layout.FilePath}");
if (layout.LayoutCustomDeviceData.DeviceImage != null && !File.Exists(Path.Combine(layoutPath, layout.LayoutCustomDeviceData.DeviceImage)))
{
await _windowService.ShowConfirmContentDialog(
"Device image not found",
$"{Path.Combine(layoutPath, layout.LayoutCustomDeviceData.DeviceImage)} does not exist.",
"Close",
null
);
return false;
}
foreach (ArtemisLedLayout ledLayout in layout.Leds)
{
if (ledLayout.LayoutCustomLedData.LogicalLayouts == null)
continue;
foreach (LayoutCustomLedDataLogicalLayout customData in ledLayout.LayoutCustomLedData.LogicalLayouts)
{
if (customData.Image == null || File.Exists(Path.Combine(layoutPath, customData.Image)))
continue;
await _windowService.ShowConfirmContentDialog(
$"Image not found for LED {ledLayout.RgbLayout.Id} ({customData.Name})",
$"{Path.Combine(layoutPath, customData.Image)} does not exist.",
"Close",
null
);
return false;
}
}
return true;
}
private void SetDeviceImages()
{
if (Layout == null)
return;
MemoryStream deviceWithoutLeds = new();
MemoryStream deviceWithLeds = new();
using (RenderTargetBitmap image = Layout.RenderLayout(false))
{
image.Save(deviceWithoutLeds);
deviceWithoutLeds.Seek(0, SeekOrigin.Begin);
}
using (RenderTargetBitmap image = Layout.RenderLayout(true))
{
image.Save(deviceWithLeds);
@ -116,7 +169,7 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
foreach (Stream stateImage in State.Images)
stateImage.Dispose();
State.Images.Clear();
// Go through the hassle of resizing the image to 128x128 without losing aspect ratio, padding is added for this
State.Icon = ResizeImage(deviceWithoutLeds, 128);
State.Images.Add(deviceWithoutLeds);
@ -126,11 +179,11 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
private Stream ResizeImage(Stream image, int size)
{
MemoryStream output = new();
using MemoryStream input = new();
using MemoryStream input = new();
image.CopyTo(input);
input.Seek(0, SeekOrigin.Begin);
using SKBitmap? sourceBitmap = SKBitmap.Decode(input);
int sourceWidth = sourceBitmap.Width;
int sourceHeight = sourceBitmap.Height;

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -131,12 +132,34 @@ public partial class UploadStepViewModel : SubmissionViewModel
State.ChangeScreen<SubmitStepViewModel>();
return null;
}
foreach (Stream image in State.Images.ToList())
{
// Upload image
try
{
ImageUploadResult imageUploadResult = await _workshopService.UploadEntryImage(entryId.Value, _progress, image, cancellationToken);
if (!imageUploadResult.IsSuccess)
throw new ArtemisWorkshopException(imageUploadResult.Message);
State.Images.Remove(image);
}
catch (Exception e)
{
// It's not critical if this fails
await _windowService.ShowConfirmContentDialog("Failed to upload image", "Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message, "Continue", null);
}
if (cancellationToken.IsCancellationRequested)
{
State.ChangeScreen<SubmitStepViewModel>();
return null;
}
}
if (State.Icon == null)
return entryId;
// Upload image
// Upload icon
try
{
ImageUploadResult imageUploadResult = await _workshopService.SetEntryIcon(entryId.Value, _progress, State.Icon, cancellationToken);
@ -146,12 +169,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
catch (Exception e)
{
// It's not critical if this fails
await _windowService.ShowConfirmContentDialog(
"Failed to upload icon",
"Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message,
"Continue",
null
);
await _windowService.ShowConfirmContentDialog("Failed to upload icon", "Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message, "Continue", null);
}
return entryId;

View File

@ -53,4 +53,10 @@
<ItemGroup>
<Folder Include="Handlers\" />
</ItemGroup>
<ItemGroup>
<Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=handlers_005Cinstallationhandlers_005Cimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=handlers_005Cuploadhandlers_005Cimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,5 +1,4 @@
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers.Implementations;
using DryIoc;
using DryIoc;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;

View File

@ -4,7 +4,7 @@ using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers.Implementations;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public class ProfileEntryInstallationHandler : IEntryInstallationHandler
{

View File

@ -1,5 +1,7 @@
using System.Xml.Serialization;
using System.IO.Compression;
using Artemis.Core;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Exceptions;
using RGB.NET.Layout;
namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
@ -11,15 +13,56 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
{
if (entrySource is not LayoutEntrySource source)
throw new InvalidOperationException("Can only create releases for layouts");
// Create a copy of the layout, image paths are about to be rewritten
XmlSerializer serializer = new(typeof(DeviceLayout));
using MemoryStream ms = new();
await using StreamWriter writer = new(ms);
serializer.Serialize(writer, source.Layout.RgbLayout);
await writer.FlushAsync();
ms.Seek(0, SeekOrigin.Begin);
using MemoryStream archiveStream = new();
using MemoryStream layoutStream = new();
source.Layout.RgbLayout.Save(layoutStream);
layoutStream.Seek(0, SeekOrigin.Begin);
// Create an archive
string? layoutPath = Path.GetDirectoryName(source.Layout.FilePath);
if (layoutPath == null)
throw new ArtemisWorkshopException($"Could not determine directory of {source.Layout.FilePath}");
using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true))
{
// Add the layout to the archive
ZipArchiveEntry archiveEntry = archive.CreateEntry("layout.xml");
await using (Stream layoutArchiveStream = archiveEntry.Open())
await layoutStream.CopyToAsync(layoutArchiveStream, cancellationToken);
// Add the layout image to the archive
CopyImage(layoutPath, source.Layout.LayoutCustomDeviceData.DeviceImage, archive);
// Add the LED images to the archive
foreach (ArtemisLedLayout ledLayout in source.Layout.Leds)
{
if (ledLayout.LayoutCustomLedData.LogicalLayouts == null)
continue;
foreach (LayoutCustomLedDataLogicalLayout customData in ledLayout.LayoutCustomLedData.LogicalLayouts)
CopyImage(layoutPath, customData.Image, archive);
}
}
archiveStream.Seek(0, SeekOrigin.Begin);
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string filePath = Path.Combine(desktopPath, "layout-test.zip");
await using (FileStream fileStream = new(filePath, FileMode.Create, FileAccess.Write))
{
archiveStream.WriteTo(fileStream);
}
return new EntryUploadResult();
}
private static void CopyImage(string layoutPath, string? imagePath, ZipArchive archive)
{
if (imagePath == null)
return;
string fullPath = Path.Combine(layoutPath, imagePath);
archive.CreateEntryFromFile(fullPath, imagePath);
}
}

View File

@ -7,6 +7,7 @@ public interface IWorkshopService
{
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(long entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken);
Task<ImageUploadResult> UploadEntryImage(long entryId, Progress<StreamProgress> progress, Stream image, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(long entryId, EntryType entryType);

View File

@ -56,6 +56,27 @@ public class WorkshopService : IWorkshopService
return ImageUploadResult.FromSuccess();
}
/// <inheritdoc />
public async Task<ImageUploadResult> UploadEntryImage(long entryId, Progress<StreamProgress> progress, Stream image, CancellationToken cancellationToken)
{
image.Seek(0, SeekOrigin.Begin);
// Submit the archive
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
// Construct the request
MultipartFormDataContent content = new();
ProgressableStreamContent streamContent = new(image, progress);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
content.Add(streamContent, "file", "file.png");
// Submit
HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/image", content, cancellationToken);
if (!response.IsSuccessStatusCode)
return ImageUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
return ImageUploadResult.FromSuccess();
}
/// <inheritdoc />
public async Task<IWorkshopService.WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken)
{