mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
a793a08213
@ -116,6 +116,11 @@ public class PluginInfo : IPrerequisitesSubject
|
|||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public Version? Api { get; internal init; } = new(1, 0, 0);
|
public Version? Api { get; internal init; } = new(1, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum version of Artemis required by this plugin
|
||||||
|
/// </summary>
|
||||||
|
public Version? MinimumVersion { get; internal init; } = new(1, 0, 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the plugin this info is associated with
|
/// Gets the plugin this info is associated with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -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
|
/// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api.Major >= Constants.PluginApiVersion;
|
public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api.Major >= Constants.PluginApiVersion && MatchesMinimumVersion();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -156,4 +161,13 @@ public class PluginInfo : IPrerequisitesSubject
|
|||||||
{
|
{
|
||||||
return $"{Name} v{Version} - {Guid}";
|
return $"{Name} v{Version} - {Guid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool MatchesMinimumVersion()
|
||||||
|
{
|
||||||
|
if (Constants.CurrentVersion == "local")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Version currentVersion = new(Constants.CurrentVersion);
|
||||||
|
return currentVersion >= MinimumVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +43,12 @@ public class ArtemisDbContext : DbContext
|
|||||||
.HasConversion(
|
.HasConversion(
|
||||||
v => JsonSerializer.Serialize(v, JsonSerializerOptions),
|
v => JsonSerializer.Serialize(v, JsonSerializerOptions),
|
||||||
v => JsonSerializer.Deserialize<Dictionary<string, JsonNode>>(v, JsonSerializerOptions) ?? new Dictionary<string, JsonNode>());
|
v => JsonSerializer.Deserialize<Dictionary<string, JsonNode>>(v, JsonSerializerOptions) ?? new Dictionary<string, JsonNode>());
|
||||||
|
|
||||||
|
modelBuilder.Entity<EntryEntity>()
|
||||||
|
.Property(e => e.Categories)
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonSerializer.Serialize(v, JsonSerializerOptions),
|
||||||
|
v => JsonSerializer.Deserialize<List<EntryCategoryEntity>>(v, JsonSerializerOptions) ?? new List<EntryCategoryEntity>());
|
||||||
|
|
||||||
modelBuilder.Entity<ProfileContainerEntity>()
|
modelBuilder.Entity<ProfileContainerEntity>()
|
||||||
.Property(e => e.ProfileConfiguration)
|
.Property(e => e.ProfileConfiguration)
|
||||||
|
|||||||
@ -14,11 +14,20 @@ public class EntryEntity
|
|||||||
public int EntryType { get; set; }
|
public int EntryType { get; set; }
|
||||||
|
|
||||||
public string Author { get; set; } = string.Empty;
|
public string Author { get; set; } = string.Empty;
|
||||||
|
public bool IsOfficial { get; set; }
|
||||||
public string Name { 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 long ReleaseId { get; set; }
|
||||||
public string ReleaseVersion { get; set; } = string.Empty;
|
public string ReleaseVersion { get; set; } = string.Empty;
|
||||||
public DateTimeOffset InstalledAt { get; set; }
|
public DateTimeOffset InstalledAt { get; set; }
|
||||||
|
public bool AutoUpdate { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, JsonNode>? Metadata { get; set; }
|
public Dictionary<string, JsonNode>? Metadata { get; set; }
|
||||||
}
|
public List<EntryCategoryEntity>? Categories { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EntryCategoryEntity(string Name, string Icon);
|
||||||
374
src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.Designer.cs
generated
Normal file
374
src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.Designer.cs
generated
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("InstalledAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("PluginGuid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PluginGuid")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Plugins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PluginEntityId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PluginEntityId");
|
||||||
|
|
||||||
|
b.ToTable("PluginFeatures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("PluginGuid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCollapsed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSuspended")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ProfileCategories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Icon")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Profile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProfileCategoryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProfileConfiguration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProfileCategoryId");
|
||||||
|
|
||||||
|
b.ToTable("ProfileContainers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<float>("BlueScale")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Categories")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceProvider")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<float>("GreenScale")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LayoutParameter")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LayoutType")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LogicalLayout")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PhysicalLayout")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<float>("RedScale")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<float>("Rotation")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<float>("Scale")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<float>("X")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<float>("Y")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<int>("ZIndex")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Devices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoUpdate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Categories")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("Downloads")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("EntryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("EntryType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("InstalledAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOfficial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("LatestReleaseId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Metadata")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("ReleaseId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ReleaseVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<Artemis.Storage.Entities.Surface.DeviceInputIdentifierEntity>", "InputIdentifiers", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<string>("DeviceEntityId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.HasKey("DeviceEntityId");
|
||||||
|
|
||||||
|
b1.ToTable("Devices");
|
||||||
|
|
||||||
|
b1.ToJson("InputIdentifiers");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("DeviceEntityId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("System.Collections.Generic.List<Artemis.Storage.Entities.Surface.InputMappingEntity>", "InputMappings", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<string>("DeviceEntityId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<int>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.cs
Normal file
110
src/Artemis.Storage/Migrations/20240722084220_AutoUpdating.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Artemis.Storage.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoUpdating : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AutoUpdate",
|
||||||
|
table: "Entries",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Categories",
|
||||||
|
table: "Entries",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
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<long>(
|
||||||
|
name: "Downloads",
|
||||||
|
table: "Entries",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsOfficial",
|
||||||
|
table: "Entries",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "LatestReleaseId",
|
||||||
|
table: "Entries",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
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
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#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 =>
|
modelBuilder.Entity("Artemis.Storage.Entities.General.ReleaseEntity", b =>
|
||||||
{
|
{
|
||||||
@ -38,7 +38,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
b.HasIndex("Version")
|
b.HasIndex("Version")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Releases", (string)null);
|
b.ToTable("Releases");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b =>
|
||||||
@ -58,7 +58,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
b.HasIndex("PluginGuid")
|
b.HasIndex("PluginGuid")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Plugins", (string)null);
|
b.ToTable("Plugins");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
|
||||||
@ -81,7 +81,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
b.HasIndex("PluginEntityId");
|
b.HasIndex("PluginEntityId");
|
||||||
|
|
||||||
b.ToTable("PluginFeatures", (string)null);
|
b.ToTable("PluginFeatures");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b =>
|
||||||
@ -109,7 +109,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
b.HasIndex("Name", "PluginGuid")
|
b.HasIndex("Name", "PluginGuid")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("PluginSettings", (string)null);
|
b.ToTable("PluginSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b =>
|
||||||
@ -137,7 +137,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
b.HasIndex("Name")
|
b.HasIndex("Name")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("ProfileCategories", (string)null);
|
b.ToTable("ProfileCategories");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b =>
|
||||||
@ -165,7 +165,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ProfileCategoryId");
|
b.HasIndex("ProfileCategoryId");
|
||||||
|
|
||||||
b.ToTable("ProfileContainers", (string)null);
|
b.ToTable("ProfileContainers");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
|
||||||
@ -227,7 +227,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Devices", (string)null);
|
b.ToTable("Devices");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b =>
|
||||||
@ -240,6 +240,18 @@ namespace Artemis.Storage.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoUpdate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Categories")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("Downloads")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<long>("EntryId")
|
b.Property<long>("EntryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -249,6 +261,12 @@ namespace Artemis.Storage.Migrations
|
|||||||
b.Property<DateTimeOffset>("InstalledAt")
|
b.Property<DateTimeOffset>("InstalledAt")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOfficial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("LatestReleaseId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Metadata")
|
b.Property<string>("Metadata")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -263,12 +281,16 @@ namespace Artemis.Storage.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("EntryId")
|
b.HasIndex("EntryId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Entries", (string)null);
|
b.ToTable("Entries");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b =>
|
||||||
@ -291,7 +313,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
|
modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b =>
|
||||||
{
|
{
|
||||||
b.OwnsOne("Artemis.Storage.Entities.Surface.DeviceEntity.InputIdentifiers#System.Collections.Generic.List<Artemis.Storage.Entities.Surface.DeviceInputIdentifierEntity>", "InputIdentifiers", b1 =>
|
b.OwnsOne("System.Collections.Generic.List<Artemis.Storage.Entities.Surface.DeviceInputIdentifierEntity>", "InputIdentifiers", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<string>("DeviceEntityId")
|
b1.Property<string>("DeviceEntityId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@ -301,7 +323,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
b1.HasKey("DeviceEntityId");
|
b1.HasKey("DeviceEntityId");
|
||||||
|
|
||||||
b1.ToTable("Devices", (string)null);
|
b1.ToTable("Devices");
|
||||||
|
|
||||||
b1.ToJson("InputIdentifiers");
|
b1.ToJson("InputIdentifiers");
|
||||||
|
|
||||||
@ -309,7 +331,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
.HasForeignKey("DeviceEntityId");
|
.HasForeignKey("DeviceEntityId");
|
||||||
});
|
});
|
||||||
|
|
||||||
b.OwnsOne("Artemis.Storage.Entities.Surface.DeviceEntity.InputMappings#System.Collections.Generic.List<Artemis.Storage.Entities.Surface.InputMappingEntity>", "InputMappings", b1 =>
|
b.OwnsOne("System.Collections.Generic.List<Artemis.Storage.Entities.Surface.InputMappingEntity>", "InputMappings", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<string>("DeviceEntityId")
|
b1.Property<string>("DeviceEntityId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@ -319,7 +341,7 @@ namespace Artemis.Storage.Migrations
|
|||||||
|
|
||||||
b1.HasKey("DeviceEntityId");
|
b1.HasKey("DeviceEntityId");
|
||||||
|
|
||||||
b1.ToTable("Devices", (string)null);
|
b1.ToTable("Devices");
|
||||||
|
|
||||||
b1.ToJson("InputMappings");
|
b1.ToJson("InputMappings");
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
path = NavigateUp(_currentRouteSubject.Value, path);
|
path = NavigateUp(_currentRouteSubject.Value, path);
|
||||||
else
|
else
|
||||||
path = path.ToLower().Trim(' ', '/', '\\');
|
path = path.ToLower().Trim(' ', '/', '\\');
|
||||||
|
|
||||||
options ??= new RouterNavigationOptions();
|
options ??= new RouterNavigationOptions();
|
||||||
|
|
||||||
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
// 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()
|
public async Task Reload()
|
||||||
{
|
{
|
||||||
string path = _currentRouteSubject.Value ?? "blank";
|
string path = _currentRouteSubject.Value ?? "blank";
|
||||||
|
|
||||||
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
||||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
@ -128,8 +128,12 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
await navigation.Navigate(args);
|
await navigation.Navigate(args);
|
||||||
|
|
||||||
// If it was cancelled before completion, don't add it to history or update the current path
|
// 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)
|
if (navigation.Cancelled)
|
||||||
|
{
|
||||||
|
await Reload();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.AddToHistory && previousPath != null)
|
if (options.AddToHistory && previousPath != null)
|
||||||
{
|
{
|
||||||
@ -172,7 +176,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
public async Task<bool> GoUp(RouterNavigationOptions? options = null)
|
public async Task<bool> GoUp(RouterNavigationOptions? options = null)
|
||||||
{
|
{
|
||||||
string? currentPath = _currentRouteSubject.Value;
|
string? currentPath = _currentRouteSubject.Value;
|
||||||
|
|
||||||
// Keep removing segments until we find a parent route that resolves
|
// Keep removing segments until we find a parent route that resolves
|
||||||
while (currentPath != null && currentPath.Contains('/'))
|
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);
|
_logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string NavigateUp(string current, string path)
|
private string NavigateUp(string current, string path)
|
||||||
{
|
{
|
||||||
string[] pathParts = current.Split('/');
|
string[] pathParts = current.Split('/');
|
||||||
|
|||||||
@ -173,11 +173,6 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
|||||||
/// <returns>The command scope that will group any commands until disposed.</returns>
|
/// <returns>The command scope that will group any commands until disposed.</returns>
|
||||||
ProfileEditorCommandScope CreateCommandScope(string name);
|
ProfileEditorCommandScope CreateCommandScope(string name);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the current profile.
|
|
||||||
/// </summary>
|
|
||||||
void SaveProfile();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously saves the current profile.
|
/// Asynchronously saves the current profile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -391,19 +391,12 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SaveProfile()
|
|
||||||
{
|
|
||||||
Profile? profile = _profileConfigurationSubject.Value?.Profile;
|
|
||||||
if (profile != null)
|
|
||||||
_profileService.SaveProfile(profile, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SaveProfileAsync()
|
public async Task SaveProfileAsync()
|
||||||
{
|
{
|
||||||
await Task.Run(SaveProfile);
|
Profile? profile = _profileConfigurationSubject.Value?.Profile;
|
||||||
|
if (profile != null)
|
||||||
|
await Task.Run(() => _profileService.SaveProfile(profile, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.UI.Notifications;
|
using Windows.UI.Notifications;
|
||||||
using Artemis.UI.Screens.Settings;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Services.Updating;
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Toolkit.Uwp.Notifications;
|
using Microsoft.Toolkit.Uwp.Notifications;
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Windows.Providers;
|
namespace Artemis.UI.Windows.Providers;
|
||||||
|
|
||||||
@ -20,18 +18,34 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
|
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
|
private readonly IWorkshopUpdateService _workshopUpdateService;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
|
||||||
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, IRouter router, Func<Guid, ReleaseInstaller> getReleaseInstaller)
|
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
|
||||||
|
IUpdateService updateService,
|
||||||
|
IWorkshopUpdateService workshopUpdateService,
|
||||||
|
IRouter router, Func<Guid, ReleaseInstaller> getReleaseInstaller)
|
||||||
{
|
{
|
||||||
_mainWindowService = mainWindowService;
|
_mainWindowService = mainWindowService;
|
||||||
_updateService = updateService;
|
_updateService = updateService;
|
||||||
|
_workshopUpdateService = workshopUpdateService;
|
||||||
_router = router;
|
_router = router;
|
||||||
_getReleaseInstaller = getReleaseInstaller;
|
_getReleaseInstaller = getReleaseInstaller;
|
||||||
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
|
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ShowNotification(Guid releaseId, string releaseVersion)
|
public void ShowNotification(Guid releaseId, string releaseVersion)
|
||||||
{
|
{
|
||||||
@ -57,14 +71,8 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
|
|
||||||
private void ViewRelease(Guid? releaseId)
|
private void ViewRelease(Guid? releaseId)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(async () =>
|
string route = releaseId != null && releaseId.Value != Guid.Empty ? $"settings/releases/{releaseId}" : "settings/releases";
|
||||||
{
|
NavigateToRoute(route);
|
||||||
_mainWindowService.OpenMainWindow();
|
|
||||||
if (releaseId != null && releaseId.Value != Guid.Empty)
|
|
||||||
await _router.Navigate($"settings/releases/{releaseId}");
|
|
||||||
else
|
|
||||||
await _router.Navigate("settings/releases");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InstallRelease(Guid releaseId, string releaseVersion)
|
private async Task InstallRelease(Guid releaseId, string releaseVersion)
|
||||||
@ -153,11 +161,9 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
ToastArguments args = ToastArguments.Parse(e.Argument);
|
ToastArguments args = ToastArguments.Parse(e.Argument);
|
||||||
|
|
||||||
Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty;
|
Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty;
|
||||||
string releaseVersion = args.Get("releaseVersion");
|
string releaseVersion = args.Contains("releaseVersion") ? args.Get("releaseVersion") : string.Empty;
|
||||||
string action = "view-changes";
|
string action = args.Contains("action") ? args.Get("action") : "view-changes";
|
||||||
if (args.Contains("action"))
|
|
||||||
action = args.Get("action");
|
|
||||||
|
|
||||||
if (action == "install")
|
if (action == "install")
|
||||||
await InstallRelease(releaseId, releaseVersion);
|
await InstallRelease(releaseId, releaseVersion);
|
||||||
else if (action == "view-changes")
|
else if (action == "view-changes")
|
||||||
@ -166,5 +172,18 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
else if (action == "restart-for-update")
|
else if (action == "restart-for-update")
|
||||||
_updateService.RestartForUpdate("WindowsNotification", false);
|
_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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayout
|
|||||||
Entries = new ObservableCollection<InstalledEntry>(workshopService.GetInstalledEntries().Where(e => e.EntryType == EntryType.Layout));
|
Entries = new ObservableCollection<InstalledEntry>(workshopService.GetInstalledEntries().Where(e => e.EntryType == EntryType.Layout));
|
||||||
|
|
||||||
this.WhenAnyValue(vm => vm.SelectedEntry).Subscribe(ApplyEntry);
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -70,7 +70,7 @@ public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayout
|
|||||||
|
|
||||||
private void ApplyEntry(InstalledEntry? entry)
|
private void ApplyEntry(InstalledEntry? entry)
|
||||||
{
|
{
|
||||||
if (entry == null || Device.LayoutSelection.Parameter == entry.EntryId.ToString())
|
if (entry == null || Device.LayoutSelection.Parameter == entry.Id.ToString())
|
||||||
return;
|
return;
|
||||||
_layoutProvider.ConfigureDevice(Device, entry);
|
_layoutProvider.ConfigureDevice(Device, entry);
|
||||||
Save();
|
Save();
|
||||||
|
|||||||
@ -4,8 +4,10 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
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"
|
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">
|
||||||
<Border Classes="router-container">
|
<Border Classes="router-container">
|
||||||
<Grid RowDefinitions="200,*">
|
<Grid RowDefinitions="200,*">
|
||||||
<Image Grid.Row="0"
|
<Image Grid.Row="0"
|
||||||
@ -40,7 +42,7 @@
|
|||||||
Under Settings > 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.
|
Under Settings > 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.
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<controls:HyperlinkButton Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins?mtm_campaign=artemis&mtm_kwd=home" HorizontalAlignment="Right">
|
<controls:HyperlinkButton Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" HorizontalAlignment="Right" Command="{CompiledBinding GetMorePlugins}">
|
||||||
<controls:HyperlinkButton.ContextMenu>
|
<controls:HyperlinkButton.ContextMenu>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MenuItem Header="Test"></MenuItem>
|
<MenuItem Header="Test"></MenuItem>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Artemis.Core.Services;
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Screens.StartupWizard;
|
using Artemis.UI.Screens.StartupWizard;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
@ -9,12 +10,20 @@ namespace Artemis.UI.Screens.Home;
|
|||||||
|
|
||||||
public class HomeViewModel : RoutableScreen, IMainScreenViewModel
|
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
|
// Show the startup wizard if it hasn't been completed
|
||||||
if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value)
|
if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value)
|
||||||
Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync<StartupWizardViewModel, bool>());
|
Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync<StartupWizardViewModel, bool>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewModelBase? TitleBarViewModel => null;
|
public ViewModelBase? TitleBarViewModel => null;
|
||||||
|
|
||||||
|
public async Task GetMorePlugins()
|
||||||
|
{
|
||||||
|
await _router.Navigate("workshop/entries/plugins");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -12,7 +12,6 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using Avalonia.Threading;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
|
||||||
@ -23,9 +22,9 @@ public class DataBindingViewModel : ActivatableViewModelBase
|
|||||||
private readonly IProfileEditorService _profileEditorService;
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private ObservableAsPropertyHelper<bool>? _dataBindingEnabled;
|
private ObservableAsPropertyHelper<bool>? _dataBindingEnabled;
|
||||||
private bool _editorOpen;
|
|
||||||
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
|
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
|
||||||
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
|
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
|
||||||
|
private bool _editorOpen;
|
||||||
private bool _playing;
|
private bool _playing;
|
||||||
|
|
||||||
public DataBindingViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService, ISettingsService settingsService)
|
public DataBindingViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService, ISettingsService settingsService)
|
||||||
@ -106,6 +105,6 @@ public class DataBindingViewModel : ActivatableViewModelBase
|
|||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
if (!_editorOpen)
|
if (!_editorOpen)
|
||||||
_profileEditorService.SaveProfile();
|
_profileEditorService.SaveProfileAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,8 +15,11 @@ using Artemis.UI.Screens.ProfileEditor.StatusBar;
|
|||||||
using Artemis.UI.Screens.ProfileEditor.VisualEditor;
|
using Artemis.UI.Screens.ProfileEditor.VisualEditor;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
@ -30,10 +33,12 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
|
|||||||
private readonly IProfileService _profileService;
|
private readonly IProfileService _profileService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
private readonly SourceList<IToolViewModel> _tools;
|
private readonly SourceList<IToolViewModel> _tools;
|
||||||
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
|
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
|
||||||
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
|
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
|
||||||
|
|
||||||
[Notify] private ProfileConfiguration? _profileConfiguration;
|
[Notify] private ProfileConfiguration? _profileConfiguration;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -48,12 +53,16 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
|
|||||||
StatusBarViewModel statusBarViewModel,
|
StatusBarViewModel statusBarViewModel,
|
||||||
IEnumerable<IToolViewModel> toolViewModels,
|
IEnumerable<IToolViewModel> toolViewModels,
|
||||||
IMainWindowService mainWindowService,
|
IMainWindowService mainWindowService,
|
||||||
IInputService inputService)
|
IInputService inputService,
|
||||||
|
IWorkshopService workshopService,
|
||||||
|
IWindowService windowService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_profileEditorService = profileEditorService;
|
_profileEditorService = profileEditorService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_mainWindowService = mainWindowService;
|
_mainWindowService = mainWindowService;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
_windowService = windowService;
|
||||||
|
|
||||||
_tools = new SourceList<IToolViewModel>();
|
_tools = new SourceList<IToolViewModel>();
|
||||||
_tools.AddRange(toolViewModels);
|
_tools.AddRange(toolViewModels);
|
||||||
@ -144,7 +153,7 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
|
|||||||
{
|
{
|
||||||
if (!Shared.UI.KeyBindingsEnabled || !_mainWindowService.IsMainWindowFocused)
|
if (!Shared.UI.KeyBindingsEnabled || !_mainWindowService.IsMainWindowFocused)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (e.Modifiers == KeyboardModifierKey.Control && e.Key == KeyboardKey.Z)
|
if (e.Modifiers == KeyboardModifierKey.Control && e.Key == KeyboardKey.Z)
|
||||||
History?.Undo.Execute().Subscribe();
|
History?.Undo.Execute().Subscribe();
|
||||||
else if (e.Modifiers == KeyboardModifierKey.Control && e.Key == KeyboardKey.Y)
|
else if (e.Modifiers == KeyboardModifierKey.Control && e.Key == KeyboardKey.Y)
|
||||||
@ -195,6 +204,23 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the profile is from the workshop, warn the user that auto-updates will be disabled
|
||||||
|
InstalledEntry? workshopEntry = _workshopService.GetInstalledEntryByProfile(profileConfiguration);
|
||||||
|
if (workshopEntry != null && workshopEntry.AutoUpdate)
|
||||||
|
{
|
||||||
|
bool confirmed = await _windowService.ShowConfirmContentDialog(
|
||||||
|
"Editing a workshop profile",
|
||||||
|
"You are about to edit a profile from the workshop, to preserve your changes auto-updating will be disabled.",
|
||||||
|
"Disable auto-update");
|
||||||
|
if (confirmed)
|
||||||
|
_workshopService.SetAutoUpdate(workshopEntry, false);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
args.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
|
await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
|
||||||
ProfileConfiguration = profileConfiguration;
|
ProfileConfiguration = profileConfiguration;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,7 +194,7 @@
|
|||||||
Auto-install updates
|
Auto-install updates
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock Classes="subtitle" TextWrapping="Wrap">
|
<TextBlock Classes="subtitle" TextWrapping="Wrap">
|
||||||
If enabled, new updates will automatically be installed.
|
Automatically install new versions of Artemis in the background when available.
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
@ -202,6 +202,21 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto" IsVisible="{CompiledBinding IsWindows}">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock>
|
||||||
|
Show workshop update notifications
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle" TextWrapping="Wrap">
|
||||||
|
Show a desktop notification whenever workshop updates are installed.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<ToggleSwitch IsChecked="{CompiledBinding WorkshopShowNotifications.Value}" MinWidth="0" OnContent="Yes" OffContent="No" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||||
|
|||||||
@ -102,6 +102,7 @@ public class GeneralTabViewModel : RoutableScreen
|
|||||||
|
|
||||||
public bool IsAutoRunSupported => _autoRunProvider != null;
|
public bool IsAutoRunSupported => _autoRunProvider != null;
|
||||||
public bool IsWindows11 => OSVersionHelper.IsWindows11();
|
public bool IsWindows11 => OSVersionHelper.IsWindows11();
|
||||||
|
public bool IsWindows => OSVersionHelper.IsWindows();
|
||||||
|
|
||||||
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
|
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
|
||||||
public ObservableCollection<string> GraphicsContexts { get; }
|
public ObservableCollection<string> GraphicsContexts { get; }
|
||||||
@ -158,6 +159,7 @@ public class GeneralTabViewModel : RoutableScreen
|
|||||||
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
||||||
public PluginSetting<bool> EnableMica => _settingsService.GetSetting("UI.EnableMica", true);
|
public PluginSetting<bool> EnableMica => _settingsService.GetSetting("UI.EnableMica", true);
|
||||||
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
||||||
|
public PluginSetting<bool> WorkshopShowNotifications => _settingsService.GetSetting("Workshop.ShowNotifications", true);
|
||||||
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
||||||
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
||||||
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
|
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<TextBox Classes="clearButton" Text="{CompiledBinding SearchPluginInput}" Watermark="Search plugins" Margin="0 0 10 0" />
|
<TextBox Classes="clearButton" Text="{CompiledBinding SearchPluginInput}" Watermark="Search plugins" Margin="0 0 10 0" />
|
||||||
|
|
||||||
<StackPanel Spacing="5" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal">
|
<StackPanel Spacing="5" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
<controls:HyperlinkButton VerticalAlignment="Top" NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins?mtm_campaign=artemis&mtm_kwd=plugins">
|
<controls:HyperlinkButton VerticalAlignment="Top" Command="{CompiledBinding GetMorePlugins}">
|
||||||
Get more plugins
|
Get more plugins
|
||||||
</controls:HyperlinkButton>
|
</controls:HyperlinkButton>
|
||||||
<Button Classes="accent" Command="{CompiledBinding ImportPlugin}">Import plugin</Button>
|
<Button Classes="accent" Command="{CompiledBinding ImportPlugin}">Import plugin</Button>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
@ -9,5 +10,4 @@ public partial class PluginsTabView : ReactiveUserControl<PluginsTabViewModel>
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -23,12 +23,14 @@ namespace Artemis.UI.Screens.Settings;
|
|||||||
public partial class PluginsTabViewModel : RoutableScreen
|
public partial class PluginsTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
|
private readonly IRouter _router;
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
[Notify] private string? _searchPluginInput;
|
[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;
|
_pluginManagementService = pluginManagementService;
|
||||||
_notificationService = notificationService;
|
_notificationService = notificationService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
@ -113,4 +115,9 @@ public partial class PluginsTabViewModel : RoutableScreen
|
|||||||
return data => data.Info.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
|
return data => data.Info.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
(data.Info.Description != null && data.Info.Description.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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -59,8 +59,18 @@
|
|||||||
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"
|
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"
|
||||||
Margin="0 15" />
|
Margin="0 15" />
|
||||||
|
|
||||||
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
<avalonia:MaterialIcon IsVisible="{CompiledBinding Entry.IsOfficial}"
|
||||||
|
Kind="ShieldStar"
|
||||||
|
Foreground="{DynamicResource SystemAccentColorLight1}"
|
||||||
|
Margin="2 0 0 0"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip.Tip="Official entry by the Artemis team" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
|
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
|
||||||
|
|
||||||
<!-- Categories -->
|
<!-- Categories -->
|
||||||
|
|||||||
@ -37,11 +37,23 @@
|
|||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
|
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||||
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
<TextBlock Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
|
||||||
<Run Classes="subtitle">by</Run>
|
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
<Run Classes="subtitle">by</Run>
|
||||||
</TextBlock>
|
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
</TextBlock>
|
||||||
|
<avalonia:MaterialIcon
|
||||||
|
IsVisible="{CompiledBinding Entry.IsOfficial}"
|
||||||
|
Kind="ShieldStar"
|
||||||
|
Foreground="{DynamicResource SystemAccentColorLight1}"
|
||||||
|
Margin="2 -2 0 0"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip.Tip="Official entry by the Artemis team" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
Classes="subtitle"
|
Classes="subtitle"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
@ -75,15 +87,15 @@
|
|||||||
<Run>downloads</Run>
|
<Run>downloads</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Install state -->
|
<!-- Install state -->
|
||||||
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
|
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
|
||||||
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding !UpdateAvailable}">
|
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding !UpdateAvailable}">
|
||||||
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
|
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20" />
|
||||||
<Run>installed</Run>
|
<Run>installed</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
|
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
|
||||||
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
|
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20" />
|
||||||
<Run>update available</Run>
|
<Run>update available</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@ -15,8 +15,17 @@
|
|||||||
</Styles>
|
</Styles>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto" HorizontalAlignment="Stretch" MaxWidth="1330">
|
||||||
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top" Width="300" IsVisible="{CompiledBinding ShowCategoryFilter}">
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0" Grid.RowSpan="3"
|
||||||
|
Margin="0 0 10 0"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Width="300"
|
||||||
|
IsVisible="{CompiledBinding ShowCategoryFilter}">
|
||||||
<Border Classes="card" VerticalAlignment="Stretch">
|
<Border Classes="card" VerticalAlignment="Stretch">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||||
@ -27,7 +36,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
||||||
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
|
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}"/>
|
||||||
|
|
||||||
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" Offset="{CompiledBinding ScrollOffset}">
|
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" Offset="{CompiledBinding ScrollOffset}">
|
||||||
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||||
@ -38,7 +47,7 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 8"></ContentControl>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -101,20 +101,35 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
|
|||||||
return;
|
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();
|
_cts = new CancellationTokenSource();
|
||||||
InstallProgress = 0;
|
InstallProgress = 0;
|
||||||
InstallationInProgress = true;
|
InstallationInProgress = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token);
|
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();
|
_notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
|
||||||
InstallationInProgress = false;
|
InstallationInProgress = false;
|
||||||
await Manage();
|
await Manage();
|
||||||
}
|
}
|
||||||
else if (!_cts.IsCancellationRequested)
|
else if (!_cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
_notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show();
|
_notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -156,7 +171,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
|
|||||||
await UninstallPluginPrerequisites(installedEntry);
|
await UninstallPluginPrerequisites(installedEntry);
|
||||||
|
|
||||||
await _workshopService.UninstallEntry(installedEntry, CancellationToken.None);
|
await _workshopService.UninstallEntry(installedEntry, CancellationToken.None);
|
||||||
|
|
||||||
_notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show();
|
_notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
||||||
x:DataType="layout:LayoutDetailsViewModel">
|
x:DataType="layout:LayoutDetailsViewModel">
|
||||||
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*" ColumnDefinitions="300,*,300" MaxWidth="1600" HorizontalAlignment="Stretch">
|
||||||
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
||||||
<Border Classes="card" VerticalAlignment="Top">
|
<Border Classes="card" VerticalAlignment="Top">
|
||||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListDefaultView"
|
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListDefaultView"
|
||||||
x:DataType="layout:LayoutListDefaultViewModel">
|
x:DataType="layout:LayoutListDefaultViewModel">
|
||||||
<Grid ColumnDefinitions="400,*">
|
<Grid ColumnDefinitions="400,*" MaxWidth="1420">
|
||||||
<Border Grid.Column="0" Classes="card" Margin="0 0 10 0" VerticalAlignment="Top">
|
<Border Grid.Column="0" Classes="card" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
|
|
||||||
@ -6,7 +5,6 @@ namespace Artemis.UI.Screens.Workshop.Layout;
|
|||||||
|
|
||||||
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
|
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
|
||||||
{
|
{
|
||||||
private readonly EntryListViewModel _entryListViewModel;
|
|
||||||
public override RoutableScreen DefaultScreen { get; }
|
public override RoutableScreen DefaultScreen { get; }
|
||||||
|
|
||||||
public LayoutListViewModel(LayoutListDefaultViewModel defaultViewModel)
|
public LayoutListViewModel(LayoutListDefaultViewModel defaultViewModel)
|
||||||
|
|||||||
@ -19,13 +19,14 @@ namespace Artemis.UI.Screens.Workshop.LayoutFinder;
|
|||||||
public partial class LayoutFinderViewModel : ActivatableViewModelBase
|
public partial class LayoutFinderViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly SourceList<IRGBDeviceInfo> _devices;
|
|
||||||
[Notify] private ReadOnlyObservableCollection<LayoutFinderDeviceViewModel> _deviceViewModels;
|
[Notify] private ReadOnlyObservableCollection<LayoutFinderDeviceViewModel> _deviceViewModels;
|
||||||
|
|
||||||
public LayoutFinderViewModel(ILogger logger, IDeviceService deviceService, Func<ArtemisDevice, LayoutFinderDeviceViewModel> getDeviceViewModel)
|
public LayoutFinderViewModel(ILogger logger, IDeviceService deviceService, Func<ArtemisDevice, LayoutFinderDeviceViewModel> getDeviceViewModel)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
SearchAll = ReactiveCommand.CreateFromTask(ExecuteSearchAll);
|
SearchAll = ReactiveCommand.CreateFromTask(ExecuteSearchAll);
|
||||||
|
DeviceViewModels = new ReadOnlyObservableCollection<LayoutFinderDeviceViewModel>([]);
|
||||||
|
|
||||||
this.WhenActivated((CompositeDisposable _) =>
|
this.WhenActivated((CompositeDisposable _) =>
|
||||||
{
|
{
|
||||||
IEnumerable<LayoutFinderDeviceViewModel> deviceGroups = deviceService.EnabledDevices.Select(getDeviceViewModel);
|
IEnumerable<LayoutFinderDeviceViewModel> deviceGroups = deviceService.EnabledDevices.Select(getDeviceViewModel);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
|
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:converters="clr-namespace:Artemis.UI.Converters"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
@ -13,47 +13,94 @@
|
|||||||
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Button MinHeight="65"
|
<Button MinHeight="110"
|
||||||
MaxHeight="110"
|
MaxHeight="140"
|
||||||
Padding="6"
|
Padding="12"
|
||||||
Margin="0 0 0 5"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
Command="{CompiledBinding ViewWorkshopPage}">
|
Command="{CompiledBinding ViewWorkshopPage}">
|
||||||
<Grid ColumnDefinitions="Auto,2*,*,*,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto" RowDefinitions="*, Auto">
|
||||||
|
<!-- Icon -->
|
||||||
<Border Grid.Column="0"
|
<Border Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0 0 10 0"
|
Margin="0 0 10 0"
|
||||||
Width="50"
|
Width="80"
|
||||||
Height="50"
|
Height="80"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding InstalledEntry.EntryId, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
<!-- Body -->
|
||||||
<TextBlock TextTrimming="CharacterEllipsis"
|
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||||
Text="{CompiledBinding InstalledEntry.Name, FallbackValue=Title}" />
|
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||||
<TextBlock Classes="subtitle"
|
<TextBlock Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
|
||||||
|
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
|
<Run Classes="subtitle">by</Run>
|
||||||
|
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
</TextBlock>
|
||||||
|
<avalonia:MaterialIcon
|
||||||
|
IsVisible="{CompiledBinding Entry.IsOfficial}"
|
||||||
|
Kind="ShieldStar"
|
||||||
|
Foreground="{DynamicResource SystemAccentColorLight1}"
|
||||||
|
Margin="2 -2 0 0"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip.Tip="Official entry by the Artemis team" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Classes="subtitle"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
Text="{CompiledBinding InstalledEntry.Author, FallbackValue=Summary}">
|
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}">
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"></StackPanel>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||||
|
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Info -->
|
||||||
|
<StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
|
||||||
|
<TextBlock TextAlignment="Right">
|
||||||
|
<avalonia:MaterialIcon Kind="Harddisk" />
|
||||||
|
<Run Text="{CompiledBinding Entry.ReleaseVersion}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.EntryType}"></TextBlock>
|
<!-- Install state -->
|
||||||
<TextBlock Grid.Column="3" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.ReleaseVersion}"></TextBlock>
|
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom">
|
||||||
<TextBlock Grid.Column="4" VerticalAlignment="Center">
|
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
|
||||||
<Run>Installed</Run>
|
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20" />
|
||||||
<Run Text="{CompiledBinding InstalledEntry.InstalledAt, FallbackValue=01-01-1337, Mode=OneWay, Converter={StaticResource DateTimeConverter}}" />
|
<Run>update available</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<StackPanel Grid.Column="5" VerticalAlignment="Center" Orientation="Horizontal" Spacing="6">
|
|
||||||
<Button Command="{CompiledBinding ViewLocal}">Open</Button>
|
|
||||||
<Button Command="{CompiledBinding Uninstall}" Theme="{StaticResource TransparentButton}" Height="32">
|
|
||||||
<avalonia:MaterialIcon Kind="Trash"/>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Management -->
|
||||||
|
<Border Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<Button Command="{CompiledBinding ViewLocal}" HorizontalAlignment="Stretch" >Open</Button>
|
||||||
|
<Button Command="{CompiledBinding Uninstall}" HorizontalAlignment="Stretch">Uninstall</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox MinHeight="26" Margin="0 4 0 0" IsChecked="{CompiledBinding AutoUpdate}">Auto-update</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Button>
|
</Button>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,10 +1,8 @@
|
|||||||
using Avalonia;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||||
|
|
||||||
public partial class InstalledTabItemView : UserControl
|
public partial class InstalledTabItemView : ReactiveUserControl<InstalledTabItemViewModel>
|
||||||
{
|
{
|
||||||
public InstalledTabItemView()
|
public InstalledTabItemView()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.DryIoc.Factories;
|
using Artemis.UI.DryIoc.Factories;
|
||||||
|
using Artemis.UI.Extensions;
|
||||||
using Artemis.UI.Screens.Plugins;
|
using Artemis.UI.Screens.Plugins;
|
||||||
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
@ -15,70 +19,97 @@ using Artemis.WebClient.Workshop.Models;
|
|||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
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 IWorkshopService _workshopService;
|
||||||
|
private readonly IWorkshopUpdateService _workshopUpdateService;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
private readonly ISettingsVmFactory _settingsVmFactory;
|
private readonly ISettingsVmFactory _settingsVmFactory;
|
||||||
|
|
||||||
public InstalledTabItemViewModel(InstalledEntry installedEntry,
|
[Notify] private bool _updateAvailable;
|
||||||
|
[Notify] private bool _autoUpdate;
|
||||||
|
|
||||||
|
public InstalledTabItemViewModel(InstalledEntry entry,
|
||||||
|
IWorkshopClient client,
|
||||||
IWorkshopService workshopService,
|
IWorkshopService workshopService,
|
||||||
IRouter router,
|
IWorkshopUpdateService workshopUpdateService,
|
||||||
|
IRouter router,
|
||||||
IWindowService windowService,
|
IWindowService windowService,
|
||||||
IPluginManagementService pluginManagementService,
|
IPluginManagementService pluginManagementService,
|
||||||
ISettingsVmFactory settingsVmFactory)
|
ISettingsVmFactory settingsVmFactory)
|
||||||
{
|
{
|
||||||
|
_client = client;
|
||||||
_workshopService = workshopService;
|
_workshopService = workshopService;
|
||||||
|
_workshopUpdateService = workshopUpdateService;
|
||||||
_router = router;
|
_router = router;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_pluginManagementService = pluginManagementService;
|
_pluginManagementService = pluginManagementService;
|
||||||
_settingsVmFactory = settingsVmFactory;
|
_settingsVmFactory = settingsVmFactory;
|
||||||
InstalledEntry = installedEntry;
|
_autoUpdate = entry.AutoUpdate;
|
||||||
|
|
||||||
|
Entry = entry;
|
||||||
|
|
||||||
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
|
this.WhenActivatedAsync(async _ =>
|
||||||
ViewLocal = ReactiveCommand.CreateFromTask(ExecuteViewLocal);
|
|
||||||
Uninstall = ReactiveCommand.CreateFromTask(ExecuteUninstall);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstalledEntry InstalledEntry { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
|
|
||||||
public ReactiveCommand<Unit,Unit> ViewLocal { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> 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))
|
|
||||||
{
|
{
|
||||||
await _router.Navigate($"profile-editor/{profileId}");
|
// Grab the latest entry summary from the workshop
|
||||||
}
|
try
|
||||||
|
{
|
||||||
|
IOperationResult<IGetEntrySummaryByIdResult> 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.");
|
bool confirmed = await _windowService.ShowConfirmContentDialog("Do you want to uninstall this entry?", "Both the entry and its contents will be removed.");
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Ideally the installation handler does this but it doesn't have access to the required view models
|
// 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 UninstallPluginPrerequisites();
|
||||||
|
|
||||||
await _workshopService.UninstallEntry(InstalledEntry, cancellationToken);
|
await _workshopService.UninstallEntry(Entry, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UninstallPluginPrerequisites()
|
private async Task UninstallPluginPrerequisites()
|
||||||
{
|
{
|
||||||
if (!InstalledEntry.TryGetMetadata("PluginId", out Guid pluginId))
|
if (!Entry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||||
return;
|
return;
|
||||||
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||||
if (plugin == null)
|
if (plugin == null)
|
||||||
@ -87,4 +118,18 @@ public partial class InstalledTabItemViewModel : ViewModelBase
|
|||||||
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
|
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
|
||||||
await pluginViewModel.ExecuteRemovePrerequisites(true);
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -15,8 +15,16 @@
|
|||||||
</Styles>
|
</Styles>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Panel>
|
<Grid RowDefinitions="Auto,*">
|
||||||
<StackPanel IsVisible="{CompiledBinding Empty}" Margin="0 50 0 0" Classes="empty-state">
|
<Grid Grid.Row="0" Grid.Column="0" MaxWidth="1000" Margin="0 22 0 10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition MinWidth="165" MaxWidth="400" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Classes="search-box" Text="{CompiledBinding SearchEntryInput}" Watermark="Search library" Margin="0 0 10 0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" IsVisible="{CompiledBinding Empty}" Margin="0 50 0 0" Classes="empty-state">
|
||||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Not much here yet, huh!</TextBlock>
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Not much here yet, huh!</TextBlock>
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
<Run>Any entries you download from the workshop you can later manage here</Run>
|
<Run>Any entries you download from the workshop you can later manage here</Run>
|
||||||
@ -24,21 +32,34 @@
|
|||||||
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||||
<Button HorizontalAlignment="Center" Command="{CompiledBinding OpenWorkshop}">Browse the Workshop</Button>
|
<Button HorizontalAlignment="Center" Command="{CompiledBinding OpenWorkshop}">Browse the Workshop</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ScrollViewer IsVisible="{CompiledBinding !Empty}">
|
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
|
||||||
<ItemsControl ItemsSource="{CompiledBinding InstalledEntries}">
|
<ItemsControl ItemsSource="{CompiledBinding EntryGroups}" MaxWidth="1000">
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<ContentControl Content="{CompiledBinding}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<VirtualizingStackPanel />
|
<StackPanel Spacing="10"/>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}" Text="{Binding Key}" Margin="0 0 0 5"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding Items}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ContentControl Content="{Binding}" Margin="0 0 0 8" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Panel>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,14 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
|
using DynamicData.List;
|
||||||
|
using Humanizer;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
@ -16,45 +18,45 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
|||||||
|
|
||||||
public partial class InstalledTabViewModel : RoutableScreen
|
public partial class InstalledTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private SourceList<InstalledEntry> _installedEntries = new();
|
private SourceList<InstalledEntry> _entries = new();
|
||||||
|
|
||||||
[Notify] private string? _searchEntryInput;
|
[Notify] private string? _searchEntryInput;
|
||||||
|
private readonly ObservableAsPropertyHelper<bool> _empty;
|
||||||
|
|
||||||
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
|
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
|
||||||
{
|
{
|
||||||
IObservable<Func<InstalledEntry, bool>> pluginFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
|
IObservable<Func<InstalledEntry, bool>> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
|
||||||
|
|
||||||
_installedEntries.Connect()
|
_entries.Connect()
|
||||||
.Filter(pluginFilter)
|
.Filter(searchFilter)
|
||||||
.Sort(SortExpressionComparer<InstalledEntry>.Descending(p => p.InstalledAt))
|
.Sort(SortExpressionComparer<InstalledEntry>.Descending(p => p.InstalledAt))
|
||||||
.Transform(getInstalledTabItemViewModel)
|
.Transform(getInstalledTabItemViewModel)
|
||||||
.Bind(out ReadOnlyObservableCollection<InstalledTabItemViewModel> installedEntryViewModels)
|
.GroupWithImmutableState(vm => vm.Entry.EntryType.Humanize(LetterCasing.Title).Pluralize())
|
||||||
|
.Bind(out ReadOnlyObservableCollection<IGrouping<InstalledTabItemViewModel, string>> entryViewModels)
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
|
_empty = _entries.Connect().Count().Select(c => c == 0).ToProperty(this, vm => vm.Empty);
|
||||||
|
_entries.AddRange(workshopService.GetInstalledEntries());
|
||||||
|
|
||||||
List<InstalledEntry> entries = workshopService.GetInstalledEntries();
|
EntryGroups = entryViewModels;
|
||||||
_installedEntries.AddRange(entries);
|
|
||||||
|
|
||||||
Empty = entries.Count == 0;
|
|
||||||
InstalledEntries = installedEntryViewModels;
|
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
|
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
|
||||||
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
|
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
|
OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
|
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
|
||||||
{
|
{
|
||||||
_installedEntries.Remove(e);
|
_entries.Remove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Empty { get; }
|
public bool Empty => _empty.Value;
|
||||||
public ReactiveCommand<Unit, Unit> OpenWorkshop { get; }
|
public ReactiveCommand<Unit, Unit> OpenWorkshop { get; }
|
||||||
public ReadOnlyObservableCollection<InstalledTabItemViewModel> InstalledEntries { get; }
|
public ReadOnlyObservableCollection<IGrouping<InstalledTabItemViewModel, string>> EntryGroups { get; }
|
||||||
|
|
||||||
private Func<InstalledEntry, bool> CreatePredicate(string? text)
|
private Func<InstalledEntry, bool> CreatePredicate(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
@ -3,41 +3,44 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
|
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
|
||||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabItemView"
|
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabItemView"
|
||||||
x:DataType="tabs:SubmissionsTabItemViewModel">
|
x:DataType="tabs:SubmissionsTabItemViewModel">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Button MinHeight="80"
|
<Button MinHeight="110"
|
||||||
MaxHeight="110"
|
MaxHeight="140"
|
||||||
Padding="12 6"
|
Padding="12"
|
||||||
Margin="0 0 0 5"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
Command="{CompiledBinding NavigateToEntry}">
|
Command="{CompiledBinding NavigateToEntry}"
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*, Auto">
|
||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<Border Grid.Column="0"
|
<Border Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0 0 10 0"
|
Margin="0 0 10 0"
|
||||||
Width="50"
|
Width="80"
|
||||||
Height="50"
|
Height="80"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
|
||||||
Classes="h5 no-margin"
|
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
TextTrimming="CharacterEllipsis"
|
<Run Classes="subtitle">by you</Run>
|
||||||
Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
<Run Text="{CompiledBinding Emoji}" />
|
||||||
|
</TextBlock>
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
Classes="subtitle"
|
Classes="subtitle"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
@ -54,7 +57,7 @@
|
|||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<avalonia1:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia1:MaterialIcon>
|
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||||
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@ -63,10 +66,10 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<StackPanel Grid.Column="2" Margin="0 0 4 0">
|
<StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
|
||||||
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
|
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
|
||||||
<TextBlock TextAlignment="Right">
|
<TextBlock TextAlignment="Right">
|
||||||
<avalonia1:MaterialIcon Kind="Downloads" />
|
<avalonia:MaterialIcon Kind="Downloads" />
|
||||||
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
|
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
|
||||||
<Run>downloads</Run>
|
<Run>downloads</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -10,6 +11,7 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
|||||||
|
|
||||||
public class SubmissionsTabItemViewModel : ViewModelBase
|
public class SubmissionsTabItemViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private static readonly string[] Emojis = ["❤️", "🧡", "💛", "💚", "💙", "💜", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "😍", "🥰"];
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
|
|
||||||
public SubmissionsTabItemViewModel(IGetSubmittedEntries_SubmittedEntries entry, IRouter router)
|
public SubmissionsTabItemViewModel(IGetSubmittedEntries_SubmittedEntries entry, IRouter router)
|
||||||
@ -18,9 +20,11 @@ public class SubmissionsTabItemViewModel : ViewModelBase
|
|||||||
Entry = entry;
|
Entry = entry;
|
||||||
|
|
||||||
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
|
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
|
||||||
|
Emoji = Emojis[Random.Shared.Next(0, Emojis.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IGetSubmittedEntries_SubmittedEntries Entry { get; }
|
public IGetSubmittedEntries_SubmittedEntries Entry { get; }
|
||||||
|
public string Emoji { get; }
|
||||||
public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
|
public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
|
||||||
|
|
||||||
private async Task ExecuteNavigateToEntry(CancellationToken cancellationToken)
|
private async Task ExecuteNavigateToEntry(CancellationToken cancellationToken)
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView"
|
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView"
|
||||||
x:DataType="tabs:SubmissionsTabViewModel">
|
x:DataType="tabs:SubmissionsTabViewModel">
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
<Style Selector="StackPanel.empty-state > TextBlock">
|
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||||
@ -26,8 +25,16 @@
|
|||||||
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}">Log in</Button>
|
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}">Log in</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Panel IsVisible="{CompiledBinding IsLoggedIn^}">
|
<Grid IsVisible="{CompiledBinding IsLoggedIn^}" RowDefinitions="Auto,*">
|
||||||
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
<Grid Grid.Row="0" Grid.Column="0" MaxWidth="1000" Margin="0 22 0 10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition MinWidth="165" MaxWidth="400" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Classes="search-box" Text="{CompiledBinding SearchEntryInput}" Watermark="Search submissions" Margin="0 0 10 0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" IsVisible="{CompiledBinding Empty}" Margin="0 50 0 0" Classes="empty-state">
|
||||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Oh boy, it's empty here 🤔</TextBlock>
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Oh boy, it's empty here 🤔</TextBlock>
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
<Run>Any entries you submit to the workshop you can later manage here</Run>
|
<Run>Any entries you submit to the workshop you can later manage here</Run>
|
||||||
@ -36,25 +43,35 @@
|
|||||||
<Button HorizontalAlignment="Center" Command="{CompiledBinding AddSubmission}">Submit new entry</Button>
|
<Button HorizontalAlignment="Center" Command="{CompiledBinding AddSubmission}">Submit new entry</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
|
||||||
<Button Grid.Row="0" Margin="0 0 0 15" HorizontalAlignment="Right" Command="{CompiledBinding AddSubmission}">Submit new entry</Button>
|
<ItemsControl ItemsSource="{CompiledBinding EntryGroups}" MaxWidth="1000">
|
||||||
<ScrollViewer Grid.Row="1" IsVisible="{CompiledBinding Entries.Count}">
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsControl ItemsSource="{CompiledBinding Entries}">
|
<ItemsPanelTemplate>
|
||||||
<ItemsControl.ItemTemplate>
|
<StackPanel Spacing="10"/>
|
||||||
<DataTemplate>
|
</ItemsPanelTemplate>
|
||||||
<ContentControl Content="{CompiledBinding}" />
|
</ItemsControl.ItemsPanel>
|
||||||
</DataTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
<DataTemplate>
|
||||||
<ItemsControl.ItemsPanel>
|
<StackPanel>
|
||||||
<ItemsPanelTemplate>
|
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}" Text="{Binding Key}" Margin="0 0 0 5"/>
|
||||||
<VirtualizingStackPanel />
|
<ItemsControl ItemsSource="{Binding Items}">
|
||||||
</ItemsPanelTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
<DataTemplate>
|
||||||
</ItemsControl>
|
<ContentControl Content="{Binding}" Margin="0 0 0 8" />
|
||||||
</ScrollViewer>
|
</DataTemplate>
|
||||||
</Grid>
|
</ItemsControl.ItemTemplate>
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
</Panel>
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Extensions;
|
using Artemis.UI.Extensions;
|
||||||
@ -11,6 +12,9 @@ using Artemis.UI.Shared.Services;
|
|||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
|
using DynamicData.Aggregation;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using Humanizer;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
@ -22,28 +26,37 @@ public partial class SubmissionsTabViewModel : RoutableScreen
|
|||||||
private readonly IWorkshopClient _client;
|
private readonly IWorkshopClient _client;
|
||||||
private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, long> _entries;
|
private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, long> _entries;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
|
private readonly ObservableAsPropertyHelper<bool> _empty;
|
||||||
|
|
||||||
[Notify] private bool _isLoading = true;
|
[Notify] private bool _isLoading = true;
|
||||||
[Notify] private bool _workshopReachable;
|
[Notify] private bool _workshopReachable;
|
||||||
|
[Notify] private string? _searchEntryInput;
|
||||||
|
|
||||||
public SubmissionsTabViewModel(IWorkshopClient client,
|
public SubmissionsTabViewModel(IWorkshopClient client,
|
||||||
IAuthenticationService authenticationService,
|
IAuthenticationService authenticationService,
|
||||||
IWindowService windowService,
|
IWindowService windowService,
|
||||||
IWorkshopService workshopService,
|
IWorkshopService workshopService,
|
||||||
Func<IGetSubmittedEntries_SubmittedEntries, SubmissionsTabItemViewModel> getSubmissionsTabItemViewModel)
|
Func<IGetSubmittedEntries_SubmittedEntries, SubmissionsTabItemViewModel> getSubmissionsTabItemViewModel)
|
||||||
{
|
{
|
||||||
|
IObservable<Func<IGetSubmittedEntries_SubmittedEntries, bool>> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
|
||||||
|
|
||||||
_client = client;
|
_client = client;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_entries = new SourceCache<IGetSubmittedEntries_SubmittedEntries, long>(e => e.Id);
|
_entries = new SourceCache<IGetSubmittedEntries_SubmittedEntries, long>(e => e.Id);
|
||||||
_entries.Connect()
|
_entries.Connect()
|
||||||
|
.Filter(searchFilter)
|
||||||
|
.Sort(SortExpressionComparer<IGetSubmittedEntries_SubmittedEntries>.Descending(p => p.CreatedAt))
|
||||||
.Transform(getSubmissionsTabItemViewModel)
|
.Transform(getSubmissionsTabItemViewModel)
|
||||||
.Bind(out ReadOnlyObservableCollection<SubmissionsTabItemViewModel> entries)
|
.GroupWithImmutableState(vm => vm.Entry.EntryType.Humanize(LetterCasing.Title).Pluralize())
|
||||||
|
.Bind(out ReadOnlyObservableCollection<IGrouping<SubmissionsTabItemViewModel, long, string>> entries)
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
|
_empty = _entries.Connect().Count().Select(c => c == 0).ToProperty(this, vm => vm.Empty);
|
||||||
|
|
||||||
AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable));
|
AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable));
|
||||||
Login = ReactiveCommand.CreateFromTask(ExecuteLogin, this.WhenAnyValue(vm => vm.WorkshopReachable));
|
Login = ReactiveCommand.CreateFromTask(ExecuteLogin, this.WhenAnyValue(vm => vm.WorkshopReachable));
|
||||||
|
|
||||||
IsLoggedIn = authenticationService.IsLoggedIn;
|
IsLoggedIn = authenticationService.IsLoggedIn;
|
||||||
Entries = entries;
|
EntryGroups = entries;
|
||||||
|
|
||||||
this.WhenActivatedAsync(async d =>
|
this.WhenActivatedAsync(async d =>
|
||||||
{
|
{
|
||||||
@ -53,10 +66,11 @@ public partial class SubmissionsTabViewModel : RoutableScreen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Empty => _empty.Value;
|
||||||
public ReactiveCommand<Unit, Unit> Login { get; }
|
public ReactiveCommand<Unit, Unit> Login { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddSubmission { get; }
|
public ReactiveCommand<Unit, Unit> AddSubmission { get; }
|
||||||
public IObservable<bool> IsLoggedIn { get; }
|
public IObservable<bool> IsLoggedIn { get; }
|
||||||
public ReadOnlyObservableCollection<SubmissionsTabItemViewModel> Entries { get; }
|
public ReadOnlyObservableCollection<IGrouping<SubmissionsTabItemViewModel, long, string>> EntryGroups { get; }
|
||||||
|
|
||||||
private async Task ExecuteLogin(CancellationToken ct)
|
private async Task ExecuteLogin(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -91,4 +105,12 @@ public partial class SubmissionsTabViewModel : RoutableScreen
|
|||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Func<IGetSubmittedEntries_SubmittedEntries, bool> CreatePredicate(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return _ => true;
|
||||||
|
|
||||||
|
return data => data.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<ItemsControl ItemsSource="{CompiledBinding Dependants}">
|
<ItemsControl ItemsSource="{CompiledBinding Dependants}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel Spacing="5"></StackPanel>
|
<StackPanel Spacing="8"></StackPanel>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
|
||||||
x:DataType="plugins:PluginDetailsViewModel">
|
x:DataType="plugins:PluginDetailsViewModel">
|
||||||
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*">
|
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*" MaxWidth="1600" HorizontalAlignment="Stretch">
|
||||||
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
||||||
<Border Classes="card" VerticalAlignment="Top">
|
<Border Classes="card" VerticalAlignment="Top">
|
||||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
<ItemsControl ItemsSource="{CompiledBinding Dependencies}">
|
<ItemsControl ItemsSource="{CompiledBinding Dependencies}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel Spacing="5"></StackPanel>
|
<StackPanel Spacing="8"></StackPanel>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
||||||
x:DataType="profile:ProfileDetailsViewModel">
|
x:DataType="profile:ProfileDetailsViewModel">
|
||||||
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*">
|
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*" MaxWidth="1600" HorizontalAlignment="Stretch">
|
||||||
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
||||||
<Border Classes="card" VerticalAlignment="Top">
|
<Border Classes="card" VerticalAlignment="Top">
|
||||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
|
|||||||
25
src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs
Normal file
25
src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Services.Interfaces;
|
||||||
|
|
||||||
|
public interface IWorkshopUpdateService : IArtemisUIService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Automatically updates all installed entries that have auto-update enabled and have a new version available.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation</returns>
|
||||||
|
Task AutoUpdateEntries();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Automatically updates the provided entry if a new version is available.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The entry to update.</param>
|
||||||
|
/// <returns>A task of <see langword="true"/> if the entry was updated, <see langword="false"/> otherwise.</returns>
|
||||||
|
Task<bool> AutoUpdateEntry(InstalledEntry entry);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable workshop update notifications.
|
||||||
|
/// </summary>
|
||||||
|
void DisableNotifications();
|
||||||
|
}
|
||||||
@ -57,6 +57,18 @@ public class BasicUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
await _router.Navigate("settings/releases");
|
await _router.Navigate("settings/releases");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ShowWorkshopNotification(int updatedEntries)
|
||||||
|
{
|
||||||
|
_notificationService.CreateNotification()
|
||||||
|
.WithTitle(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed")
|
||||||
|
.WithMessage(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed")
|
||||||
|
.WithSeverity(NotificationSeverity.Success)
|
||||||
|
.WithTimeout(TimeSpan.FromSeconds(15))
|
||||||
|
.HavingButton(b => b.WithText("View library").WithAction(async () => await _router.Navigate("settings/workshop")))
|
||||||
|
.Show();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ShowNotification(Guid releaseId, string releaseVersion)
|
public void ShowNotification(Guid releaseId, string releaseVersion)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ namespace Artemis.UI.Services.Updating;
|
|||||||
|
|
||||||
public interface IUpdateNotificationProvider
|
public interface IUpdateNotificationProvider
|
||||||
{
|
{
|
||||||
|
void ShowWorkshopNotification(int updatedEntries);
|
||||||
void ShowNotification(Guid releaseId, string releaseVersion);
|
void ShowNotification(Guid releaseId, string releaseVersion);
|
||||||
void ShowInstalledNotification(string installedVersion);
|
void ShowInstalledNotification(string installedVersion);
|
||||||
}
|
}
|
||||||
@ -8,8 +8,10 @@ using Artemis.Core.Services;
|
|||||||
using Artemis.Storage.Repositories;
|
using Artemis.Storage.Repositories;
|
||||||
using Artemis.Storage.Repositories.Interfaces;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using Artemis.UI.Exceptions;
|
using Artemis.UI.Exceptions;
|
||||||
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Artemis.WebClient.Updating;
|
using Artemis.WebClient.Updating;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
@ -26,6 +28,7 @@ public class UpdateService : IUpdateService
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
private readonly IReleaseRepository _releaseRepository;
|
private readonly IReleaseRepository _releaseRepository;
|
||||||
|
private readonly IWorkshopUpdateService _workshopUpdateService;
|
||||||
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||||
private readonly Platform _updatePlatform;
|
private readonly Platform _updatePlatform;
|
||||||
private readonly IUpdatingClient _updatingClient;
|
private readonly IUpdatingClient _updatingClient;
|
||||||
@ -38,6 +41,7 @@ public class UpdateService : IUpdateService
|
|||||||
IMainWindowService mainWindowService,
|
IMainWindowService mainWindowService,
|
||||||
IUpdatingClient updatingClient,
|
IUpdatingClient updatingClient,
|
||||||
IReleaseRepository releaseRepository,
|
IReleaseRepository releaseRepository,
|
||||||
|
IWorkshopUpdateService workshopUpdateService,
|
||||||
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
||||||
Func<Guid, ReleaseInstaller> getReleaseInstaller)
|
Func<Guid, ReleaseInstaller> getReleaseInstaller)
|
||||||
{
|
{
|
||||||
@ -45,6 +49,7 @@ public class UpdateService : IUpdateService
|
|||||||
_mainWindowService = mainWindowService;
|
_mainWindowService = mainWindowService;
|
||||||
_updatingClient = updatingClient;
|
_updatingClient = updatingClient;
|
||||||
_releaseRepository = releaseRepository;
|
_releaseRepository = releaseRepository;
|
||||||
|
_workshopUpdateService = workshopUpdateService;
|
||||||
_updateNotificationProvider = updateNotificationProvider;
|
_updateNotificationProvider = updateNotificationProvider;
|
||||||
_getReleaseInstaller = getReleaseInstaller;
|
_getReleaseInstaller = getReleaseInstaller;
|
||||||
|
|
||||||
@ -65,72 +70,7 @@ public class UpdateService : IUpdateService
|
|||||||
timer.Elapsed += HandleAutoUpdateEvent;
|
timer.Elapsed += HandleAutoUpdateEvent;
|
||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessReleaseStatus()
|
|
||||||
{
|
|
||||||
string currentVersion = Constants.CurrentVersion;
|
|
||||||
bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion);
|
|
||||||
PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version;
|
|
||||||
|
|
||||||
if (!Directory.Exists(Constants.UpdatingFolder))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Clean up the update folder, leaving only the last ZIP
|
|
||||||
foreach (string file in Directory.GetFiles(Constants.UpdatingFolder))
|
|
||||||
{
|
|
||||||
if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.Debug("Cleaning up old update file at {FilePath}", file);
|
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.Warning(e, "Failed to clean up old update file at {FilePath}", file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated)
|
|
||||||
_updateNotificationProvider.Value.ShowInstalledNotification(currentVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
|
|
||||||
{
|
|
||||||
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
|
|
||||||
{
|
|
||||||
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
|
|
||||||
await installer.InstallAsync(CancellationToken.None);
|
|
||||||
RestartForUpdate("AutoInstallUpdate", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (Constants.CurrentVersion == "local")
|
|
||||||
return;
|
|
||||||
|
|
||||||
// The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes
|
|
||||||
if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59))
|
|
||||||
return;
|
|
||||||
_lastAutoUpdateCheck = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (!_autoCheck.Value || _suspendAutoCheck)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await CheckForUpdate();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Warning(ex, "Auto update-check failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Channel { get; private set; } = "master";
|
public string Channel { get; private set; } = "master";
|
||||||
|
|
||||||
@ -139,7 +79,7 @@ public class UpdateService : IUpdateService
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
|
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CacheLatestRelease()
|
public async Task CacheLatestRelease()
|
||||||
{
|
{
|
||||||
@ -257,4 +197,86 @@ public class UpdateService : IUpdateService
|
|||||||
_logger.Information("Update service initialized for {Channel} channel", Channel);
|
_logger.Information("Update service initialized for {Channel} channel", Channel);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> AutoCheckForUpdates()
|
||||||
|
{
|
||||||
|
// Don't perform auto-updates if the current version is local
|
||||||
|
if (Constants.CurrentVersion == "local")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Don't perform auto-updates if the setting is disabled or an update was found but not yet installed
|
||||||
|
if (!_autoCheck.Value || _suspendAutoCheck)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CheckForUpdate() && _autoInstall.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex, "Auto update-check failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessReleaseStatus()
|
||||||
|
{
|
||||||
|
string currentVersion = Constants.CurrentVersion;
|
||||||
|
bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion);
|
||||||
|
PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version;
|
||||||
|
|
||||||
|
if (!Directory.Exists(Constants.UpdatingFolder))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Clean up the update folder, leaving only the last ZIP
|
||||||
|
foreach (string file in Directory.GetFiles(Constants.UpdatingFolder))
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Debug("Cleaning up old update file at {FilePath}", file);
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warning(e, "Failed to clean up old update file at {FilePath}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated)
|
||||||
|
_updateNotificationProvider.Value.ShowInstalledNotification(currentVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
|
||||||
|
{
|
||||||
|
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
|
||||||
|
{
|
||||||
|
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
|
||||||
|
await installer.InstallAsync(CancellationToken.None);
|
||||||
|
RestartForUpdate("AutoInstallUpdate", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes
|
||||||
|
if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastAutoUpdateCheck = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (await AutoCheckForUpdates())
|
||||||
|
{
|
||||||
|
_logger.Information("Auto-installing update, not performing workshop update check");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _workshopUpdateService.AutoUpdateEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
98
src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs
Normal file
98
src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using Serilog;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Services.Updating;
|
||||||
|
|
||||||
|
public class WorkshopUpdateService : IWorkshopUpdateService
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IWorkshopClient _client;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
|
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||||
|
private readonly PluginSetting<bool> _showNotifications;
|
||||||
|
|
||||||
|
public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService, Lazy<IUpdateNotificationProvider> updateNotificationProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_client = client;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
_updateNotificationProvider = updateNotificationProvider;
|
||||||
|
_showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AutoUpdateEntries()
|
||||||
|
{
|
||||||
|
_logger.Information("Checking for workshop updates");
|
||||||
|
int checkedEntries = 0;
|
||||||
|
int updatedEntries = 0;
|
||||||
|
|
||||||
|
foreach (InstalledEntry entry in _workshopService.GetInstalledEntries())
|
||||||
|
{
|
||||||
|
if (!entry.AutoUpdate)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
checkedEntries++;
|
||||||
|
bool updated = await AutoUpdateEntry(entry);
|
||||||
|
if (updated)
|
||||||
|
updatedEntries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information("Checked {CheckedEntries} entries, updated {UpdatedEntries}", checkedEntries, updatedEntries);
|
||||||
|
|
||||||
|
if (updatedEntries > 0 && _showNotifications.Value)
|
||||||
|
_updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AutoUpdateEntry(InstalledEntry entry)
|
||||||
|
{
|
||||||
|
// Query the latest version
|
||||||
|
IOperationResult<IGetEntryLatestReleaseByIdResult> latestReleaseResult = await _client.GetEntryLatestReleaseById.ExecuteAsync(entry.Id);
|
||||||
|
|
||||||
|
if (latestReleaseResult.Data?.Entry?.LatestRelease is not IRelease latestRelease)
|
||||||
|
return false;
|
||||||
|
if (latestRelease.Id == entry.ReleaseId)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_logger.Information("Auto-updating entry {Entry} to version {Version}", entry, latestRelease.Version);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EntryInstallResult updateResult = await _workshopService.InstallEntry(entry, latestRelease, new Progress<StreamProgress>(), CancellationToken.None);
|
||||||
|
|
||||||
|
// This happens during installation too but not on our reference of the entry
|
||||||
|
if (updateResult.IsSuccess)
|
||||||
|
entry.ApplyRelease(latestRelease);
|
||||||
|
|
||||||
|
if (updateResult.IsSuccess)
|
||||||
|
_logger.Information("Auto-update successful for entry {Entry}", entry);
|
||||||
|
else
|
||||||
|
_logger.Warning("Auto-update failed for entry {Entry}: {Message}", entry, updateResult.Message);
|
||||||
|
|
||||||
|
return updateResult.IsSuccess;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warning(e, "Auto-update failed for entry {Entry}", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void DisableNotifications()
|
||||||
|
{
|
||||||
|
_showNotifications.Value = false;
|
||||||
|
_showNotifications.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,4 +26,10 @@ public class EntryInstallResult
|
|||||||
Entry = installedEntry
|
Entry = installedEntry
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{nameof(IsSuccess)}: {IsSuccess}, {nameof(Message)}: {Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
|
|||||||
// Remove the layout from any devices currently using it
|
// Remove the layout from any devices currently using it
|
||||||
foreach (ArtemisDevice device in _deviceService.Devices)
|
foreach (ArtemisDevice device in _deviceService.Devices)
|
||||||
{
|
{
|
||||||
if (device.LayoutSelection.Type == WorkshopLayoutProvider.LayoutType && device.LayoutSelection.Parameter == installedEntry.EntryId.ToString())
|
if (device.LayoutSelection.Type == WorkshopLayoutProvider.LayoutType && device.LayoutSelection.Parameter == installedEntry.Id.ToString())
|
||||||
{
|
{
|
||||||
_defaultLayoutProvider.ConfigureDevice(device);
|
_defaultLayoutProvider.ConfigureDevice(device);
|
||||||
_deviceService.SaveDevice(device);
|
_deviceService.SaveDevice(device);
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
|
|||||||
{
|
{
|
||||||
// If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done
|
// If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done
|
||||||
if (installedEntry.GetReleaseDirectory(release).Exists)
|
if (installedEntry.GetReleaseDirectory(release).Exists)
|
||||||
return EntryInstallResult.FromSuccess(installedEntry);
|
return ApplyAndSave(installedEntry, release);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -102,10 +102,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
|
|||||||
return EntryInstallResult.FromFailure(e.Message);
|
return EntryInstallResult.FromFailure(e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
installedEntry.ApplyRelease(release);
|
return ApplyAndSave(installedEntry, release);
|
||||||
|
|
||||||
_workshopService.SaveInstalledEntry(installedEntry);
|
|
||||||
return EntryInstallResult.FromSuccess(installedEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
|
public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
|
||||||
@ -135,4 +132,11 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
|
|||||||
_workshopService.RemoveInstalledEntry(installedEntry);
|
_workshopService.RemoveInstalledEntry(installedEntry);
|
||||||
return Task.FromResult(EntryUninstallResult.FromSuccess(message));
|
return Task.FromResult(EntryUninstallResult.FromSuccess(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EntryInstallResult ApplyAndSave(InstalledEntry installedEntry, IRelease release)
|
||||||
|
{
|
||||||
|
installedEntry.ApplyRelease(release);
|
||||||
|
_workshopService.SaveInstalledEntry(installedEntry);
|
||||||
|
return EntryInstallResult.FromSuccess(installedEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,9 +6,22 @@ using Artemis.Storage.Entities.Workshop;
|
|||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Models;
|
namespace Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
public class InstalledEntry
|
public class InstalledEntry : CorePropertyChanged, IEntrySummary
|
||||||
{
|
{
|
||||||
private Dictionary<string, JsonNode> _metadata = new();
|
private Dictionary<string, JsonNode> _metadata = new();
|
||||||
|
private long _id;
|
||||||
|
private string _author;
|
||||||
|
private bool _isOfficial;
|
||||||
|
private string _name;
|
||||||
|
private string _summary;
|
||||||
|
private EntryType _entryType;
|
||||||
|
private long _downloads;
|
||||||
|
private DateTimeOffset _createdAt;
|
||||||
|
private long? _latestReleaseId;
|
||||||
|
private IReadOnlyList<IGetDependantEntries_Entries_Items_Categories> _categories;
|
||||||
|
private long _releaseId;
|
||||||
|
private string _releaseVersion = string.Empty;
|
||||||
|
private bool _autoUpdate;
|
||||||
|
|
||||||
internal InstalledEntry(EntryEntity entity)
|
internal InstalledEntry(EntryEntity entity)
|
||||||
{
|
{
|
||||||
@ -20,55 +33,74 @@ public class InstalledEntry
|
|||||||
{
|
{
|
||||||
Entity = new EntryEntity();
|
Entity = new EntryEntity();
|
||||||
|
|
||||||
EntryId = entry.Id;
|
ApplyEntrySummary(entry);
|
||||||
EntryType = entry.EntryType;
|
|
||||||
|
|
||||||
Author = entry.Author;
|
|
||||||
Name = entry.Name;
|
|
||||||
InstalledAt = DateTimeOffset.Now;
|
InstalledAt = DateTimeOffset.Now;
|
||||||
ReleaseId = release.Id;
|
ReleaseId = release.Id;
|
||||||
ReleaseVersion = release.Version;
|
ReleaseVersion = release.Version;
|
||||||
|
AutoUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long EntryId { get; set; }
|
internal EntryEntity Entity { get; }
|
||||||
public EntryType EntryType { get; set; }
|
|
||||||
|
|
||||||
public string Author { get; set; } = string.Empty;
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public long ReleaseId { get; set; }
|
|
||||||
public string ReleaseVersion { get; set; } = string.Empty;
|
|
||||||
public DateTimeOffset InstalledAt { get; set; }
|
public DateTimeOffset InstalledAt { get; set; }
|
||||||
|
|
||||||
internal EntryEntity Entity { get; }
|
public long ReleaseId
|
||||||
|
{
|
||||||
|
get => _releaseId;
|
||||||
|
set => SetAndNotify(ref _releaseId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReleaseVersion
|
||||||
|
{
|
||||||
|
get => _releaseVersion;
|
||||||
|
set => SetAndNotify(ref _releaseVersion, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutoUpdate
|
||||||
|
{
|
||||||
|
get => _autoUpdate;
|
||||||
|
set => SetAndNotify(ref _autoUpdate, value);
|
||||||
|
}
|
||||||
|
|
||||||
internal void Load()
|
internal void Load()
|
||||||
{
|
{
|
||||||
EntryId = Entity.EntryId;
|
Id = Entity.EntryId;
|
||||||
EntryType = (EntryType) Entity.EntryType;
|
|
||||||
|
|
||||||
Author = Entity.Author;
|
Author = Entity.Author;
|
||||||
|
IsOfficial = Entity.IsOfficial;
|
||||||
Name = Entity.Name;
|
Name = Entity.Name;
|
||||||
|
Summary = Entity.Summary;
|
||||||
|
EntryType = (EntryType) Entity.EntryType;
|
||||||
|
Downloads = Entity.Downloads;
|
||||||
|
CreatedAt = Entity.CreatedAt;
|
||||||
|
LatestReleaseId = Entity.LatestReleaseId;
|
||||||
|
Categories = Entity.Categories?.Select(c => new GetDependantEntries_Entries_Items_Categories_Category(c.Name, c.Icon)).ToList() ?? [];
|
||||||
|
|
||||||
ReleaseId = Entity.ReleaseId;
|
ReleaseId = Entity.ReleaseId;
|
||||||
ReleaseVersion = Entity.ReleaseVersion;
|
ReleaseVersion = Entity.ReleaseVersion;
|
||||||
InstalledAt = Entity.InstalledAt;
|
InstalledAt = Entity.InstalledAt;
|
||||||
|
AutoUpdate = Entity.AutoUpdate;
|
||||||
|
|
||||||
_metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : new Dictionary<string, JsonNode>();
|
_metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Save()
|
internal void Save()
|
||||||
{
|
{
|
||||||
Entity.EntryId = EntryId;
|
Entity.EntryId = Id;
|
||||||
Entity.EntryType = (int) EntryType;
|
Entity.EntryType = (int) EntryType;
|
||||||
|
|
||||||
Entity.Author = Author;
|
Entity.Author = Author;
|
||||||
|
Entity.IsOfficial = IsOfficial;
|
||||||
Entity.Name = Name;
|
Entity.Name = Name;
|
||||||
|
Entity.Summary = Summary;
|
||||||
|
Entity.Downloads = Downloads;
|
||||||
|
Entity.CreatedAt = CreatedAt;
|
||||||
|
Entity.LatestReleaseId = LatestReleaseId;
|
||||||
|
Entity.Categories = Categories.Select(c => new EntryCategoryEntity(c.Name, c.Icon)).ToList();
|
||||||
|
|
||||||
Entity.ReleaseId = ReleaseId;
|
Entity.ReleaseId = ReleaseId;
|
||||||
Entity.ReleaseVersion = ReleaseVersion;
|
Entity.ReleaseVersion = ReleaseVersion;
|
||||||
Entity.InstalledAt = InstalledAt;
|
Entity.InstalledAt = InstalledAt;
|
||||||
|
Entity.AutoUpdate = AutoUpdate;
|
||||||
|
|
||||||
Entity.Metadata = new Dictionary<string, JsonNode>(_metadata);
|
Entity.Metadata = new Dictionary<string, JsonNode>(_metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +150,7 @@ public class InstalledEntry
|
|||||||
/// <returns>The directory info of the directory.</returns>
|
/// <returns>The directory info of the directory.</returns>
|
||||||
public DirectoryInfo GetDirectory()
|
public DirectoryInfo GetDirectory()
|
||||||
{
|
{
|
||||||
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{EntryId}-{StringUtilities.UrlFriendly(Name)}"));
|
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{Id}-{StringUtilities.UrlFriendly(Name)}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -141,4 +173,98 @@ public class InstalledEntry
|
|||||||
ReleaseVersion = release.Version;
|
ReleaseVersion = release.Version;
|
||||||
InstalledAt = DateTimeOffset.UtcNow;
|
InstalledAt = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyEntrySummary(IEntrySummary entry)
|
||||||
|
{
|
||||||
|
Id = entry.Id;
|
||||||
|
Author = entry.Author;
|
||||||
|
IsOfficial = entry.IsOfficial;
|
||||||
|
Name = entry.Name;
|
||||||
|
Summary = entry.Summary;
|
||||||
|
EntryType = entry.EntryType;
|
||||||
|
Downloads = entry.Downloads;
|
||||||
|
CreatedAt = entry.CreatedAt;
|
||||||
|
LatestReleaseId = entry.LatestReleaseId;
|
||||||
|
Categories = entry.Categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Implementation of IEntrySummary
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
private set => SetAndNotify(ref _id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Author
|
||||||
|
{
|
||||||
|
get => _author;
|
||||||
|
private set => SetAndNotify(ref _author, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsOfficial
|
||||||
|
{
|
||||||
|
get => _isOfficial;
|
||||||
|
private set => SetAndNotify(ref _isOfficial, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
private set => SetAndNotify(ref _name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Summary
|
||||||
|
{
|
||||||
|
get => _summary;
|
||||||
|
private set => SetAndNotify(ref _summary, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public EntryType EntryType
|
||||||
|
{
|
||||||
|
get => _entryType;
|
||||||
|
private set => SetAndNotify(ref _entryType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long Downloads
|
||||||
|
{
|
||||||
|
get => _downloads;
|
||||||
|
private set => SetAndNotify(ref _downloads, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTimeOffset CreatedAt
|
||||||
|
{
|
||||||
|
get => _createdAt;
|
||||||
|
private set => SetAndNotify(ref _createdAt, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long? LatestReleaseId
|
||||||
|
{
|
||||||
|
get => _latestReleaseId;
|
||||||
|
private set => SetAndNotify(ref _latestReleaseId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<IGetDependantEntries_Entries_Items_Categories> Categories
|
||||||
|
{
|
||||||
|
get => _categories;
|
||||||
|
private set => SetAndNotify(ref _categories, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"[{EntryType}] {Id} - {Name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -55,6 +55,6 @@ public class WorkshopLayoutProvider : ILayoutProvider
|
|||||||
throw new InvalidOperationException($"Cannot use a workshop entry of type {entry.EntryType} as a layout");
|
throw new InvalidOperationException($"Cannot use a workshop entry of type {entry.EntryType} as a layout");
|
||||||
|
|
||||||
device.LayoutSelection.Type = LayoutType;
|
device.LayoutSelection.Type = LayoutType;
|
||||||
device.LayoutSelection.Parameter = entry?.EntryId.ToString();
|
device.LayoutSelection.Parameter = entry?.Id.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,6 +31,7 @@ fragment submittedEntry on Entry {
|
|||||||
fragment entrySummary on Entry {
|
fragment entrySummary on Entry {
|
||||||
id
|
id
|
||||||
author
|
author
|
||||||
|
isOfficial
|
||||||
name
|
name
|
||||||
summary
|
summary
|
||||||
entryType
|
entryType
|
||||||
@ -45,6 +46,7 @@ fragment entrySummary on Entry {
|
|||||||
fragment entryDetails on Entry {
|
fragment entryDetails on Entry {
|
||||||
id
|
id
|
||||||
author
|
author
|
||||||
|
isOfficial
|
||||||
name
|
name
|
||||||
summary
|
summary
|
||||||
entryType
|
entryType
|
||||||
|
|||||||
@ -26,4 +26,18 @@ query GetLayoutEntryById($id: Long!) {
|
|||||||
...layoutInfo
|
...layoutInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetEntrySummaryById($id: Long!) {
|
||||||
|
entry(id: $id) {
|
||||||
|
...entrySummary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetEntryLatestReleaseById($id: Long!) {
|
||||||
|
entry(id: $id) {
|
||||||
|
latestRelease {
|
||||||
|
...releaseDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ public interface IWorkshopService
|
|||||||
/// Initializes the workshop service.
|
/// Initializes the workshop service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Initialize();
|
void Initialize();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the status of the workshop.
|
/// Represents the status of the workshop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -133,4 +133,6 @@ public interface IWorkshopService
|
|||||||
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
|
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
|
||||||
public event EventHandler<InstalledEntry>? OnEntryUninstalled;
|
public event EventHandler<InstalledEntry>? OnEntryUninstalled;
|
||||||
public event EventHandler<InstalledEntry>? OnEntryInstalled;
|
public event EventHandler<InstalledEntry>? OnEntryInstalled;
|
||||||
|
|
||||||
|
void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate);
|
||||||
}
|
}
|
||||||
@ -165,7 +165,7 @@ public class WorkshopService : IWorkshopService
|
|||||||
if (result.IsSuccess && result.Entry != null)
|
if (result.IsSuccess && result.Entry != null)
|
||||||
OnEntryInstalled?.Invoke(this, result.Entry);
|
OnEntryInstalled?.Invoke(this, result.Entry);
|
||||||
else
|
else
|
||||||
_logger.Warning("Failed to install entry {EntryId}: {Message}", entry.Id, result.Message);
|
_logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -178,7 +178,7 @@ public class WorkshopService : IWorkshopService
|
|||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
OnEntryUninstalled?.Invoke(this, installedEntry);
|
OnEntryUninstalled?.Invoke(this, installedEntry);
|
||||||
else
|
else
|
||||||
_logger.Warning("Failed to uninstall entry {EntryId}: {Message}", installedEntry.EntryId, result.Message);
|
_logger.Warning("Failed to uninstall entry {EntryId}: {Message}", installedEntry.Id, result.Message);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -254,6 +254,16 @@ public class WorkshopService : IWorkshopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate)
|
||||||
|
{
|
||||||
|
if (installedEntry.AutoUpdate == autoUpdate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
installedEntry.AutoUpdate = autoUpdate;
|
||||||
|
SaveInstalledEntry(installedEntry);
|
||||||
|
}
|
||||||
|
|
||||||
private void RemoveOrphanedFiles()
|
private void RemoveOrphanedFiles()
|
||||||
{
|
{
|
||||||
List<InstalledEntry> entries = GetInstalledEntries();
|
List<InstalledEntry> entries = GetInstalledEntries();
|
||||||
@ -308,6 +318,8 @@ public class WorkshopService : IWorkshopService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
|
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
|
||||||
|
|
||||||
public event EventHandler<InstalledEntry>? OnEntryUninstalled;
|
public event EventHandler<InstalledEntry>? OnEntryUninstalled;
|
||||||
|
|
||||||
public event EventHandler<InstalledEntry>? OnEntryInstalled;
|
public event EventHandler<InstalledEntry>? OnEntryInstalled;
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ schema: schema.graphql
|
|||||||
extensions:
|
extensions:
|
||||||
endpoints:
|
endpoints:
|
||||||
Default GraphQL Endpoint:
|
Default GraphQL Endpoint:
|
||||||
url: https://localhost:7281/graphql
|
url: https://workshop.artemis-rgb.com/graphql
|
||||||
headers:
|
headers:
|
||||||
user-agent: JS GraphQL
|
user-agent: JS GraphQL
|
||||||
introspect: true
|
introspect: true
|
||||||
|
|||||||
@ -61,6 +61,7 @@ type Entry {
|
|||||||
iconId: UUID
|
iconId: UUID
|
||||||
id: Long!
|
id: Long!
|
||||||
images: [Image!]!
|
images: [Image!]!
|
||||||
|
isOfficial: Boolean!
|
||||||
latestRelease: Release
|
latestRelease: Release
|
||||||
latestReleaseId: Long
|
latestReleaseId: Long
|
||||||
layoutInfo: [LayoutInfo!]!
|
layoutInfo: [LayoutInfo!]!
|
||||||
@ -124,6 +125,7 @@ type PluginInfo {
|
|||||||
entry: Entry!
|
entry: Entry!
|
||||||
entryId: Long!
|
entryId: Long!
|
||||||
helpPage: String
|
helpPage: String
|
||||||
|
minmumVersion: String
|
||||||
pluginGuid: UUID!
|
pluginGuid: UUID!
|
||||||
repository: String
|
repository: String
|
||||||
requiresAdmin: Boolean!
|
requiresAdmin: Boolean!
|
||||||
@ -310,6 +312,7 @@ input EntryFilterInput {
|
|||||||
iconId: UuidOperationFilterInput
|
iconId: UuidOperationFilterInput
|
||||||
id: LongOperationFilterInput
|
id: LongOperationFilterInput
|
||||||
images: ListFilterInputTypeOfImageFilterInput
|
images: ListFilterInputTypeOfImageFilterInput
|
||||||
|
isOfficial: BooleanOperationFilterInput
|
||||||
latestRelease: ReleaseFilterInput
|
latestRelease: ReleaseFilterInput
|
||||||
latestReleaseId: LongOperationFilterInput
|
latestReleaseId: LongOperationFilterInput
|
||||||
layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput
|
layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput
|
||||||
@ -331,6 +334,7 @@ input EntrySortInput {
|
|||||||
icon: ImageSortInput
|
icon: ImageSortInput
|
||||||
iconId: SortEnumType
|
iconId: SortEnumType
|
||||||
id: SortEnumType
|
id: SortEnumType
|
||||||
|
isOfficial: SortEnumType
|
||||||
latestRelease: ReleaseSortInput
|
latestRelease: ReleaseSortInput
|
||||||
latestReleaseId: SortEnumType
|
latestReleaseId: SortEnumType
|
||||||
name: SortEnumType
|
name: SortEnumType
|
||||||
@ -479,6 +483,7 @@ input PluginInfoFilterInput {
|
|||||||
entry: EntryFilterInput
|
entry: EntryFilterInput
|
||||||
entryId: LongOperationFilterInput
|
entryId: LongOperationFilterInput
|
||||||
helpPage: StringOperationFilterInput
|
helpPage: StringOperationFilterInput
|
||||||
|
minmumVersion: StringOperationFilterInput
|
||||||
or: [PluginInfoFilterInput!]
|
or: [PluginInfoFilterInput!]
|
||||||
pluginGuid: UuidOperationFilterInput
|
pluginGuid: UuidOperationFilterInput
|
||||||
repository: StringOperationFilterInput
|
repository: StringOperationFilterInput
|
||||||
@ -494,6 +499,7 @@ input PluginInfoSortInput {
|
|||||||
entry: EntrySortInput
|
entry: EntrySortInput
|
||||||
entryId: SortEnumType
|
entryId: SortEnumType
|
||||||
helpPage: SortEnumType
|
helpPage: SortEnumType
|
||||||
|
minmumVersion: SortEnumType
|
||||||
pluginGuid: SortEnumType
|
pluginGuid: SortEnumType
|
||||||
repository: SortEnumType
|
repository: SortEnumType
|
||||||
requiresAdmin: SortEnumType
|
requiresAdmin: SortEnumType
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<PackageVersion Include="Serilog" Version="4.0.0" />
|
<PackageVersion Include="Serilog" Version="4.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
|
||||||
<PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.8" />
|
||||||
<PackageVersion Include="Splat.DryIoc" Version="15.1.1" />
|
<PackageVersion Include="Splat.DryIoc" Version="15.1.1" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user