mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #501 from Noggog/modlist-gallery
Initial basic modlist gallery filtering
This commit is contained in:
commit
5893827ced
@ -10,6 +10,32 @@ namespace Wabbajack
|
||||
{
|
||||
public static class DynamicDataExt
|
||||
{
|
||||
public static IObservable<int> CollectionCount<TObject>(this IObservable<IChangeSet<TObject>> source)
|
||||
{
|
||||
int count = 0;
|
||||
return source
|
||||
.Select(changeSet =>
|
||||
{
|
||||
count += changeSet.Adds;
|
||||
count -= changeSet.Removes;
|
||||
return count;
|
||||
})
|
||||
.StartWith(0);
|
||||
}
|
||||
|
||||
public static IObservable<int> CollectionCount<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source)
|
||||
{
|
||||
int count = 0;
|
||||
return source
|
||||
.Select(changeSet =>
|
||||
{
|
||||
count += changeSet.Adds;
|
||||
count -= changeSet.Removes;
|
||||
return count;
|
||||
})
|
||||
.StartWith(0);
|
||||
}
|
||||
|
||||
public static IObservable<IChangeSet<TCache, TKey>> TransformAndCache<TObject, TKey, TCache>(
|
||||
this IObservable<IChangeSet<TObject, TKey>> obs,
|
||||
Func<TKey, TObject, TCache> onAdded,
|
||||
|
@ -31,13 +31,21 @@ namespace Wabbajack
|
||||
public BackNavigatingVM(MainWindowVM mainWindowVM)
|
||||
{
|
||||
BackCommand = ReactiveCommand.Create(
|
||||
execute: () => Utils.CatchAndLog(() => mainWindowVM.NavigateTo(NavigateBackTarget)),
|
||||
execute: () => Utils.CatchAndLog(() =>
|
||||
{
|
||||
mainWindowVM.NavigateTo(NavigateBackTarget);
|
||||
Unload();
|
||||
}),
|
||||
canExecute: this.ConstructCanNavigateBack()
|
||||
.ObserveOnGuiThread());
|
||||
|
||||
_IsActive = this.ConstructIsActive(mainWindowVM)
|
||||
.ToGuiProperty(this, nameof(IsActive));
|
||||
}
|
||||
|
||||
public virtual void Unload()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class IBackNavigatingVMExt
|
||||
|
132
Wabbajack/View Models/Gallery/ModListGalleryVM.cs
Normal file
132
Wabbajack/View Models/Gallery/ModListGalleryVM.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.ModListRegistry;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class ModListGalleryVM : BackNavigatingVM
|
||||
{
|
||||
public MainWindowVM MWVM { get; }
|
||||
|
||||
public ObservableCollectionExtended<ModListMetadataVM> ModLists { get; } = new ObservableCollectionExtended<ModListMetadataVM>();
|
||||
|
||||
private int missingHashFallbackCounter;
|
||||
|
||||
[Reactive]
|
||||
public IErrorResponse Error { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string Search { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public bool OnlyInstalled { get; set; }
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _Loaded;
|
||||
public bool Loaded => _Loaded.Value;
|
||||
|
||||
public ICommand ClearFiltersCommand { get; }
|
||||
|
||||
public ModListGalleryVM(MainWindowVM mainWindowVM)
|
||||
: base(mainWindowVM)
|
||||
{
|
||||
MWVM = mainWindowVM;
|
||||
|
||||
ClearFiltersCommand = ReactiveCommand.Create(
|
||||
() =>
|
||||
{
|
||||
OnlyInstalled = false;
|
||||
Search = string.Empty;
|
||||
});
|
||||
|
||||
var random = new Random();
|
||||
var sourceList = Observable.Return(Unit.Default)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.SelectTask(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Error = null;
|
||||
var list = await ModlistMetadata.LoadFromGithub();
|
||||
Error = ErrorResponse.Success;
|
||||
return list
|
||||
// Sort randomly initially, just to give each list a fair shake
|
||||
.Shuffle(random)
|
||||
.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.Error(ex);
|
||||
Error = ErrorResponse.Fail(ex);
|
||||
return Observable.Empty<IChangeSet<ModlistMetadata, string>>();
|
||||
}
|
||||
})
|
||||
// Unsubscribe and release when not active
|
||||
.FlowSwitch(
|
||||
this.WhenAny(x => x.IsActive),
|
||||
valueWhenOff: Observable.Return(ChangeSet<ModlistMetadata, string>.Empty))
|
||||
.Switch()
|
||||
.RefCount();
|
||||
|
||||
_Loaded = sourceList.CollectionCount()
|
||||
.Select(c => c > 0)
|
||||
.ToProperty(this, nameof(Loaded));
|
||||
|
||||
// Convert to VM and bind to resulting list
|
||||
sourceList
|
||||
.ObserveOnGuiThread()
|
||||
.Transform(m => new ModListMetadataVM(this, m))
|
||||
.DisposeMany()
|
||||
// Filter only installed
|
||||
.Filter(predicateChanged: this.WhenAny(x => x.OnlyInstalled)
|
||||
.Select<bool, Func<ModListMetadataVM, bool>>(onlyInstalled => (vm) =>
|
||||
{
|
||||
if (!onlyInstalled) return true;
|
||||
if (!GameRegistry.Games.TryGetValue(vm.Metadata.Game, out var gameMeta)) return false;
|
||||
return gameMeta.IsInstalled;
|
||||
}))
|
||||
// Filter on search box
|
||||
.Filter(predicateChanged: this.WhenAny(x => x.Search)
|
||||
.Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler)
|
||||
.Select<string, Func<ModListMetadataVM, bool>>(search => (vm) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search)) return true;
|
||||
return vm.Metadata.Title.ContainsCaseInsensitive(search);
|
||||
}))
|
||||
// Put broken lists at bottom
|
||||
.Sort(Comparer<ModListMetadataVM>.Create((a, b) => a.IsBroken.CompareTo(b.IsBroken)))
|
||||
.Bind(ModLists)
|
||||
.Subscribe()
|
||||
.DisposeWith(CompositeDisposable);
|
||||
|
||||
// Extra GC when navigating away, just to immediately clean up modlist metadata
|
||||
this.WhenAny(x => x.IsActive)
|
||||
.Where(x => !x)
|
||||
.Skip(1)
|
||||
.Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
GC.Collect();
|
||||
})
|
||||
.DisposeWith(CompositeDisposable);
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
Error = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.ModListRegistry;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class ModListGalleryVM : BackNavigatingVM
|
||||
{
|
||||
public MainWindowVM MWVM { get; }
|
||||
|
||||
public ObservableCollectionExtended<ModListMetadataVM> ModLists { get; } = new ObservableCollectionExtended<ModListMetadataVM>();
|
||||
|
||||
private int missingHashFallbackCounter;
|
||||
|
||||
[Reactive]
|
||||
public IErrorResponse Error { get; set; }
|
||||
|
||||
public ModListGalleryVM(MainWindowVM mainWindowVM)
|
||||
: base(mainWindowVM)
|
||||
{
|
||||
MWVM = mainWindowVM;
|
||||
|
||||
Observable.Return(Unit.Default)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.SelectTask(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Error = null;
|
||||
var list = await ModlistMetadata.LoadFromGithub();
|
||||
return list.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.Error(ex);
|
||||
Error = ErrorResponse.Fail(ex);
|
||||
return Observable.Empty<IChangeSet<ModlistMetadata, string>>();
|
||||
}
|
||||
})
|
||||
// Unsubscribe and release when not active
|
||||
.FlowSwitch(
|
||||
this.WhenAny(x => x.IsActive),
|
||||
valueWhenOff: Observable.Return(ChangeSet<ModlistMetadata, string>.Empty))
|
||||
// Convert to VM and bind to resulting list
|
||||
.Switch()
|
||||
.ObserveOnGuiThread()
|
||||
.Transform(m => new ModListMetadataVM(this, m))
|
||||
.DisposeMany()
|
||||
.Bind(ModLists)
|
||||
.Subscribe()
|
||||
.DisposeWith(CompositeDisposable);
|
||||
|
||||
// Extra GC when navigating away, just to immediately clean up modlist metadata
|
||||
this.WhenAny(x => x.IsActive)
|
||||
.Where(x => !x)
|
||||
.Skip(1)
|
||||
.Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
GC.Collect();
|
||||
})
|
||||
.DisposeWith(CompositeDisposable);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,7 +37,8 @@
|
||||
<ItemsControl
|
||||
x:Name="ModListGalleryControl"
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Center">
|
||||
HorizontalAlignment="Center"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
@ -51,7 +52,29 @@
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<mahapps:ProgressRing x:Name="LoadingRing" Grid.Row="1" Visibility="Collapsed" />
|
||||
<mahapps:ProgressRing
|
||||
x:Name="LoadingRing"
|
||||
Grid.Row="1"
|
||||
Visibility="Collapsed" />
|
||||
<StackPanel
|
||||
x:Name="NoneFound"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Visibility="Collapsed">
|
||||
<iconPacks:PackIconControl
|
||||
Width="50"
|
||||
Height="50"
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource Triadic2Brush}"
|
||||
Kind="{x:Static iconPacks:PackIconMaterialKind.Cancel}" />
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource ForegroundBrush}"
|
||||
Text="No Matches" />
|
||||
</StackPanel>
|
||||
<iconPacks:PackIconControl
|
||||
x:Name="ErrorIcon"
|
||||
Grid.Row="1"
|
||||
@ -68,6 +91,35 @@
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
ShadowMargin="False" />
|
||||
<WrapPanel
|
||||
Grid.Row="0"
|
||||
Height="25"
|
||||
Margin="5,5,5,10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{StaticResource ForegroundBrush}"
|
||||
Text="Search" />
|
||||
<TextBox
|
||||
x:Name="SearchBox"
|
||||
Width="160"
|
||||
VerticalContentAlignment="Center" />
|
||||
<CheckBox
|
||||
x:Name="OnlyInstalledCheckbox"
|
||||
Margin="20,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Content="Only Installed"
|
||||
Foreground="{StaticResource ForegroundBrush}" />
|
||||
<Button
|
||||
x:Name="ClearFiltersButton"
|
||||
Margin="0,0,10,0"
|
||||
Style="{StaticResource IconBareButtonStyle}"
|
||||
ToolTip="Clear All Filters">
|
||||
<iconPacks:Material Kind="FilterRemove" />
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
<Button
|
||||
x:Name="BackButton"
|
||||
Grid.Row="0"
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
@ -24,23 +25,46 @@ namespace Wabbajack
|
||||
.BindToStrict(this, x => x.ModListGalleryControl.ItemsSource)
|
||||
.DisposeWith(dispose);
|
||||
Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ViewModel.ModLists.Count)
|
||||
.Select(x => x > 0),
|
||||
this.WhenAny(x => x.ViewModel.Error)
|
||||
.Select(e => e?.Succeeded ?? true),
|
||||
resultSelector: (hasContent, succeeded) =>
|
||||
this.WhenAny(x => x.ViewModel.Error),
|
||||
this.WhenAny(x => x.ViewModel.Loaded),
|
||||
resultSelector: (err, loaded) =>
|
||||
{
|
||||
return !hasContent && succeeded;
|
||||
if (!err?.Succeeded ?? false) return true;
|
||||
return !loaded;
|
||||
})
|
||||
.DistinctUntilChanged()
|
||||
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
|
||||
.StartWith(Visibility.Collapsed)
|
||||
.BindToStrict(this, x => x.LoadingRing.Visibility)
|
||||
.DisposeWith(dispose);
|
||||
Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ViewModel.ModLists.Count)
|
||||
.Select(x => x > 0),
|
||||
this.WhenAny(x => x.ViewModel.Loaded),
|
||||
resultSelector: (hasContent, loaded) =>
|
||||
{
|
||||
return !hasContent && loaded;
|
||||
})
|
||||
.DistinctUntilChanged()
|
||||
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
|
||||
.StartWith(Visibility.Collapsed)
|
||||
.BindToStrict(this, x => x.NoneFound.Visibility)
|
||||
.DisposeWith(dispose);
|
||||
this.WhenAny(x => x.ViewModel.Error)
|
||||
.Select(e => (e?.Succeeded ?? true) ? Visibility.Collapsed : Visibility.Visible)
|
||||
.StartWith(Visibility.Collapsed)
|
||||
.BindToStrict(this, x => x.ErrorIcon.Visibility)
|
||||
.DisposeWith(dispose);
|
||||
|
||||
this.BindStrict(this.ViewModel, vm => vm.Search, x => x.SearchBox.Text)
|
||||
.DisposeWith(dispose);
|
||||
|
||||
this.BindStrict(this.ViewModel, vm => vm.OnlyInstalled, x => x.OnlyInstalledCheckbox.IsChecked)
|
||||
.DisposeWith(dispose);
|
||||
|
||||
this.WhenAny(x => x.ViewModel.ClearFiltersCommand)
|
||||
.BindToStrict(this, x => x.ClearFiltersButton.Command)
|
||||
.DisposeWith(dispose);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +152,8 @@
|
||||
x:Name="Overlay"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2" />
|
||||
Grid.ColumnSpan="2"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock
|
||||
x:Name="DescriptionTextShadow"
|
||||
Grid.Row="0"
|
||||
|
Loading…
Reference in New Issue
Block a user