From ac2ebeef29599d6e87f56d77bc3a3fe87baeec0e Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 19 Jan 2026 17:55:32 +1100 Subject: [PATCH] feat: Add library management (add/remove game from library) --- .../ViewModels/GameDetailViewModel.cs | 109 +++++++++++++++++- .../ViewModels/ShellViewModel.cs | 12 ++ .../Views/GameDetailView.axaml | 46 +++++++- 3 files changed, 160 insertions(+), 7 deletions(-) diff --git a/LANCommander.Launcher.Avalonia/ViewModels/GameDetailViewModel.cs b/LANCommander.Launcher.Avalonia/ViewModels/GameDetailViewModel.cs index 6bbdae53..14fbd341 100644 --- a/LANCommander.Launcher.Avalonia/ViewModels/GameDetailViewModel.cs +++ b/LANCommander.Launcher.Avalonia/ViewModels/GameDetailViewModel.cs @@ -68,7 +68,20 @@ public partial class GameDetailViewModel : ViewModelBase [ObservableProperty] private bool _isLoadingMedia; + [ObservableProperty] + private bool _isInLibrary; + + [ObservableProperty] + private bool _isAddingToLibrary; + + [ObservableProperty] + private bool _isRemovingFromLibrary; + + [ObservableProperty] + private string? _statusMessage; + public event EventHandler? BackRequested; + public event EventHandler? LibraryChanged; public GameDetailViewModel(IServiceProvider serviceProvider) { @@ -88,9 +101,14 @@ public partial class GameDetailViewModel : ViewModelBase ReleasedOn = game.ReleasedOn ?? DateTime.MinValue; ReleaseYear = game.ReleasedOn?.Year > 1 ? game.ReleasedOn.Value.Year.ToString() : "Unknown"; Singleplayer = game.Singleplayer; + StatusMessage = null; + + // Check library status + using var scope = _serviceProvider.CreateScope(); + var libraryService = scope.ServiceProvider.GetRequiredService(); + IsInLibrary = libraryService.IsInLibrary(game.Id); // Get media paths from local storage - using var scope = _serviceProvider.CreateScope(); var mediaService = scope.ServiceProvider.GetRequiredService(); BannerPath = GetLocalMediaPath(game.Media, MediaType.Cover, mediaService); @@ -145,6 +163,12 @@ public partial class GameDetailViewModel : ViewModelBase ReleasedOn = game.ReleasedOn; ReleaseYear = game.ReleasedOn.Year > 1 ? game.ReleasedOn.Year.ToString() : "Unknown"; Singleplayer = game.Singleplayer; + StatusMessage = null; + + // Check library status + using var scope = _serviceProvider.CreateScope(); + var libraryService = scope.ServiceProvider.GetRequiredService(); + IsInLibrary = libraryService.IsInLibrary(game.Id); // Reset media paths while loading BannerPath = null; @@ -192,7 +216,6 @@ public partial class GameDetailViewModel : ViewModelBase IsLoadingMedia = true; try { - using var scope = _serviceProvider.CreateScope(); var mediaClient = scope.ServiceProvider.GetRequiredService(); BannerPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Cover, mediaClient); @@ -210,6 +233,88 @@ public partial class GameDetailViewModel : ViewModelBase } } + [RelayCommand] + private async Task AddToLibraryAsync() + { + if (IsInLibrary || IsAddingToLibrary) return; + + IsAddingToLibrary = true; + StatusMessage = "Adding to library..."; + + try + { + using var scope = _serviceProvider.CreateScope(); + var importService = scope.ServiceProvider.GetRequiredService(); + var libraryService = scope.ServiceProvider.GetRequiredService(); + + _logger.LogInformation("Adding game {GameId} ({Title}) to library", Id, Title); + + // Import the game data + await importService.ImportGameAsync(Id); + + // Add to library + await libraryService.AddToLibraryAsync(Id); + + // Refresh library items + await libraryService.RefreshItemsAsync(); + + IsInLibrary = true; + StatusMessage = "Added to library!"; + _logger.LogInformation("Game {GameId} ({Title}) added to library", Id, Title); + + // Notify that library has changed + LibraryChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add game {GameId} ({Title}) to library", Id, Title); + StatusMessage = $"Failed to add to library: {ex.Message}"; + } + finally + { + IsAddingToLibrary = false; + } + } + + [RelayCommand] + private async Task RemoveFromLibraryAsync() + { + if (!IsInLibrary || IsRemovingFromLibrary) return; + + IsRemovingFromLibrary = true; + StatusMessage = "Removing from library..."; + + try + { + using var scope = _serviceProvider.CreateScope(); + var libraryService = scope.ServiceProvider.GetRequiredService(); + + _logger.LogInformation("Removing game {GameId} ({Title}) from library", Id, Title); + + // Remove from library + await libraryService.RemoveFromLibraryAsync(Id); + + // Refresh library items + await libraryService.RefreshItemsAsync(); + + IsInLibrary = false; + StatusMessage = "Removed from library"; + _logger.LogInformation("Game {GameId} ({Title}) removed from library", Id, Title); + + // Notify that library has changed + LibraryChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove game {GameId} ({Title}) from library", Id, Title); + StatusMessage = $"Failed to remove from library: {ex.Message}"; + } + finally + { + IsRemovingFromLibrary = false; + } + } + private string? GetLocalMediaPath(System.Collections.Generic.ICollection? mediaCollection, MediaType type, MediaService mediaService) { var media = mediaCollection?.FirstOrDefault(m => m.Type == type); diff --git a/LANCommander.Launcher.Avalonia/ViewModels/ShellViewModel.cs b/LANCommander.Launcher.Avalonia/ViewModels/ShellViewModel.cs index 89a006ba..0ff2377b 100644 --- a/LANCommander.Launcher.Avalonia/ViewModels/ShellViewModel.cs +++ b/LANCommander.Launcher.Avalonia/ViewModels/ShellViewModel.cs @@ -51,6 +51,7 @@ public partial class ShellViewModel : ViewModelBase GamesListViewModel.GameSelected += OnGameSelected; GameDetailViewModel.BackRequested += OnBackFromGameDetail; + GameDetailViewModel.LibraryChanged += OnLibraryChanged; // Import library from server and load data await ImportAndLoadAsync(); @@ -148,4 +149,15 @@ public partial class ShellViewModel : ViewModelBase { ShowDepot(); } + + private async void OnLibraryChanged(object? sender, EventArgs e) + { + _logger.LogInformation("Library changed, refreshing sidebar and games list..."); + + // Refresh the sidebar to show updated library + await Sidebar.LoadAsync(); + + // Refresh the games list to update "In Library" status + await GamesListViewModel.LoadGamesAsync(); + } } diff --git a/LANCommander.Launcher.Avalonia/Views/GameDetailView.axaml b/LANCommander.Launcher.Avalonia/Views/GameDetailView.axaml index ae0a8849..7775d2cd 100644 --- a/LANCommander.Launcher.Avalonia/Views/GameDetailView.axaml +++ b/LANCommander.Launcher.Avalonia/Views/GameDetailView.axaml @@ -169,11 +169,47 @@ Padding="16" CornerRadius="8" Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"> - - - + + + + + + + + + + + + + + + + + + + +