using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net.Http; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Text.Json; using System.Threading.Tasks; using DynamicData; using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.App.Controls; using Wabbajack.App.Models; using Wabbajack.App.ViewModels; using Wabbajack.Common; using Wabbajack.Downloaders; using Wabbajack.Downloaders.GameFile; using Wabbajack.DTOs; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; using Wabbajack.VFS; namespace Wabbajack.App.Screens; public class BrowseViewModel : ViewModelBase, IActivatableViewModel { private readonly Configuration _configuration; private readonly DownloadDispatcher _dispatcher; private readonly IResource _dispatcherLimiter; private readonly DTOSerializer _dtos; public readonly ReadOnlyObservableCollection _filteredGamesList; public readonly ReadOnlyObservableCollection _filteredModLists; private readonly GameLocator _gameLocator; private readonly FileHashCache _hashCache; private readonly HttpClient _httpClient; private readonly IResource _limiter; private readonly ILogger _logger; private readonly Client _wjClient; private readonly SourceCache _gamesList = new(x => x.Name); private readonly SourceCache _modLists = new(x => x.MachineURL); private readonly ImageCache _imageCache; public BrowseViewModel(ILogger logger, Client wjClient, HttpClient httpClient, IResource limiter, FileHashCache hashCache, IResource dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, ImageCache imageCache, DTOSerializer dtos, Configuration configuration) { LoadingLock = new LoadingLock(); Activator = new ViewModelActivator(); _wjClient = wjClient; _logger = logger; _httpClient = httpClient; _limiter = limiter; _hashCache = hashCache; _configuration = configuration; _dispatcher = dispatcher; _dispatcherLimiter = dispatcherLimiter; _gameLocator = gameLocator; _imageCache = imageCache; _dtos = dtos; var searchTextPredicates = this.ObservableForProperty(vm => vm.SearchText) .Select(change => change.Value) .StartWith("") .Select>(txt => { if (string.IsNullOrWhiteSpace(txt)) return _ => true; return item => item.Title.Contains(txt, StringComparison.InvariantCultureIgnoreCase) || item.Description.Contains(txt, StringComparison.InvariantCultureIgnoreCase); }); _gamesList.Edit(e => { e.Clear(); foreach (var game in GameRegistry.Games.Keys) e.AddOrUpdate(new GameSelectorItemViewModel(game)); }); _gamesList.Connect() .ObserveOn(RxApp.MainThreadScheduler) .Sort(Comparer.Create((a, b) => string.CompareOrdinal(a.Name, b.Name))) .Bind(out _filteredGamesList) .Subscribe(); var gameFilter = this.ObservableForProperty(vm => vm.SelectedGame) .Select(v => v.Value) .Select>(selected => { if (selected == null) return _ => true; return item => item.Game == selected.Game; }) .StartWith(_ => true); var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalledGames) .Select(v => v.Value) .Select>(onlyInstalled => { if (onlyInstalled == false) return _ => true; return item => _gameLocator.IsInstalled(item.Game); }) .StartWith(_ => true); var onlyUtilityListsFilter = this.ObservableForProperty(vm => vm.OnlyUtilityLists) .Select(v => v.Value) .Select>(utility => { if (utility == false) return item => item.IsUtilityList == false; return item => item.IsUtilityList; }) .StartWith(item => item.IsUtilityList == false); var showNSFWFilter = this.ObservableForProperty(vm => vm.ShowNSFW) .Select(v => v.Value) .Select>(showNSFW => { return item => item.IsNSFW == showNSFW; }) .StartWith(item => item.IsNSFW == false); _modLists.Connect() .ObserveOn(RxApp.MainThreadScheduler) .Filter(searchTextPredicates) .Filter(gameFilter) .Filter(onlyInstalledGamesFilter) .Filter(onlyUtilityListsFilter) .Filter(showNSFWFilter) .SortBy(x => x.State == ModListState.Disabled ? 1 : 0) .Bind(out _filteredModLists) .Subscribe(); ResetFiltersCommand = ReactiveCommand.Create(() => { SelectedGame = null; SearchText = ""; }); this.WhenActivated(disposables => { LoadSettings().FireAndForget(); LoadData().FireAndForget(); Disposable.Create(() => { SaveSettings().FireAndForget(); }).DisposeWith(disposables); /* var searchTextFilter = this.ObservableForProperty(view => view.SearchText) .Select, Func<>>(text => { if (string.IsNullOrWhiteSpace(text.Value)) return lst => true; return })*/ }); } public ReadOnlyObservableCollection ModLists => _filteredModLists; public ReadOnlyObservableCollection GamesList => _filteredGamesList; [Reactive] public GameSelectorItemViewModel? SelectedGame { get; set; } [Reactive] public string SearchText { get; set; } [Reactive] public bool OnlyInstalledGames { get; set; } [Reactive] public bool OnlyUtilityLists { get; set; } [Reactive] public bool ShowNSFW { get; set; } [Reactive] public bool IsLoading { get; set; } = false; [Reactive] public LoadingLock LoadingLock { get; set; } [Reactive] public ReactiveCommand ResetFiltersCommand { get; set; } private AbsolutePath SavedSettingsLocation => _configuration.SavedSettingsLocation.Combine("browse_view.json"); private async Task LoadData() { using var _ = LoadingLock.WithLoading(); var modlists = await _wjClient.LoadLists(); var summaries = (await _wjClient.GetListStatuses()).ToDictionary(m => m.MachineURL); var vms = modlists.Select(m => { if (!summaries.TryGetValue(m.Links.MachineURL, out var summary)) summary = new ModListSummary(); return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _imageCache, _dtos, _logger); }); _modLists.Edit(lsts => { lsts.Clear(); lsts.AddOrUpdate(vms); }); _logger.LogInformation("Loaded data for {Count} modlists", _modLists.Count); } private async Task LoadSettings() { using var _ = LoadingLock.WithLoading(); try { if (SavedSettingsLocation.FileExists()) { await using var stream = SavedSettingsLocation.Open(FileMode.Open); var data = (await JsonSerializer.DeserializeAsync(stream))!; SearchText = data.SearchText; SelectedGame = data.SelectedGame == null ? null : _gamesList.Lookup(data.SelectedGame.Value.MetaData().HumanFriendlyGameName).Value; ShowNSFW = data.ShowNSFW; OnlyUtilityLists = data.OnlyUtility; OnlyInstalledGames = data.OnlyInstalled; } } catch (Exception ex) { _logger.LogWarning(ex, "While loading gallery browse settings"); } } private async Task SaveSettings() { try { var settings = new SavedSettings { SearchText = SearchText, OnlyInstalled = OnlyInstalledGames, OnlyUtility = OnlyUtilityLists, ShowNSFW = ShowNSFW, SelectedGame = SelectedGame?.Game }; SavedSettingsLocation.Parent.CreateDirectory(); await using var stream = SavedSettingsLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None); await JsonSerializer.SerializeAsync(stream, settings); } catch (Exception ex) { _logger.LogWarning(ex, "While saving gallery browse settings"); } } private class SavedSettings { public string SearchText { get; set; } public bool ShowNSFW { get; set; } public bool OnlyUtility { get; set; } public bool OnlyInstalled { get; set; } public Game? SelectedGame { get; set; } } }