Separate views for library vs depot
This commit is contained in:
parent
0ab6ee5262
commit
caee4ca4e0
9 changed files with 800 additions and 75 deletions
|
|
@ -0,0 +1,9 @@
|
|||
namespace LANCommander.Launcher.Avalonia.ViewModels.Components;
|
||||
|
||||
/// <summary>Represents a single screenshot or video in the game detail media carousel.</summary>
|
||||
public class GameMediaItemViewModel
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public bool IsVideo { get; set; }
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LANCommander.Launcher.Avalonia.ViewModels.Components;
|
||||
using LANCommander.Launcher.Services;
|
||||
using LANCommander.Launcher.Settings.Enums;
|
||||
using LANCommander.SDK.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.Launcher.Avalonia.ViewModels;
|
||||
|
||||
/// <summary>Which dimension (if any) of the initial navigation is locked and cannot be cleared.</summary>
|
||||
public enum LockedFilterKind { None, Genre, Tag, Collection }
|
||||
|
||||
public partial class DepotBrowseViewModel : GamesCollectionViewModel
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<DepotBrowseViewModel> _logger;
|
||||
|
||||
// ── Locked filter ─────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasLockedFilter))]
|
||||
[NotifyPropertyChangedFor(nameof(LockedFilterLabel))]
|
||||
[NotifyPropertyChangedFor(nameof(IsGenreLocked))]
|
||||
[NotifyPropertyChangedFor(nameof(IsTagLocked))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCollectionLocked))]
|
||||
private LockedFilterKind _lockedFilterKind = LockedFilterKind.None;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _lockedFilterValue = string.Empty;
|
||||
|
||||
public bool HasLockedFilter => LockedFilterKind != LockedFilterKind.None;
|
||||
public bool IsGenreLocked => LockedFilterKind == LockedFilterKind.Genre;
|
||||
public bool IsTagLocked => LockedFilterKind == LockedFilterKind.Tag;
|
||||
public bool IsCollectionLocked => LockedFilterKind == LockedFilterKind.Collection;
|
||||
|
||||
public string LockedFilterLabel => LockedFilterKind switch
|
||||
{
|
||||
LockedFilterKind.Genre => "Genre",
|
||||
LockedFilterKind.Tag => "Tag",
|
||||
LockedFilterKind.Collection => "Collection",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ViewTitle))]
|
||||
private string _browseTitle = "All Games";
|
||||
|
||||
public override string ViewTitle => BrowseTitle;
|
||||
public override bool ShowInLibraryFilter => true;
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────
|
||||
|
||||
public event EventHandler? BackToDepotRequested;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public DepotBrowseViewModel(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = serviceProvider.GetRequiredService<ILogger<DepotBrowseViewModel>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate from a snapshot of already-loaded games and optionally pre-filter.
|
||||
/// Passing a genre/tag/collection locks that filter so it cannot be cleared.
|
||||
/// </summary>
|
||||
public void Initialize(
|
||||
IEnumerable<GameItemViewModel> allGames,
|
||||
string? preFilterGenre = null,
|
||||
string? preFilterTag = null,
|
||||
string? preFilterCollection = null,
|
||||
string? preFilterSearch = null)
|
||||
{
|
||||
_allGames.Clear();
|
||||
_allGames.AddRange(allGames);
|
||||
|
||||
PopulateGenres();
|
||||
PopulateTags();
|
||||
PopulateDevelopers();
|
||||
PopulatePublishers();
|
||||
|
||||
// Determine locked filter BEFORE resetting values
|
||||
LockedFilterKind = !string.IsNullOrEmpty(preFilterGenre) ? LockedFilterKind.Genre
|
||||
: !string.IsNullOrEmpty(preFilterTag) ? LockedFilterKind.Tag
|
||||
: !string.IsNullOrEmpty(preFilterCollection) ? LockedFilterKind.Collection
|
||||
: LockedFilterKind.None;
|
||||
|
||||
LockedFilterValue = preFilterGenre ?? preFilterTag ?? preFilterCollection ?? string.Empty;
|
||||
|
||||
// Reset all filters (won't trigger ApplyFilters when values haven't changed)
|
||||
SearchText = string.Empty;
|
||||
SelectedGenre = null;
|
||||
SelectedTag = null;
|
||||
SelectedDeveloper = null;
|
||||
SelectedPublisher = null;
|
||||
SelectedMultiplayerType = null;
|
||||
ShowInLibraryOnly = false;
|
||||
SelectedSortBy = SortBy.Title;
|
||||
SortAscending = true;
|
||||
SelectedGroupBy = GroupBy.None;
|
||||
|
||||
// Apply pre-filter
|
||||
if (!string.IsNullOrEmpty(preFilterGenre))
|
||||
{
|
||||
var genre = AvailableGenres.FirstOrDefault(g =>
|
||||
string.Equals(g.Name, preFilterGenre, StringComparison.OrdinalIgnoreCase));
|
||||
if (genre != null)
|
||||
SelectedGenre = genre;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(preFilterTag))
|
||||
{
|
||||
SelectedTag = AvailableTags.FirstOrDefault(t =>
|
||||
string.Equals(t, preFilterTag, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(preFilterCollection))
|
||||
SelectedGroupBy = GroupBy.Collection;
|
||||
|
||||
if (!string.IsNullOrEmpty(preFilterSearch))
|
||||
SearchText = preFilterSearch;
|
||||
|
||||
BrowseTitle = !string.IsNullOrEmpty(preFilterGenre) ? preFilterGenre
|
||||
: !string.IsNullOrEmpty(preFilterTag) ? preFilterTag
|
||||
: !string.IsNullOrEmpty(preFilterCollection) ? preFilterCollection
|
||||
: !string.IsNullOrEmpty(preFilterSearch) ? $"Search: {preFilterSearch}"
|
||||
: "All Games";
|
||||
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
// Data comes from Initialize(); no network loading needed.
|
||||
public override Task LoadGamesAsync() => Task.CompletedTask;
|
||||
|
||||
[RelayCommand]
|
||||
private void GoBack() => BackToDepotRequested?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Clears user-added filters while preserving the locked initial filter.
|
||||
/// Bound to the clear (✕) button in the view instead of the base ClearFiltersCommand.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ClearAdditionalFilters()
|
||||
{
|
||||
// Only reset search if it's not the locked dimension (search is never locked, but be explicit)
|
||||
SearchText = string.Empty;
|
||||
|
||||
if (!IsGenreLocked) SelectedGenre = null;
|
||||
if (!IsTagLocked) SelectedTag = null;
|
||||
if (!IsCollectionLocked) SelectedGroupBy = GroupBy.None;
|
||||
|
||||
SelectedDeveloper = null;
|
||||
SelectedPublisher = null;
|
||||
SelectedMultiplayerType = null;
|
||||
ShowInLibraryOnly = false;
|
||||
SelectedSortBy = SortBy.Title;
|
||||
SortAscending = true;
|
||||
}
|
||||
|
||||
protected override async Task ViewGameDetailsAsync(GameItemViewModel? gameItem)
|
||||
{
|
||||
if (gameItem == null) return;
|
||||
_logger.LogDebug("Viewing game from depot browse: {GameId}", gameItem.Id);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
|
||||
if (IsOfflineMode)
|
||||
{
|
||||
var gameService = scope.ServiceProvider.GetRequiredService<GameService>();
|
||||
var localGame = await gameService.GetAsync(gameItem.Id);
|
||||
if (localGame != null)
|
||||
RaiseGameSelected(new SDK.Models.Game
|
||||
{
|
||||
Id = localGame.Id,
|
||||
Title = localGame.Title ?? "Unknown",
|
||||
SortTitle = localGame.SortTitle,
|
||||
Description = localGame.Description,
|
||||
ReleasedOn = localGame.ReleasedOn ?? DateTime.MinValue
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace LANCommander.Launcher.Avalonia.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Depot-specific game detail view model. Extends <see cref="GameDetailViewModel"/>
|
||||
/// so the ShellView DataTemplate can route to a distinct depot detail view.
|
||||
/// FromLibrary is always false since this is only used in the depot context.
|
||||
/// </summary>
|
||||
public partial class DepotGameDetailViewModel : GameDetailViewModel
|
||||
{
|
||||
public DepotGameDetailViewModel(IServiceProvider serviceProvider)
|
||||
: base(serviceProvider)
|
||||
{
|
||||
FromLibrary = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -69,19 +70,57 @@ public partial class GameDetailViewModel : ViewModelBase
|
|||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(TagList))]
|
||||
[NotifyPropertyChangedFor(nameof(VisibleTagList))]
|
||||
[NotifyPropertyChangedFor(nameof(HasMoreTags))]
|
||||
[NotifyPropertyChangedFor(nameof(ExtraTagCount))]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMoreTagsLabel))]
|
||||
private string _tags = string.Empty;
|
||||
|
||||
// ── Tags expand/collapse ─────────────<E29480><E29480><EFBFBD>────────────────────────────────────
|
||||
|
||||
private const int TagsVisibleLimit = 5;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(VisibleTagList))]
|
||||
[NotifyPropertyChangedFor(nameof(HasMoreTags))]
|
||||
[NotifyPropertyChangedFor(nameof(ExtraTagCount))]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMoreTagsLabel))]
|
||||
private bool _tagsExpanded;
|
||||
|
||||
public IEnumerable<string> VisibleTagList =>
|
||||
TagsExpanded ? TagList : TagList.Take(TagsVisibleLimit);
|
||||
|
||||
public bool HasMoreTags => TagList.Count() > TagsVisibleLimit;
|
||||
public int ExtraTagCount => Math.Max(0, TagList.Count() - TagsVisibleLimit);
|
||||
public string ShowMoreTagsLabel =>
|
||||
TagsExpanded ? "Show less" : $"+{ExtraTagCount} more";
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleTagsExpanded() => TagsExpanded = !TagsExpanded;
|
||||
|
||||
// ── Screenshots / videos ──────────────────────<E29480><E29480><EFBFBD>────────────────────────<E29480><E29480><EFBFBD>──
|
||||
|
||||
public ObservableCollection<GameMediaItemViewModel> MediaItems { get; } = new();
|
||||
|
||||
public bool HasMedia => MediaItems.Count > 0;
|
||||
|
||||
// ── Multiplayer modes ─────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<string> MultiplayerModeDetails { get; } = new();
|
||||
|
||||
// ── Other ─────────────────────────<E29480><E29480><EFBFBD>───────────────────────────────────────
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(BackLabel))]
|
||||
private bool _fromLibrary;
|
||||
|
||||
public string BackLabel => FromLibrary ? "← Back to Library" : "← Back to Depot";
|
||||
public string BackLabel => FromLibrary ? "Back to Library" : "Back to Depot";
|
||||
|
||||
// Split list properties for chip rendering
|
||||
public IEnumerable<string> GenreList => SplitCsv(Genres);
|
||||
public IEnumerable<string> DeveloperList => SplitCsv(Developers);
|
||||
public IEnumerable<string> PublisherList => SplitCsv(Publishers);
|
||||
public IEnumerable<string> TagList => SplitCsv(Tags);
|
||||
public IEnumerable<string> GenreList => SplitCsv(Genres);
|
||||
public IEnumerable<string> DeveloperList => SplitCsv(Developers);
|
||||
public IEnumerable<string> PublisherList => SplitCsv(Publishers);
|
||||
public IEnumerable<string> TagList => SplitCsv(Tags);
|
||||
|
||||
private static IEnumerable<string> SplitCsv(string csv) =>
|
||||
csv.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0);
|
||||
|
|
@ -179,18 +218,41 @@ public partial class GameDetailViewModel : ViewModelBase
|
|||
|
||||
// Multiplayer info
|
||||
HasMultiplayer = game.MultiplayerModes != null && game.MultiplayerModes.Any();
|
||||
MultiplayerModeDetails.Clear();
|
||||
if (HasMultiplayer)
|
||||
{
|
||||
var modes = game.MultiplayerModes!
|
||||
.Select(m => m.Type.ToString())
|
||||
.Distinct();
|
||||
MultiplayerModes = string.Join(", ", modes);
|
||||
foreach (var mode in game.MultiplayerModes!)
|
||||
MultiplayerModeDetails.Add(FormatMultiplayerMode(mode));
|
||||
}
|
||||
else
|
||||
{
|
||||
MultiplayerModes = string.Empty;
|
||||
}
|
||||
|
||||
// Media items (screenshots / videos from local cache)
|
||||
MediaItems.Clear();
|
||||
TagsExpanded = false;
|
||||
if (game.Media != null)
|
||||
{
|
||||
foreach (var m in game.Media.Where(m =>
|
||||
m.Type == MediaType.Screenshot || m.Type == MediaType.Video))
|
||||
{
|
||||
var path = mediaService.FileExists(m) ? mediaService.GetImagePath(m) : null;
|
||||
if (path != null)
|
||||
MediaItems.Add(new GameMediaItemViewModel
|
||||
{
|
||||
Path = path,
|
||||
IsVideo = m.Type == MediaType.Video,
|
||||
MimeType = string.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
OnPropertyChanged(nameof(HasMedia));
|
||||
|
||||
// Load action bar state
|
||||
await ActionBar.LoadFromLocalGameAsync(game);
|
||||
}
|
||||
|
|
@ -237,18 +299,26 @@ public partial class GameDetailViewModel : ViewModelBase
|
|||
|
||||
// Multiplayer info
|
||||
HasMultiplayer = game.MultiplayerModes != null && game.MultiplayerModes.Any();
|
||||
MultiplayerModeDetails.Clear();
|
||||
if (HasMultiplayer)
|
||||
{
|
||||
var modes = game.MultiplayerModes!
|
||||
.Select(m => m.Type.ToString())
|
||||
.Distinct();
|
||||
MultiplayerModes = string.Join(", ", modes);
|
||||
foreach (var mode in game.MultiplayerModes!)
|
||||
MultiplayerModeDetails.Add(FormatMultiplayerMode(mode));
|
||||
}
|
||||
else
|
||||
{
|
||||
MultiplayerModes = string.Empty;
|
||||
}
|
||||
|
||||
// Reset media items and tags state while we re-load
|
||||
MediaItems.Clear();
|
||||
TagsExpanded = false;
|
||||
OnPropertyChanged(nameof(HasMedia));
|
||||
|
||||
// Load action bar state
|
||||
await ActionBar.LoadFromSdkGameAsync(game);
|
||||
|
||||
|
|
@ -261,10 +331,25 @@ public partial class GameDetailViewModel : ViewModelBase
|
|||
using var scope = _serviceProvider.CreateScope();
|
||||
var mediaClient = scope.ServiceProvider.GetRequiredService<MediaClient>();
|
||||
|
||||
CoverPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Cover, mediaClient);
|
||||
LogoPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Logo, mediaClient);
|
||||
BackgroundPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Background, mediaClient);
|
||||
IconPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Icon, mediaClient);
|
||||
CoverPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Cover, mediaClient);
|
||||
LogoPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Logo, mediaClient);
|
||||
BackgroundPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Background, mediaClient);
|
||||
IconPath = await GetOrDownloadMediaPathAsync(game.Media, MediaType.Icon, mediaClient);
|
||||
|
||||
// Screenshots and videos
|
||||
foreach (var media in game.Media.Where(m =>
|
||||
m.Type == MediaType.Screenshot || m.Type == MediaType.Video))
|
||||
{
|
||||
var path = await GetOrDownloadSingleMediaAsync(media, mediaClient);
|
||||
if (path != null)
|
||||
MediaItems.Add(new GameMediaItemViewModel
|
||||
{
|
||||
Path = path,
|
||||
IsVideo = media.Type == MediaType.Video,
|
||||
MimeType = media.MimeType ?? string.Empty
|
||||
});
|
||||
}
|
||||
OnPropertyChanged(nameof(HasMedia));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -277,6 +362,49 @@ public partial class GameDetailViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private static string FormatMultiplayerMode(Data.Models.MultiplayerMode mode) =>
|
||||
FormatMultiplayerMode(mode.Type, mode.MinPlayers, mode.MaxPlayers);
|
||||
|
||||
private static string FormatMultiplayerMode(SDK.Models.MultiplayerMode mode) =>
|
||||
FormatMultiplayerMode(mode.Type, mode.MinPlayers, mode.MaxPlayers);
|
||||
|
||||
private static string FormatMultiplayerMode(SDK.Enums.MultiplayerType type, int minPlayers, int maxPlayers)
|
||||
{
|
||||
var typeLabel = type switch
|
||||
{
|
||||
SDK.Enums.MultiplayerType.Local => "Local Multiplayer",
|
||||
SDK.Enums.MultiplayerType.LAN => "LAN Multiplayer",
|
||||
SDK.Enums.MultiplayerType.Online => "Online Multiplayer",
|
||||
_ => type.ToString()
|
||||
};
|
||||
|
||||
if (maxPlayers > 0)
|
||||
{
|
||||
var range = minPlayers > 1 && minPlayers < maxPlayers
|
||||
? $"{minPlayers}–{maxPlayers} players"
|
||||
: $"Up to {maxPlayers} players";
|
||||
return $"{typeLabel} · {range}";
|
||||
}
|
||||
|
||||
return typeLabel;
|
||||
}
|
||||
|
||||
private async Task<string?> GetOrDownloadSingleMediaAsync(SDK.Models.Media media, MediaClient mediaClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localPath = mediaClient.GetLocalPath(media);
|
||||
if (File.Exists(localPath)) return localPath;
|
||||
var fileInfo = await mediaClient.DownloadAsync(media, localPath);
|
||||
return fileInfo.Exists ? fileInfo.FullName : null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to download media {MediaId}", media.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetLocalMediaPath(System.Collections.Generic.ICollection<Data.Models.Media>? mediaCollection, MediaType type, MediaService mediaService)
|
||||
{
|
||||
var media = mediaCollection?.FirstOrDefault(m => m.Type == type);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ public partial class GamesListViewModel : GamesCollectionViewModel
|
|||
|
||||
public override Task LoadGamesAsync() => LoadGamesInternalAsync();
|
||||
|
||||
/// <summary>Returns a snapshot of all loaded games for use by depot browse views.</summary>
|
||||
public IEnumerable<GameItemViewModel> GetAllGames() => _allGames;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadGamesInternalAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,11 +35,13 @@ public partial class ShellViewModel : ViewModelBase
|
|||
|
||||
public string ContentViewTitle => ContentView switch
|
||||
{
|
||||
DepotViewModel _ => "Depot",
|
||||
GamesListViewModel _ => "Depot",
|
||||
LibraryViewModel _ => "My Library",
|
||||
GameDetailViewModel gd => !string.IsNullOrEmpty(gd.Title) ? gd.Title : string.Empty,
|
||||
SettingsViewModel _ => "Settings",
|
||||
DepotViewModel _ => "Depot",
|
||||
DepotBrowseViewModel db => db.BrowseTitle,
|
||||
GamesListViewModel _ => "Depot",
|
||||
LibraryViewModel _ => "My Library",
|
||||
DepotGameDetailViewModel gd => !string.IsNullOrEmpty(gd.Title) ? gd.Title : string.Empty,
|
||||
GameDetailViewModel gd => !string.IsNullOrEmpty(gd.Title) ? gd.Title : string.Empty,
|
||||
SettingsViewModel _ => "Settings",
|
||||
DownloadQueueViewModel _ => "Downloads",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
|
@ -54,6 +56,11 @@ public partial class ShellViewModel : ViewModelBase
|
|||
oldDetail.PropertyChanged -= OnGameDetailPropertyChanged;
|
||||
if (newValue is GameDetailViewModel newDetail)
|
||||
newDetail.PropertyChanged += OnGameDetailPropertyChanged;
|
||||
|
||||
if (oldValue is DepotBrowseViewModel oldBrowse)
|
||||
oldBrowse.PropertyChanged -= OnDepotBrowsePropertyChanged;
|
||||
if (newValue is DepotBrowseViewModel newBrowse)
|
||||
newBrowse.PropertyChanged += OnDepotBrowsePropertyChanged;
|
||||
}
|
||||
|
||||
private void OnGameDetailPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
|
|
@ -62,6 +69,12 @@ public partial class ShellViewModel : ViewModelBase
|
|||
OnPropertyChanged(nameof(ContentViewTitle));
|
||||
}
|
||||
|
||||
private void OnDepotBrowsePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(DepotBrowseViewModel.BrowseTitle))
|
||||
OnPropertyChanged(nameof(ContentViewTitle));
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isCheckingConnection;
|
||||
|
||||
|
|
@ -73,17 +86,29 @@ public partial class ShellViewModel : ViewModelBase
|
|||
public bool CanGoOnline => IsOfflineMode && !IsCheckingConnection;
|
||||
|
||||
// Child view models
|
||||
public DepotViewModel DepotViewModel { get; private set; } = null!;
|
||||
public GamesListViewModel GamesListViewModel { get; private set; } = null!;
|
||||
public LibraryViewModel LibraryViewModel { get; private set; } = null!;
|
||||
public GameDetailViewModel GameDetailViewModel { get; private set; } = null!;
|
||||
public DownloadQueueViewModel DownloadQueue { get; private set; } = null!;
|
||||
public SettingsViewModel SettingsViewModel { get; private set; } = null!;
|
||||
public ProfileViewModel Profile { get; private set; } = null!;
|
||||
public DepotViewModel DepotViewModel { get; private set; } = null!;
|
||||
public DepotBrowseViewModel DepotBrowseViewModel { get; private set; } = null!;
|
||||
public DepotGameDetailViewModel DepotGameDetailViewModel { get; private set; } = null!;
|
||||
public GamesListViewModel GamesListViewModel { get; private set; } = null!;
|
||||
public LibraryViewModel LibraryViewModel { get; private set; } = null!;
|
||||
public GameDetailViewModel GameDetailViewModel { get; private set; } = null!;
|
||||
public DownloadQueueViewModel DownloadQueue { get; private set; } = null!;
|
||||
public SettingsViewModel SettingsViewModel { get; private set; } = null!;
|
||||
public ProfileViewModel Profile { get; private set; } = null!;
|
||||
public ChatWindowViewModel Chat { get; private set; } = null!;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasUnreadChat))]
|
||||
private int _chatUnreadCount;
|
||||
|
||||
public bool HasUnreadChat => ChatUnreadCount > 0;
|
||||
|
||||
// Tracks previous view within the depot context for back navigation
|
||||
private ViewModelBase? _depotReturnView;
|
||||
|
||||
// Tracks the most recent depot browse filter so the view can be refreshed after library changes
|
||||
private (string? Genre, string? Tag, string? Collection, string? Search) _lastDepotBrowseFilter;
|
||||
|
||||
public event EventHandler? LogoutRequested;
|
||||
|
||||
public ShellViewModel(IServiceProvider serviceProvider)
|
||||
|
|
@ -105,28 +130,49 @@ public partial class ShellViewModel : ViewModelBase
|
|||
{
|
||||
_logger.LogInformation("ShellViewModel initializing... (Offline: {IsOffline})", IsOfflineMode);
|
||||
|
||||
DepotViewModel = new DepotViewModel(_serviceProvider);
|
||||
GamesListViewModel = new GamesListViewModel(_serviceProvider);
|
||||
LibraryViewModel = new LibraryViewModel(_serviceProvider);
|
||||
GameDetailViewModel = new GameDetailViewModel(_serviceProvider);
|
||||
DownloadQueue = new DownloadQueueViewModel(_serviceProvider);
|
||||
SettingsViewModel = new SettingsViewModel(_serviceProvider);
|
||||
DepotViewModel = new DepotViewModel(_serviceProvider);
|
||||
DepotBrowseViewModel = new DepotBrowseViewModel(_serviceProvider);
|
||||
DepotGameDetailViewModel = new DepotGameDetailViewModel(_serviceProvider);
|
||||
GamesListViewModel = new GamesListViewModel(_serviceProvider);
|
||||
LibraryViewModel = new LibraryViewModel(_serviceProvider);
|
||||
GameDetailViewModel = new GameDetailViewModel(_serviceProvider);
|
||||
DownloadQueue = new DownloadQueueViewModel(_serviceProvider);
|
||||
SettingsViewModel = new SettingsViewModel(_serviceProvider);
|
||||
// Profile is already created in the constructor; reuse it here
|
||||
|
||||
DepotViewModel.IsOfflineMode = IsOfflineMode;
|
||||
GamesListViewModel.IsOfflineMode = IsOfflineMode;
|
||||
LibraryViewModel.IsOfflineMode = IsOfflineMode;
|
||||
GameDetailViewModel.IsOfflineMode = IsOfflineMode;
|
||||
Chat = new ChatWindowViewModel(_serviceProvider);
|
||||
await Chat.InitializeAsync();
|
||||
Chat.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ChatWindowViewModel.TotalUnreadCount))
|
||||
ChatUnreadCount = Chat.TotalUnreadCount;
|
||||
};
|
||||
|
||||
DepotViewModel.GameSelected += OnGameSelected;
|
||||
DepotViewModel.SearchRequested += OnSearchRequested;
|
||||
DepotViewModel.BrowseByGenreRequested += OnDepotBrowseByGenre;
|
||||
DepotViewModel.BrowseByTagRequested += OnDepotBrowseByTag;
|
||||
DepotViewModel.IsOfflineMode = IsOfflineMode;
|
||||
DepotBrowseViewModel.IsOfflineMode = IsOfflineMode;
|
||||
DepotGameDetailViewModel.IsOfflineMode = IsOfflineMode;
|
||||
GamesListViewModel.IsOfflineMode = IsOfflineMode;
|
||||
LibraryViewModel.IsOfflineMode = IsOfflineMode;
|
||||
GameDetailViewModel.IsOfflineMode = IsOfflineMode;
|
||||
|
||||
DepotViewModel.GameSelected += OnDepotGameSelected;
|
||||
DepotViewModel.SearchRequested += OnSearchRequested;
|
||||
DepotViewModel.BrowseByGenreRequested += OnDepotBrowseByGenre;
|
||||
DepotViewModel.BrowseByTagRequested += OnDepotBrowseByTag;
|
||||
DepotViewModel.BrowseByCollectionRequested += OnDepotBrowseByCollection;
|
||||
DepotViewModel.BrowseAllRequested += OnDepotBrowseAll;
|
||||
DepotViewModel.BrowseAllRequested += OnDepotBrowseAll;
|
||||
|
||||
DepotBrowseViewModel.GameSelected += OnDepotGameSelected;
|
||||
DepotBrowseViewModel.BackToDepotRequested += OnBackFromDepotBrowse;
|
||||
|
||||
GamesListViewModel.GameSelected += OnGameSelected;
|
||||
LibraryViewModel.GameSelected += OnGameSelected;
|
||||
|
||||
DepotGameDetailViewModel.BackRequested += OnBackFromGameDetail;
|
||||
DepotGameDetailViewModel.LibraryChanged += OnLibraryChanged;
|
||||
DepotGameDetailViewModel.InstallRequested += OnInstallRequested;
|
||||
DepotGameDetailViewModel.SearchRequested += OnSearchRequested;
|
||||
|
||||
GameDetailViewModel.BackRequested += OnBackFromGameDetail;
|
||||
GameDetailViewModel.LibraryChanged += OnLibraryChanged;
|
||||
GameDetailViewModel.InstallRequested += OnInstallRequested;
|
||||
|
|
@ -207,10 +253,12 @@ public partial class ShellViewModel : ViewModelBase
|
|||
if (connectionClient.IsConnected())
|
||||
{
|
||||
IsOfflineMode = false;
|
||||
DepotViewModel.IsOfflineMode = false;
|
||||
GamesListViewModel.IsOfflineMode = false;
|
||||
LibraryViewModel.IsOfflineMode = false;
|
||||
GameDetailViewModel.IsOfflineMode = false;
|
||||
DepotViewModel.IsOfflineMode = false;
|
||||
DepotBrowseViewModel.IsOfflineMode = false;
|
||||
DepotGameDetailViewModel.IsOfflineMode = false;
|
||||
GamesListViewModel.IsOfflineMode = false;
|
||||
LibraryViewModel.IsOfflineMode = false;
|
||||
GameDetailViewModel.IsOfflineMode = false;
|
||||
|
||||
await ImportAndLoadAsync();
|
||||
return;
|
||||
|
|
@ -235,10 +283,12 @@ public partial class ShellViewModel : ViewModelBase
|
|||
if (IsOfflineMode) return;
|
||||
|
||||
IsOfflineMode = true;
|
||||
DepotViewModel.IsOfflineMode = true;
|
||||
GamesListViewModel.IsOfflineMode = true;
|
||||
LibraryViewModel.IsOfflineMode = true;
|
||||
GameDetailViewModel.IsOfflineMode = true;
|
||||
DepotViewModel.IsOfflineMode = true;
|
||||
DepotBrowseViewModel.IsOfflineMode = true;
|
||||
DepotGameDetailViewModel.IsOfflineMode = true;
|
||||
GamesListViewModel.IsOfflineMode = true;
|
||||
LibraryViewModel.IsOfflineMode = true;
|
||||
GameDetailViewModel.IsOfflineMode = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -268,6 +318,15 @@ public partial class ShellViewModel : ViewModelBase
|
|||
ContentView = LibraryViewModel;
|
||||
}
|
||||
|
||||
/// <summary>Game selected from the depot context (DepotView or DepotBrowseView).</summary>
|
||||
private void OnDepotGameSelected(object? sender, SDK.Models.Game game)
|
||||
{
|
||||
_depotReturnView = sender is DepotBrowseViewModel ? DepotBrowseViewModel : DepotViewModel;
|
||||
ContentView = DepotGameDetailViewModel;
|
||||
_ = DepotGameDetailViewModel.LoadGameAsync(game);
|
||||
}
|
||||
|
||||
/// <summary>Game selected from the library context.</summary>
|
||||
private void OnGameSelected(object? sender, SDK.Models.Game game)
|
||||
{
|
||||
GameDetailViewModel.FromLibrary = !IsDepotActive;
|
||||
|
|
@ -322,17 +381,9 @@ public partial class ShellViewModel : ViewModelBase
|
|||
.FirstOrDefault(g => string.Equals(g.Name, term, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchedGenre != null)
|
||||
{
|
||||
GamesListViewModel.SearchText = string.Empty;
|
||||
GamesListViewModel.SelectedGenre = matchedGenre;
|
||||
}
|
||||
NavigateToDepotBrowse(genre: matchedGenre.Name);
|
||||
else
|
||||
{
|
||||
GamesListViewModel.SelectedGenre = null;
|
||||
GamesListViewModel.SearchText = term;
|
||||
}
|
||||
|
||||
NavigateToGamesListFromDepot();
|
||||
NavigateToDepotBrowse(search: term);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -356,47 +407,44 @@ public partial class ShellViewModel : ViewModelBase
|
|||
|
||||
private void OnDepotBrowseByGenre(object? sender, string genreName)
|
||||
{
|
||||
GamesListViewModel.ClearFiltersCommand.Execute(null);
|
||||
var genre = GamesListViewModel.AvailableGenres
|
||||
.FirstOrDefault(g => string.Equals(g.Name, genreName, StringComparison.OrdinalIgnoreCase));
|
||||
if (genre != null)
|
||||
GamesListViewModel.SelectedGenre = genre;
|
||||
NavigateToGamesListFromDepot();
|
||||
NavigateToDepotBrowse(genre: genreName);
|
||||
}
|
||||
|
||||
private void OnDepotBrowseByTag(object? sender, string tagName)
|
||||
{
|
||||
GamesListViewModel.ClearFiltersCommand.Execute(null);
|
||||
GamesListViewModel.SelectedTag = GamesListViewModel.AvailableTags
|
||||
.FirstOrDefault(t => string.Equals(t, tagName, StringComparison.OrdinalIgnoreCase));
|
||||
NavigateToGamesListFromDepot();
|
||||
NavigateToDepotBrowse(tag: tagName);
|
||||
}
|
||||
|
||||
private void OnDepotBrowseByCollection(object? sender, string collectionName)
|
||||
{
|
||||
GamesListViewModel.ClearFiltersCommand.Execute(null);
|
||||
// Group by collection so the user sees games sorted into that collection
|
||||
GamesListViewModel.SelectedGroupBy = LANCommander.Launcher.Settings.Enums.GroupBy.Collection;
|
||||
NavigateToGamesListFromDepot();
|
||||
NavigateToDepotBrowse(collection: collectionName);
|
||||
}
|
||||
|
||||
private void OnDepotBrowseAll(object? sender, EventArgs e)
|
||||
{
|
||||
GamesListViewModel.ClearFiltersCommand.Execute(null);
|
||||
NavigateToGamesListFromDepot();
|
||||
NavigateToDepotBrowse();
|
||||
}
|
||||
|
||||
/// <summary>Navigate to the full games list while staying in depot context.</summary>
|
||||
private void NavigateToGamesListFromDepot()
|
||||
private void OnBackFromDepotBrowse(object? sender, EventArgs e)
|
||||
{
|
||||
IsDepotActive = true;
|
||||
_depotReturnView = DepotViewModel;
|
||||
ContentView = GamesListViewModel;
|
||||
_depotReturnView = null;
|
||||
ContentView = DepotViewModel;
|
||||
}
|
||||
|
||||
/// <summary>Initialize and navigate to the depot-only browse grid with an optional pre-filter.</summary>
|
||||
private void NavigateToDepotBrowse(string? genre = null, string? tag = null, string? collection = null, string? search = null)
|
||||
{
|
||||
_lastDepotBrowseFilter = (genre, tag, collection, search);
|
||||
DepotBrowseViewModel.Initialize(GamesListViewModel.GetAllGames(), genre, tag, collection, search);
|
||||
IsDepotActive = true;
|
||||
_depotReturnView = DepotBrowseViewModel;
|
||||
ContentView = DepotBrowseViewModel;
|
||||
}
|
||||
|
||||
private void OnBackFromGameDetail(object? sender, EventArgs e)
|
||||
{
|
||||
if (IsDepotActive)
|
||||
if (sender is DepotGameDetailViewModel || IsDepotActive)
|
||||
ContentView = _depotReturnView ?? DepotViewModel;
|
||||
else
|
||||
ShowLibrary();
|
||||
|
|
@ -407,6 +455,14 @@ public partial class ShellViewModel : ViewModelBase
|
|||
await LibraryViewModel.LoadGamesAsync();
|
||||
await GamesListViewModel.LoadGamesAsync();
|
||||
await DepotViewModel.LoadAsync();
|
||||
// Re-initialize the browse view with fresh data so "in library" badges update
|
||||
if (ContentView == DepotBrowseViewModel)
|
||||
DepotBrowseViewModel.Initialize(
|
||||
GamesListViewModel.GetAllGames(),
|
||||
_lastDepotBrowseFilter.Genre,
|
||||
_lastDepotBrowseFilter.Tag,
|
||||
_lastDepotBrowseFilter.Collection,
|
||||
_lastDepotBrowseFilter.Search);
|
||||
}
|
||||
|
||||
private void OnInstallRequested(object? sender, EventArgs e) => DownloadQueue.Show();
|
||||
|
|
@ -417,6 +473,9 @@ public partial class ShellViewModel : ViewModelBase
|
|||
await GamesListViewModel.LoadGamesAsync();
|
||||
await DepotViewModel.LoadAsync();
|
||||
|
||||
if (DepotGameDetailViewModel.Id == gameId)
|
||||
await DepotGameDetailViewModel.RefreshInstallStatusAsync();
|
||||
|
||||
if (GameDetailViewModel.Id == gameId)
|
||||
await GameDetailViewModel.RefreshInstallStatusAsync();
|
||||
}
|
||||
|
|
@ -428,6 +487,20 @@ public partial class ShellViewModel : ViewModelBase
|
|||
ContentView = SettingsViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenChatAsync()
|
||||
{
|
||||
if (Chat == null) return;
|
||||
|
||||
// Lazy-load threads on first open
|
||||
await Chat.LoadThreadsAsync();
|
||||
|
||||
// Raise event so the view layer can show the window
|
||||
OpenChatRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public event EventHandler? OpenChatRequested;
|
||||
|
||||
private void OnBackFromSettings(object? sender, EventArgs e)
|
||||
{
|
||||
if (IsDepotActive) ShowDepot();
|
||||
|
|
|
|||
252
LANCommander.Launcher.Avalonia/Views/DepotBrowseView.axaml
Normal file
252
LANCommander.Launcher.Avalonia/Views/DepotBrowseView.axaml
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LANCommander.Launcher.Avalonia.ViewModels"
|
||||
xmlns:enums="using:LANCommander.Launcher.Settings.Enums"
|
||||
xmlns:local="using:LANCommander.Launcher.Avalonia.Views"
|
||||
x:Class="LANCommander.Launcher.Avalonia.Views.DepotBrowseView"
|
||||
x:DataType="vm:DepotBrowseViewModel">
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Button.active">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
|
||||
</Style>
|
||||
<!-- Locked filter chip: looks like an active tag but non-interactive (no hover change) -->
|
||||
<Style Selector="Border.locked-chip">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- ── Filter Bar ──────────────────────────────────────────────────── -->
|
||||
<StackPanel DockPanel.Dock="Bottom"
|
||||
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">
|
||||
|
||||
<!-- Advanced panel — expands upward above the quick row -->
|
||||
<Border IsVisible="{Binding IsAdvancedFilterOpen}"
|
||||
Padding="12,10,12,4"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<WrapPanel Orientation="Horizontal" ItemWidth="180">
|
||||
|
||||
<!-- Sort -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8">
|
||||
<TextBlock Text="Sort" FontSize="11" Opacity="0.6" />
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<ComboBox Grid.Column="0"
|
||||
SelectedItem="{Binding SelectedSortBy}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.Items>
|
||||
<enums:SortBy>Title</enums:SortBy>
|
||||
<enums:SortBy>DateReleased</enums:SortBy>
|
||||
</ComboBox.Items>
|
||||
</ComboBox>
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding ToggleSortDirectionCommand}"
|
||||
Padding="8,6" Margin="4,0,0,0">
|
||||
<Panel>
|
||||
<Icon Type="Regular" Value="ArrowUp" Width="12" Height="12" IsVisible="{Binding SortAscending}" />
|
||||
<Icon Type="Regular" Value="ArrowDown" Width="12" Height="12" IsVisible="{Binding !SortAscending}" />
|
||||
</Panel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Group by (hidden when collection is locked — grouping is implicit) -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8"
|
||||
IsVisible="{Binding !IsCollectionLocked}">
|
||||
<TextBlock Text="Group By" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{Binding AvailableGroupByOptions}"
|
||||
SelectedItem="{Binding SelectedGroupBy}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Genre — hidden when genre is locked -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8"
|
||||
IsVisible="{Binding !IsGenreLocked}">
|
||||
<TextBlock Text="Genre" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{Binding AvailableGenres}"
|
||||
SelectedItem="{Binding SelectedGenre}"
|
||||
PlaceholderText="Any"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Tags — hidden when tag is locked -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8"
|
||||
IsVisible="{Binding !IsTagLocked}">
|
||||
<TextBlock Text="Tag" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{Binding AvailableTags}"
|
||||
SelectedItem="{Binding SelectedTag}"
|
||||
PlaceholderText="Any"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Developer -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8">
|
||||
<TextBlock Text="Developer" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{Binding AvailableDevelopers}"
|
||||
SelectedItem="{Binding SelectedDeveloper}"
|
||||
PlaceholderText="Any"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Publisher -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8">
|
||||
<TextBlock Text="Publisher" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{Binding AvailablePublishers}"
|
||||
SelectedItem="{Binding SelectedPublisher}"
|
||||
PlaceholderText="Any"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Multiplayer -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8">
|
||||
<TextBlock Text="Multiplayer" FontSize="11" Opacity="0.6" />
|
||||
<ComboBox ItemsSource="{x:Static vm:GamesCollectionViewModel.AvailableMultiplayerTypes}"
|
||||
SelectedItem="{Binding SelectedMultiplayerType}"
|
||||
PlaceholderText="Any"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- In Library (depot only) -->
|
||||
<StackPanel Spacing="4" Margin="0,0,12,8"
|
||||
IsVisible="{Binding ShowInLibraryFilter}">
|
||||
<TextBlock Text="Ownership" FontSize="11" Opacity="0.6" />
|
||||
<ToggleButton IsChecked="{Binding ShowInLibraryOnly}"
|
||||
Content="In Library Only"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Quick row -->
|
||||
<Border Padding="12,8">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
|
||||
|
||||
<!-- Back to Depot button (always visible on solid bar background) -->
|
||||
<Button Grid.Column="0"
|
||||
Command="{Binding GoBackCommand}"
|
||||
Classes="Text"
|
||||
Padding="8,6"
|
||||
Margin="0,0,8,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Icon Type="Regular" Value="ArrowLeft" Width="13" Height="13" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Depot" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Locked filter chip — shown when navigated from genre/tag/collection -->
|
||||
<Border Grid.Column="1"
|
||||
Classes="locked-chip"
|
||||
Margin="0,0,8,0"
|
||||
IsVisible="{Binding HasLockedFilter}"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="{Binding LockedFilterLabel}"
|
||||
FontSize="11"
|
||||
Opacity="0.7"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding LockedFilterValue}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Icon Type="Regular" Value="Lock" Width="10" Height="10"
|
||||
Opacity="0.7" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBox Grid.Column="2"
|
||||
Watermark="Search games..."
|
||||
Text="{Binding SearchText}"
|
||||
Margin="0,0,8,0" />
|
||||
|
||||
<!-- View type toggle -->
|
||||
<Border Grid.Column="3"
|
||||
CornerRadius="2"
|
||||
Margin="0,0,8,0"
|
||||
Padding="4"
|
||||
Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="8,6"
|
||||
ToolTip.Tip="Grid view"
|
||||
Classes.active="{Binding IsGridView}"
|
||||
Click="GridViewButton_Click">
|
||||
<Icon Type="Regular" Value="SquaresFour" Width="14" Height="14" />
|
||||
</Button>
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="8,6"
|
||||
ToolTip.Tip="List view"
|
||||
Classes.active="{Binding IsListView}"
|
||||
Click="ListViewButton_Click">
|
||||
<Icon Type="Regular" Value="Rows" Width="14" Height="14" />
|
||||
</Button>
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="8,6"
|
||||
ToolTip.Tip="Horizontal view"
|
||||
Classes.active="{Binding IsHorizontalView}"
|
||||
Click="HorizontalViewButton_Click">
|
||||
<Icon Type="Regular" Value="ArrowsHorizontal" Width="14" Height="14" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Advanced toggle -->
|
||||
<ToggleButton Grid.Column="4"
|
||||
Classes="Text"
|
||||
IsChecked="{Binding IsAdvancedFilterOpen}"
|
||||
Padding="8,6"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip.Tip="Advanced filters">
|
||||
<Icon Type="Regular" Value="FunnelSimple" Width="14" Height="14" />
|
||||
</ToggleButton>
|
||||
|
||||
<!-- Clear button — uses ClearAdditionalFiltersCommand to preserve locked filter -->
|
||||
<Button Grid.Column="5"
|
||||
Command="{Binding ClearAdditionalFiltersCommand}"
|
||||
Content="✕"
|
||||
Classes="Text Error"
|
||||
Padding="8,6" />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Loading ─────────────────────────────────────────────────────── -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
IsVisible="{Binding IsLoading}"
|
||||
Padding="16,54,16,10"
|
||||
HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||
<ProgressBar IsIndeterminate="True" Width="120" Height="3" />
|
||||
<TextBlock Text="Loading..." VerticalAlignment="Center" Opacity="0.6" FontSize="13" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Error ──────────────────────────────────────────────────────── -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
IsVisible="{Binding HasError}"
|
||||
Padding="12"
|
||||
Margin="16,44,16,0"
|
||||
CornerRadius="0">
|
||||
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" Foreground="IndianRed" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Sub-views ──────────────────────────────────────────────────── -->
|
||||
<Grid>
|
||||
<local:GamesGridView IsVisible="{Binding IsGridViewFlat}" />
|
||||
<local:GamesRowView IsVisible="{Binding IsListViewFlat}" />
|
||||
<local:GamesShelfView IsVisible="{Binding IsHorizontalViewFlat}" />
|
||||
<local:GamesGroupedView IsVisible="{Binding IsGrouped}" />
|
||||
</Grid>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LANCommander.Launcher.Avalonia.ViewModels;
|
||||
using LANCommander.Launcher.Settings.Enums;
|
||||
|
||||
namespace LANCommander.Launcher.Avalonia.Views;
|
||||
|
||||
public partial class DepotBrowseView : UserControl
|
||||
{
|
||||
public DepotBrowseView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void GridViewButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is GamesCollectionViewModel vm)
|
||||
vm.SelectedViewType = GameViewType.Grid;
|
||||
}
|
||||
|
||||
private void ListViewButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is GamesCollectionViewModel vm)
|
||||
vm.SelectedViewType = GameViewType.List;
|
||||
}
|
||||
|
||||
private void HorizontalViewButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is GamesCollectionViewModel vm)
|
||||
vm.SelectedViewType = GameViewType.Horizontal;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,14 @@
|
|||
<DataTemplate DataType="vm:DepotViewModel">
|
||||
<views:DepotView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:DepotBrowseViewModel">
|
||||
<views:DepotBrowseView />
|
||||
</DataTemplate>
|
||||
<!-- DepotGameDetailViewModel must appear before GameDetailViewModel
|
||||
so the more-specific type is matched first. -->
|
||||
<DataTemplate DataType="vm:DepotGameDetailViewModel">
|
||||
<views:GameDetailView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:GamesListViewModel">
|
||||
<views:GamesListView />
|
||||
</DataTemplate>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue