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 @@ - - + + + + + + update available + + + + + + + + + + Auto-update + + \ 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 @@ - - - - - - - - + + + - + + + + + + + + + + + + + + + + + + + + - - + \ 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"> - + -