1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2026-02-04 02:43:32 +00:00

Merge branch 'master' into development

This commit is contained in:
Robert Beekman 2025-12-24 14:04:57 +01:00
commit 36761509e2
13 changed files with 656 additions and 640 deletions

View File

@ -22,9 +22,8 @@ jobs:
run: | run: |
$MidnightUtc = [DateTime]::UtcNow.Date $MidnightUtc = [DateTime]::UtcNow.Date
$BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-') $BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-')
$ApiVersion = (Select-Xml -Path 'src/Artemis.Core/Artemis.Core.csproj' -XPath '//PluginApiVersion').Node.InnerText
$NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines $NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines
$VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday" $VersionNumber = "1.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday"
# If we're not in master, add the branch name to the version so it counts as prerelease # If we're not in master, add the branch name to the version so it counts as prerelease
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" } if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
"version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT "version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT
@ -55,7 +54,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '9.0.x' dotnet-version: '10.0.x'
- name: Publish Artemis - name: Publish Artemis
run: dotnet publish --configuration Release -p:Version=${{ needs.version.outputs.version-number }} --runtime ${{ matrix.rid }} --output build/${{ matrix.rid }} --self-contained Artemis/src/Artemis.UI.${{ matrix.csproj }}/Artemis.UI.${{ matrix.csproj }}.csproj run: dotnet publish --configuration Release -p:Version=${{ needs.version.outputs.version-number }} --runtime ${{ matrix.rid }} --output build/${{ matrix.rid }} --self-contained Artemis/src/Artemis.UI.${{ matrix.csproj }}/Artemis.UI.${{ matrix.csproj }}.csproj
- name: Upload Artifact - name: Upload Artifact

View File

@ -22,9 +22,8 @@ jobs:
run: | run: |
$MidnightUtc = [DateTime]::UtcNow.Date $MidnightUtc = [DateTime]::UtcNow.Date
$BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-') $BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-')
$ApiVersion = (Select-Xml -Path 'src/Artemis.Core/Artemis.Core.csproj' -XPath '//PluginApiVersion').Node.InnerText
$NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines $NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines
$VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday" $VersionNumber = "1.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday"
# If we're not in master, add the branch name to the version so it counts as prerelease # If we're not in master, add the branch name to the version so it counts as prerelease
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" } if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
"version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT "version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT
@ -37,7 +36,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '9.0.x' dotnet-version: '10.0.x'
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Pack Artemis.Core - name: Pack Artemis.Core

View File

@ -41,7 +41,7 @@ public static class Constants
/// The full path to the Artemis data folder /// The full path to the Artemis data folder
/// </summary> /// </summary>
#if DEBUG #if DEBUG
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis-dev"); public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
#else #else
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
#endif #endif

View File

@ -16,8 +16,8 @@ public static class ReleaseExtensions
{ {
if (release.MinimumVersion == null || Constants.CurrentVersion == "local") if (release.MinimumVersion == null || Constants.CurrentVersion == "local")
return true; return true;
return release.MinimumVersion <= Version.Parse(Constants.CurrentVersion).ArtemisVersionToLong(); return Version.Parse(release.MinimumVersion) <= Version.Parse(Constants.CurrentVersion);
} }
} }
} }

View File

@ -1,45 +0,0 @@
using System;
namespace Artemis.UI.Extensions;
public static class VersionExtensions
{
/// <param name="version">The version to convert</param>
extension(Version version)
{
/// <summary>
/// Convert a Version to a long representation for easy comparison in PostgreSQL
/// <remarks>Assumes format: major.year.dayOfYear.revision (e.g., 1.2024.0225.2)</remarks>
/// </summary>
/// <returns>A long value that preserves version comparison order</returns>
public long ArtemisVersionToLong()
{
// Format: major.year.dayOfYear.revision
// Convert to: majorYYYYDDDRRRR (16 digits)
// Major: 1 digit (0-9)
// Year: 4 digits (e.g., 2024)
// Day: 3 digits (001-366, padded)
// Revision: 4 digits (0000-9999, padded)
long major = Math.Max(0, Math.Min(9, version.Major));
long year = Math.Max(1000, Math.Min(9999, version.Minor));
long day = Math.Max(1, Math.Min(366, version.Build));
long revision = Math.Max(0, Math.Min(9999, version.Revision >= 0 ? version.Revision : 0));
return major * 100000000000L +
year * 10000000L +
day * 10000L +
revision;
}
public static Version FromLong(long versionLong)
{
int major = (int)(versionLong / 100000000000L);
int year = (int)((versionLong / 10000000L) % 10000);
int day = (int)((versionLong / 10000L) % 1000);
int revision = (int)(versionLong % 10000L);
return new Version(major, year, day, revision);
}
}
}

View File

@ -69,11 +69,11 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
}).DisposeWith(d); }).DisposeWith(d);
IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id; IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id;
IncompatibilityReason = Release != null && !Release.IsCompatible() ? $"Requires Artemis v{Version.FromLong(Release.MinimumVersion!.Value)} or later" : null; IncompatibilityReason = Release != null && !Release.IsCompatible() ? $"Requires Artemis v{Release.MinimumVersion} or later" : null;
}); });
this.WhenAnyValue(vm => vm.Release).Subscribe(r => IsCurrentVersion = r != null && _workshopService.GetInstalledEntry(r.Entry.Id)?.ReleaseId == r.Id); this.WhenAnyValue(vm => vm.Release).Subscribe(r => IsCurrentVersion = r != null && _workshopService.GetInstalledEntry(r.Entry.Id)?.ReleaseId == r.Id);
this.WhenAnyValue(vm => vm.Release).Subscribe(r => IncompatibilityReason = r != null && !r.IsCompatible() ? $"Requires Artemis v{Version.FromLong(r.MinimumVersion!.Value)} or later" : null); this.WhenAnyValue(vm => vm.Release).Subscribe(r => IncompatibilityReason = r != null && !r.IsCompatible() ? $"Requires Artemis v{r.MinimumVersion} or later" : null);
InDetailsScreen = true; InDetailsScreen = true;
} }

View File

@ -37,7 +37,7 @@ public partial class EntryReleaseItemViewModel : ActivatableViewModelBase
}).DisposeWith(d); }).DisposeWith(d);
IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id; IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id;
IncompatibilityReason = !Release.IsCompatible() ? $"Requires Artemis v{Version.FromLong(Release.MinimumVersion!.Value)} or later" : null; IncompatibilityReason = !Release.IsCompatible() ? $"Requires Artemis v{Release.MinimumVersion} or later" : null;
}); });
} }

View File

@ -20,15 +20,21 @@ public class WorkshopUpdateService : IWorkshopUpdateService
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly IPluginManagementService _pluginManagementService;
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider; private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
private readonly PluginSetting<bool> _showNotifications; private readonly PluginSetting<bool> _showNotifications;
public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService, public WorkshopUpdateService(ILogger logger,
IWorkshopClient client,
IWorkshopService workshopService,
ISettingsService settingsService,
IPluginManagementService pluginManagementService,
Lazy<IUpdateNotificationProvider> updateNotificationProvider) Lazy<IUpdateNotificationProvider> updateNotificationProvider)
{ {
_logger = logger; _logger = logger;
_client = client; _client = client;
_workshopService = workshopService; _workshopService = workshopService;
_pluginManagementService = pluginManagementService;
_updateNotificationProvider = updateNotificationProvider; _updateNotificationProvider = updateNotificationProvider;
_showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true); _showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true);
} }
@ -88,6 +94,18 @@ public class WorkshopUpdateService : IWorkshopUpdateService
else else
_logger.Warning("Auto-update failed for entry {Entry}: {Message}", entry, updateResult.Message); _logger.Warning("Auto-update failed for entry {Entry}: {Message}", entry, updateResult.Message);
if (!updateResult.IsSuccess || updateResult.Installed is not Plugin {IsEnabled: false} updatedPlugin)
return updateResult.IsSuccess;
try
{
_pluginManagementService.EnablePlugin(updatedPlugin, true, true);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to auto-enable updated plugin {Plugin}", updatedPlugin);
}
return updateResult.IsSuccess; return updateResult.IsSuccess;
} }
catch (Exception e) catch (Exception e)

View File

@ -101,9 +101,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
{ {
// ignored, will get cleaned up as an orphaned file // ignored, will get cleaned up as an orphaned file
} }
if (installedEntry.Entity.Id != Guid.Empty)
_workshopService.RemoveInstalledEntry(installedEntry);
return EntryInstallResult.FromException(e); return EntryInstallResult.FromException(e);
} }

View File

@ -225,7 +225,6 @@ public class InstalledEntry : CorePropertyChanged
IsOfficial = Entity.IsOfficial; IsOfficial = Entity.IsOfficial;
Name = Entity.Name; Name = Entity.Name;
Summary = Entity.Summary; Summary = Entity.Summary;
EntryType = (EntryType) Entity.EntryType;
Downloads = Entity.Downloads; Downloads = Entity.Downloads;
CreatedAt = Entity.CreatedAt; CreatedAt = Entity.CreatedAt;
LatestReleaseId = Entity.LatestReleaseId; LatestReleaseId = Entity.LatestReleaseId;
@ -235,6 +234,15 @@ public class InstalledEntry : CorePropertyChanged
ReleaseVersion = Entity.ReleaseVersion; ReleaseVersion = Entity.ReleaseVersion;
InstalledAt = Entity.InstalledAt; InstalledAt = Entity.InstalledAt;
AutoUpdate = Entity.AutoUpdate; AutoUpdate = Entity.AutoUpdate;
// Avoiding a cast here, the enum has shifted around before as it is generated from the GraphQL schema
EntryType = Entity.EntryType switch
{
0 => EntryType.Plugin,
1 => EntryType.Profile,
2 => EntryType.Layout,
_ => EntryType
};
_metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : []; _metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : [];
} }
@ -242,8 +250,6 @@ public class InstalledEntry : CorePropertyChanged
internal void Save() internal void Save()
{ {
Entity.EntryId = Id; Entity.EntryId = Id;
Entity.EntryType = (int) EntryType;
Entity.Author = Author; Entity.Author = Author;
Entity.IsOfficial = IsOfficial; Entity.IsOfficial = IsOfficial;
Entity.Name = Name; Entity.Name = Name;
@ -257,6 +263,15 @@ public class InstalledEntry : CorePropertyChanged
Entity.ReleaseVersion = ReleaseVersion; Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt; Entity.InstalledAt = InstalledAt;
Entity.AutoUpdate = AutoUpdate; Entity.AutoUpdate = AutoUpdate;
// Avoiding a cast here, the enum has shifted around before as it is generated from the GraphQL schema
Entity.EntryType = EntryType switch
{
EntryType.Plugin => 0,
EntryType.Profile => 1,
EntryType.Layout => 2,
_ => Entity.EntryType
};
Entity.Metadata = new Dictionary<string, JsonNode>(_metadata); Entity.Metadata = new Dictionary<string, JsonNode>(_metadata);
} }

View File

@ -26,9 +26,8 @@ public class WorkshopService : IWorkshopService
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;
private readonly PluginSetting<bool> _migratedBuiltInPlugins; private readonly PluginSetting<bool> _migratedBuiltInPlugins;
private bool _initialized; private bool _initialized;
private bool _mutating;
public WorkshopService(ILogger logger, public WorkshopService(ILogger logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@ -173,27 +172,45 @@ public class WorkshopService : IWorkshopService
/// <inheritdoc /> /// <inheritdoc />
public async Task<EntryInstallResult> InstallEntry(IEntrySummary entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallEntry(IEntrySummary entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
IEntryInstallationHandler handler = _factory.CreateHandler(entry.EntryType); _mutating = true;
EntryInstallResult result = await handler.InstallAsync(entry, release, progress, cancellationToken);
if (result.IsSuccess && result.Entry != null)
OnEntryInstalled?.Invoke(this, result.Entry);
else
_logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message);
return result; try
{
IEntryInstallationHandler handler = _factory.CreateHandler(entry.EntryType);
EntryInstallResult result = await handler.InstallAsync(entry, release, progress, cancellationToken);
if (result.IsSuccess && result.Entry != null)
OnEntryInstalled?.Invoke(this, result.Entry);
else
_logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message);
return result;
}
finally
{
_mutating = false;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<EntryUninstallResult> UninstallEntry(InstalledEntry installedEntry, CancellationToken cancellationToken) public async Task<EntryUninstallResult> UninstallEntry(InstalledEntry installedEntry, CancellationToken cancellationToken)
{ {
IEntryInstallationHandler handler = _factory.CreateHandler(installedEntry.EntryType); _mutating = true;
EntryUninstallResult result = await handler.UninstallAsync(installedEntry, cancellationToken);
if (result.IsSuccess)
OnEntryUninstalled?.Invoke(this, installedEntry);
else
_logger.Warning("Failed to uninstall entry {EntryId}: {Message}", installedEntry.Id, result.Message);
return result; try
{
IEntryInstallationHandler handler = _factory.CreateHandler(installedEntry.EntryType);
EntryUninstallResult result = await handler.UninstallAsync(installedEntry, cancellationToken);
if (result.IsSuccess)
OnEntryUninstalled?.Invoke(this, installedEntry);
else
_logger.Warning("Failed to uninstall entry {EntryId}: {Message}", installedEntry.Id, result.Message);
return result;
}
finally
{
_mutating = false;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -316,16 +333,28 @@ public class WorkshopService : IWorkshopService
// If already migrated, do nothing // If already migrated, do nothing
if (_migratedBuiltInPlugins.Value) if (_migratedBuiltInPlugins.Value)
return; return;
MigratingBuildInPlugins?.Invoke(this, EventArgs.Empty);
bool migrated = await BuiltInPluginsMigrator.Migrate(this, _workshopClient, _logger, _pluginRepository); _mutating = true;
_migratedBuiltInPlugins.Value = migrated;
_migratedBuiltInPlugins.Save(); try
{
MigratingBuildInPlugins?.Invoke(this, EventArgs.Empty);
bool migrated = await BuiltInPluginsMigrator.Migrate(this, _workshopClient, _logger, _pluginRepository);
_migratedBuiltInPlugins.Value = migrated;
_migratedBuiltInPlugins.Save();
}
finally
{
_mutating = false;
}
} }
private void ProfileServiceOnProfileRemoved(object? sender, ProfileConfigurationEventArgs e) private void ProfileServiceOnProfileRemoved(object? sender, ProfileConfigurationEventArgs e)
{ {
if (_mutating)
return;
InstalledEntry? entry = GetInstalledEntryByProfile(e.ProfileConfiguration); InstalledEntry? entry = GetInstalledEntryByProfile(e.ProfileConfiguration);
if (entry == null) if (entry == null)
return; return;
@ -336,6 +365,9 @@ public class WorkshopService : IWorkshopService
private void PluginManagementServiceOnPluginRemoved(object? sender, PluginEventArgs e) private void PluginManagementServiceOnPluginRemoved(object? sender, PluginEventArgs e)
{ {
if (_mutating)
return;
InstalledEntry? entry = GetInstalledEntryByPlugin(e.Plugin); InstalledEntry? entry = GetInstalledEntryByPlugin(e.Plugin);
if (entry == null) if (entry == null)
return; return;
@ -349,6 +381,6 @@ public class WorkshopService : IWorkshopService
public event EventHandler<InstalledEntry>? OnEntryUninstalled; public event EventHandler<InstalledEntry>? OnEntryUninstalled;
public event EventHandler<InstalledEntry>? OnEntryInstalled; public event EventHandler<InstalledEntry>? OnEntryInstalled;
public event EventHandler? MigratingBuildInPlugins; public event EventHandler? MigratingBuildInPlugins;
} }

File diff suppressed because it is too large Load Diff

View File

@ -59,8 +59,8 @@
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" /> <PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.1" /> <PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="3.119.1" /> <PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.9" />
<PackageVersion Include="Splat.DryIoc" Version="17.1.1" /> <PackageVersion Include="Splat.DryIoc" Version="17.1.1" />
<PackageVersion Include="StrawberryShake.Server" Version="15.1.11" /> <PackageVersion Include="StrawberryShake.Server" Version="15.1.11" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" /> <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />