2026-03-27 18:59:17 -04:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-06-06 05:49:24 -05:00
|
|
|
using LANCommander.Launcher.ViewModels.Components;
|
2026-03-27 18:59:17 -04:00
|
|
|
using LANCommander.Launcher.Settings.Enums;
|
|
|
|
|
using LANCommander.SDK.Models;
|
2026-03-28 21:12:04 -05:00
|
|
|
using LANCommander.SDK.Enums;
|
2026-03-27 18:59:17 -04:00
|
|
|
|
2026-06-06 05:49:24 -05:00
|
|
|
namespace LANCommander.Launcher.ViewModels;
|
2026-03-27 18:59:17 -04:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Shared base for Depot (GamesListViewModel) and Library (LibraryViewModel).
|
|
|
|
|
/// Owns all filtering, sorting, grouping, and view-type state.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public abstract partial class GamesCollectionViewModel : ViewModelBase
|
|
|
|
|
{
|
|
|
|
|
// Store all games before filtering
|
|
|
|
|
protected List<GameItemViewModel> _allGames = new();
|
|
|
|
|
|
|
|
|
|
// ── Data ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// <summary>Flat, filtered list — used by Grid and List view types when not grouped.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<GameItemViewModel> _games = new();
|
|
|
|
|
|
2026-03-28 16:54:37 -05:00
|
|
|
/// <summary>Currently highlighted game — driven by ListBox keyboard/gamepad navigation.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private GameItemViewModel? _selectedGame;
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
/// <summary>Filtered list organised into named groups — used when GroupBy != None.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<GameGroupViewModel> _groupedGames = new();
|
|
|
|
|
|
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _statusMessage = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isLoading;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _hasError;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isOfflineMode;
|
|
|
|
|
|
2026-06-07 12:08:44 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _hasGames;
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
// ── View appearance ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// <summary>Human-readable heading shown in the view header.</summary>
|
|
|
|
|
public abstract string ViewTitle { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>Whether to show the "In Library" filter toggle (depot only).</summary>
|
|
|
|
|
public abstract bool ShowInLibraryFilter { get; }
|
|
|
|
|
|
2026-04-09 21:50:28 -05:00
|
|
|
/// <summary>Whether to show the "Installed" filter toggle (library only).</summary>
|
|
|
|
|
public abstract bool ShowInstalledFilter { get; }
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
// ── View type ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsGridView))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsListView))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsHorizontalView))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsGridViewFlat))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsListViewFlat))]
|
2026-05-24 16:36:32 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsLibraryListViewFlat))]
|
2026-03-27 18:59:17 -04:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsHorizontalViewFlat))]
|
2026-03-28 21:12:04 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(AvailableGroupByOptions))]
|
2026-03-27 18:59:17 -04:00
|
|
|
private GameViewType _selectedViewType = GameViewType.Grid;
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
partial void OnSelectedViewTypeChanged(GameViewType value)
|
|
|
|
|
{
|
|
|
|
|
if (value == GameViewType.Horizontal && SelectedGroupBy == GroupBy.None)
|
|
|
|
|
SelectedGroupBy = GroupBy.FirstLetter;
|
2026-05-24 16:36:32 -05:00
|
|
|
else if (value != GameViewType.Horizontal && IsLibraryContext && SelectedGroupBy != GroupBy.None)
|
|
|
|
|
SelectedGroupBy = GroupBy.None;
|
2026-03-28 21:12:04 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
public bool IsGridView => SelectedViewType == GameViewType.Grid;
|
|
|
|
|
public bool IsListView => SelectedViewType == GameViewType.List;
|
|
|
|
|
public bool IsHorizontalView => SelectedViewType == GameViewType.Horizontal;
|
|
|
|
|
|
|
|
|
|
// Flat variants: only true when NOT grouped
|
|
|
|
|
public bool IsGridViewFlat => IsGridView && !IsGrouped;
|
2026-05-24 16:36:32 -05:00
|
|
|
public bool IsListViewFlat => IsListView && !IsGrouped && !IsLibraryContext;
|
|
|
|
|
public bool IsLibraryListViewFlat => IsListView && !IsGrouped && IsLibraryContext;
|
2026-03-27 18:59:17 -04:00
|
|
|
public bool IsHorizontalViewFlat => IsHorizontalView && !IsGrouped;
|
|
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
/// <summary>Override in LibraryViewModel to enable the library-specific list layout.</summary>
|
|
|
|
|
public virtual bool IsLibraryContext => false;
|
|
|
|
|
|
|
|
|
|
/// <summary>Whether a collection filter is active (used by LibraryRowView).</summary>
|
|
|
|
|
public virtual bool IsCollectionFiltered => false;
|
|
|
|
|
|
|
|
|
|
/// <summary>Name of the currently filtered collection (used by LibraryRowView).</summary>
|
|
|
|
|
public virtual string FilteredCollectionName => string.Empty;
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
// ── Grouping ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsGrouped))]
|
2026-03-28 16:54:37 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsGroupByFirstLetter))]
|
2026-03-27 18:59:17 -04:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsGridViewFlat))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsListViewFlat))]
|
2026-05-24 16:36:32 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsLibraryListViewFlat))]
|
2026-03-27 18:59:17 -04:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsHorizontalViewFlat))]
|
2026-05-24 16:36:32 -05:00
|
|
|
private GroupBy? _selectedGroupBy = GroupBy.None;
|
2026-03-27 18:59:17 -04:00
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
public bool IsGrouped => SelectedGroupBy != null && SelectedGroupBy != GroupBy.None;
|
2026-03-28 16:54:37 -05:00
|
|
|
public bool IsGroupByFirstLetter => SelectedGroupBy == GroupBy.FirstLetter;
|
2026-03-27 18:59:17 -04:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
public IReadOnlyList<GroupBy> AvailableGroupByOptions =>
|
|
|
|
|
IsHorizontalView
|
|
|
|
|
? [GroupBy.FirstLetter, GroupBy.Genre, GroupBy.Collection]
|
|
|
|
|
: [GroupBy.None, GroupBy.FirstLetter, GroupBy.Genre, GroupBy.Collection];
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
// ── Filters ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _searchText = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _showInLibraryOnly;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private Genre? _selectedGenre;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<Genre> _availableGenres = new();
|
|
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsCollectionFiltered))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(FilteredCollectionName))]
|
|
|
|
|
private string? _selectedCollection;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<string> _availableCollections = new();
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedTag;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<string> _availableTags = new();
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedDeveloper;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<string> _availableDevelopers = new();
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedPublisher;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableCollection<string> _availablePublishers = new();
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedMultiplayerType;
|
|
|
|
|
|
2026-04-09 21:50:28 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _showInstalledOnly;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedMinPlayers;
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isAdvancedFilterOpen;
|
|
|
|
|
|
2026-04-09 21:50:28 -05:00
|
|
|
public static readonly IReadOnlyList<string> AvailableMultiplayerTypes = ["Singleplayer", "Local", "LAN", "Online"];
|
|
|
|
|
|
|
|
|
|
public static readonly IReadOnlyList<string> AvailablePlayerCounts = ["2+", "4+", "8+", "16+", "32+"];
|
2026-03-28 21:12:04 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
// ── Sorting ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private SortBy _selectedSortBy = SortBy.Title;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _sortAscending = true;
|
|
|
|
|
|
|
|
|
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public event EventHandler<SDK.Models.Game>? GameSelected;
|
|
|
|
|
|
|
|
|
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void ToggleSortDirection() => SortAscending = !SortAscending;
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void ToggleAdvancedFilter() => IsAdvancedFilterOpen = !IsAdvancedFilterOpen;
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void ClearFilters()
|
|
|
|
|
{
|
2026-03-28 21:12:04 -05:00
|
|
|
SearchText = string.Empty;
|
|
|
|
|
SelectedGenre = null;
|
2026-05-24 16:36:32 -05:00
|
|
|
SelectedCollection = null;
|
2026-03-28 21:12:04 -05:00
|
|
|
SelectedTag = null;
|
|
|
|
|
SelectedDeveloper = null;
|
|
|
|
|
SelectedPublisher = null;
|
|
|
|
|
SelectedMultiplayerType = null;
|
2026-04-09 21:50:28 -05:00
|
|
|
SelectedMinPlayers = null;
|
2026-03-28 21:12:04 -05:00
|
|
|
ShowInLibraryOnly = false;
|
2026-04-09 21:50:28 -05:00
|
|
|
ShowInstalledOnly = false;
|
2026-03-28 21:12:04 -05:00
|
|
|
SelectedSortBy = SortBy.Title;
|
|
|
|
|
SortAscending = true;
|
|
|
|
|
SelectedGroupBy = IsHorizontalView ? GroupBy.FirstLetter : GroupBy.None;
|
2026-03-27 18:59:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
protected abstract Task ViewGameDetailsAsync(GameItemViewModel? gameItem);
|
|
|
|
|
|
|
|
|
|
// Subclasses implement the actual loading logic
|
|
|
|
|
public abstract Task LoadGamesAsync();
|
|
|
|
|
|
|
|
|
|
// Bindable command that wraps LoadGamesAsync for use in XAML
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private Task Refresh() => LoadGamesAsync();
|
|
|
|
|
|
|
|
|
|
// ── Filter / group pipeline ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
protected void ApplyFilters()
|
|
|
|
|
{
|
|
|
|
|
var filtered = _allGames.AsEnumerable();
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(SearchText))
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
g.Title.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
g.SortTitle.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
if (ShowInLibraryOnly)
|
|
|
|
|
filtered = filtered.Where(g => g.InLibrary);
|
|
|
|
|
|
|
|
|
|
if (SelectedGenre != null)
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
!string.IsNullOrEmpty(g.Genres) &&
|
|
|
|
|
g.Genres.Contains(SelectedGenre.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
if (!string.IsNullOrEmpty(SelectedCollection))
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
!string.IsNullOrEmpty(g.Collections) &&
|
|
|
|
|
g.Collections.Split(", ", StringSplitOptions.RemoveEmptyEntries)
|
|
|
|
|
.Any(c => c.Equals(SelectedCollection, StringComparison.OrdinalIgnoreCase)));
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
if (!string.IsNullOrEmpty(SelectedTag))
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
!string.IsNullOrEmpty(g.Tags) &&
|
|
|
|
|
g.Tags.Contains(SelectedTag, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(SelectedDeveloper))
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
!string.IsNullOrEmpty(g.Developers) &&
|
|
|
|
|
g.Developers.Contains(SelectedDeveloper, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(SelectedPublisher))
|
|
|
|
|
filtered = filtered.Where(g =>
|
|
|
|
|
!string.IsNullOrEmpty(g.Publishers) &&
|
|
|
|
|
g.Publishers.Contains(SelectedPublisher, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
2026-04-09 21:50:28 -05:00
|
|
|
if (ShowInstalledOnly)
|
|
|
|
|
filtered = filtered.Where(g => g.IsInstalled);
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
if (!string.IsNullOrEmpty(SelectedMultiplayerType))
|
|
|
|
|
filtered = SelectedMultiplayerType switch
|
|
|
|
|
{
|
2026-04-09 21:50:28 -05:00
|
|
|
"Singleplayer" => filtered.Where(g => g.Singleplayer),
|
2026-03-28 21:12:04 -05:00
|
|
|
"Local" => filtered.Where(g => g.HasLocalMultiplayer),
|
|
|
|
|
"LAN" => filtered.Where(g => g.HasLanMultiplayer),
|
|
|
|
|
"Online" => filtered.Where(g => g.HasOnlineMultiplayer),
|
|
|
|
|
_ => filtered
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 21:50:28 -05:00
|
|
|
if (!string.IsNullOrEmpty(SelectedMinPlayers) && int.TryParse(SelectedMinPlayers.TrimEnd('+'), out var minPlayers))
|
|
|
|
|
filtered = filtered.Where(g => g.MaxPlayers >= minPlayers);
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
filtered = SelectedSortBy switch
|
|
|
|
|
{
|
|
|
|
|
SortBy.Title => SortAscending
|
|
|
|
|
? filtered.OrderBy(g => string.IsNullOrEmpty(g.SortTitle) ? g.Title : g.SortTitle, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
: filtered.OrderByDescending(g => string.IsNullOrEmpty(g.SortTitle) ? g.Title : g.SortTitle, StringComparer.OrdinalIgnoreCase),
|
|
|
|
|
SortBy.DateReleased => SortAscending
|
|
|
|
|
? filtered.OrderBy(g => g.ReleasedOn)
|
|
|
|
|
: filtered.OrderByDescending(g => g.ReleasedOn),
|
|
|
|
|
_ => filtered
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var materialised = filtered.ToList();
|
|
|
|
|
|
|
|
|
|
Games.Clear();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
foreach (var g in materialised)
|
|
|
|
|
Games.Add(g);
|
|
|
|
|
|
|
|
|
|
RebuildGroups(materialised);
|
|
|
|
|
|
2026-06-07 12:08:44 -05:00
|
|
|
HasGames = Games.Count > 0;
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
var suffix = IsOfflineMode ? " (offline)" : string.Empty;
|
|
|
|
|
StatusMessage = $"{Games.Count} of {_allGames.Count} games{suffix}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RebuildGroups(List<GameItemViewModel> items)
|
|
|
|
|
{
|
|
|
|
|
GroupedGames.Clear();
|
|
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
if (SelectedGroupBy is null or GroupBy.None)
|
2026-03-27 18:59:17 -04:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
IEnumerable<GameGroupViewModel> groups = SelectedGroupBy switch
|
|
|
|
|
{
|
|
|
|
|
GroupBy.FirstLetter => items
|
|
|
|
|
.GroupBy(g => GetFirstLetter(g))
|
|
|
|
|
.OrderBy(gr => gr.Key)
|
|
|
|
|
.Select(gr => new GameGroupViewModel { Name = gr.Key, Items = new(gr) }),
|
|
|
|
|
|
|
|
|
|
GroupBy.Genre => items
|
|
|
|
|
.SelectMany(g => SplitOrDefault(g.Genres, "Uncategorized")
|
|
|
|
|
.Select(genre => (Key: genre, Game: g)))
|
|
|
|
|
.GroupBy(x => x.Key, x => x.Game)
|
|
|
|
|
.OrderBy(gr => gr.Key)
|
|
|
|
|
.Select(gr => new GameGroupViewModel { Name = gr.Key, Items = new(gr) }),
|
|
|
|
|
|
|
|
|
|
GroupBy.Collection => items
|
|
|
|
|
.SelectMany(g => SplitOrDefault(g.Collections, "Uncategorized")
|
|
|
|
|
.Select(col => (Key: col, Game: g)))
|
|
|
|
|
.GroupBy(x => x.Key, x => x.Game)
|
|
|
|
|
.OrderBy(gr => gr.Key)
|
|
|
|
|
.Select(gr => new GameGroupViewModel { Name = gr.Key, Items = new(gr) }),
|
|
|
|
|
|
|
|
|
|
_ => []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (var group in groups)
|
|
|
|
|
GroupedGames.Add(group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetFirstLetter(GameItemViewModel g)
|
|
|
|
|
{
|
|
|
|
|
var title = (string.IsNullOrEmpty(g.SortTitle) ? g.Title : g.SortTitle).TrimStart();
|
|
|
|
|
var first = title.FirstOrDefault();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
return char.IsLetter(first) ? char.ToUpper(first).ToString() : "#";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static IEnumerable<string> SplitOrDefault(string value, string fallback)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
return [fallback];
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
return value.Split(", ", StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Change handlers ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private CancellationTokenSource? _searchDebounce;
|
|
|
|
|
|
|
|
|
|
partial void OnSearchTextChanged(string value)
|
|
|
|
|
{
|
|
|
|
|
_searchDebounce?.Cancel();
|
2026-05-09 19:42:59 -05:00
|
|
|
_searchDebounce?.Dispose();
|
2026-03-27 18:59:17 -04:00
|
|
|
_searchDebounce = new CancellationTokenSource();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
var token = _searchDebounce.Token;
|
|
|
|
|
|
|
|
|
|
_ = Task.Delay(250, token).ContinueWith(
|
|
|
|
|
_ => ApplyFilters(),
|
|
|
|
|
token,
|
|
|
|
|
TaskContinuationOptions.OnlyOnRanToCompletion,
|
|
|
|
|
TaskScheduler.FromCurrentSynchronizationContext());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
partial void OnSelectedSortByChanged(SortBy value) => ApplyFilters();
|
|
|
|
|
partial void OnSortAscendingChanged(bool value) => ApplyFilters();
|
|
|
|
|
partial void OnShowInLibraryOnlyChanged(bool value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedGenreChanged(Genre? value) => ApplyFilters();
|
2026-05-24 16:36:32 -05:00
|
|
|
partial void OnSelectedGroupByChanged(GroupBy? value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedCollectionChanged(string? value) => ApplyFilters();
|
2026-03-28 21:12:04 -05:00
|
|
|
partial void OnSelectedTagChanged(string? value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedDeveloperChanged(string? value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedPublisherChanged(string? value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedMultiplayerTypeChanged(string? value) => ApplyFilters();
|
2026-04-09 21:50:28 -05:00
|
|
|
partial void OnShowInstalledOnlyChanged(bool value) => ApplyFilters();
|
|
|
|
|
partial void OnSelectedMinPlayersChanged(string? value) => ApplyFilters();
|
2026-03-27 18:59:17 -04:00
|
|
|
|
|
|
|
|
// ── Helpers for subclasses ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
protected void RaiseGameSelected(SDK.Models.Game game) =>
|
|
|
|
|
GameSelected?.Invoke(this, game);
|
|
|
|
|
|
2026-05-24 16:36:32 -05:00
|
|
|
protected void PopulateCollections()
|
|
|
|
|
{
|
|
|
|
|
AvailableCollections.Clear();
|
|
|
|
|
|
|
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
foreach (var g in _allGames)
|
|
|
|
|
if (!string.IsNullOrEmpty(g.Collections))
|
|
|
|
|
foreach (var c in g.Collections.Split(", ", StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
seen.Add(c);
|
|
|
|
|
|
|
|
|
|
foreach (var c in seen.OrderBy(x => x))
|
|
|
|
|
AvailableCollections.Add(c);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
protected void PopulateTags()
|
|
|
|
|
{
|
|
|
|
|
AvailableTags.Clear();
|
2026-05-24 16:36:32 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-05-24 16:36:32 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var g in _allGames)
|
|
|
|
|
if (!string.IsNullOrEmpty(g.Tags))
|
|
|
|
|
foreach (var t in g.Tags.Split(", ", StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
seen.Add(t);
|
2026-05-24 16:36:32 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var t in seen.OrderBy(x => x))
|
|
|
|
|
AvailableTags.Add(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void PopulateDevelopers()
|
|
|
|
|
{
|
|
|
|
|
AvailableDevelopers.Clear();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var g in _allGames)
|
|
|
|
|
if (!string.IsNullOrEmpty(g.Developers))
|
|
|
|
|
foreach (var d in g.Developers.Split(", ", StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
seen.Add(d);
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var d in seen.OrderBy(x => x))
|
|
|
|
|
AvailableDevelopers.Add(d);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void PopulatePublishers()
|
|
|
|
|
{
|
|
|
|
|
AvailablePublishers.Clear();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var g in _allGames)
|
|
|
|
|
if (!string.IsNullOrEmpty(g.Publishers))
|
|
|
|
|
foreach (var p in g.Publishers.Split(", ", StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
seen.Add(p);
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-28 21:12:04 -05:00
|
|
|
foreach (var p in seen.OrderBy(x => x))
|
|
|
|
|
AvailablePublishers.Add(p);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
protected void PopulateGenres()
|
|
|
|
|
{
|
|
|
|
|
AvailableGenres.Clear();
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
var genres = new HashSet<Genre>(new GenreComparer());
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
foreach (var g in _allGames)
|
|
|
|
|
if (!string.IsNullOrEmpty(g.Genres))
|
|
|
|
|
foreach (var name in g.Genres.Split(", ", StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
genres.Add(new Genre { Name = name });
|
2026-05-12 17:47:03 -05:00
|
|
|
|
2026-03-27 18:59:17 -04:00
|
|
|
foreach (var genre in genres.OrderBy(g => g.Name))
|
|
|
|
|
AvailableGenres.Add(genre);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class GenreComparer : IEqualityComparer<Genre>
|
|
|
|
|
{
|
|
|
|
|
public bool Equals(Genre? x, Genre? y) => string.Equals(x?.Name, y?.Name, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
public int GetHashCode(Genre obj) => (obj.Name ?? string.Empty).ToLowerInvariant().GetHashCode();
|
|
|
|
|
}
|
|
|
|
|
}
|