From 99d11e1921d1fcaeede35aab5cfed92cd424e173 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 13 Jul 2024 20:25:40 +0200 Subject: [PATCH 1/4] Workshop - Redesigned library Workshop - Limit screen width to keep main content at 1000px Workshop - Auto-updating WIP --- src/Artemis.Core/Plugins/PluginInfo.cs | 16 +- src/Artemis.Storage/ArtemisDbContext.cs | 6 + .../Entities/Workshop/EntryEntity.cs | 14 +- ...706131336_ExpandInstalledEntry.Designer.cs | 371 ++++++++++++++++++ .../20240706131336_ExpandInstalledEntry.cs | 83 ++++ .../ArtemisDbContextModelSnapshot.cs | 45 ++- .../WorkshopLayoutViewModel.cs | 4 +- src/Artemis.UI/Screens/Home/HomeView.axaml | 6 +- src/Artemis.UI/Screens/Home/HomeViewModel.cs | 13 +- .../Settings/Tabs/PluginsTabView.axaml | 2 +- .../Settings/Tabs/PluginsTabView.axaml.cs | 2 +- .../Settings/Tabs/PluginsTabViewModel.cs | 9 +- .../Workshop/Entries/List/EntryListView.axaml | 17 +- .../Workshop/Layout/LayoutDetailsView.axaml | 2 +- .../Layout/LayoutListDefaultView.axaml | 2 +- .../Workshop/Layout/LayoutListViewModel.cs | 2 - .../LayoutFinder/LayoutFinderViewModel.cs | 3 +- .../Library/Tabs/InstalledTabItemView.axaml | 92 +++-- .../Tabs/InstalledTabItemView.axaml.cs | 3 +- .../Library/Tabs/InstalledTabItemViewModel.cs | 81 ++-- .../Library/Tabs/InstalledTabView.axaml | 47 ++- .../Library/Tabs/InstalledTabViewModel.cs | 38 +- .../Library/Tabs/SubmissionsTabItemView.axaml | 43 +- .../Tabs/SubmissionsTabItemViewModel.cs | 4 + .../Library/Tabs/SubmissionsTabView.axaml | 61 +-- .../Library/Tabs/SubmissionsTabViewModel.cs | 32 +- .../Plugins/PluginDescriptionView.axaml | 2 +- .../Workshop/Plugins/PluginDetailsView.axaml | 2 +- .../Profile/ProfileDescriptionView.axaml | 2 +- .../Workshop/Profile/ProfileDetailsView.axaml | 2 +- .../LayoutEntryInstallationHandler.cs | 2 +- .../Models/InstalledEntry.cs | 132 ++++++- .../Providers/WorkshopLayoutProvider.cs | 2 +- .../Queries/GetEntryById.graphql | 6 + .../Services/Interfaces/IWorkshopService.cs | 3 +- .../Services/WorkshopService.cs | 2 +- src/Directory.Packages.props | 2 +- 37 files changed, 955 insertions(+), 200 deletions(-) create mode 100644 src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.Designer.cs create mode 100644 src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.cs 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..f45ac5327 100644 --- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs +++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs @@ -15,10 +15,18 @@ public class EntryEntity public string Author { get; set; } = string.Empty; 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/20240706131336_ExpandInstalledEntry.Designer.cs b/src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.Designer.cs new file mode 100644 index 000000000..47d82c429 --- /dev/null +++ b/src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.Designer.cs @@ -0,0 +1,371 @@ +// +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("20240706131336_ExpandInstalledEntry")] + partial class ExpandInstalledEntry + { + /// + 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("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/20240706131336_ExpandInstalledEntry.cs b/src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.cs new file mode 100644 index 000000000..d159d55e7 --- /dev/null +++ b/src/Artemis.Storage/Migrations/20240706131336_ExpandInstalledEntry.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Artemis.Storage.Migrations +{ + /// + public partial class ExpandInstalledEntry : 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: "LatestReleaseId", + table: "Entries", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "Summary", + table: "Entries", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + 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: "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..2be8631f6 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,9 @@ namespace Artemis.Storage.Migrations b.Property("InstalledAt") .HasColumnType("TEXT"); + b.Property("LatestReleaseId") + .HasColumnType("INTEGER"); + b.Property("Metadata") .HasColumnType("TEXT"); @@ -263,12 +278,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 +310,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 +320,7 @@ namespace Artemis.Storage.Migrations b1.HasKey("DeviceEntityId"); - b1.ToTable("Devices", (string)null); + b1.ToTable("Devices"); b1.ToJson("InputIdentifiers"); @@ -309,7 +328,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 +338,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/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/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/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/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..7576497be 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,85 @@ - - + + + + + + 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..1ff8406e2 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,11 @@ 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..990db9401 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs @@ -1,11 +1,13 @@ using System; using System.Linq; using System.Reactive; +using System.Reactive.Disposables; 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.Shared; using Artemis.UI.Shared.Routing; @@ -15,70 +17,89 @@ 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 IRouter _router; private readonly IWindowService _windowService; private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; - public InstalledTabItemViewModel(InstalledEntry installedEntry, + [Notify] private bool _updateAvailable; + + public InstalledTabItemViewModel(InstalledEntry entry, + IWorkshopClient client, IWorkshopService workshopService, - IRouter router, + IRouter router, IWindowService windowService, IPluginManagementService pluginManagementService, ISettingsVmFactory settingsVmFactory) { + _client = client; _workshopService = workshopService; _router = router; _windowService = windowService; _pluginManagementService = pluginManagementService; _settingsVmFactory = settingsVmFactory; - InstalledEntry = installedEntry; + 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; + } + }); } - - 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) 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..d9b7e978b 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"> - + -