feat: Add library management (add/remove game from library)

This commit is contained in:
Aaron Powell 2026-01-19 17:55:32 +11:00
parent e3b3d0ab90
commit ac2ebeef29
3 changed files with 160 additions and 7 deletions

View file

@ -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<LibraryService>();
IsInLibrary = libraryService.IsInLibrary(game.Id);
// Get media paths from local storage
using var scope = _serviceProvider.CreateScope();
var mediaService = scope.ServiceProvider.GetRequiredService<MediaService>();
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<LibraryService>();
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<MediaClient>();
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<ImportService>();
var libraryService = scope.ServiceProvider.GetRequiredService<LibraryService>();
_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<LibraryService>();
_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<Data.Models.Media>? mediaCollection, MediaType type, MediaService mediaService)
{
var media = mediaCollection?.FirstOrDefault(m => m.Type == type);

View file

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

View file

@ -169,11 +169,47 @@
Padding="16"
CornerRadius="8"
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">
<StackPanel Spacing="8">
<TextBlock Text="Actions" FontWeight="SemiBold" />
<TextBlock Text="Install, Play, and other game actions will be available here in a future update."
Opacity="0.6"
TextWrapping="Wrap" />
<StackPanel Spacing="12">
<TextBlock Text="Library" FontWeight="SemiBold" FontSize="16" />
<!-- Status message -->
<TextBlock Text="{Binding StatusMessage}"
Opacity="0.7"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<!-- Add to Library button (shown when not in library) -->
<Button Command="{Binding AddToLibraryCommand}"
IsVisible="{Binding !IsInLibrary}"
IsEnabled="{Binding !IsAddingToLibrary}"
HorizontalAlignment="Left"
Padding="16,10">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+" FontWeight="Bold" IsVisible="{Binding !IsAddingToLibrary}" />
<TextBlock Text="Add to Library" IsVisible="{Binding !IsAddingToLibrary}" />
<TextBlock Text="Adding..." IsVisible="{Binding IsAddingToLibrary}" />
</StackPanel>
</Button>
<!-- In Library indicator + Remove button (shown when in library) -->
<StackPanel Orientation="Horizontal" Spacing="12" IsVisible="{Binding IsInLibrary}">
<Border Background="{DynamicResource SystemAccentColor}"
CornerRadius="4"
Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="✓" Foreground="White" />
<TextBlock Text="In Library" Foreground="White" />
</StackPanel>
</Border>
<Button Command="{Binding RemoveFromLibraryCommand}"
IsEnabled="{Binding !IsRemovingFromLibrary}"
Padding="12,8">
<Panel>
<TextBlock Text="Remove from Library" Foreground="IndianRed" IsVisible="{Binding !IsRemovingFromLibrary}" />
<TextBlock Text="Removing..." Foreground="IndianRed" IsVisible="{Binding IsRemovingFromLibrary}" />
</Panel>
</Button>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>