LANCommander/LANCommander.Launcher/ViewModels/LibraryViewModel.cs
Pat Hartl b183e4b18c Remove old launcher, rename Avalonia launcher
Also adds ARM builds for Linux + Windows launcher
2026-06-06 05:49:24 -05:00

283 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LANCommander.Launcher.ViewModels.Components;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.Settings.Enums;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LANCommander.Launcher.ViewModels;
public partial class LibraryViewModel : GamesCollectionViewModel
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<LibraryViewModel> _logger;
public override string ViewTitle => "My Library";
public override bool ShowInLibraryFilter => false;
public override bool ShowInstalledFilter => true;
public override bool IsLibraryContext => true;
// ── Recently Played & Collections ────────────────────────────────────────
[ObservableProperty] private IReadOnlyList<GameItemViewModel> _recentlyPlayedGames = [];
[ObservableProperty] private IReadOnlyList<GenreCarouselButtomViewModel> _libraryCollections = [];
[ObservableProperty] private bool _hasRecentlyPlayed;
[ObservableProperty] private bool _hasCollections;
public override bool IsCollectionFiltered => !string.IsNullOrEmpty(SelectedCollection);
public override string FilteredCollectionName => SelectedCollection ?? string.Empty;
public LibraryViewModel(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_logger = serviceProvider.GetRequiredService<ILogger<LibraryViewModel>>();
// Default to List view for library
SelectedViewType = GameViewType.List;
}
public override Task LoadGamesAsync() => LoadLibraryAsync();
[RelayCommand]
private async Task LoadLibraryAsync()
{
IsLoading = true;
HasError = false;
StatusMessage = "Loading library...";
_logger.LogInformation("Loading library (offline: {IsOffline})...", IsOfflineMode);
try
{
// Run DB query and media lookups off the UI thread
var (collected, recentlyPlayed, collections) = await Task.Run(async () =>
{
using var scope = _serviceProvider.CreateScope();
var libraryService = scope.ServiceProvider.GetRequiredService<LibraryService>();
var mediaService = scope.ServiceProvider.GetRequiredService<MediaService>();
var mediaClient = scope.ServiceProvider.GetRequiredService<MediaClient>();
var dbContext = scope.ServiceProvider.GetRequiredService<DbContext>();
var items = await libraryService.GetItemsAsync();
var results = new List<GameItemViewModel>();
var gameModels = new List<LANCommander.Launcher.Data.Models.Game>();
foreach (var item in items ?? [])
{
if (item.DataItem is not LANCommander.Launcher.Data.Models.Game game)
continue;
gameModels.Add(game);
string? coverPath = null;
var coverMedia = game.Media?.FirstOrDefault(m => m.Type == MediaType.Cover);
if (coverMedia != null && mediaService.FileExists(coverMedia))
coverPath = mediaService.GetImagePath(coverMedia);
var iconPath = await GetOrDownloadIconPathAsync(game, mediaService, mediaClient);
var vm = new GameItemViewModel(game, coverPath, coverMedia?.MimeType, inLibrary: true, showInLibraryBadge: false);
vm.IconPath = iconPath;
results.Add(vm);
}
// Recently Played: query play sessions for library games, client-side grouping
var gameIds = gameModels.Select(g => g.Id).ToHashSet();
var allSessions = await dbContext.Set<LANCommander.Launcher.Data.Models.PlaySession>()
.Where(ps => ps.GameId != null && gameIds.Contains(ps.GameId.Value) && ps.End != null)
.Select(ps => new { GameId = ps.GameId!.Value, ps.End })
.ToListAsync();
var recentGameIds = allSessions
.GroupBy(ps => ps.GameId)
.Select(g => new { GameId = g.Key, LastPlayed = g.Max(ps => ps.End) })
.OrderByDescending(x => x.LastPlayed)
.Take(15)
.Select(x => x.GameId)
.ToList();
var recentItems = new List<GameItemViewModel>();
foreach (var gameId in recentGameIds)
{
var game = gameModels.FirstOrDefault(g => g.Id == gameId);
if (game == null) continue;
string? coverPath = null;
var coverMedia = game.Media?.FirstOrDefault(m => m.Type == MediaType.Cover);
if (coverMedia != null && mediaService.FileExists(coverMedia))
coverPath = mediaService.GetImagePath(coverMedia);
recentItems.Add(new GameItemViewModel(game, coverPath, coverMedia?.MimeType, inLibrary: true, showInLibraryBadge: false));
}
// Collections: distinct collections from library games
var collectionItems = new List<GenreCarouselButtomViewModel>();
var collectionGroups = gameModels
.Where(g => g.Collections?.Any() == true)
.SelectMany(g => g.Collections.Select(c => new { Collection = c, Game = g }))
.GroupBy(x => x.Collection.Name)
.OrderBy(g => g.Key);
foreach (var group in collectionGroups)
{
// Use cover of first game in collection as background
string? bgPath = null;
var representativeGame = group.First().Game;
var bgMedia = representativeGame.Media?.FirstOrDefault(m => m.Type == MediaType.Cover);
if (bgMedia != null && mediaService.FileExists(bgMedia))
bgPath = mediaService.GetImagePath(bgMedia);
collectionItems.Add(new GenreCarouselButtomViewModel
{
Name = group.Key,
BackgroundPath = bgPath,
HasBackground = !string.IsNullOrEmpty(bgPath)
});
}
return (results, recentItems, collectionItems);
});
Games.Clear();
_allGames.Clear();
AvailableGenres.Clear();
AvailableCollections.Clear();
AvailableTags.Clear();
AvailableDevelopers.Clear();
AvailablePublishers.Clear();
foreach (var vm in collected)
_allGames.Add(vm);
RecentlyPlayedGames = recentlyPlayed;
LibraryCollections = collections;
HasRecentlyPlayed = RecentlyPlayedGames.Count > 0;
HasCollections = LibraryCollections.Count > 0;
PopulateGenres();
PopulateCollections();
PopulateTags();
PopulateDevelopers();
PopulatePublishers();
ApplyFilters();
_logger.LogInformation("Loaded {Count} library games", _allGames.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load library");
StatusMessage = $"Failed to load library: {ex.Message}";
HasError = true;
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private void FilterByCollection(string? collectionName)
{
if (string.IsNullOrEmpty(collectionName))
return;
SelectedCollection = collectionName;
}
[RelayCommand]
private void ClearCollectionFilter()
{
SelectedCollection = null;
}
protected override async Task ViewGameDetailsAsync(GameItemViewModel? gameItem)
{
if (gameItem == null) return;
try
{
using var scope = _serviceProvider.CreateScope();
if (IsOfflineMode)
{
var gameService = scope.ServiceProvider.GetRequiredService<GameService>();
var localGame = await gameService.GetAsync(gameItem.Id);
if (localGame != null)
{
var sdkGame = new SDK.Models.Game
{
Id = localGame.Id,
Title = localGame.Title ?? "Unknown",
SortTitle = localGame.SortTitle,
Description = localGame.Description,
ReleasedOn = localGame.ReleasedOn ?? DateTime.MinValue
};
RaiseGameSelected(sdkGame);
}
}
else
{
var gameClient = scope.ServiceProvider.GetRequiredService<GameClient>();
var game = await gameClient.GetAsync(gameItem.Id);
if (game != null)
RaiseGameSelected(game);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch game {GameId}", gameItem.Id);
}
}
private async Task<string?> GetOrDownloadIconPathAsync(
LANCommander.Launcher.Data.Models.Game game,
MediaService mediaService,
MediaClient mediaClient)
{
var iconMedia = game.Media?.FirstOrDefault(m => m.Type == MediaType.Icon);
if (iconMedia == null) return null;
if (mediaService.FileExists(iconMedia))
return mediaService.GetImagePath(iconMedia);
if (IsOfflineMode)
return null;
try
{
var sdkMedia = new SDK.Models.Media
{
Id = iconMedia.Id,
FileId = iconMedia.FileId,
Crc32 = iconMedia.Crc32,
MimeType = iconMedia.MimeType,
Type = iconMedia.Type,
};
var path = mediaService.GetImagePath(iconMedia);
var fileInfo = await mediaClient.DownloadAsync(sdkMedia, path);
if (fileInfo.Exists)
return fileInfo.FullName;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to download icon for game {GameId}", game.Id);
}
return null;
}
}