Merge pull request #501 from Noggog/modlist-gallery

Initial basic modlist gallery filtering
This commit is contained in:
Timothy Baldridge 2020-02-10 21:07:21 -07:00 committed by GitHub
commit 5893827ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 89 deletions

View File

@ -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,

View File

@ -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

View 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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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"

View File

@ -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);
});
}
}

View File

@ -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"