diff --git a/Directory.Packages.props b/Directory.Packages.props index 54187def..9b95c44a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/LANCommander.Launcher.Avalonia/ViewModels/Components/GameActionBarViewModel.cs b/LANCommander.Launcher.Avalonia/ViewModels/Components/GameActionBarViewModel.cs index 992352a7..9c183c7a 100644 --- a/LANCommander.Launcher.Avalonia/ViewModels/Components/GameActionBarViewModel.cs +++ b/LANCommander.Launcher.Avalonia/ViewModels/Components/GameActionBarViewModel.cs @@ -13,6 +13,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LANCommander.Launcher.Avalonia.Views; +using LANCommander.SDK.Clients; using LANCommander.Launcher.Data.Models; using LANCommander.Launcher.Services; using LANCommander.Launcher.Services.PowerShell; @@ -500,6 +501,8 @@ public partial class GameActionBarViewModel : ViewModelBase { StatusMessage = "Starting..."; + var discordClient = _serviceProvider.GetRequiredService(); + try { using var scope = _serviceProvider.CreateScope(); @@ -524,6 +527,11 @@ public partial class GameActionBarViewModel : ViewModelBase _logger.LogInformation("Running action {ActionName} for game {GameId}", primaryAction.Name, GameId); + var discordAppId = localGame.ExternalIds? + .FirstOrDefault(e => string.Equals(e.Provider, "Discord", StringComparison.OrdinalIgnoreCase)) + ?.ExternalId; + discordClient.UpdatePresence(localGame.Title, discordAppId); + // Run the game await gameService.Run(localGame, primaryAction); @@ -537,6 +545,7 @@ public partial class GameActionBarViewModel : ViewModelBase } finally { + discordClient.ClearPresence(); IsStarting = false; } } @@ -552,6 +561,8 @@ public partial class GameActionBarViewModel : ViewModelBase IsStarting = true; StatusMessage = $"Starting {action.Name}..."; + var discordClient = _serviceProvider.GetRequiredService(); + try { using var scope = _serviceProvider.CreateScope(); @@ -565,6 +576,11 @@ public partial class GameActionBarViewModel : ViewModelBase _logger.LogInformation("Running action {ActionName} for game {GameId}", action.Name, GameId); + var discordAppId = localGame.ExternalIds? + .FirstOrDefault(e => string.Equals(e.Provider, "Discord", StringComparison.OrdinalIgnoreCase)) + ?.ExternalId; + discordClient.UpdatePresence(localGame.Title, discordAppId); + // Run the game with the specific action await gameService.Run(localGame, action); @@ -578,6 +594,7 @@ public partial class GameActionBarViewModel : ViewModelBase } finally { + discordClient.ClearPresence(); IsStarting = false; } } @@ -1045,6 +1062,7 @@ public partial class GameActionBarViewModel : ViewModelBase { _logger.LogError(ex, "Failed to add game {GameId} ({Title}) to library", GameId, Title); StatusMessage = $"Failed to add: {ex.Message}"; + await Views.AlertOverlay.ShowAsync("Failed to Add to Library", ex.Message); } finally { @@ -1078,6 +1096,7 @@ public partial class GameActionBarViewModel : ViewModelBase { _logger.LogError(ex, "Failed to remove game {GameId} ({Title}) from library", GameId, Title); StatusMessage = $"Failed to remove: {ex.Message}"; + await Views.AlertOverlay.ShowAsync("Failed to Remove from Library", ex.Message); } finally { diff --git a/LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.Designer.cs b/LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.Designer.cs new file mode 100644 index 00000000..61cb0fcb --- /dev/null +++ b/LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.Designer.cs @@ -0,0 +1,974 @@ +// +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("20260517194258_AddGameExternalIds")] + partial class AddGameExternalIds + { + /// + 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("GameTool", b => + { + b.Property("GamesId") + .HasColumnType("TEXT"); + + b.Property("ToolsId") + .HasColumnType("TEXT"); + + b.HasKey("GamesId", "ToolsId"); + + b.HasIndex("ToolsId"); + + b.ToTable("GameTool"); + }); + + 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("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.GameExternalId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("GameExternalIds"); + }); + + 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("SortOrder") + .HasColumnType("INTEGER"); + + 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.Tool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + 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("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Tools"); + }); + + 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("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") + .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.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("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/20260513000000_AddGameExternalIds.cs b/LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.cs similarity index 86% rename from LANCommander.Launcher.Data/Migrations/20260513000000_AddGameExternalIds.cs rename to LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.cs index 701188a6..7c0b76cb 100644 --- a/LANCommander.Launcher.Data/Migrations/20260513000000_AddGameExternalIds.cs +++ b/LANCommander.Launcher.Data/Migrations/20260517194258_AddGameExternalIds.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -41,9 +41,9 @@ namespace LANCommander.Launcher.Data.Migrations // Migrate existing IGDBId data migrationBuilder.Sql(@" - INSERT INTO GameExternalIds (Id, GameId, Provider, ExternalId, CreatedOn, UpdatedOn) + INSERT INTO GameExternalIds (Id, GameId, Provider, ExternalId, ImportedOn, CreatedOn, UpdatedOn) SELECT lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), - Id, 'IGDB', CAST(IGDBId AS TEXT), CreatedOn, UpdatedOn + Id, 'IGDB', CAST(IGDBId AS TEXT), CreatedOn, CreatedOn, UpdatedOn FROM Games WHERE IGDBId IS NOT NULL "); @@ -56,21 +56,14 @@ namespace LANCommander.Launcher.Data.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "GameExternalIds"); + migrationBuilder.AddColumn( name: "IGDBId", table: "Games", type: "INTEGER", nullable: true); - - // Migrate data back - migrationBuilder.Sql(@" - UPDATE Games - SET IGDBId = CAST((SELECT ExternalId FROM GameExternalIds WHERE GameExternalIds.GameId = Games.Id AND Provider = 'IGDB' LIMIT 1) AS INTEGER) - WHERE Id IN (SELECT GameId FROM GameExternalIds WHERE Provider = 'IGDB') - "); - - migrationBuilder.DropTable( - name: "GameExternalIds"); } } } diff --git a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs index b27aeea4..fba57e51 100644 --- a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -271,9 +271,6 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("EngineId") .HasColumnType("TEXT"); - b.Property("IGDBId") - .HasColumnType("INTEGER"); - b.Property("ImportedOn") .HasColumnType("TEXT"); @@ -323,6 +320,41 @@ namespace LANCommander.Launcher.Data.Migrations b.ToTable("Games"); }); + modelBuilder.Entity("LANCommander.Launcher.Data.Models.GameExternalId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("GameExternalIds"); + }); + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Genre", b => { b.Property("Id") @@ -832,6 +864,16 @@ namespace LANCommander.Launcher.Data.Migrations 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.Library", b => { b.HasOne("LANCommander.Launcher.Data.Models.User", "User") @@ -908,6 +950,8 @@ namespace LANCommander.Launcher.Data.Migrations { b.Navigation("DependentGames"); + b.Navigation("ExternalIds"); + b.Navigation("Media"); b.Navigation("MultiplayerModes"); diff --git a/LANCommander.SDK/Clients/DiscordClient.cs b/LANCommander.SDK/Clients/DiscordClient.cs new file mode 100644 index 00000000..bb8154c5 --- /dev/null +++ b/LANCommander.SDK/Clients/DiscordClient.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using DiscordRPC; +using DiscordRPC.Logging; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Clients +{ + public class DiscordClient : IDisposable + { + private const string DefaultApplicationId = "1272690976791461950"; + + private readonly ILogger _logger; + + private DiscordRpcClient _client; + + public DiscordClient(ILogger logger) + { + _logger = logger; + } + + public void UpdatePresence(Game game) + { + var discordAppId = game.ExternalIds? + .FirstOrDefault(e => string.Equals(e.Provider, "Discord", StringComparison.OrdinalIgnoreCase)) + ?.ExternalId; + + UpdatePresence(game.Title, discordAppId); + } + + public void UpdatePresence(string gameTitle, string discordAppId = null) + { + try + { + InitializeClient(discordAppId ?? DefaultApplicationId); + + var presence = new RichPresence() + .WithDetails(gameTitle) + .WithTimestamps(new Timestamps(DateTime.UtcNow)); + + if (discordAppId != null) + { + presence.WithAssets(new Assets() + { + LargeImageKey = "logo", + LargeImageText = gameTitle + }); + } + + _client?.SetPresence(presence); + + _logger.LogDebug("Discord presence updated for game {GameTitle}", gameTitle); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update Discord presence for game {GameTitle}", gameTitle); + } + } + + public void ClearPresence() + { + try + { + _client?.ClearPresence(); + _client?.Dispose(); + _client = null; + + _logger.LogDebug("Discord presence cleared"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to clear Discord presence"); + } + } + + private void InitializeClient(string applicationId) + { + if (_client != null) + { + if (_client.ApplicationID != applicationId) + { + _client.ClearPresence(); + _client.Dispose(); + _client = null; + } + else + { + return; + } + } + + _client = new DiscordRpcClient(applicationId) + { + Logger = new NullLogger() + }; + + _client.OnError += (sender, args) => + { + _logger.LogWarning("Discord RPC error: {Message}", args.Message); + }; + + _client.Initialize(); + } + + public void Dispose() + { + _client?.ClearPresence(); + _client?.Dispose(); + _client = null; + } + + private class NullLogger : DiscordRPC.Logging.ILogger + { + public DiscordRPC.Logging.LogLevel Level { get; set; } = DiscordRPC.Logging.LogLevel.None; + + public void Error(string message, params object[] args) { } + public void Info(string message, params object[] args) { } + public void Trace(string message, params object[] args) { } + public void Warning(string message, params object[] args) { } + } + } +} diff --git a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs index 9d8f609f..0958407a 100644 --- a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs +++ b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Clients; using LANCommander.SDK.Factories; using LANCommander.SDK.Models; using LANCommander.SDK.PowerShell; @@ -37,6 +38,7 @@ public static class IServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index aa6740d9..f0860c35 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -18,6 +18,7 @@ +