mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #391 from Noggog/image-loading-gui-GC
Image Loading Display and GUI GC correctness pass
This commit is contained in:
commit
f959c3871e
@ -1,5 +1,9 @@
|
||||
### Changelog
|
||||
|
||||
* Progress ring displays when downloading modlist images
|
||||
* GUI releases memory of download modlists better when navigating around
|
||||
* Fixed phrasing after failed installations to say "failed".
|
||||
|
||||
#### Version - 1.0 beta 15 - 1/6/2020
|
||||
* Don't delete the download folder when deleting empty folders during an update
|
||||
* If `Game Folder Files` exists in the MO2 folder during compilation the Game folder will be ignored as a file source
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Concurrency;
|
||||
@ -67,7 +67,7 @@ namespace Wabbajack
|
||||
/// <param name="filterSwitch">On/Off signal of whether to subscribe to source observable</param>
|
||||
/// <param name="valueOnOff">Value to fire when switching off</param>
|
||||
/// <returns>Observable that publishes data from source, if the switch is on.</returns>
|
||||
public static IObservable<T> FilterSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch, T valueWhenOff)
|
||||
public static IObservable<T> FlowSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch, T valueWhenOff)
|
||||
{
|
||||
return filterSwitch
|
||||
.DistinctUntilChanged()
|
||||
@ -88,7 +88,7 @@ namespace Wabbajack
|
||||
/// Inspiration:
|
||||
/// http://reactivex.io/documentation/operators/debounce.html
|
||||
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
|
||||
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler scheduler = null)
|
||||
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler scheduler)
|
||||
{
|
||||
scheduler = scheduler ?? Scheduler.Default;
|
||||
return Observable.Create<T>(o =>
|
||||
@ -209,15 +209,6 @@ namespace Wabbajack
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay)
|
||||
{
|
||||
return source.FlowSwitch(
|
||||
Observable.Return(System.Reactive.Unit.Default)
|
||||
.Delay(delay)
|
||||
.Select(_ => true)
|
||||
.StartWith(false));
|
||||
}
|
||||
|
||||
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
|
||||
{
|
||||
return source.FlowSwitch(
|
||||
@ -226,5 +217,21 @@ namespace Wabbajack
|
||||
.Select(_ => true)
|
||||
.StartWith(false));
|
||||
}
|
||||
|
||||
public static IObservable<T> DisposeOld<T>(this IObservable<T> source)
|
||||
where T : IDisposable
|
||||
{
|
||||
return source
|
||||
.StartWith(default(T))
|
||||
.Pairwise()
|
||||
.Do(x =>
|
||||
{
|
||||
if (x.Previous != null)
|
||||
{
|
||||
x.Previous.Dispose();
|
||||
}
|
||||
})
|
||||
.Select(x => x.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1131,9 +1131,7 @@ namespace Wabbajack.Common
|
||||
|
||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> coll)
|
||||
{
|
||||
var hs = new HashSet<T>();
|
||||
coll.Do(v => hs.Add(v));
|
||||
return hs;
|
||||
return new HashSet<T>(coll);
|
||||
}
|
||||
|
||||
public static HashSet<T> ToHashSet<T>(this T[] coll)
|
||||
|
@ -2,10 +2,12 @@
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using DynamicData;
|
||||
using DynamicData.Kernel;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Lib;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
@ -78,6 +80,29 @@ namespace Wabbajack
|
||||
return new ChangeSet<TObject, TKey>(changes);
|
||||
}
|
||||
|
||||
public static ObservableAsPropertyHelper<TRet> ToGuiProperty<TRet>(
|
||||
this IObservable<TRet> source,
|
||||
ViewModel vm,
|
||||
string property,
|
||||
TRet initialValue = default,
|
||||
bool deferSubscription = false)
|
||||
{
|
||||
return source
|
||||
.ToProperty(vm, property, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
||||
.DisposeWith(vm.CompositeDisposable);
|
||||
}
|
||||
|
||||
public static void ToGuiProperty<TRet>(
|
||||
this IObservable<TRet> source,
|
||||
ViewModel vm,
|
||||
string property,
|
||||
out ObservableAsPropertyHelper<TRet> result,
|
||||
TRet initialValue = default,
|
||||
bool deferSubscription = false)
|
||||
{
|
||||
source.ToProperty(vm, property, out result, initialValue, deferSubscription, RxApp.MainThreadScheduler)
|
||||
.DisposeWith(vm.CompositeDisposable);
|
||||
}
|
||||
|
||||
internal static Optional<Change<TObject, TKey>> Reduce<TObject, TKey>(Optional<Change<TObject, TKey>> previous, Change<TObject, TKey> next)
|
||||
{
|
||||
|
@ -80,7 +80,7 @@ namespace Wabbajack
|
||||
this.WhenAny(x => x.TargetPath)
|
||||
// Dont want to debounce the initial value, because we know it's null
|
||||
.Skip(1)
|
||||
.Debounce(TimeSpan.FromMilliseconds(200), RxApp.TaskpoolScheduler)
|
||||
.Debounce(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler)
|
||||
.StartWith(default(string)),
|
||||
resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path))
|
||||
.StartWith((ExistsOption: ExistCheckOption, Type: PathType, Path: TargetPath))
|
||||
@ -147,9 +147,8 @@ namespace Wabbajack
|
||||
}
|
||||
})
|
||||
.DistinctUntilChanged()
|
||||
.ObserveOnGuiThread()
|
||||
.StartWith(false)
|
||||
.ToProperty(this, nameof(Exists));
|
||||
.ToGuiProperty(this, nameof(Exists));
|
||||
|
||||
var passesFilters = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.TargetPath),
|
||||
@ -218,12 +217,11 @@ namespace Wabbajack
|
||||
if (filter.Failed) return filter;
|
||||
return ErrorResponse.Convert(err);
|
||||
})
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ErrorState));
|
||||
.ToGuiProperty(this, nameof(ErrorState));
|
||||
|
||||
_inError = this.WhenAny(x => x.ErrorState)
|
||||
.Select(x => !x.Succeeded)
|
||||
.ToProperty(this, nameof(InError));
|
||||
.ToGuiProperty(this, nameof(InError));
|
||||
|
||||
// Doesn't derive from ErrorState, as we want to bubble non-empty tooltips,
|
||||
// which is slightly different logic
|
||||
@ -244,8 +242,7 @@ namespace Wabbajack
|
||||
if (!string.IsNullOrWhiteSpace(filters)) return filters;
|
||||
return err?.Reason;
|
||||
})
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ErrorTooltip));
|
||||
.ToGuiProperty(this, nameof(ErrorTooltip));
|
||||
}
|
||||
|
||||
public ICommand ConstructTypicalPickerCommand()
|
||||
|
@ -35,9 +35,8 @@ namespace Wabbajack
|
||||
canExecute: this.ConstructCanNavigateBack()
|
||||
.ObserveOnGuiThread());
|
||||
|
||||
_IsActive = mainWindowVM.WhenAny(x => x.ActivePane)
|
||||
.Select(x => object.ReferenceEquals(this, x))
|
||||
.ToProperty(this, nameof(IsActive));
|
||||
_IsActive = this.ConstructIsActive(mainWindowVM)
|
||||
.ToGuiProperty(this, nameof(IsActive));
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,5 +47,11 @@ namespace Wabbajack
|
||||
return vm.WhenAny(x => x.NavigateBackTarget)
|
||||
.Select(x => x != null);
|
||||
}
|
||||
|
||||
public static IObservable<bool> ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm)
|
||||
{
|
||||
return mwvm.WhenAny(x => x.ActivePane)
|
||||
.Select(x => object.ReferenceEquals(vm, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,15 +115,15 @@ namespace Wabbajack
|
||||
pair.Previous?.Unload();
|
||||
})
|
||||
.Select(p => p.Current)
|
||||
.ToProperty(this, nameof(Compiler));
|
||||
.ToGuiProperty(this, nameof(Compiler));
|
||||
|
||||
// Let sub VM determine what settings we're displaying and when
|
||||
_currentModlistSettings = this.WhenAny(x => x.Compiler.ModlistSettings)
|
||||
.ToProperty(this, nameof(CurrentModlistSettings));
|
||||
.ToGuiProperty(this, nameof(CurrentModlistSettings));
|
||||
|
||||
_image = this.WhenAny(x => x.CurrentModlistSettings.ImagePath.TargetPath)
|
||||
// Throttle so that it only loads image after any sets of swaps have completed
|
||||
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.TaskpoolScheduler)
|
||||
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
|
||||
.DistinctUntilChanged()
|
||||
.ObserveOnGuiThread()
|
||||
.Select(path =>
|
||||
@ -135,12 +135,11 @@ namespace Wabbajack
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.ToProperty(this, nameof(Image));
|
||||
.ToGuiProperty(this, nameof(Image));
|
||||
|
||||
_compiling = this.WhenAny(x => x.Compiler.ActiveCompilation)
|
||||
.Select(compilation => compilation != null)
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(Compiling));
|
||||
.ToGuiProperty(this, nameof(Compiling));
|
||||
|
||||
BackCommand = ReactiveCommand.Create(
|
||||
execute: () =>
|
||||
@ -175,8 +174,8 @@ namespace Wabbajack
|
||||
return compiler.PercentCompleted.StartWith(0);
|
||||
})
|
||||
.Switch()
|
||||
.Debounce(TimeSpan.FromMilliseconds(25))
|
||||
.ToProperty(this, nameof(PercentCompleted));
|
||||
.Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
|
||||
.ToGuiProperty(this, nameof(PercentCompleted));
|
||||
|
||||
BeginCommand = ReactiveCommand.CreateFromTask(
|
||||
canExecute: this.WhenAny(x => x.Compiler.CanCompile)
|
||||
@ -217,8 +216,7 @@ namespace Wabbajack
|
||||
_ActiveGlobalUserIntervention = activeInterventions.Connect()
|
||||
.Filter(x => x.CpuID == WorkQueue.UnassignedCpuId)
|
||||
.QueryWhenChanged(query => query.FirstOrDefault())
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
|
||||
.ToGuiProperty(this, nameof(ActiveGlobalUserIntervention));
|
||||
|
||||
CloseWhenCompleteCommand = ReactiveCommand.Create(
|
||||
canExecute: this.WhenAny(x => x.Completed)
|
||||
@ -243,26 +241,31 @@ namespace Wabbajack
|
||||
}
|
||||
});
|
||||
|
||||
_progressTitle = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.Compiling),
|
||||
this.WhenAny(x => x.StartedCompilation),
|
||||
resultSelector: (compiling, started) =>
|
||||
_progressTitle = this.WhenAnyValue(
|
||||
x => x.Compiling,
|
||||
x => x.StartedCompilation,
|
||||
x => x.Completed,
|
||||
selector: (compiling, started, completed) =>
|
||||
{
|
||||
if (compiling)
|
||||
{
|
||||
return "Compiling";
|
||||
}
|
||||
else if (started)
|
||||
{
|
||||
if (completed == null) return "Compiling";
|
||||
return completed.Value.Succeeded ? "Compiled" : "Failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
return started ? "Compiled" : "Configuring";
|
||||
return "Configuring";
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(ProgressTitle));
|
||||
.ToGuiProperty(this, nameof(ProgressTitle));
|
||||
|
||||
_CurrentCpuCount = this.WhenAny(x => x.Compiler.ActiveCompilation.Queue.CurrentCpuCount)
|
||||
.Switch()
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(CurrentCpuCount));
|
||||
.ToGuiProperty(this, nameof(CurrentCpuCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ namespace Wabbajack
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(Mo2Folder));
|
||||
.ToGuiProperty(this, nameof(Mo2Folder));
|
||||
_moProfile = this.WhenAny(x => x.ModListLocation.TargetPath)
|
||||
.Select(loc =>
|
||||
{
|
||||
@ -82,7 +82,7 @@ namespace Wabbajack
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(MOProfile));
|
||||
.ToGuiProperty(this, nameof(MOProfile));
|
||||
|
||||
// Wire missing Mo2Folder to signal error state for ModList Location
|
||||
ModListLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder)
|
||||
@ -98,7 +98,7 @@ namespace Wabbajack
|
||||
(this).WhenAny(x => x.ModListLocation.TargetPath),
|
||||
resultSelector: (state, path) => (State: state, Path: path))
|
||||
// A short throttle is a quick hack to make the above changes "atomic"
|
||||
.Throttle(TimeSpan.FromMilliseconds(25))
|
||||
.Throttle(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
|
||||
.Select(u =>
|
||||
{
|
||||
if (u.State.Failed) return null;
|
||||
@ -116,9 +116,7 @@ namespace Wabbajack
|
||||
pair.Current?.Init();
|
||||
})
|
||||
.Select(x => x.Current)
|
||||
// Save to property
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ModlistSettings));
|
||||
.ToGuiProperty(this, nameof(ModlistSettings));
|
||||
|
||||
CanCompile = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModListLocation.InError),
|
||||
@ -143,8 +141,8 @@ namespace Wabbajack
|
||||
.DisposeWith(CompositeDisposable);
|
||||
|
||||
// If Mo2 folder changes and download location is empty, set it for convenience
|
||||
(this).WhenAny(x => x.Mo2Folder)
|
||||
.DelayInitial(TimeSpan.FromMilliseconds(100))
|
||||
this.WhenAny(x => x.Mo2Folder)
|
||||
.DelayInitial(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler)
|
||||
.Where(x => Directory.Exists(x))
|
||||
.FlowSwitch(
|
||||
(this).WhenAny(x => x.DownloadLocation.Exists)
|
||||
|
@ -94,9 +94,7 @@ namespace Wabbajack
|
||||
current?.Init();
|
||||
})
|
||||
.Select(x => x.Current)
|
||||
// Save to property
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ModlistSettings));
|
||||
.ToGuiProperty(this, nameof(ModlistSettings));
|
||||
|
||||
CanCompile = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.GameLocation.InError),
|
||||
|
@ -30,8 +30,8 @@ namespace Wabbajack
|
||||
|
||||
public MainWindowVM MWVM { get; }
|
||||
|
||||
public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Mouth_No_Text.png")).Stream);
|
||||
public BitmapImage WabbajackErrLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Ded.png")).Stream);
|
||||
public static BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Mouth_No_Text.png")).Stream);
|
||||
public static BitmapImage WabbajackErrLogo { get; } = UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/Resources/Wabba_Ded.png")).Stream);
|
||||
|
||||
private readonly ObservableAsPropertyHelper<ModListVM> _modList;
|
||||
public ModListVM ModList => _modList.Value;
|
||||
@ -89,6 +89,12 @@ namespace Wabbajack
|
||||
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
|
||||
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _LoadingModlist;
|
||||
public bool LoadingModlist => _LoadingModlist.Value;
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _IsActive;
|
||||
public bool IsActive => _IsActive.Value;
|
||||
|
||||
// Command properties
|
||||
public IReactiveCommand ShowReportCommand { get; }
|
||||
public IReactiveCommand OpenReadmeCommand { get; }
|
||||
@ -141,7 +147,7 @@ namespace Wabbajack
|
||||
pair.Previous?.Unload();
|
||||
})
|
||||
.Select(p => p.Current)
|
||||
.ToProperty(this, nameof(Installer));
|
||||
.ToGuiProperty(this, nameof(Installer));
|
||||
|
||||
// Load settings
|
||||
MWVM.Settings.SaveSignal
|
||||
@ -151,27 +157,64 @@ namespace Wabbajack
|
||||
})
|
||||
.DisposeWith(CompositeDisposable);
|
||||
|
||||
_modList = this.WhenAny(x => x.ModListLocation.TargetPath)
|
||||
_IsActive = this.ConstructIsActive(MWVM)
|
||||
.ToGuiProperty(this, nameof(IsActive));
|
||||
|
||||
// Active path represents the path to currently have loaded
|
||||
// If we're not actively showing, then "unload" the active path
|
||||
var activePath = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModListLocation.TargetPath),
|
||||
this.WhenAny(x => x.IsActive),
|
||||
resultSelector: (path, active) => (path, active))
|
||||
.Select(x =>
|
||||
{
|
||||
if (!x.active) return default(string);
|
||||
return x.path;
|
||||
})
|
||||
// Throttle slightly so changes happen more atomically
|
||||
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
|
||||
_modList = activePath
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
// Convert from active path to modlist VM
|
||||
.Select(modListPath =>
|
||||
{
|
||||
if (modListPath == null) return default(ModListVM);
|
||||
if (!File.Exists(modListPath)) return default(ModListVM);
|
||||
return new ModListVM(modListPath);
|
||||
})
|
||||
.DisposeOld()
|
||||
.ObserveOnGuiThread()
|
||||
.StartWith(default(ModListVM))
|
||||
.ToProperty(this, nameof(ModList));
|
||||
.ToGuiProperty(this, nameof(ModList));
|
||||
|
||||
// Force GC collect when modlist changes, just to make sure we clean up any loose large items immediately
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
|
||||
.Subscribe(x =>
|
||||
{
|
||||
GC.Collect();
|
||||
});
|
||||
|
||||
_LoadingModlist = Observable.Merge(
|
||||
// When active path changes, mark as loading
|
||||
activePath
|
||||
.Select(_ => true),
|
||||
// When the resulting modlist comes in, mark it as done
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Select(_ => false))
|
||||
.ToGuiProperty(this, nameof(LoadingModlist));
|
||||
_htmlReport = this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.ReportHTML)
|
||||
.ToProperty(this, nameof(HTMLReport));
|
||||
.ToGuiProperty(this, nameof(HTMLReport));
|
||||
_installing = this.WhenAny(x => x.Installer.ActiveInstallation)
|
||||
.Select(i => i != null)
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(Installing));
|
||||
.ToGuiProperty(this, nameof(Installing));
|
||||
_TargetManager = this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.ModManager)
|
||||
.ToProperty(this, nameof(TargetManager));
|
||||
.ToGuiProperty(this, nameof(TargetManager));
|
||||
|
||||
// Add additional error check on ModList
|
||||
ModListLocation.AdditionalError = this.WhenAny(x => x.ModList)
|
||||
@ -209,8 +252,8 @@ namespace Wabbajack
|
||||
return installer.PercentCompleted.StartWith(0f);
|
||||
})
|
||||
.Switch()
|
||||
.Debounce(TimeSpan.FromMilliseconds(25))
|
||||
.ToProperty(this, nameof(PercentCompleted));
|
||||
.Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
|
||||
.ToGuiProperty(this, nameof(PercentCompleted));
|
||||
|
||||
Slideshow = new SlideShow(this);
|
||||
|
||||
@ -219,23 +262,24 @@ namespace Wabbajack
|
||||
_image = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList.Error),
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Select(x => x?.ImageObservable ?? Observable.Return(WabbajackLogo))
|
||||
.Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage)))
|
||||
.Switch()
|
||||
.StartWith(WabbajackLogo),
|
||||
.StartWith(default(BitmapImage)),
|
||||
this.WhenAny(x => x.Slideshow.Image)
|
||||
.StartWith(default(BitmapImage)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (err, modList, slideshow, installing) =>
|
||||
this.WhenAny(x => x.LoadingModlist),
|
||||
resultSelector: (err, modList, slideshow, installing, loading) =>
|
||||
{
|
||||
if (err != null)
|
||||
{
|
||||
return WabbajackErrLogo;
|
||||
}
|
||||
var ret = installing ? slideshow : modList;
|
||||
return ret ?? WabbajackLogo;
|
||||
if (loading) return default;
|
||||
return installing ? slideshow : modList;
|
||||
})
|
||||
.Select<BitmapImage, ImageSource>(x => x)
|
||||
.ToProperty(this, nameof(Image));
|
||||
.ToGuiProperty(this, nameof(Image));
|
||||
_titleText = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.Name ?? string.Empty),
|
||||
@ -243,7 +287,7 @@ namespace Wabbajack
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(TitleText));
|
||||
.ToGuiProperty(this, nameof(TitleText));
|
||||
_authorText = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.Author ?? string.Empty),
|
||||
@ -251,7 +295,7 @@ namespace Wabbajack
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(AuthorText));
|
||||
.ToGuiProperty(this, nameof(AuthorText));
|
||||
_description = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.Description ?? string.Empty),
|
||||
@ -259,7 +303,7 @@ namespace Wabbajack
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(Description));
|
||||
.ToGuiProperty(this, nameof(Description));
|
||||
_modListName = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList.Error)
|
||||
.Select(x => x != null),
|
||||
@ -270,7 +314,7 @@ namespace Wabbajack
|
||||
if (err) return "Corrupted Modlist";
|
||||
return name;
|
||||
})
|
||||
.ToProperty(this, nameof(ModListName));
|
||||
.ToGuiProperty(this, nameof(ModListName));
|
||||
|
||||
// Define commands
|
||||
ShowReportCommand = ReactiveCommand.Create(ShowReport);
|
||||
@ -285,21 +329,27 @@ namespace Wabbajack
|
||||
.Select(x => x?.StartsWith("https://") ?? false)
|
||||
.ObserveOnGuiThread());
|
||||
|
||||
_progressTitle = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.Installing),
|
||||
this.WhenAny(x => x.StartedInstallation),
|
||||
resultSelector: (installing, started) =>
|
||||
_progressTitle = this.WhenAnyValue(
|
||||
x => x.Installing,
|
||||
x => x.StartedInstallation,
|
||||
x => x.Completed,
|
||||
selector: (installing, started, completed) =>
|
||||
{
|
||||
if (installing)
|
||||
{
|
||||
return "Installing";
|
||||
}
|
||||
else if (started)
|
||||
{
|
||||
if (completed == null) return "Installing";
|
||||
return completed.Value.Succeeded ? "Installed" : "Failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
return started ? "Installed" : "Configuring";
|
||||
return "Configuring";
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(ProgressTitle));
|
||||
.ToGuiProperty(this, nameof(ProgressTitle));
|
||||
|
||||
UIUtils.BindCpuStatus(
|
||||
this.WhenAny(x => x.Installer.ActiveInstallation)
|
||||
@ -354,8 +404,7 @@ namespace Wabbajack
|
||||
_ActiveGlobalUserIntervention = activeInterventions.Connect()
|
||||
.Filter(x => x.CpuID == WorkQueue.UnassignedCpuId)
|
||||
.QueryWhenChanged(query => query.FirstOrDefault())
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
|
||||
.ToGuiProperty(this, nameof(ActiveGlobalUserIntervention));
|
||||
|
||||
CloseWhenCompleteCommand = ReactiveCommand.Create(
|
||||
canExecute: this.WhenAny(x => x.Completed)
|
||||
@ -378,8 +427,7 @@ namespace Wabbajack
|
||||
|
||||
_CurrentCpuCount = this.WhenAny(x => x.Installer.ActiveInstallation.Queue.CurrentCpuCount)
|
||||
.Switch()
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(CurrentCpuCount));
|
||||
.ToGuiProperty(this, nameof(CurrentCpuCount));
|
||||
}
|
||||
|
||||
private void ShowReport()
|
||||
|
@ -88,7 +88,7 @@ namespace Wabbajack
|
||||
// Load settings
|
||||
_CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath)
|
||||
.Select(path => path == null ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path))
|
||||
.ToProperty(this, nameof(CurrentSettings));
|
||||
.ToGuiProperty(this, nameof(CurrentSettings));
|
||||
this.WhenAny(x => x.CurrentSettings)
|
||||
.Pairwise()
|
||||
.Subscribe(settingsPair =>
|
||||
|
@ -39,7 +39,7 @@ namespace Wabbajack
|
||||
Parent = installerVM;
|
||||
|
||||
_TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType)
|
||||
.ToProperty(this, nameof(TargetGame));
|
||||
.ToGuiProperty(this, nameof(TargetGame));
|
||||
|
||||
CanInstall = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.TargetGame)
|
||||
|
@ -21,30 +21,43 @@ namespace Wabbajack
|
||||
|
||||
public ObservableCollectionExtended<ModListMetadataVM> ModLists { get; } = new ObservableCollectionExtended<ModListMetadataVM>();
|
||||
|
||||
public IReactiveCommand RefreshCommand { get; }
|
||||
|
||||
private int missingHashFallbackCounter;
|
||||
|
||||
public ModListGalleryVM(MainWindowVM mainWindowVM)
|
||||
: base(mainWindowVM)
|
||||
{
|
||||
MWVM = mainWindowVM;
|
||||
RefreshCommand = ReactiveCommand.Create(() => { });
|
||||
|
||||
RefreshCommand.StartingExecution()
|
||||
.StartWith(Unit.Default)
|
||||
Observable.Return(Unit.Default)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.SelectTask(async _ =>
|
||||
{
|
||||
return (await ModlistMetadata.LoadFromGithub())
|
||||
.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}");
|
||||
})
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,9 @@ namespace Wabbajack
|
||||
private readonly ObservableAsPropertyHelper<BitmapImage> _Image;
|
||||
public BitmapImage Image => _Image.Value;
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _LoadingImage;
|
||||
public bool LoadingImage => _LoadingImage.Value;
|
||||
|
||||
public ModListMetadataVM(ModListGalleryVM parent, ModlistMetadata metadata)
|
||||
{
|
||||
_parent = parent;
|
||||
@ -119,11 +122,18 @@ namespace Wabbajack
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(Exists));
|
||||
.ToGuiProperty(this, nameof(Exists));
|
||||
|
||||
_Image = Observable.Return(Metadata.Links.ImageUri)
|
||||
.DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}"))
|
||||
.ToProperty(this, nameof(Image));
|
||||
var imageObs = Observable.Return(Metadata.Links.ImageUri)
|
||||
.DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}"));
|
||||
|
||||
_Image = imageObs
|
||||
.ToGuiProperty(this, nameof(Image));
|
||||
|
||||
_LoadingImage = imageObs
|
||||
.Select(x => false)
|
||||
.StartWith(true)
|
||||
.ToGuiProperty(this, nameof(LoadingImage));
|
||||
}
|
||||
|
||||
private Task Download()
|
||||
|
@ -13,7 +13,7 @@ namespace Wabbajack
|
||||
{
|
||||
public class ModListVM : ViewModel
|
||||
{
|
||||
public ModList SourceModList { get; }
|
||||
public ModList SourceModList { get; private set; }
|
||||
public Exception Error { get; }
|
||||
public string ModListPath { get; }
|
||||
public string Name => SourceModList?.Name;
|
||||
@ -42,6 +42,7 @@ namespace Wabbajack
|
||||
}
|
||||
|
||||
ImageObservable = Observable.Return(Unit.Default)
|
||||
// Download and retrieve bytes on background thread
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Select(filePath =>
|
||||
{
|
||||
@ -66,6 +67,7 @@ namespace Wabbajack
|
||||
return default(MemoryStream);
|
||||
}
|
||||
})
|
||||
// Create Bitmap image on GUI thread
|
||||
.ObserveOnGuiThread()
|
||||
.Select(memStream =>
|
||||
{
|
||||
@ -80,6 +82,11 @@ namespace Wabbajack
|
||||
return default(BitmapImage);
|
||||
}
|
||||
})
|
||||
// If ever would return null, show WJ logo instead
|
||||
.Select(x =>
|
||||
{
|
||||
return x ?? InstallerVM.WabbajackLogo;
|
||||
})
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
}
|
||||
@ -116,5 +123,13 @@ namespace Wabbajack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
// Just drop reference explicitly, as it's large, so it can be GCed
|
||||
// Even if someone is holding a stale reference to the VM
|
||||
this.SourceModList = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,7 @@ namespace Wabbajack
|
||||
{
|
||||
Login = login;
|
||||
_MetaInfo = (login.MetaInfo ?? Observable.Return(""))
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(MetaInfo));
|
||||
.ToGuiProperty(this, nameof(MetaInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace Wabbajack
|
||||
this.WhenAny(x => x.Installer.Installing),
|
||||
resultSelector: (enabled, installing) => enabled && installing))
|
||||
// Block spam
|
||||
.Debounce(TimeSpan.FromMilliseconds(250))
|
||||
.Debounce(TimeSpan.FromMilliseconds(250), RxApp.MainThreadScheduler)
|
||||
.Scan(
|
||||
seed: 0,
|
||||
accumulator: (i, _) => i + 1)
|
||||
@ -80,19 +80,20 @@ namespace Wabbajack
|
||||
{
|
||||
if (modList?.SourceModList?.Archives == null)
|
||||
{
|
||||
return Observable.Empty<ModVM>()
|
||||
return Observable.Empty<NexusDownloader.State>()
|
||||
.ToObservableChangeSet(x => x.ModID);
|
||||
}
|
||||
return modList.SourceModList.Archives
|
||||
.Select(m => m.State)
|
||||
.OfType<NexusDownloader.State>()
|
||||
.Select(nexus => new ModVM(nexus))
|
||||
// Shuffle it
|
||||
.Shuffle(_random)
|
||||
.AsObservableChangeSet(x => x.ModID);
|
||||
})
|
||||
// Switch to the new list after every ModList change
|
||||
.Switch()
|
||||
.Transform(nexus => new ModVM(nexus))
|
||||
.DisposeMany()
|
||||
// Filter out any NSFW slides if we don't want them
|
||||
.AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW))
|
||||
.Filter(slide => !slide.IsNSFW || ShowNSFW)
|
||||
@ -108,15 +109,14 @@ namespace Wabbajack
|
||||
return query.Items.ElementAtOrDefault(index);
|
||||
})
|
||||
.StartWith(default(ModVM))
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(TargetMod));
|
||||
.ToGuiProperty(this, nameof(TargetMod));
|
||||
|
||||
// Mark interest and materialize image of target mod
|
||||
_image = this.WhenAny(x => x.TargetMod)
|
||||
// We want to Switch here, not SelectMany, as we want to hotswap to newest target without waiting on old ones
|
||||
.Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage)))
|
||||
.Switch()
|
||||
.ToProperty(this, nameof(Image));
|
||||
.ToGuiProperty(this, nameof(Image));
|
||||
|
||||
VisitNexusSiteCommand = ReactiveCommand.Create(
|
||||
execute: () => Process.Start(TargetMod.ModURL),
|
||||
|
@ -4,10 +4,12 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="lib:ViewModel"
|
||||
ClipToBounds="True"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
|
@ -1,15 +1,20 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Wabbajack.Lib;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for DetailImageView.xaml
|
||||
/// </summary>
|
||||
public partial class DetailImageView : UserControlRx
|
||||
public partial class DetailImageView : UserControlRx<ViewModel>
|
||||
{
|
||||
public ImageSource Image
|
||||
{
|
||||
@ -51,30 +56,36 @@ namespace Wabbajack
|
||||
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(DetailImageView),
|
||||
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _showAuthor;
|
||||
public bool ShowAuthor => _showAuthor.Value;
|
||||
[Reactive]
|
||||
public bool ShowAuthor { get; private set; }
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _showDescription;
|
||||
public bool ShowDescription => _showDescription.Value;
|
||||
[Reactive]
|
||||
public bool ShowDescription { get; private set; }
|
||||
|
||||
private readonly ObservableAsPropertyHelper<bool> _showTitle;
|
||||
public bool ShowTitle => _showTitle.Value;
|
||||
[Reactive]
|
||||
public bool ShowTitle { get; private set; }
|
||||
|
||||
public DetailImageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_showAuthor = this.WhenAny(x => x.Author)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToProperty(this, nameof(ShowAuthor));
|
||||
this.WhenActivated(dispose =>
|
||||
{
|
||||
this.WhenAny(x => x.Author)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Subscribe(x => ShowAuthor = x)
|
||||
.DisposeWith(dispose);
|
||||
|
||||
_showDescription = this.WhenAny(x => x.Description)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToProperty(this, nameof(ShowDescription));
|
||||
this.WhenAny(x => x.Description)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Subscribe(x => ShowDescription = x)
|
||||
.DisposeWith(dispose);
|
||||
|
||||
_showTitle = this.WhenAny(x => x.Title)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToProperty(this, nameof(ShowTitle));
|
||||
this.WhenAny(x => x.Title)
|
||||
.Select(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Subscribe(x => ShowTitle = x)
|
||||
.DisposeWith(dispose);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="lib:ViewModel"
|
||||
BorderThickness="0"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
|
@ -2,13 +2,17 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Lib;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for TopProgressView.xaml
|
||||
/// </summary>
|
||||
public partial class TopProgressView : UserControlRx
|
||||
public partial class TopProgressView : UserControlRx<ViewModel>
|
||||
{
|
||||
public double ProgressPercent
|
||||
{
|
||||
@ -50,18 +54,22 @@ namespace Wabbajack
|
||||
public static readonly DependencyProperty ShadowMarginProperty = DependencyProperty.Register(nameof(ShadowMargin), typeof(bool), typeof(TopProgressView),
|
||||
new FrameworkPropertyMetadata(true));
|
||||
|
||||
private readonly ObservableAsPropertyHelper<double> _ProgressOpacityPercent;
|
||||
public double ProgressOpacityPercent => _ProgressOpacityPercent.Value;
|
||||
[Reactive]
|
||||
public double ProgressOpacityPercent { get; private set; }
|
||||
|
||||
public TopProgressView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_ProgressOpacityPercent = this.WhenAny(x => x.ProgressPercent)
|
||||
.Select(x =>
|
||||
{
|
||||
return 0.3 + x * 0.7;
|
||||
})
|
||||
.ToProperty(this, nameof(ProgressOpacityPercent));
|
||||
this.WhenActivated(dispose =>
|
||||
{
|
||||
this.WhenAny(x => x.ProgressPercent)
|
||||
.Select(x =>
|
||||
{
|
||||
return 0.3 + x * 0.7;
|
||||
})
|
||||
.Subscribe(x => ProgressOpacityPercent = x)
|
||||
.DisposeWith(dispose);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DataContext="{d:DesignInstance local:InstallerVM}"
|
||||
d:DesignHeight="500"
|
||||
@ -48,11 +49,14 @@
|
||||
Grid.Row="1"
|
||||
Margin="5,0,5,5">
|
||||
<Border BorderBrush="{StaticResource BorderInterestBrush}" BorderThickness="1,0,1,1">
|
||||
<local:DetailImageView
|
||||
Title="{Binding TitleText, Mode=OneWay}"
|
||||
Author="{Binding AuthorText, Mode=OneWay}"
|
||||
Description="{Binding Description, Mode=OneWay}"
|
||||
Image="{Binding Image, Mode=OneWay}" />
|
||||
<Grid>
|
||||
<local:DetailImageView
|
||||
Title="{Binding TitleText, Mode=OneWay}"
|
||||
Author="{Binding AuthorText, Mode=OneWay}"
|
||||
Description="{Binding Description, Mode=OneWay}"
|
||||
Image="{Binding Image, Mode=OneWay}" />
|
||||
<mahapps:ProgressRing Visibility="{Binding LoadingModlist, Mode=OneWay, Converter={StaticResource bool2VisibilityHiddenConverter}}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid
|
||||
Margin="0,20,25,0"
|
||||
@ -422,8 +426,8 @@
|
||||
<local:CpuView
|
||||
Grid.Column="2"
|
||||
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
|
||||
ViewModel="{Binding}"
|
||||
SettingsHook="{Binding MWVM.Settings}"
|
||||
ViewModel="{Binding}"
|
||||
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
|
||||
<local:AttentionBorder Grid.Column="2" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
|
||||
<local:AttentionBorder.DisplayContent>
|
||||
|
@ -86,6 +86,7 @@
|
||||
BorderBrush="{StaticResource ButtonNormalBorder}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid ClipToBounds="True">
|
||||
<mahapps:ProgressRing x:Name="LoadingProgress" />
|
||||
<Viewbox
|
||||
Height="340"
|
||||
HorizontalAlignment="Center"
|
||||
@ -119,7 +120,7 @@
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Value="True">
|
||||
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
|
@ -54,6 +54,10 @@ namespace Wabbajack
|
||||
this.WhenAny(x => x.ViewModel.Image)
|
||||
.BindToStrict(this, x => x.ModListImage.Source)
|
||||
.DisposeWith(dispose);
|
||||
this.WhenAny(x => x.ViewModel.LoadingImage)
|
||||
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
|
||||
.BindToStrict(this, x => x.LoadingProgress.Visibility)
|
||||
.DisposeWith(dispose);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,40 +7,6 @@ using System.Windows.Controls;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class UserControlRx : UserControl, IDisposable, IReactiveObject
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangingEventHandler PropertyChanging;
|
||||
|
||||
public void RaisePropertyChanging(PropertyChangingEventArgs args)
|
||||
{
|
||||
PropertyChanging?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void RaisePropertyChanged(PropertyChangedEventArgs args)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private readonly Lazy<CompositeDisposable> _compositeDisposable = new Lazy<CompositeDisposable>();
|
||||
public CompositeDisposable CompositeDisposable => _compositeDisposable.Value;
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (_compositeDisposable.IsValueCreated)
|
||||
{
|
||||
_compositeDisposable.Value.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is UserControlRx control)) return;
|
||||
if (Equals(e.OldValue, e.NewValue)) return;
|
||||
control.RaisePropertyChanged(e.Property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public class UserControlRx<TViewModel> : ReactiveUserControl<TViewModel>, IReactiveObject
|
||||
where TViewModel : class
|
||||
{
|
||||
@ -59,7 +25,7 @@ namespace Wabbajack
|
||||
|
||||
protected static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is UserControlRx control)) return;
|
||||
if (!(d is UserControlRx<TViewModel> control)) return;
|
||||
if (Equals(e.OldValue, e.NewValue)) return;
|
||||
control.RaisePropertyChanged(e.Property.Name);
|
||||
}
|
||||
|
@ -614,6 +614,8 @@
|
||||
<ItemGroup>
|
||||
<SplashScreen Include="Resources\Wabba_Mouth_Small.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<Folder Include="Interfaces\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user