diff --git a/LANCommander.Launcher.Data/Enums/GameType.cs b/LANCommander.Launcher.Data/Enums/GameType.cs
deleted file mode 100644
index cad57435..00000000
--- a/LANCommander.Launcher.Data/Enums/GameType.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-
-namespace LANCommander.Launcher.Data.Enums
-{
- public enum GameType
- {
- [Display(Name = "Main Game")]
- MainGame,
- Expansion,
- [Display(Name = "Standalone Expansion")]
- StandaloneExpansion,
- Mod,
- [Display(Name = "Standalone Mod")]
- StandaloneMod
- }
-}
diff --git a/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs
new file mode 100644
index 00000000..ab1e9299
--- /dev/null
+++ b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs
@@ -0,0 +1,852 @@
+//
+using System;
+using LANCommander.Launcher.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace LANCommander.Launcher.Data.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20251130001620_AddImportedOn")]
+ partial class AddImportedOn
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
+
+ modelBuilder.Entity("CategoryGame", b =>
+ {
+ b.Property("CategoriesId")
+ .HasColumnType("TEXT");
+
+ b.Property("GamesId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("CategoriesId", "GamesId");
+
+ b.HasIndex("GamesId");
+
+ b.ToTable("CategoryGame");
+ });
+
+ modelBuilder.Entity("CollectionGame", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("TEXT");
+
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("CollectionId", "GameId");
+
+ b.HasIndex("GameId");
+
+ b.ToTable("CollectionGame");
+ });
+
+ modelBuilder.Entity("GameDeveloper", b =>
+ {
+ b.Property("DeveloperId")
+ .HasColumnType("TEXT");
+
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("DeveloperId", "GameId");
+
+ b.HasIndex("GameId");
+
+ b.ToTable("GameDeveloper");
+ });
+
+ modelBuilder.Entity("GameGenre", b =>
+ {
+ b.Property("GamesId")
+ .HasColumnType("TEXT");
+
+ b.Property("GenresId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GamesId", "GenresId");
+
+ b.HasIndex("GenresId");
+
+ b.ToTable("GameGenre");
+ });
+
+ modelBuilder.Entity("GamePlatform", b =>
+ {
+ b.Property("GamesId")
+ .HasColumnType("TEXT");
+
+ b.Property("PlatformsId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GamesId", "PlatformsId");
+
+ b.HasIndex("PlatformsId");
+
+ b.ToTable("GamePlatform");
+ });
+
+ modelBuilder.Entity("GamePublisher", b =>
+ {
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("PublisherId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GameId", "PublisherId");
+
+ b.HasIndex("PublisherId");
+
+ b.ToTable("GamePublisher");
+ });
+
+ modelBuilder.Entity("GameRedistributable", b =>
+ {
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("RedistributableId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GameId", "RedistributableId");
+
+ b.HasIndex("RedistributableId");
+
+ b.ToTable("GameRedistributable");
+ });
+
+ modelBuilder.Entity("GameTag", b =>
+ {
+ b.Property("GamesId")
+ .HasColumnType("TEXT");
+
+ b.Property("TagsId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GamesId", "TagsId");
+
+ b.HasIndex("TagsId");
+
+ b.ToTable("GameTag");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("Categories");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Collection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Collections");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Company", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Companies");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Engine", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Engines");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("BaseGameId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("EngineId")
+ .HasColumnType("TEXT");
+
+ b.Property("IGDBId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("InstallDirectory")
+ .HasColumnType("TEXT");
+
+ b.Property("Installed")
+ .HasColumnType("INTEGER");
+
+ b.Property("InstalledOn")
+ .HasColumnType("TEXT");
+
+ b.Property("InstalledVersion")
+ .HasColumnType("TEXT");
+
+ b.Property("LatestVersion")
+ .HasColumnType("TEXT");
+
+ b.Property("Notes")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleasedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Singleplayer")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BaseGameId");
+
+ b.HasIndex("EngineId");
+
+ b.ToTable("Games");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Genres");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("Libraries");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Media", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Crc32")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("FileId")
+ .HasColumnType("TEXT");
+
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("MimeType")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SourceUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameId");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("Media");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.MultiplayerMode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("MaxPlayers")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinPlayers")
+ .HasColumnType("INTEGER");
+
+ b.Property("NetworkProtocol")
+ .HasColumnType("INTEGER");
+
+ b.Property("Spectators")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameId");
+
+ b.ToTable("MultiplayerModes");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Platform", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Platforms");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.PlaySession", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("End")
+ .HasColumnType("TEXT");
+
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Start")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameId");
+
+ b.ToTable("PlaySessions");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Redistributable", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Notes")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Redistributables");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Alias")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("LibraryGame", b =>
+ {
+ b.Property("GameId")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("GameId", "LibraryId");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("LibraryGame");
+ });
+
+ modelBuilder.Entity("CategoryGame", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Category", null)
+ .WithMany()
+ .HasForeignKey("CategoriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GamesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("CollectionGame", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Collection", null)
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GameDeveloper", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Company", null)
+ .WithMany()
+ .HasForeignKey("DeveloperId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GameGenre", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GamesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Genre", null)
+ .WithMany()
+ .HasForeignKey("GenresId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GamePlatform", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GamesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Platform", null)
+ .WithMany()
+ .HasForeignKey("PlatformsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GamePublisher", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Company", null)
+ .WithMany()
+ .HasForeignKey("PublisherId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GameRedistributable", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Redistributable", null)
+ .WithMany()
+ .HasForeignKey("RedistributableId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GameTag", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GamesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Tag", null)
+ .WithMany()
+ .HasForeignKey("TagsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Category", "Parent")
+ .WithMany("Children")
+ .HasForeignKey("ParentId");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", "BaseGame")
+ .WithMany("DependentGames")
+ .HasForeignKey("BaseGameId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Engine", "Engine")
+ .WithMany("Games")
+ .HasForeignKey("EngineId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("BaseGame");
+
+ b.Navigation("Engine");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.User", "User")
+ .WithOne("Library")
+ .HasForeignKey("LANCommander.Launcher.Data.Models.Library", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Media", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
+ .WithMany("Media")
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("LANCommander.Launcher.Data.Models.User", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("LANCommander.Launcher.Data.Models.Media", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Game");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.MultiplayerMode", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
+ .WithMany("MultiplayerModes")
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Game");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.PlaySession", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
+ .WithMany("PlaySessions")
+ .HasForeignKey("GameId");
+
+ b.Navigation("Game");
+ });
+
+ modelBuilder.Entity("LibraryGame", b =>
+ {
+ b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
+ .WithMany()
+ .HasForeignKey("GameId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("LANCommander.Launcher.Data.Models.Library", null)
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
+ {
+ b.Navigation("Children");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Engine", b =>
+ {
+ b.Navigation("Games");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b =>
+ {
+ b.Navigation("DependentGames");
+
+ b.Navigation("Media");
+
+ b.Navigation("MultiplayerModes");
+
+ b.Navigation("PlaySessions");
+ });
+
+ modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b =>
+ {
+ b.Navigation("Avatar");
+
+ b.Navigation("Library");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs
new file mode 100644
index 00000000..bad830ab
--- /dev/null
+++ b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs
@@ -0,0 +1,180 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace LANCommander.Launcher.Data.Migrations
+{
+ ///
+ public partial class AddImportedOn : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Users",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Tags",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Redistributables",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "PlaySessions",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Platforms",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "MultiplayerModes",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Media",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Libraries",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Genres",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AlterColumn(
+ name: "ImportedOn",
+ table: "Games",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Engines",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Companies",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Collections",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "ImportedOn",
+ table: "Categories",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Tags");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Redistributables");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "PlaySessions");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Platforms");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "MultiplayerModes");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Media");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Libraries");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Genres");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Engines");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Companies");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Collections");
+
+ migrationBuilder.DropColumn(
+ name: "ImportedOn",
+ table: "Categories");
+
+ migrationBuilder.AlterColumn(
+ name: "ImportedOn",
+ table: "Games",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT");
+ }
+ }
+}
diff --git a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs
index f80e68d3..efe3f132 100644
--- a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs
+++ b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace LANCommander.Launcher.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("CategoryGame", b =>
{
@@ -146,6 +146,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -172,6 +175,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -193,6 +199,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -214,6 +223,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -247,7 +259,7 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("IGDBId")
.HasColumnType("INTEGER");
- b.Property("ImportedOn")
+ b.Property("ImportedOn")
.HasColumnType("TEXT");
b.Property("InstallDirectory")
@@ -305,6 +317,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -326,6 +341,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("UpdatedOn")
.HasColumnType("TEXT");
@@ -360,6 +378,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("GameId")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("MimeType")
.HasMaxLength(255)
.HasColumnType("TEXT");
@@ -406,6 +427,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("GameId")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("MaxPlayers")
.HasColumnType("INTEGER");
@@ -440,6 +464,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -467,6 +494,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("GameId")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Start")
.HasColumnType("TEXT");
@@ -495,6 +525,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("Description")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -519,6 +552,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.IsRequired()
.HasColumnType("TEXT");
@@ -543,6 +579,9 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property("CreatedOn")
.HasColumnType("TEXT");
+ b.Property("ImportedOn")
+ .HasColumnType("TEXT");
+
b.Property("UpdatedOn")
.HasColumnType("TEXT");
diff --git a/LANCommander.Launcher.Data/Models/BaseModel.cs b/LANCommander.Launcher.Data/Models/BaseModel.cs
index c1190774..d6d7fe48 100644
--- a/LANCommander.Launcher.Data/Models/BaseModel.cs
+++ b/LANCommander.Launcher.Data/Models/BaseModel.cs
@@ -8,6 +8,9 @@ namespace LANCommander.Launcher.Data.Models
{
[Key]
public Guid Id { get; set; }
+
+ [Display(Name = "Imported On")]
+ public DateTime ImportedOn { get; set; }
[Display(Name = "Created On")]
public DateTime CreatedOn { get; set; }
diff --git a/LANCommander.Launcher.Data/Models/Game.cs b/LANCommander.Launcher.Data/Models/Game.cs
index 3dc5ffec..b5539c78 100644
--- a/LANCommander.Launcher.Data/Models/Game.cs
+++ b/LANCommander.Launcher.Data/Models/Game.cs
@@ -1,6 +1,6 @@
-using LANCommander.Launcher.Data.Enums;
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
+using LANCommander.SDK.Enums;
namespace LANCommander.Launcher.Data.Models
{
@@ -23,8 +23,6 @@ namespace LANCommander.Launcher.Data.Models
[Display(Name = "Released On")]
public DateTime? ReleasedOn { get; set; }
-
- public DateTime? ImportedOn { get; set; }
public GameType Type { get; set; }
public Guid? BaseGameId { get; set; }
diff --git a/LANCommander.Launcher.Data/Models/Media.cs b/LANCommander.Launcher.Data/Models/Media.cs
index a42bbb82..7a9742e2 100644
--- a/LANCommander.Launcher.Data/Models/Media.cs
+++ b/LANCommander.Launcher.Data/Models/Media.cs
@@ -1,5 +1,4 @@
-using LANCommander.Launcher.Data.Enums;
-using LANCommander.SDK.Enums;
+using LANCommander.SDK.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
diff --git a/LANCommander.Launcher.Data/Models/MultiplayerMode.cs b/LANCommander.Launcher.Data/Models/MultiplayerMode.cs
index a794ec52..7fc034a6 100644
--- a/LANCommander.Launcher.Data/Models/MultiplayerMode.cs
+++ b/LANCommander.Launcher.Data/Models/MultiplayerMode.cs
@@ -1,5 +1,4 @@
-using LANCommander.Launcher.Data.Enums;
-using LANCommander.SDK.Enums;
+using LANCommander.SDK.Enums;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
diff --git a/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs b/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs
index a1e5bbe1..deaf099e 100644
--- a/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs
+++ b/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs
@@ -9,7 +9,7 @@ namespace LANCommander.Launcher.Models
{
public class ActionSelectorDialogOptions
{
- public IEnumerable Actions { get; set; }
+ public IEnumerable Actions { get; set; }
public string Title { get; set; }
public Game Game { get; set; }
}
diff --git a/LANCommander.Launcher.Services/CommandLineService.cs b/LANCommander.Launcher.Services/CommandLineService.cs
index 6cefa831..d3a75406 100644
--- a/LANCommander.Launcher.Services/CommandLineService.cs
+++ b/LANCommander.Launcher.Services/CommandLineService.cs
@@ -123,14 +123,14 @@ namespace LANCommander.Launcher.Services
try
{
- var game = await gameService.GetAsync(options.GameId);
- var manifest = await ManifestHelper.ReadAsync(game.InstallDirectory, game.Id);
+ /*var game = await gameService.GetAsync(options.GameId);
+ var manifest = await ManifestHelper.ReadAsync(game.InstallDirectory, game.Id);
var action = manifest.Actions.FirstOrDefault(a => a.Id == options.ActionId);
if (action == null)
action = manifest.Actions.OrderBy(a => a.SortOrder).FirstOrDefault(a => a.IsPrimaryAction);
- await gameService.Run(game, action);
+ await gameService.Run(game, action);*/
}
catch (Exception ex)
{
@@ -142,7 +142,7 @@ namespace LANCommander.Launcher.Services
{
Logger.LogInformation("Syncing games from server...");
- importService.OnImportComplete += async () =>
+ /*importService.OnImportComplete += async () =>
{
Logger.LogInformation("Sync complete!");
};
@@ -150,7 +150,7 @@ namespace LANCommander.Launcher.Services
importService.OnImportFailed += async (Exception ex) =>
{
Logger.LogError(ex, "Sync failed!");
- };
+ };*/
await importService.ImportAsync();
}
diff --git a/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs b/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs
new file mode 100644
index 00000000..125b3ea5
--- /dev/null
+++ b/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs
@@ -0,0 +1,17 @@
+namespace LANCommander.Launcher.Services.Exceptions;
+
+public class ImportSkippedException : Exception where TRecord : class
+{
+ public TRecord Record { get; set; }
+
+ public ImportSkippedException(TRecord record, string message) : base(message)
+ {
+ Record = record;
+ }
+
+ public ImportSkippedException(TRecord record, string message, Exception innerException) : base(message,
+ innerException)
+ {
+ Record = record;
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs
index dd028458..b62d3676 100644
--- a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs
+++ b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,7 @@
using LANCommander.Launcher.Data;
+using LANCommander.Launcher.Services.Import;
+using LANCommander.Launcher.Services.Import.Factories;
+using LANCommander.Launcher.Services.Import.Importers;
using LANCommander.Launcher.Services.PowerShell;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -17,7 +20,10 @@ namespace LANCommander.Launcher.Services.Extensions
{
public static IServiceCollection AddLANCommanderLauncher(this IServiceCollection services, Action configure)
{
- services.AddDbContext();
+ services.AddDbContext(options =>
+ {
+ options.EnableSensitiveDataLogging();
+ });
#region Register Client
var options = new LANCommanderOptions();
@@ -55,6 +61,21 @@ namespace LANCommander.Launcher.Services.Extensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+
+ #region Import
+
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ #endregion
return services;
}
diff --git a/LANCommander.Launcher.Services/FilterService.cs b/LANCommander.Launcher.Services/FilterService.cs
index c3710bcb..5ac99b9c 100644
--- a/LANCommander.Launcher.Services/FilterService.cs
+++ b/LANCommander.Launcher.Services/FilterService.cs
@@ -1,5 +1,6 @@
using LANCommander.Launcher.Data.Models;
using LANCommander.Launcher.Models;
+using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using Microsoft.Extensions.Logging;
@@ -116,7 +117,7 @@ namespace LANCommander.Launcher.Services
if (Filter.Installed)
items = items.Where(i => (i.DataItem as Game).Installed);
- items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(Data.Enums.GameType.MainGame, Data.Enums.GameType.StandaloneExpansion, Data.Enums.GameType.StandaloneMod));
+ items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(GameType.MainGame, GameType.StandaloneExpansion, GameType.StandaloneMod));
op.Complete();
}
diff --git a/LANCommander.Launcher.Services/GameService.cs b/LANCommander.Launcher.Services/GameService.cs
index 6a82328a..80e13484 100644
--- a/LANCommander.Launcher.Services/GameService.cs
+++ b/LANCommander.Launcher.Services/GameService.cs
@@ -65,7 +65,7 @@ namespace LANCommander.Launcher.Services
}
}
- public async Task Run(Game game, SDK.Models.Action action)
+ public async Task Run(Game game, SDK.Models.Manifest.Action action)
{
Guid userId;
diff --git a/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs b/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs
new file mode 100644
index 00000000..c41b94e8
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs
@@ -0,0 +1,13 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace LANCommander.Launcher.Services.Import.Factories;
+
+public class ImportContextFactory(IServiceProvider serviceProvider)
+{
+ public ImportContext Create()
+ {
+ var scope = serviceProvider.CreateScope();
+
+ return scope.ServiceProvider.GetRequiredService();
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/IImportItemInfo.cs b/LANCommander.Launcher.Services/Import/IImportItemInfo.cs
new file mode 100644
index 00000000..347f6385
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/IImportItemInfo.cs
@@ -0,0 +1,9 @@
+namespace LANCommander.Launcher.Services.Import;
+
+public interface IImportItemInfo
+{
+ string Key { get; set; }
+ string Type { get; set; }
+ string Name { get; set; }
+ bool Processed { get; set; }
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/IImporter.cs b/LANCommander.Launcher.Services/Import/IImporter.cs
new file mode 100644
index 00000000..91be2030
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/IImporter.cs
@@ -0,0 +1,11 @@
+namespace LANCommander.Launcher.Services.Import;
+
+public interface IImporter where TRecord : class
+{
+ void UseContext(ImportContext importContext);
+ Task> GetImportInfoAsync(TRecord record);
+ Task CanImportAsync(TRecord record);
+ Task AddAsync(TRecord record);
+ Task UpdateAsync(TRecord record);
+ Task ExistsAsync(TRecord record);
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/ImportContext.cs b/LANCommander.Launcher.Services/Import/ImportContext.cs
new file mode 100644
index 00000000..076a0be0
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/ImportContext.cs
@@ -0,0 +1,150 @@
+using LANCommander.Launcher.Services.Import.Importers;
+using LANCommander.SDK.Models.Manifest;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace LANCommander.Launcher.Services.Import;
+
+public class ImportContext
+{
+ public IImportItemInfo CurrentItem { get; set; }
+ public int Processed => _queue.Count(qi => qi.Processed);
+ public int Total => _queue.Count;
+
+ public EventHandler OnImportStarted { get; set; }
+ public EventHandler OnImportProgress { get; set; }
+ public EventHandler OnImportCompleted { get; set; }
+
+ private List _queue { get; set; } = new();
+
+ #region Importers
+
+ private CollectionImporter _collections;
+ private DeveloperImporter _developers;
+ private EngineImporter _engines;
+ private GameImporter _games;
+ private GenreImporter _genres;
+ private MultiplayerModeImporter _multiplayerModes;
+ private PlatformImporter _platforms;
+ private PublisherImporter _publishers;
+ private TagImporter _tags;
+ #endregion
+
+ public ImportContext(IServiceProvider serviceProvider)
+ {
+ _collections = serviceProvider.GetRequiredService();
+ _developers = serviceProvider.GetRequiredService();
+ _engines = serviceProvider.GetRequiredService();
+ _games = serviceProvider.GetRequiredService();
+ _genres = serviceProvider.GetRequiredService();
+ _multiplayerModes = serviceProvider.GetRequiredService();
+ _platforms = serviceProvider.GetRequiredService();
+ _publishers = serviceProvider.GetRequiredService();
+ _tags = serviceProvider.GetRequiredService();
+ }
+
+ public async Task AddAsync(Game game)
+ {
+ foreach (var collection in game.Collections)
+ if (!InQueue(collection, _collections) && await _collections.CanImportAsync(collection))
+ _queue.Add(await _collections.GetImportInfoAsync(collection));
+
+ foreach (var developer in game.Developers)
+ if (!InQueue(developer, _developers) && await _developers.CanImportAsync(developer))
+ _queue.Add(await _developers.GetImportInfoAsync(developer));
+
+ if (!InQueue(game.Engine, _engines) && await _engines.CanImportAsync(game.Engine))
+ _queue.Add(await _engines.GetImportInfoAsync(game.Engine));
+
+ foreach (var genre in game.Genres)
+ if (!InQueue(genre, _genres) && await _genres.CanImportAsync(genre))
+ _queue.Add(await _genres.GetImportInfoAsync(genre));
+
+ foreach (var multiplayerMode in game.MultiplayerModes)
+ if (!InQueue(multiplayerMode, _multiplayerModes) && await _multiplayerModes.CanImportAsync(multiplayerMode))
+ _queue.Add(await _multiplayerModes.GetImportInfoAsync(multiplayerMode));
+
+ foreach (var platform in game.Platforms)
+ if (!InQueue(platform, _platforms) && await _platforms.CanImportAsync(platform))
+ _queue.Add(await _platforms.GetImportInfoAsync(platform));
+
+ foreach (var publisher in game.Publishers)
+ if (!InQueue(publisher, _publishers) && await _publishers.CanImportAsync(publisher))
+ _queue.Add(await _publishers.GetImportInfoAsync(publisher));
+
+ foreach (var tag in game.Tags)
+ if (!InQueue(tag, _tags) && await _tags.CanImportAsync(tag))
+ _queue.Add(await _tags.GetImportInfoAsync(tag));
+
+ if (!InQueue(game, _games) && await _games.CanImportAsync(game))
+ _queue.Add(await _games.GetImportInfoAsync(game));
+ }
+
+ private bool InQueue(TRecord record, BaseImporter importer)
+ where TRecord : class =>
+ _queue.Any(qi => qi.Key == importer.GetKey(record));
+
+ public async Task ImportQueueAsync()
+ {
+ OnImportStarted?.Invoke(this, EventArgs.Empty);
+
+ // Import metadata before games
+ _queue = _queue
+ .OrderBy(qi => qi.Type == nameof(Game))
+ .ThenBy(qi => qi.Type)
+ .ThenBy(qi => qi.Name)
+ .ToList();
+
+ foreach (var queueItem in _queue)
+ {
+ CurrentItem = queueItem;
+
+ OnImportProgress?.Invoke(this, new ImportStatusUpdate
+ {
+ CurrentItem = CurrentItem,
+ Index = Processed,
+ Total = Total,
+ });
+
+ switch (queueItem.Type)
+ {
+ case nameof(Collection):
+ await _collections.ImportAsync(queueItem);
+ break;
+
+ case "Developer":
+ await _developers.ImportAsync(queueItem);
+ break;
+
+ case nameof(Engine):
+ await _engines.ImportAsync(queueItem);
+ break;
+
+ case nameof(Game):
+ await _games.ImportAsync(queueItem);
+ break;
+
+ case nameof(Genre):
+ await _genres.ImportAsync(queueItem);
+ break;
+
+ case nameof(MultiplayerMode):
+ await _multiplayerModes.ImportAsync(queueItem);
+ break;
+
+ case nameof(Platform):
+ await _platforms.ImportAsync(queueItem);
+ break;
+
+ case "Publisher":
+ await _publishers.ImportAsync(queueItem);
+ break;
+
+ case nameof(Tag):
+ await _tags.ImportAsync(queueItem);
+ break;
+ }
+ }
+
+ OnImportCompleted?.Invoke(this, EventArgs.Empty);
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/ImportItemInfo.cs b/LANCommander.Launcher.Services/Import/ImportItemInfo.cs
new file mode 100644
index 00000000..3c62f5a0
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/ImportItemInfo.cs
@@ -0,0 +1,10 @@
+namespace LANCommander.Launcher.Services.Import;
+
+public class ImportItemInfo : IImportItemInfo where T : class
+{
+ public string Key { get; set; }
+ public string Type { get; set; }
+ public string Name { get; set; }
+ public bool Processed { get; set; }
+ public T Record { get; set; }
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Models/ImportStatusUpdate.cs b/LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs
similarity index 50%
rename from LANCommander.Launcher.Models/ImportStatusUpdate.cs
rename to LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs
index 030b2a9a..55ca092a 100644
--- a/LANCommander.Launcher.Models/ImportStatusUpdate.cs
+++ b/LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs
@@ -1,8 +1,8 @@
-namespace LANCommander.Launcher.Models;
+namespace LANCommander.Launcher.Services.Import;
public class ImportStatusUpdate
{
public int Index { get; set; }
public int Total { get; set; }
- public ImportItem CurrentItem { get; set; }
+ public IImportItemInfo CurrentItem { get; set; }
}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs b/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs
new file mode 100644
index 00000000..20f38bf7
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs
@@ -0,0 +1,37 @@
+using LANCommander.Launcher.Services.Exceptions;
+
+namespace LANCommander.Launcher.Services.Import.Importers;
+
+public abstract class BaseImporter : IImporter where TRecord : class
+{
+ protected ImportContext ImportContext { get; private set; }
+
+ public void UseContext(ImportContext importContext)
+ {
+ ImportContext = importContext;
+ }
+
+ public async Task ImportAsync(IImportItemInfo importItem)
+ {
+ if (importItem is ImportItemInfo importItemInfo)
+ {
+ if (await ExistsAsync(importItemInfo.Record))
+ {
+ importItem.Processed = true;
+ return await UpdateAsync(importItemInfo.Record);
+ }
+
+ importItem.Processed = true;
+ return await AddAsync(importItemInfo.Record);
+ }
+
+ throw new ImportSkippedException(null, "Import item record is not supported by this importer.");
+ }
+
+ public abstract string GetKey(TRecord record);
+ public abstract Task> GetImportInfoAsync(TRecord record);
+ public abstract Task CanImportAsync(TRecord record);
+ public abstract Task AddAsync(TRecord record);
+ public abstract Task UpdateAsync(TRecord record);
+ public abstract Task ExistsAsync(TRecord record);
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs b/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs
new file mode 100644
index 00000000..ad52407d
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs
@@ -0,0 +1,74 @@
+using LANCommander.Launcher.Services.Exceptions;
+using LANCommander.SDK.Models.Manifest;
+using Microsoft.Extensions.Logging;
+
+namespace LANCommander.Launcher.Services.Import.Importers;
+
+public class CollectionImporter(
+ CollectionService collectionService,
+ ILogger logger) : BaseImporter
+{
+ public override async Task> GetImportInfoAsync(Collection record)
+ {
+ return new ImportItemInfo
+ {
+ Key = GetKey(record),
+ Name = record.Name,
+ Type = nameof(Collection),
+ Record = record,
+ };
+ }
+
+ public override string GetKey(Collection record) => $"{nameof(Collection)}/{record.Name}";
+
+ public override async Task CanImportAsync(Collection record)
+ {
+ var existing = await collectionService.FirstOrDefaultAsync(c => c.Name == record.Name);
+
+ if (existing == null)
+ return true;
+
+ return record.UpdatedOn > existing.ImportedOn;
+ }
+
+ public override async Task AddAsync(Collection record)
+ {
+ var collection = new Data.Models.Collection
+ {
+ Name = record.Name,
+ CreatedOn = record.CreatedOn,
+ UpdatedOn = record.UpdatedOn,
+ ImportedOn = DateTime.UtcNow,
+ };
+
+ try
+ {
+ return await collectionService.AddAsync(collection);
+ }
+ catch (Exception ex)
+ {
+ throw new ImportSkippedException(record, "An unknown error occurred while trying to add collection", ex);
+ }
+ }
+
+ public override async Task UpdateAsync(Collection record)
+ {
+ var existing = await collectionService.FirstOrDefaultAsync(c => c.Name == record.Name);
+
+ try
+ {
+ existing.Name = record.Name;
+ existing.CreatedOn = record.CreatedOn;
+ existing.UpdatedOn = record.UpdatedOn;
+ existing.ImportedOn = DateTime.UtcNow;
+
+ return existing;
+ }
+ catch (Exception ex)
+ {
+ throw new ImportSkippedException(record, "An unknown error occurred while trying to update collection", ex);
+ }
+ }
+
+ public override async Task ExistsAsync(Collection record) => await collectionService.ExistsAsync(c => c.Name == record.Name);
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs b/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs
new file mode 100644
index 00000000..d1504196
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs
@@ -0,0 +1,74 @@
+using LANCommander.Launcher.Services.Exceptions;
+using LANCommander.SDK.Models.Manifest;
+using Microsoft.Extensions.Logging;
+
+namespace LANCommander.Launcher.Services.Import.Importers;
+
+public class DeveloperImporter(
+ CompanyService companyService,
+ ILogger logger) : BaseImporter
+{
+ public override async Task> GetImportInfoAsync(Company record)
+ {
+ return new ImportItemInfo
+ {
+ Key = GetKey(record),
+ Name = record.Name,
+ Type = "Developer",
+ Record = record,
+ };
+ }
+
+ public override string GetKey(Company record) => $"{nameof(Company)}/{record.Name}";
+
+ public override async Task CanImportAsync(Company record)
+ {
+ var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name);
+
+ if (existing == null)
+ return true;
+
+ return record.UpdatedOn > existing.ImportedOn;
+ }
+
+ public override async Task AddAsync(Company record)
+ {
+ var company = new Data.Models.Company
+ {
+ Name = record.Name,
+ CreatedOn = record.CreatedOn,
+ UpdatedOn = record.UpdatedOn,
+ ImportedOn = DateTime.UtcNow,
+ };
+
+ try
+ {
+ return await companyService.AddAsync(company);
+ }
+ catch (Exception ex)
+ {
+ throw new ImportSkippedException(record, "An unknown error occurred while trying to add company", ex);
+ }
+ }
+
+ public override async Task UpdateAsync(Company record)
+ {
+ var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name);
+
+ try
+ {
+ existing.Name = record.Name;
+ existing.CreatedOn = record.CreatedOn;
+ existing.UpdatedOn = record.UpdatedOn;
+ existing.ImportedOn = DateTime.UtcNow;
+
+ return existing;
+ }
+ catch (Exception ex)
+ {
+ throw new ImportSkippedException(record, "An unknown error occurred while trying to update company", ex);
+ }
+ }
+
+ public override async Task ExistsAsync(Company record) => await companyService.ExistsAsync(c => c.Name == record.Name);
+}
\ No newline at end of file
diff --git a/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs b/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs
new file mode 100644
index 00000000..6d365f80
--- /dev/null
+++ b/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs
@@ -0,0 +1,74 @@
+using LANCommander.Launcher.Services.Exceptions;
+using LANCommander.SDK.Models.Manifest;
+using Microsoft.Extensions.Logging;
+
+namespace LANCommander.Launcher.Services.Import.Importers;
+
+public class EngineImporter(
+ EngineService engineService,
+ ILogger logger) : BaseImporter
+{
+ public override async Task