Track tools per game

Ref #414
This commit is contained in:
Pat Hartl 2026-06-27 11:51:55 -05:00
parent fb0df46572
commit b21efc63d0
13 changed files with 1405 additions and 108 deletions

View file

@ -139,6 +139,14 @@ namespace LANCommander.Launcher.Data
gr => gr.HasOne<Game>().WithMany().HasForeignKey("GameId")
);
builder.Entity<Game>()
.HasMany(g => g.Tools)
.WithMany(t => t.Games)
.UsingEntity<GameTool>(
gt => gt.HasOne(x => x.Tool).WithMany(t => t.GameTools).HasForeignKey(x => x.ToolId).OnDelete(DeleteBehavior.Cascade),
gt => gt.HasOne(x => x.Game).WithMany(g => g.GameTools).HasForeignKey(x => x.GameId).OnDelete(DeleteBehavior.Cascade),
gt => gt.HasKey(x => new { x.GameId, x.ToolId }));
builder.Entity<Game>()
.HasMany(g => g.DependentGames)
.WithOne(g => g.BaseGame)

View file

@ -0,0 +1,985 @@
// <auto-generated />
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("20260627164452_AddPerGameToolInstallState")]
partial class AddPerGameToolInstallState
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("CategoryGame", b =>
{
b.Property<Guid>("CategoriesId")
.HasColumnType("TEXT");
b.Property<Guid>("GamesId")
.HasColumnType("TEXT");
b.HasKey("CategoriesId", "GamesId");
b.HasIndex("GamesId");
b.ToTable("CategoryGame");
});
modelBuilder.Entity("CollectionGame", b =>
{
b.Property<Guid>("CollectionId")
.HasColumnType("TEXT");
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.HasKey("CollectionId", "GameId");
b.HasIndex("GameId");
b.ToTable("CollectionGame");
});
modelBuilder.Entity("GameDeveloper", b =>
{
b.Property<Guid>("DeveloperId")
.HasColumnType("TEXT");
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.HasKey("DeveloperId", "GameId");
b.HasIndex("GameId");
b.ToTable("GameDeveloper");
});
modelBuilder.Entity("GameGenre", b =>
{
b.Property<Guid>("GamesId")
.HasColumnType("TEXT");
b.Property<Guid>("GenresId")
.HasColumnType("TEXT");
b.HasKey("GamesId", "GenresId");
b.HasIndex("GenresId");
b.ToTable("GameGenre");
});
modelBuilder.Entity("GamePlatform", b =>
{
b.Property<Guid>("GamesId")
.HasColumnType("TEXT");
b.Property<Guid>("PlatformsId")
.HasColumnType("TEXT");
b.HasKey("GamesId", "PlatformsId");
b.HasIndex("PlatformsId");
b.ToTable("GamePlatform");
});
modelBuilder.Entity("GamePublisher", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("PublisherId")
.HasColumnType("TEXT");
b.HasKey("GameId", "PublisherId");
b.HasIndex("PublisherId");
b.ToTable("GamePublisher");
});
modelBuilder.Entity("GameRedistributable", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("RedistributableId")
.HasColumnType("TEXT");
b.HasKey("GameId", "RedistributableId");
b.HasIndex("RedistributableId");
b.ToTable("GameRedistributable");
});
modelBuilder.Entity("GameTag", b =>
{
b.Property<Guid>("GamesId")
.HasColumnType("TEXT");
b.Property<Guid>("TagsId")
.HasColumnType("TEXT");
b.HasKey("GamesId", "TagsId");
b.HasIndex("TagsId");
b.ToTable("GameTag");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("ParentId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("Categories");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Collection", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Collections");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Company", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Companies");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Engine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Engines");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("BaseGameId")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<Guid?>("EngineId")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("InstallDirectory")
.HasColumnType("TEXT");
b.Property<bool>("Installed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("InstalledOn")
.HasColumnType("TEXT");
b.Property<string>("InstalledVersion")
.HasColumnType("TEXT");
b.Property<string>("LatestVersion")
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<DateTime?>("ReleasedOn")
.HasColumnType("TEXT");
b.Property<bool>("Singleplayer")
.HasColumnType("INTEGER");
b.Property<string>("SortTitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BaseGameId");
b.HasIndex("EngineId");
b.ToTable("Games");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameExternalId", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("ExternalId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Provider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GameId");
b.ToTable("GameExternalIds");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameTool", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("ToolId")
.HasColumnType("TEXT");
b.Property<string>("InstallDirectory")
.HasColumnType("TEXT");
b.Property<bool>("Installed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("InstalledOn")
.HasColumnType("TEXT");
b.Property<string>("InstalledVersion")
.HasColumnType("TEXT");
b.HasKey("GameId", "ToolId");
b.HasIndex("ToolId");
b.ToTable("GameTool");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Genre", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Genres");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("Libraries");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Media", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Crc32")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("FileId")
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("MimeType")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("SourceUrl")
.HasMaxLength(2048)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<int>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("MinPlayers")
.HasColumnType("INTEGER");
b.Property<int>("NetworkProtocol")
.HasColumnType("INTEGER");
b.Property<int>("Spectators")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GameId");
b.ToTable("MultiplayerModes");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Platform", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Platforms");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.PlaySession", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime?>("End")
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<DateTime?>("Start")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GameId");
b.ToTable("PlaySessions");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Redistributable", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Redistributables");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Tags");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tool", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("LatestVersion")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Tools");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Alias")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("LibraryGame", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("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.GameExternalId", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
.WithMany("ExternalIds")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Game");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameTool", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
.WithMany("GameTools")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Launcher.Data.Models.Tool", "Tool")
.WithMany("GameTools")
.HasForeignKey("ToolId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Tool");
});
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("ExternalIds");
b.Navigation("GameTools");
b.Navigation("Media");
b.Navigation("MultiplayerModes");
b.Navigation("PlaySessions");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tool", b =>
{
b.Navigation("GameTools");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b =>
{
b.Navigation("Avatar");
b.Navigation("Library");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,199 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Launcher.Data.Migrations
{
/// <inheritdoc />
public partial class AddPerGameToolInstallState : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_GameTool_Games_GamesId",
table: "GameTool");
migrationBuilder.DropForeignKey(
name: "FK_GameTool_Tools_ToolsId",
table: "GameTool");
migrationBuilder.RenameColumn(
name: "ToolsId",
table: "GameTool",
newName: "ToolId");
migrationBuilder.RenameColumn(
name: "GamesId",
table: "GameTool",
newName: "GameId");
migrationBuilder.RenameIndex(
name: "IX_GameTool_ToolsId",
table: "GameTool",
newName: "IX_GameTool_ToolId");
migrationBuilder.AddColumn<string>(
name: "InstallDirectory",
table: "GameTool",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "Installed",
table: "GameTool",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "InstalledOn",
table: "GameTool",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "InstalledVersion",
table: "GameTool",
type: "TEXT",
nullable: true);
// Tool install state was previously tracked globally on the Tools table. Migrate it onto
// the join row for the game the tool was actually installed under (the game whose install
// directory matches the tool's recorded directory) before the old columns are dropped.
migrationBuilder.Sql(@"
UPDATE GameTool
SET
Installed = 1,
InstallDirectory = (SELECT t.InstallDirectory FROM Tools t WHERE t.Id = GameTool.ToolId),
InstalledVersion = (SELECT t.InstalledVersion FROM Tools t WHERE t.Id = GameTool.ToolId),
InstalledOn = (SELECT t.InstalledOn FROM Tools t WHERE t.Id = GameTool.ToolId)
WHERE EXISTS (
SELECT 1
FROM Tools t
JOIN Games g ON g.Id = GameTool.GameId
WHERE t.Id = GameTool.ToolId
AND t.Installed = 1
AND t.InstallDirectory IS NOT NULL
AND t.InstallDirectory = g.InstallDirectory
);");
migrationBuilder.DropColumn(
name: "InstallDirectory",
table: "Tools");
migrationBuilder.DropColumn(
name: "Installed",
table: "Tools");
migrationBuilder.DropColumn(
name: "InstalledOn",
table: "Tools");
migrationBuilder.DropColumn(
name: "InstalledVersion",
table: "Tools");
migrationBuilder.AddForeignKey(
name: "FK_GameTool_Games_GameId",
table: "GameTool",
column: "GameId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_GameTool_Tools_ToolId",
table: "GameTool",
column: "ToolId",
principalTable: "Tools",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_GameTool_Games_GameId",
table: "GameTool");
migrationBuilder.DropForeignKey(
name: "FK_GameTool_Tools_ToolId",
table: "GameTool");
migrationBuilder.DropColumn(
name: "InstallDirectory",
table: "GameTool");
migrationBuilder.DropColumn(
name: "Installed",
table: "GameTool");
migrationBuilder.DropColumn(
name: "InstalledOn",
table: "GameTool");
migrationBuilder.DropColumn(
name: "InstalledVersion",
table: "GameTool");
migrationBuilder.RenameColumn(
name: "ToolId",
table: "GameTool",
newName: "ToolsId");
migrationBuilder.RenameColumn(
name: "GameId",
table: "GameTool",
newName: "GamesId");
migrationBuilder.RenameIndex(
name: "IX_GameTool_ToolId",
table: "GameTool",
newName: "IX_GameTool_ToolsId");
migrationBuilder.AddColumn<string>(
name: "InstallDirectory",
table: "Tools",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "Installed",
table: "Tools",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "InstalledOn",
table: "Tools",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "InstalledVersion",
table: "Tools",
type: "TEXT",
nullable: true);
migrationBuilder.AddForeignKey(
name: "FK_GameTool_Games_GamesId",
table: "GameTool",
column: "GamesId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_GameTool_Tools_ToolsId",
table: "GameTool",
column: "ToolsId",
principalTable: "Tools",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -137,21 +137,6 @@ namespace LANCommander.Launcher.Data.Migrations
b.ToTable("GameTag");
});
modelBuilder.Entity("GameTool", b =>
{
b.Property<Guid>("GamesId")
.HasColumnType("TEXT");
b.Property<Guid>("ToolsId")
.HasColumnType("TEXT");
b.HasKey("GamesId", "ToolsId");
b.HasIndex("ToolsId");
b.ToTable("GameTool");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
{
b.Property<Guid>("Id")
@ -355,6 +340,33 @@ namespace LANCommander.Launcher.Data.Migrations
b.ToTable("GameExternalIds");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameTool", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("ToolId")
.HasColumnType("TEXT");
b.Property<string>("InstallDirectory")
.HasColumnType("TEXT");
b.Property<bool>("Installed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("InstalledOn")
.HasColumnType("TEXT");
b.Property<string>("InstalledVersion")
.HasColumnType("TEXT");
b.HasKey("GameId", "ToolId");
b.HasIndex("ToolId");
b.ToTable("GameTool");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Genre", b =>
{
b.Property<Guid>("Id")
@ -632,18 +644,6 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property<DateTime>("ImportedOn")
.HasColumnType("TEXT");
b.Property<string>("InstallDirectory")
.HasColumnType("TEXT");
b.Property<bool>("Installed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("InstalledOn")
.HasColumnType("TEXT");
b.Property<string>("InstalledVersion")
.HasColumnType("TEXT");
b.Property<string>("LatestVersion")
.HasColumnType("TEXT");
@ -823,21 +823,6 @@ namespace LANCommander.Launcher.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("GameTool", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.Game", null)
.WithMany()
.HasForeignKey("GamesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Launcher.Data.Models.Tool", null)
.WithMany()
.HasForeignKey("ToolsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.Category", "Parent")
@ -874,6 +859,25 @@ namespace LANCommander.Launcher.Data.Migrations
b.Navigation("Game");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameTool", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game")
.WithMany("GameTools")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Launcher.Data.Models.Tool", "Tool")
.WithMany("GameTools")
.HasForeignKey("ToolId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Tool");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b =>
{
b.HasOne("LANCommander.Launcher.Data.Models.User", "User")
@ -952,6 +956,8 @@ namespace LANCommander.Launcher.Data.Migrations
b.Navigation("ExternalIds");
b.Navigation("GameTools");
b.Navigation("Media");
b.Navigation("MultiplayerModes");
@ -959,6 +965,11 @@ namespace LANCommander.Launcher.Data.Migrations
b.Navigation("PlaySessions");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tool", b =>
{
b.Navigation("GameTools");
});
modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b =>
{
b.Navigation("Avatar");

View file

@ -44,6 +44,7 @@ namespace LANCommander.Launcher.Data.Models
public virtual ICollection<Platform>? Platforms { get; set; } = new List<Platform>();
public virtual ICollection<Redistributable>? Redistributables { get; set; } = new List<Redistributable>();
public virtual ICollection<Tool>? Tools { get; set; } = new List<Tool>();
public virtual ICollection<GameTool> GameTools { get; set; } = new List<GameTool>();
public virtual ICollection<Media>? Media { get; set; } = new List<Media>();
public virtual ICollection<Collection> Collections { get; set; } = new List<Collection>();
public virtual ICollection<Game> DependentGames { get; set; } = new List<Game>();

View file

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LANCommander.Launcher.Data.Models
{
/// <summary>
/// Join entity between <see cref="Game"/> and <see cref="Tool"/> that tracks the install
/// state of a tool for a specific game. Because a tool can be shared by multiple games and is
/// installed into each game's own directory, the install state must be tracked per game rather
/// than on the tool itself.
/// </summary>
[Table("GameTool")]
public class GameTool
{
public Guid GameId { get; set; }
public virtual Game Game { get; set; }
public Guid ToolId { get; set; }
public virtual Tool Tool { get; set; }
public bool Installed { get; set; }
public string? InstallDirectory { get; set; }
public string? InstalledVersion { get; set; }
public DateTime? InstalledOn { get; set; }
}
}

View file

@ -8,13 +8,10 @@ namespace LANCommander.Launcher.Data.Models
public string Name { get; set; }
public string? Description { get; set; }
public string? Notes { get; set; }
public bool Installed { get; set; }
public string? InstallDirectory { get; set; }
public string? InstalledVersion { get; set; }
public DateTime? InstalledOn { get; set; }
public string? LatestVersion { get; set; }
public virtual ICollection<Game>? Games { get; set; } = new List<Game>();
public virtual ICollection<GameTool> GameTools { get; set; } = new List<GameTool>();
}
}

View file

@ -70,29 +70,23 @@ namespace LANCommander.Launcher.Services
}
}
// Uninstall any tools that were installed alongside this game. Tools are
// installed into the game's directory, so this must happen before the
// install directory is cleared in ClearGameState.
var installedTools = await toolService
.Query(t => t.Installed && t.Games.Any(g => g.Id == game.Id))
.ToListAsync();
// Uninstall any tools that were installed for this game. Tools are installed
// into the game's own directory and tracked per game, so uninstalling this
// game only removes its copy and leaves the tool intact for other games that
// share it. This must run before ClearGameState clears the install directory.
var installedTools = await toolService.GetInstalledToolsForGameAsync(game.Id);
foreach (var tool in installedTools)
foreach (var gameTool in installedTools)
{
try
{
await toolClient.UninstallAsync(game.InstallDirectory, tool.Id);
await toolClient.UninstallAsync(game.InstallDirectory, gameTool.ToolId);
tool.Installed = false;
tool.InstallDirectory = null;
tool.InstalledVersion = null;
tool.InstalledOn = null;
await toolService.UpdateAsync(tool);
await toolService.SetToolUninstalledAsync(game.Id, gameTool.ToolId);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not uninstall tool {ToolId} from game {GameId}", tool.Id, game.Id);
Logger?.LogError(ex, "Could not uninstall tool {ToolId} from game {GameId}", gameTool.ToolId, game.Id);
}
}

View file

@ -8,7 +8,6 @@ namespace LANCommander.Launcher.Services.Import.Importers;
public class ToolImporter(
ToolService toolService,
LibraryService libraryService,
ILogger<ToolImporter> logger) : BaseImporter<Tool>
{
public override async Task<ImportItemInfo<Tool>> GetImportInfoAsync(Tool record, BaseManifest manifest) =>
@ -77,9 +76,14 @@ public class ToolImporter(
await toolService.UpdateAsync(existing);
await UpdateRelationships(importItemInfo.Record);
if (await libraryService.IsInstalledAsync(existing.Id) && existing.LatestVersion == existing.InstalledVersion)
await ManifestHelper.WriteAsync(importItemInfo.Record, existing.InstallDirectory);
// Refresh the on-disk manifest for every game this tool is installed for and up to date
// on. Install state is tracked per game because a tool can be shared by several games.
foreach (var gameTool in await toolService.GetInstalledGameToolsAsync(existing.Id))
{
if (existing.LatestVersion == gameTool.InstalledVersion && !string.IsNullOrEmpty(gameTool.InstallDirectory))
await ManifestHelper.WriteAsync(importItemInfo.Record, gameTool.InstallDirectory);
}
return true;
}

View file

@ -600,28 +600,22 @@ namespace LANCommander.Launcher.Services
await _gameClient.RestoreFilesAsync(localGame.InstallDirectory, localGame.Id, uninstallResult.FileList, installResult.FileList);
// Uninstall any tools that were deselected. Selected tools are installed
// via their own queue items, so we only handle removal here.
// via their own queue items, so we only handle removal here. Tool install
// state is tracked per game, so this only affects this game's copy.
var selectedToolIds = queueItem.ToolIds ?? [];
var toolsToRemove = await _toolService
.Query(t => t.Installed && t.Games.Any(g => g.Id == localGame.Id))
.ToListAsync();
var installedTools = await _toolService.GetInstalledToolsForGameAsync(localGame.Id);
foreach (var localTool in toolsToRemove.Where(t => !selectedToolIds.Contains(t.Id)))
foreach (var gameTool in installedTools.Where(gt => !selectedToolIds.Contains(gt.ToolId)))
{
try
{
await _toolClient.UninstallAsync(localGame.InstallDirectory, localTool.Id);
await _toolClient.UninstallAsync(localGame.InstallDirectory, gameTool.ToolId);
localTool.Installed = false;
localTool.InstallDirectory = null;
localTool.InstalledVersion = null;
localTool.InstalledOn = null;
await _toolService.UpdateAsync(localTool);
await _toolService.SetToolUninstalledAsync(localGame.Id, gameTool.ToolId);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not uninstall tool {ToolId} from game {GameId}", localTool.Id, localGame.Id);
Logger?.LogError(ex, "Could not uninstall tool {ToolId} from game {GameId}", gameTool.ToolId, localGame.Id);
}
}
@ -687,7 +681,10 @@ namespace LANCommander.Launcher.Services
return;
}
if (localTool.Installed)
var alreadyInstalled = queueItem.DependsOnId.HasValue
&& await _toolService.IsToolInstalledForGameAsync(queueItem.DependsOnId.Value, localTool.Id);
if (alreadyInstalled)
{
// Modify — currently no-op
}
@ -871,6 +868,19 @@ namespace LANCommander.Launcher.Services
// Update manifest and scripts on disk
await _gameClient.UpdateGameInstallationAsync(localGame.InstallDirectory, remoteGame);
// Bug #1 convergence: after applying all updates and re-importing, the installed
// version may still trail the server's resolved latest version (the last applied
// archive's version string is not guaranteed to equal the canonical latest version).
// Converge explicitly so the game leaves the "update available" state.
if (!string.IsNullOrWhiteSpace(localGame.LatestVersion))
{
localGame.InstalledVersion = localGame.LatestVersion;
await _gameService.UpdateAsync(localGame);
Logger?.LogInformation("[InstallQueue] Update(Game): Converged InstalledVersion to LatestVersion {LatestVersion} for {Title}",
localGame.LatestVersion, currentItem.Title);
}
}
else
{
@ -937,6 +947,8 @@ namespace LANCommander.Launcher.Services
currentItem.Status = InstallStatus.Downloading;
OnQueueChanged?.Invoke();
string toolInstallDirectory = null;
try
{
var planItem = new InstallPlanItem
@ -950,7 +962,7 @@ namespace LANCommander.Launcher.Services
var result = await _toolClient.ExecuteInstallPlanItemAsync(planItem, currentItem.CancellationToken.Token);
UpdateToolState(currentItem, localTool, result.InstallDirectory);
toolInstallDirectory = result.InstallDirectory;
}
catch (InstallCanceledException ex)
{
@ -978,7 +990,12 @@ namespace LANCommander.Launcher.Services
try
{
await _toolService.UpdateAsync(localTool);
// Install state is tracked per game because a tool can be shared by several
// games and is installed into each game's own directory.
if (currentItem.DependsOnId.HasValue)
await _toolService.SetToolInstalledAsync(currentItem.DependsOnId.Value, localTool.Id, toolInstallDirectory, currentItem.Version);
else
Logger?.LogWarning("Tool {ToolName} ({ToolId}) was installed without an associated game; install state not recorded", localTool.Name, localTool.Id);
}
catch (Exception ex)
{
@ -1200,14 +1217,6 @@ namespace LANCommander.Launcher.Services
}
}
private static void UpdateToolState(InstallQueueTool currentItem, Tool localTool, string installDirectory)
{
localTool.InstallDirectory = installDirectory;
localTool.Installed = true;
localTool.InstalledVersion = currentItem.Version;
localTool.InstalledOn ??= DateTime.Now;
}
public async Task Move(IInstallQueueItem currentItem, Game localGame, SDK.Models.Game remoteGame)
{
using (var operation = Logger.BeginOperation("Moving game {GameTitle} ({GameId}) to {Destination}", localGame.Title, localGame.Id, currentItem.InstallDirectory))

View file

@ -1,5 +1,6 @@
using LANCommander.Launcher.Data;
using LANCommander.Launcher.Data;
using LANCommander.Launcher.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace LANCommander.Launcher.Services
@ -8,5 +9,75 @@ namespace LANCommander.Launcher.Services
ILogger<ToolService> logger,
DatabaseContext dbContext) : BaseDatabaseService<Tool>(dbContext, logger)
{
/// <summary>
/// Returns the install-state join rows for every tool currently installed for the given game.
/// The related <see cref="Tool"/> is eagerly loaded.
/// </summary>
public async Task<List<GameTool>> GetInstalledToolsForGameAsync(Guid gameId)
{
return await Context.Set<GameTool>()
.Include(gt => gt.Tool)
.Where(gt => gt.GameId == gameId && gt.Installed)
.ToListAsync();
}
/// <summary>
/// Returns the install-state join rows for every game the given tool is installed for.
/// </summary>
public async Task<List<GameTool>> GetInstalledGameToolsAsync(Guid toolId)
{
return await Context.Set<GameTool>()
.Where(gt => gt.ToolId == toolId && gt.Installed)
.ToListAsync();
}
public async Task<bool> IsToolInstalledForGameAsync(Guid gameId, Guid toolId)
{
return await Context.Set<GameTool>()
.AnyAsync(gt => gt.GameId == gameId && gt.ToolId == toolId && gt.Installed);
}
/// <summary>
/// Records that a tool has been installed for a specific game. Creates the join row if the
/// game/tool association does not yet exist locally.
/// </summary>
public async Task SetToolInstalledAsync(Guid gameId, Guid toolId, string installDirectory, string version)
{
var gameTool = await Context.Set<GameTool>()
.FirstOrDefaultAsync(gt => gt.GameId == gameId && gt.ToolId == toolId);
if (gameTool == null)
{
gameTool = new GameTool { GameId = gameId, ToolId = toolId };
await Context.Set<GameTool>().AddAsync(gameTool);
}
gameTool.Installed = true;
gameTool.InstallDirectory = installDirectory;
gameTool.InstalledVersion = version;
gameTool.InstalledOn ??= DateTime.Now;
await Context.SaveChangesAsync();
}
/// <summary>
/// Clears the install state for a tool on a specific game without removing the game/tool
/// association. Other games keep their own install state.
/// </summary>
public async Task SetToolUninstalledAsync(Guid gameId, Guid toolId)
{
var gameTool = await Context.Set<GameTool>()
.FirstOrDefaultAsync(gt => gt.GameId == gameId && gt.ToolId == toolId);
if (gameTool == null)
return;
gameTool.Installed = false;
gameTool.InstallDirectory = null;
gameTool.InstalledVersion = null;
gameTool.InstalledOn = null;
await Context.SaveChangesAsync();
}
}
}

View file

@ -1062,7 +1062,7 @@ public partial class GameActionBarViewModel : ViewModelBase, IDisposable
var localGame = await dbContext.Set<Data.Models.Game>()
.Include(g => g.DependentGames)
.Include(g => g.Tools)
.Include(g => g.GameTools)
.FirstOrDefaultAsync(g => g.Id == GameId);
if (localGame == null)
@ -1099,11 +1099,11 @@ public partial class GameActionBarViewModel : ViewModelBase, IDisposable
.Where(a => a.Installed)
.Select(a => a.Id));
// Build set of currently installed tool IDs
// Build set of currently installed tool IDs (tracked per game)
var installedToolIds = new HashSet<Guid>(
(localGame.Tools ?? [])
.Where(t => t.Installed)
.Select(t => t.Id));
(localGame.GameTools ?? [])
.Where(gt => gt.Installed)
.Select(gt => gt.ToolId));
// ── Build options VM ───────────────────────────────────────────────
var optionsVm = new InstallOptionsViewModel();

View file

@ -523,8 +523,8 @@ public partial class GameDetailViewModel : ViewModelBase
{
foreach (var tool in tools)
{
var localTool = await toolService.GetAsync(tool.Id);
Tools.Add(new ToolItemViewModel(tool, localTool));
var isInstalled = await toolService.IsToolInstalledForGameAsync(game.Id, tool.Id);
Tools.Add(new ToolItemViewModel(tool, isInstalled));
}
}
}
@ -559,17 +559,10 @@ public partial class ToolItemViewModel : ViewModelBase
public string Name { get; }
public bool IsInstalled { get; }
public ToolItemViewModel(SDK.Models.Tool tool, Data.Models.Tool? localTool)
public ToolItemViewModel(SDK.Models.Tool tool, bool isInstalled)
{
Id = tool.Id;
Name = tool.Name ?? "Unknown Tool";
IsInstalled = localTool?.Installed ?? false;
}
public ToolItemViewModel(Data.Models.Tool tool)
{
Id = tool.Id;
Name = tool.Name ?? "Unknown Tool";
IsInstalled = tool.Installed;
IsInstalled = isInstalled;
}
}