diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs
index 5563a67fd..a682fb135 100644
--- a/src/Artemis.Core/Plugins/PluginInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginInfo.cs
@@ -116,6 +116,11 @@ public class PluginInfo : IPrerequisitesSubject
[JsonInclude]
public Version? Api { get; internal init; } = new(1, 0, 0);
+ ///
+ /// Gets the minimum version of Artemis required by this plugin
+ ///
+ public Version? MinimumVersion { get; internal init; } = new(1, 0, 0);
+
///
/// Gets the plugin this info is associated with
///
@@ -132,7 +137,7 @@ public class PluginInfo : IPrerequisitesSubject
/// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version
///
[JsonIgnore]
- public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api.Major >= Constants.PluginApiVersion;
+ public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api.Major >= Constants.PluginApiVersion && MatchesMinimumVersion();
///
[JsonIgnore]
@@ -156,4 +161,13 @@ public class PluginInfo : IPrerequisitesSubject
{
return $"{Name} v{Version} - {Guid}";
}
+
+ private bool MatchesMinimumVersion()
+ {
+ if (Constants.CurrentVersion == "local")
+ return true;
+
+ Version currentVersion = new(Constants.CurrentVersion);
+ return currentVersion >= MinimumVersion;
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.Storage/ArtemisDbContext.cs b/src/Artemis.Storage/ArtemisDbContext.cs
index 32acba6a6..de514f79a 100644
--- a/src/Artemis.Storage/ArtemisDbContext.cs
+++ b/src/Artemis.Storage/ArtemisDbContext.cs
@@ -43,6 +43,12 @@ public class ArtemisDbContext : DbContext
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions),
v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary());
+
+ modelBuilder.Entity()
+ .Property(e => e.Categories)
+ .HasConversion(
+ v => JsonSerializer.Serialize(v, JsonSerializerOptions),
+ v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new List());
modelBuilder.Entity()
.Property(e => e.ProfileConfiguration)
diff --git a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
index ad11d6188..64f476c51 100644
--- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
+++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
@@ -14,11 +14,20 @@ public class EntryEntity
public int EntryType { get; set; }
public string Author { get; set; } = string.Empty;
+ public bool IsOfficial { get; set; }
public string Name { get; set; } = string.Empty;
-
+ public string Summary { get; set; } = string.Empty;
+ public long Downloads { get; set; }
+ public DateTimeOffset CreatedAt { get; set; }
+ public long? LatestReleaseId { get; set; }
+
public long ReleaseId { get; set; }
public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; }
-
+ public bool AutoUpdate { get; set; }
+
public Dictionary? Metadata { get; set; }
-}
\ No newline at end of file
+ public List? Categories { get; set; }
+}
+
+public record EntryCategoryEntity(string Name, string Icon);
\ No newline at end of file
diff --git a/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.Designer.cs b/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.Designer.cs
new file mode 100644
index 000000000..588333d41
--- /dev/null
+++ b/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.Designer.cs
@@ -0,0 +1,374 @@
+//
+using System;
+using Artemis.Storage;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Artemis.Storage.Migrations
+{
+ [DbContext(typeof(ArtemisDbContext))]
+ [Migration("20240722084220_AutoUpdating")]
+ partial class AutoUpdating
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
+
+ modelBuilder.Entity("Artemis.Storage.Entities.General.ReleaseEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("InstalledAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Version")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstalledAt");
+
+ b.HasIndex("Version")
+ .IsUnique();
+
+ b.ToTable("Releases");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("PluginGuid")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PluginGuid")
+ .IsUnique();
+
+ b.ToTable("Plugins");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("PluginEntityId")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PluginEntityId");
+
+ b.ToTable("PluginFeatures");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("TEXT");
+
+ b.Property("PluginGuid")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PluginGuid");
+
+ b.HasIndex("Name", "PluginGuid")
+ .IsUnique();
+
+ b.ToTable("PluginSettings");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsCollapsed")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuspended")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ProfileCategories");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasColumnType("BLOB");
+
+ b.Property("Profile")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ProfileCategoryId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProfileConfiguration")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProfileCategoryId");
+
+ b.ToTable("ProfileContainers");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("BlueScale")
+ .HasColumnType("REAL");
+
+ b.Property("Categories")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceProvider")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("GreenScale")
+ .HasColumnType("REAL");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LayoutParameter")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("LayoutType")
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("LogicalLayout")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("PhysicalLayout")
+ .HasColumnType("INTEGER");
+
+ b.Property("RedScale")
+ .HasColumnType("REAL");
+
+ b.Property("Rotation")
+ .HasColumnType("REAL");
+
+ b.Property("Scale")
+ .HasColumnType("REAL");
+
+ b.Property("X")
+ .HasColumnType("REAL");
+
+ b.Property("Y")
+ .HasColumnType("REAL");
+
+ b.Property("ZIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Author")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AutoUpdate")
+ .HasColumnType("INTEGER");
+
+ b.Property("Categories")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Downloads")
+ .HasColumnType("INTEGER");
+
+ b.Property("EntryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EntryType")
+ .HasColumnType("INTEGER");
+
+ b.Property("InstalledAt")
+ .HasColumnType("TEXT");
+
+ b.Property("IsOfficial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LatestReleaseId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Metadata")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReleaseVersion")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EntryId")
+ .IsUnique();
+
+ b.ToTable("Entries");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
+ {
+ b.HasOne("Artemis.Storage.Entities.Plugins.PluginEntity", null)
+ .WithMany("Features")
+ .HasForeignKey("PluginEntityId");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
+ {
+ b.HasOne("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", "ProfileCategory")
+ .WithMany("ProfileConfigurations")
+ .HasForeignKey("ProfileCategoryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ProfileCategory");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
+ {
+ b.OwnsOne("System.Collections.Generic.List", "InputIdentifiers", b1 =>
+ {
+ b1.Property("DeviceEntityId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Capacity")
+ .HasColumnType("INTEGER");
+
+ b1.HasKey("DeviceEntityId");
+
+ b1.ToTable("Devices");
+
+ b1.ToJson("InputIdentifiers");
+
+ b1.WithOwner()
+ .HasForeignKey("DeviceEntityId");
+ });
+
+ b.OwnsOne("System.Collections.Generic.List", "InputMappings", b1 =>
+ {
+ b1.Property("DeviceEntityId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Capacity")
+ .HasColumnType("INTEGER");
+
+ b1.HasKey("DeviceEntityId");
+
+ b1.ToTable("Devices");
+
+ b1.ToJson("InputMappings");
+
+ b1.WithOwner()
+ .HasForeignKey("DeviceEntityId");
+ });
+
+ b.Navigation("InputIdentifiers")
+ .IsRequired();
+
+ b.Navigation("InputMappings")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b =>
+ {
+ b.Navigation("Features");
+ });
+
+ modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b =>
+ {
+ b.Navigation("ProfileConfigurations");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.cs b/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.cs
new file mode 100644
index 000000000..fe1aaa710
--- /dev/null
+++ b/src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.cs
@@ -0,0 +1,110 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Artemis.Storage.Migrations
+{
+ ///
+ public partial class AutoUpdating : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AutoUpdate",
+ table: "Entries",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "Categories",
+ table: "Entries",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "CreatedAt",
+ table: "Entries",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
+
+ migrationBuilder.AddColumn(
+ name: "Downloads",
+ table: "Entries",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0L);
+
+ migrationBuilder.AddColumn(
+ name: "IsOfficial",
+ table: "Entries",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "LatestReleaseId",
+ table: "Entries",
+ type: "INTEGER",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Summary",
+ table: "Entries",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: "");
+
+ // Enable auto-update on all entries that are not profiles
+ migrationBuilder.Sql("UPDATE Entries SET AutoUpdate = 1 WHERE EntryType != 2");
+
+ // Enable auto-update on all entries of profiles that are fresh imports
+ migrationBuilder.Sql("""
+ UPDATE Entries
+ SET AutoUpdate = 1
+ WHERE EntryType = 2
+ AND EXISTS (
+ SELECT 1
+ FROM ProfileContainers
+ WHERE json_extract(ProfileContainers.Profile, '$.Id') = json_extract(Entries.Metadata, '$.ProfileId')
+ AND json_extract(ProfileContainers.Profile, '$.IsFreshImport') = 1
+ );
+ """);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "AutoUpdate",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "Categories",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "CreatedAt",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "Downloads",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "IsOfficial",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "LatestReleaseId",
+ table: "Entries");
+
+ migrationBuilder.DropColumn(
+ name: "Summary",
+ table: "Entries");
+ }
+ }
+}
diff --git a/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs b/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs
index fbda34982..04a34e87b 100644
--- a/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs
+++ b/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Artemis.Storage.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.2");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("Artemis.Storage.Entities.General.ReleaseEntity", b =>
{
@@ -38,7 +38,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("Version")
.IsUnique();
- b.ToTable("Releases", (string)null);
+ b.ToTable("Releases");
});
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b =>
@@ -58,7 +58,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("PluginGuid")
.IsUnique();
- b.ToTable("Plugins", (string)null);
+ b.ToTable("Plugins");
});
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
@@ -81,7 +81,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("PluginEntityId");
- b.ToTable("PluginFeatures", (string)null);
+ b.ToTable("PluginFeatures");
});
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b =>
@@ -109,7 +109,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("Name", "PluginGuid")
.IsUnique();
- b.ToTable("PluginSettings", (string)null);
+ b.ToTable("PluginSettings");
});
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b =>
@@ -137,7 +137,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("Name")
.IsUnique();
- b.ToTable("ProfileCategories", (string)null);
+ b.ToTable("ProfileCategories");
});
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
@@ -165,7 +165,7 @@ namespace Artemis.Storage.Migrations
b.HasIndex("ProfileCategoryId");
- b.ToTable("ProfileContainers", (string)null);
+ b.ToTable("ProfileContainers");
});
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
@@ -227,7 +227,7 @@ namespace Artemis.Storage.Migrations
b.HasKey("Id");
- b.ToTable("Devices", (string)null);
+ b.ToTable("Devices");
});
modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b =>
@@ -240,6 +240,18 @@ namespace Artemis.Storage.Migrations
.IsRequired()
.HasColumnType("TEXT");
+ b.Property("AutoUpdate")
+ .HasColumnType("INTEGER");
+
+ b.Property("Categories")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Downloads")
+ .HasColumnType("INTEGER");
+
b.Property("EntryId")
.HasColumnType("INTEGER");
@@ -249,6 +261,12 @@ namespace Artemis.Storage.Migrations
b.Property("InstalledAt")
.HasColumnType("TEXT");
+ b.Property("IsOfficial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LatestReleaseId")
+ .HasColumnType("INTEGER");
+
b.Property("Metadata")
.HasColumnType("TEXT");
@@ -263,12 +281,16 @@ namespace Artemis.Storage.Migrations
.IsRequired()
.HasColumnType("TEXT");
+ b.Property("Summary")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
b.HasKey("Id");
b.HasIndex("EntryId")
.IsUnique();
- b.ToTable("Entries", (string)null);
+ b.ToTable("Entries");
});
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
@@ -291,7 +313,7 @@ namespace Artemis.Storage.Migrations
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
{
- b.OwnsOne("Artemis.Storage.Entities.Surface.DeviceEntity.InputIdentifiers#System.Collections.Generic.List", "InputIdentifiers", b1 =>
+ b.OwnsOne("System.Collections.Generic.List", "InputIdentifiers", b1 =>
{
b1.Property("DeviceEntityId")
.HasColumnType("TEXT");
@@ -301,7 +323,7 @@ namespace Artemis.Storage.Migrations
b1.HasKey("DeviceEntityId");
- b1.ToTable("Devices", (string)null);
+ b1.ToTable("Devices");
b1.ToJson("InputIdentifiers");
@@ -309,7 +331,7 @@ namespace Artemis.Storage.Migrations
.HasForeignKey("DeviceEntityId");
});
- b.OwnsOne("Artemis.Storage.Entities.Surface.DeviceEntity.InputMappings#System.Collections.Generic.List", "InputMappings", b1 =>
+ b.OwnsOne("System.Collections.Generic.List", "InputMappings", b1 =>
{
b1.Property("DeviceEntityId")
.HasColumnType("TEXT");
@@ -319,7 +341,7 @@ namespace Artemis.Storage.Migrations
b1.HasKey("DeviceEntityId");
- b1.ToTable("Devices", (string)null);
+ b1.ToTable("Devices");
b1.ToJson("InputMappings");
diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs
index 2850b88a3..caa95b3e1 100644
--- a/src/Artemis.UI.Shared/Routing/Router/Router.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs
@@ -79,7 +79,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
path = NavigateUp(_currentRouteSubject.Value, path);
else
path = path.ToLower().Trim(' ', '/', '\\');
-
+
options ??= new RouterNavigationOptions();
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
@@ -90,7 +90,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
public async Task Reload()
{
string path = _currentRouteSubject.Value ?? "blank";
-
+
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
await Dispatcher.UIThread.InvokeAsync(async () =>
{
@@ -128,8 +128,12 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
await navigation.Navigate(args);
// If it was cancelled before completion, don't add it to history or update the current path
+ // Do reload the current path because it may have been partially navigated away from
if (navigation.Cancelled)
+ {
+ await Reload();
return;
+ }
if (options.AddToHistory && previousPath != null)
{
@@ -172,7 +176,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
public async Task GoUp(RouterNavigationOptions? options = null)
{
string? currentPath = _currentRouteSubject.Value;
-
+
// Keep removing segments until we find a parent route that resolves
while (currentPath != null && currentPath.Contains('/'))
{
@@ -223,8 +227,8 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
_logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace);
}
-
-
+
+
private string NavigateUp(string current, string path)
{
string[] pathParts = current.Split('/');
diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
index a0c9cea17..dc4ff5e1d 100644
--- a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
+++ b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
@@ -173,11 +173,6 @@ public interface IProfileEditorService : IArtemisSharedUIService
/// The command scope that will group any commands until disposed.
ProfileEditorCommandScope CreateCommandScope(string name);
- ///
- /// Saves the current profile.
- ///
- void SaveProfile();
-
///
/// Asynchronously saves the current profile.
///
diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
index 6a69bdd3b..a9f17c2ee 100644
--- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
+++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
@@ -391,19 +391,12 @@ internal class ProfileEditorService : IProfileEditorService
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
}
-
- ///
- public void SaveProfile()
- {
- Profile? profile = _profileConfigurationSubject.Value?.Profile;
- if (profile != null)
- _profileService.SaveProfile(profile, true);
- }
-
///
public async Task SaveProfileAsync()
{
- await Task.Run(SaveProfile);
+ Profile? profile = _profileConfigurationSubject.Value?.Profile;
+ if (profile != null)
+ await Task.Run(() => _profileService.SaveProfile(profile, true));
}
///
diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs
index cd7d5c186..c98a0d6df 100644
--- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs
+++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs
@@ -1,17 +1,15 @@
using System;
using System.ComponentModel;
using System.Globalization;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI.Notifications;
-using Artemis.UI.Screens.Settings;
+using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading;
using Microsoft.Toolkit.Uwp.Notifications;
-using ReactiveUI;
namespace Artemis.UI.Windows.Providers;
@@ -20,18 +18,34 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
private readonly Func _getReleaseInstaller;
private readonly IMainWindowService _mainWindowService;
private readonly IUpdateService _updateService;
+ private readonly IWorkshopUpdateService _workshopUpdateService;
private readonly IRouter _router;
private CancellationTokenSource? _cancellationTokenSource;
- public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, IRouter router, Func getReleaseInstaller)
+ public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
+ IUpdateService updateService,
+ IWorkshopUpdateService workshopUpdateService,
+ IRouter router, Func getReleaseInstaller)
{
_mainWindowService = mainWindowService;
_updateService = updateService;
+ _workshopUpdateService = workshopUpdateService;
_router = router;
_getReleaseInstaller = getReleaseInstaller;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
}
+ ///
+ public void ShowWorkshopNotification(int updatedEntries)
+ {
+ new ToastContentBuilder().AddText(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed")
+ .AddText(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed")
+ .AddArgument("action", "view-library")
+ .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-library"))
+ .AddButton(new ToastButton().SetContent("Don't show again").AddArgument("action", "disable-workshop-notifications"))
+ .Show();
+ }
+
///
public void ShowNotification(Guid releaseId, string releaseVersion)
{
@@ -57,14 +71,8 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
private void ViewRelease(Guid? releaseId)
{
- Dispatcher.UIThread.Invoke(async () =>
- {
- _mainWindowService.OpenMainWindow();
- if (releaseId != null && releaseId.Value != Guid.Empty)
- await _router.Navigate($"settings/releases/{releaseId}");
- else
- await _router.Navigate("settings/releases");
- });
+ string route = releaseId != null && releaseId.Value != Guid.Empty ? $"settings/releases/{releaseId}" : "settings/releases";
+ NavigateToRoute(route);
}
private async Task InstallRelease(Guid releaseId, string releaseVersion)
@@ -153,11 +161,9 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
ToastArguments args = ToastArguments.Parse(e.Argument);
Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty;
- string releaseVersion = args.Get("releaseVersion");
- string action = "view-changes";
- if (args.Contains("action"))
- action = args.Get("action");
-
+ string releaseVersion = args.Contains("releaseVersion") ? args.Get("releaseVersion") : string.Empty;
+ string action = args.Contains("action") ? args.Get("action") : "view-changes";
+
if (action == "install")
await InstallRelease(releaseId, releaseVersion);
else if (action == "view-changes")
@@ -166,5 +172,18 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
_cancellationTokenSource?.Cancel();
else if (action == "restart-for-update")
_updateService.RestartForUpdate("WindowsNotification", false);
+ else if (action == "disable-workshop-notifications")
+ _workshopUpdateService.DisableNotifications();
+ else if (action == "view-library")
+ NavigateToRoute("workshop/library");
+ }
+
+ private void NavigateToRoute(string route)
+ {
+ Dispatcher.UIThread.Invoke(async () =>
+ {
+ _mainWindowService.OpenMainWindow();
+ await _router.Navigate(route);
+ });
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs
index 231093e79..a18211bea 100644
--- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs
+++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs
@@ -36,7 +36,7 @@ public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayout
Entries = new ObservableCollection(workshopService.GetInstalledEntries().Where(e => e.EntryType == EntryType.Layout));
this.WhenAnyValue(vm => vm.SelectedEntry).Subscribe(ApplyEntry);
- this.WhenActivated((CompositeDisposable _) => SelectedEntry = Entries.FirstOrDefault(e => e.EntryId.ToString() == Device.LayoutSelection.Parameter));
+ this.WhenActivated((CompositeDisposable _) => SelectedEntry = Entries.FirstOrDefault(e => e.Id.ToString() == Device.LayoutSelection.Parameter));
}
///
@@ -70,7 +70,7 @@ public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayout
private void ApplyEntry(InstalledEntry? entry)
{
- if (entry == null || Device.LayoutSelection.Parameter == entry.EntryId.ToString())
+ if (entry == null || Device.LayoutSelection.Parameter == entry.Id.ToString())
return;
_layoutProvider.ConfigureDevice(Device, entry);
Save();
diff --git a/src/Artemis.UI/Screens/Home/HomeView.axaml b/src/Artemis.UI/Screens/Home/HomeView.axaml
index 219afbb0a..9a4079d81 100644
--- a/src/Artemis.UI/Screens/Home/HomeView.axaml
+++ b/src/Artemis.UI/Screens/Home/HomeView.axaml
@@ -4,8 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:home="clr-namespace:Artemis.UI.Screens.Home"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="900"
- x:Class="Artemis.UI.Screens.Home.HomeView">
+ x:Class="Artemis.UI.Screens.Home.HomeView"
+ x:DataType="home:HomeViewModel">
Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. We're also keeping track of a list of third-party plugins on our wiki.
-
+
diff --git a/src/Artemis.UI/Screens/Home/HomeViewModel.cs b/src/Artemis.UI/Screens/Home/HomeViewModel.cs
index 3460ea13b..98fb415d2 100644
--- a/src/Artemis.UI/Screens/Home/HomeViewModel.cs
+++ b/src/Artemis.UI/Screens/Home/HomeViewModel.cs
@@ -1,4 +1,5 @@
-using Artemis.Core.Services;
+using System.Threading.Tasks;
+using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
@@ -9,12 +10,20 @@ namespace Artemis.UI.Screens.Home;
public class HomeViewModel : RoutableScreen, IMainScreenViewModel
{
- public HomeViewModel(ISettingsService settingsService, IWindowService windowService)
+ private readonly IRouter _router;
+
+ public HomeViewModel(IRouter router, ISettingsService settingsService, IWindowService windowService)
{
+ _router = router;
// Show the startup wizard if it hasn't been completed
if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value)
Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync());
}
public ViewModelBase? TitleBarViewModel => null;
+
+ public async Task GetMorePlugins()
+ {
+ await _router.Navigate("workshop/entries/plugins");
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs
index 6e1da26b5..5ae472de2 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs
@@ -12,7 +12,6 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
-using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
@@ -23,9 +22,9 @@ public class DataBindingViewModel : ActivatableViewModelBase
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper? _dataBindingEnabled;
- private bool _editorOpen;
private ObservableAsPropertyHelper? _layerProperty;
private ObservableAsPropertyHelper? _nodeScriptViewModel;
+ private bool _editorOpen;
private bool _playing;
public DataBindingViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService, ISettingsService settingsService)
@@ -106,6 +105,6 @@ public class DataBindingViewModel : ActivatableViewModelBase
private void Save()
{
if (!_editorOpen)
- _profileEditorService.SaveProfile();
+ _profileEditorService.SaveProfileAsync();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
index eca2ee350..c92e34dcc 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
@@ -15,8 +15,11 @@ using Artemis.UI.Screens.ProfileEditor.StatusBar;
using Artemis.UI.Screens.ProfileEditor.VisualEditor;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Shared.Services.ProfileEditor;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
@@ -30,10 +33,12 @@ public partial class ProfileEditorViewModel : RoutableScreen _tools;
private ObservableAsPropertyHelper? _history;
private ObservableAsPropertyHelper? _suspendedEditing;
-
+
[Notify] private ProfileConfiguration? _profileConfiguration;
///
@@ -48,12 +53,16 @@ public partial class ProfileEditorViewModel : RoutableScreen toolViewModels,
IMainWindowService mainWindowService,
- IInputService inputService)
+ IInputService inputService,
+ IWorkshopService workshopService,
+ IWindowService windowService)
{
_profileService = profileService;
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_mainWindowService = mainWindowService;
+ _workshopService = workshopService;
+ _windowService = windowService;
_tools = new SourceList();
_tools.AddRange(toolViewModels);
@@ -144,7 +153,7 @@ public partial class ProfileEditorViewModel : RoutableScreen
- If enabled, new updates will automatically be installed.
+ Automatically install new versions of Artemis in the background when available.
@@ -202,6 +202,21 @@
+
+
+
+
+ Show workshop update notifications
+
+
+ Show a desktop notification whenever workshop updates are installed.
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs
index 87bb9c949..10ff14cc2 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs
@@ -102,6 +102,7 @@ public class GeneralTabViewModel : RoutableScreen
public bool IsAutoRunSupported => _autoRunProvider != null;
public bool IsWindows11 => OSVersionHelper.IsWindows11();
+ public bool IsWindows => OSVersionHelper.IsWindows();
public ObservableCollection LayerBrushDescriptors { get; }
public ObservableCollection GraphicsContexts { get; }
@@ -158,6 +159,7 @@ public class GeneralTabViewModel : RoutableScreen
public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
public PluginSetting EnableMica => _settingsService.GetSetting("UI.EnableMica", true);
public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
+ public PluginSetting WorkshopShowNotifications => _settingsService.GetSetting("Workshop.ShowNotifications", true);
public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml
index b9f28f4a6..38c86d765 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml
+++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml
@@ -17,7 +17,7 @@
-
+
Get more plugins
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml.cs
index 555c1c1c2..7a5c54a6a 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml.cs
@@ -1,3 +1,4 @@
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
@@ -9,5 +10,4 @@ public partial class PluginsTabView : ReactiveUserControl
{
InitializeComponent();
}
-
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs
index 9111aecf6..5c9c76cb4 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs
@@ -23,12 +23,14 @@ namespace Artemis.UI.Screens.Settings;
public partial class PluginsTabViewModel : RoutableScreen
{
private readonly INotificationService _notificationService;
+ private readonly IRouter _router;
private readonly IPluginManagementService _pluginManagementService;
private readonly IWindowService _windowService;
[Notify] private string? _searchPluginInput;
- public PluginsTabViewModel(IPluginManagementService pluginManagementService, INotificationService notificationService, IWindowService windowService, ISettingsVmFactory settingsVmFactory)
+ public PluginsTabViewModel(IRouter router, IPluginManagementService pluginManagementService, INotificationService notificationService, IWindowService windowService, ISettingsVmFactory settingsVmFactory)
{
+ _router = router;
_pluginManagementService = pluginManagementService;
_notificationService = notificationService;
_windowService = windowService;
@@ -113,4 +115,9 @@ public partial class PluginsTabViewModel : RoutableScreen
return data => data.Info.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
(data.Info.Description != null && data.Info.Description.Contains(text, StringComparison.InvariantCultureIgnoreCase));
}
+
+ public async Task GetMorePlugins()
+ {
+ await _router.Navigate("workshop/entries/plugins");
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
index 7792f300c..94b2e1688 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
@@ -59,8 +59,18 @@
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"
Margin="0 15" />
-
-
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
index 712d7fc68..22d723a06 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
@@ -37,11 +37,23 @@
-
-
- by
-
-
+
+
+
+ by
+
+
+
+
+
downloads
-
+
-
+
installed
-
+
update available
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml
index 5a235b3bf..8dbdd04b5 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml
@@ -15,8 +15,17 @@
-
-
+
+
+
+
+
+
Categories
@@ -27,7 +36,7 @@
-
+
@@ -38,7 +47,7 @@
-
+
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs
index db9f6d8c2..3d5cba990 100644
--- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs
@@ -101,20 +101,35 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
return;
}
+ // If not the latest version, warn and offer to disable auto-updates
+ bool disableAutoUpdates = false;
+ if (Release.Id != Release.Entry.LatestReleaseId)
+ {
+ disableAutoUpdates = await _windowService.ShowConfirmContentDialog(
+ "You are installing an older version of this entry",
+ "Would you like to disable auto-updates for this entry?",
+ "Yes",
+ "No"
+ );
+ }
+
_cts = new CancellationTokenSource();
InstallProgress = 0;
InstallationInProgress = true;
try
{
EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token);
- if (result.IsSuccess)
+ if (result.IsSuccess && result.Entry != null)
{
+ _workshopService.SetAutoUpdate(result.Entry, !disableAutoUpdates);
_notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
InstallationInProgress = false;
await Manage();
}
else if (!_cts.IsCancellationRequested)
+ {
_notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show();
+ }
}
catch (Exception e)
{
@@ -156,7 +171,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
await UninstallPluginPrerequisites(installedEntry);
await _workshopService.UninstallEntry(installedEntry, CancellationToken.None);
-
+
_notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show();
}
finally
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
index 7f021d50f..b94b3b471 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
@@ -8,7 +8,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
x:DataType="layout:LayoutDetailsViewModel">
-
+
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultView.axaml
index 4aa66067a..a7fb6413c 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultView.axaml
@@ -6,7 +6,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListDefaultView"
x:DataType="layout:LayoutListDefaultViewModel">
-
+
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
index 014bde191..93aad1f59 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
@@ -1,4 +1,3 @@
-using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
@@ -6,7 +5,6 @@ namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListViewModel : RoutableHostScreen
{
- private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen { get; }
public LayoutListViewModel(LayoutListDefaultViewModel defaultViewModel)
diff --git a/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderViewModel.cs b/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderViewModel.cs
index 24be58538..e5e162216 100644
--- a/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderViewModel.cs
@@ -19,13 +19,14 @@ namespace Artemis.UI.Screens.Workshop.LayoutFinder;
public partial class LayoutFinderViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
- private readonly SourceList _devices;
[Notify] private ReadOnlyObservableCollection _deviceViewModels;
public LayoutFinderViewModel(ILogger logger, IDeviceService deviceService, Func getDeviceViewModel)
{
_logger = logger;
SearchAll = ReactiveCommand.CreateFromTask(ExecuteSearchAll);
+ DeviceViewModels = new ReadOnlyObservableCollection([]);
+
this.WhenActivated((CompositeDisposable _) =>
{
IEnumerable deviceGroups = deviceService.EnabledDevices.Select(getDeviceViewModel);
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
index 4f5994982..71ab5983c 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
- xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
+ xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@@ -13,47 +13,94 @@
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs
index be6199b60..54504c0c8 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs
@@ -1,10 +1,8 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
-public partial class InstalledTabItemView : UserControl
+public partial class InstalledTabItemView : ReactiveUserControl
{
public InstalledTabItemView()
{
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs
index 951bc149b..6307a8557 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs
@@ -1,12 +1,16 @@
using System;
using System.Linq;
using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
+using Artemis.UI.Extensions;
using Artemis.UI.Screens.Plugins;
+using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
@@ -15,70 +19,97 @@ using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
+using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
-public partial class InstalledTabItemViewModel : ViewModelBase
+public partial class InstalledTabItemViewModel : ActivatableViewModelBase
{
+ private readonly IWorkshopClient _client;
private readonly IWorkshopService _workshopService;
+ private readonly IWorkshopUpdateService _workshopUpdateService;
private readonly IRouter _router;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
- public InstalledTabItemViewModel(InstalledEntry installedEntry,
+ [Notify] private bool _updateAvailable;
+ [Notify] private bool _autoUpdate;
+
+ public InstalledTabItemViewModel(InstalledEntry entry,
+ IWorkshopClient client,
IWorkshopService workshopService,
- IRouter router,
+ IWorkshopUpdateService workshopUpdateService,
+ IRouter router,
IWindowService windowService,
IPluginManagementService pluginManagementService,
ISettingsVmFactory settingsVmFactory)
{
+ _client = client;
_workshopService = workshopService;
+ _workshopUpdateService = workshopUpdateService;
_router = router;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_settingsVmFactory = settingsVmFactory;
- InstalledEntry = installedEntry;
+ _autoUpdate = entry.AutoUpdate;
+
+ Entry = entry;
- ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
- ViewLocal = ReactiveCommand.CreateFromTask(ExecuteViewLocal);
- Uninstall = ReactiveCommand.CreateFromTask(ExecuteUninstall);
- }
-
- public InstalledEntry InstalledEntry { get; }
- public ReactiveCommand ViewWorkshopPage { get; }
- public ReactiveCommand ViewLocal { get; }
- public ReactiveCommand Uninstall { get; }
-
- private async Task ExecuteViewWorkshopPage()
- {
- await _workshopService.NavigateToEntry(InstalledEntry.EntryId, InstalledEntry.EntryType);
- }
-
- private async Task ExecuteViewLocal(CancellationToken cancellationToken)
- {
- if (InstalledEntry.EntryType == EntryType.Profile && InstalledEntry.TryGetMetadata("ProfileId", out Guid profileId))
+ this.WhenActivatedAsync(async _ =>
{
- await _router.Navigate($"profile-editor/{profileId}");
- }
+ // Grab the latest entry summary from the workshop
+ try
+ {
+ IOperationResult entrySummary = await _client.GetEntrySummaryById.ExecuteAsync(Entry.Id);
+ if (entrySummary.Data?.Entry != null)
+ {
+ Entry.ApplyEntrySummary(entrySummary.Data.Entry);
+ _workshopService.SaveInstalledEntry(Entry);
+ }
+ }
+ finally
+ {
+ UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId;
+ }
+ });
+
+ this.WhenAnyValue(vm => vm.AutoUpdate).Skip(1).Subscribe(_ => AutoUpdateToggled());
}
-
- private async Task ExecuteUninstall(CancellationToken cancellationToken)
+
+ public InstalledEntry Entry { get; }
+
+ public async Task ViewWorkshopPage()
+ {
+ await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
+ }
+
+ public async Task ViewLocal()
+ {
+ if (Entry.EntryType == EntryType.Profile && Entry.TryGetMetadata("ProfileId", out Guid profileId))
+ await _router.Navigate($"profile-editor/{profileId}");
+ else if (Entry.EntryType == EntryType.Plugin)
+ await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}/manage");
+ else if (Entry.EntryType == EntryType.Layout)
+ await _router.Navigate($"workshop/entries/layouts/details/{Entry.Id}/manage");
+ }
+
+ public async Task Uninstall()
{
bool confirmed = await _windowService.ShowConfirmContentDialog("Do you want to uninstall this entry?", "Both the entry and its contents will be removed.");
if (!confirmed)
return;
// Ideally the installation handler does this but it doesn't have access to the required view models
- if (InstalledEntry.EntryType == EntryType.Plugin)
+ if (Entry.EntryType == EntryType.Plugin)
await UninstallPluginPrerequisites();
-
- await _workshopService.UninstallEntry(InstalledEntry, cancellationToken);
+
+ await _workshopService.UninstallEntry(Entry, CancellationToken.None);
}
private async Task UninstallPluginPrerequisites()
{
- if (!InstalledEntry.TryGetMetadata("PluginId", out Guid pluginId))
+ if (!Entry.TryGetMetadata("PluginId", out Guid pluginId))
return;
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
if (plugin == null)
@@ -87,4 +118,18 @@ public partial class InstalledTabItemViewModel : ViewModelBase
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
await pluginViewModel.ExecuteRemovePrerequisites(true);
}
+
+ private void AutoUpdateToggled()
+ {
+ _workshopService.SetAutoUpdate(Entry, AutoUpdate);
+
+ if (!AutoUpdate)
+ return;
+
+ Task.Run(async () =>
+ {
+ await _workshopUpdateService.AutoUpdateEntry(Entry);
+ UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId;
+ });
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabView.axaml
index a3c21adfb..d91ed2eb5 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabView.axaml
@@ -15,8 +15,16 @@
-
-
+
+
+
+
+
+
+
+
+
+
Not much here yet, huh!
Any entries you download from the workshop you can later manage here
@@ -24,21 +32,34 @@
Browse the Workshop
-
-
-
-
-
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs
index 982ec1aba..3a3267ecb 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs
@@ -1,14 +1,16 @@
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using DynamicData;
using DynamicData.Binding;
+using DynamicData.List;
+using Humanizer;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
@@ -16,45 +18,45 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class InstalledTabViewModel : RoutableScreen
{
- private SourceList _installedEntries = new();
-
+ private SourceList _entries = new();
+
[Notify] private string? _searchEntryInput;
+ private readonly ObservableAsPropertyHelper _empty;
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func getInstalledTabItemViewModel)
{
- IObservable> pluginFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
+ IObservable> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
- _installedEntries.Connect()
- .Filter(pluginFilter)
+ _entries.Connect()
+ .Filter(searchFilter)
.Sort(SortExpressionComparer.Descending(p => p.InstalledAt))
.Transform(getInstalledTabItemViewModel)
- .Bind(out ReadOnlyObservableCollection installedEntryViewModels)
+ .GroupWithImmutableState(vm => vm.Entry.EntryType.Humanize(LetterCasing.Title).Pluralize())
+ .Bind(out ReadOnlyObservableCollection> entryViewModels)
.Subscribe();
+ _empty = _entries.Connect().Count().Select(c => c == 0).ToProperty(this, vm => vm.Empty);
+ _entries.AddRange(workshopService.GetInstalledEntries());
- List entries = workshopService.GetInstalledEntries();
- _installedEntries.AddRange(entries);
-
- Empty = entries.Count == 0;
- InstalledEntries = installedEntryViewModels;
-
+ EntryGroups = entryViewModels;
+
this.WhenActivated(d =>
{
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
});
-
+
OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
}
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
{
- _installedEntries.Remove(e);
+ _entries.Remove(e);
}
- public bool Empty { get; }
+ public bool Empty => _empty.Value;
public ReactiveCommand OpenWorkshop { get; }
- public ReadOnlyObservableCollection InstalledEntries { get; }
-
+ public ReadOnlyObservableCollection> EntryGroups { get; }
+
private Func CreatePredicate(string? text)
{
if (string.IsNullOrWhiteSpace(text))
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml
index 583f14c4f..b45a8d3e8 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml
@@ -3,41 +3,44 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
- xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
- xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabItemView"
x:DataType="tabs:SubmissionsTabItemViewModel">
-
+
-
-
+ Command="{CompiledBinding NavigateToEntry}"
+ IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
+
-
+
-
-
+
+
+
+ by you
+
+
-
+
@@ -63,10 +66,10 @@
-
+
-
+
downloads
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemViewModel.cs
index 21c8098ee..9e703d78b 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemViewModel.cs
@@ -1,3 +1,4 @@
+using System;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
@@ -10,6 +11,7 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class SubmissionsTabItemViewModel : ViewModelBase
{
+ private static readonly string[] Emojis = ["❤️", "🧡", "💛", "💚", "💙", "💜", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "😍", "🥰"];
private readonly IRouter _router;
public SubmissionsTabItemViewModel(IGetSubmittedEntries_SubmittedEntries entry, IRouter router)
@@ -18,9 +20,11 @@ public class SubmissionsTabItemViewModel : ViewModelBase
Entry = entry;
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
+ Emoji = Emojis[Random.Shared.Next(0, Emojis.Length)];
}
public IGetSubmittedEntries_SubmittedEntries Entry { get; }
+ public string Emoji { get; }
public ReactiveCommand NavigateToEntry { get; }
private async Task ExecuteNavigateToEntry(CancellationToken cancellationToken)
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml
index 5d00c720e..63f5272e0 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml
@@ -6,7 +6,6 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView"
x:DataType="tabs:SubmissionsTabViewModel">
-