Add Discord rich presence

This commit is contained in:
Pat Hartl 2026-05-17 20:38:50 -05:00
parent 0200a6df5c
commit 8772b3a000
8 changed files with 1173 additions and 16 deletions

View file

@ -5,6 +5,7 @@
<ItemGroup Label="Aspire">
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.1.1" />
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageVersion Include="EasyMDE.Blazor" Version="1.0.5" />
<PackageVersion Include="HotAvalonia" Version="3.1.0" />
<PackageVersion Include="LiveChartsCore" Version="2.0.0-rc5.4" />

View file

@ -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<DiscordClient>();
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<DiscordClient>();
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
{

View file

@ -0,0 +1,974 @@
// <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("20260517194258_AddGameExternalIds")]
partial class AddGameExternalIds
{
/// <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("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")
.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.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>("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>("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("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
}
}
}

View file

@ -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
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GameExternalIds");
migrationBuilder.AddColumn<long>(
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");
}
}
}

View file

@ -271,9 +271,6 @@ namespace LANCommander.Launcher.Data.Migrations
b.Property<Guid?>("EngineId")
.HasColumnType("TEXT");
b.Property<long?>("IGDBId")
.HasColumnType("INTEGER");
b.Property<DateTime>("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<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.Genre", b =>
{
b.Property<Guid>("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");

View file

@ -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<DiscordClient> _logger;
private DiscordRpcClient _client;
public DiscordClient(ILogger<DiscordClient> 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) { }
}
}
}

View file

@ -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<ChatHubClient>();
services.AddSingleton<IConnectionClient, ConnectionClient>();
services.AddSingleton<DepotClient>();
services.AddSingleton<DiscordClient>();
services.AddSingleton<GameClient>();
services.AddSingleton<IssueClient>();
services.AddSingleton<LauncherClient>();

View file

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Crc32.NET" />
<PackageReference Include="DiscordRichPresence" />
<PackageReference Include="Facepunch.Steamworks" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client.SourceGenerator" />