mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #140 from Noggog/slideshow-refactoring
Image cache and slideshow refactor
This commit is contained in:
commit
f39fe51328
@ -303,34 +303,6 @@ namespace Wabbajack.Lib.NexusApi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<Slide> CachedSlideShow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Directory.Exists(Consts.NexusCacheDirectory)) return new Slide[] { };
|
||||
|
||||
return Directory.EnumerateFiles(Consts.NexusCacheDirectory)
|
||||
.Where(f => f.EndsWith(".json"))
|
||||
.Select(f =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return f.FromJSON<ModInfo>();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
File.Delete(f);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(m => m != null)
|
||||
.Where(m => m._internal_version == CACHED_VERSION_NUMBER && m.picture_url != null)
|
||||
.Select(m => new Slide(m.name,m.mod_id,m.summary,m.author,m.contains_adult_content,GetModURL(m.game_name,m.mod_id),m.picture_url));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class DownloadLink
|
||||
{
|
||||
public string URI { get; set; }
|
||||
|
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Wabbajack.Lib
|
||||
{
|
||||
public class Slide
|
||||
{
|
||||
public Slide(string modName, string modID, string modDescription, string modAuthor, bool isNSFW, string modUrl, string imageURL)
|
||||
{
|
||||
ModName = modName;
|
||||
ModDescription = modDescription;
|
||||
ModAuthor = modAuthor;
|
||||
IsNSFW = isNSFW;
|
||||
ModURL = modUrl;
|
||||
ModID = modID;
|
||||
ImageURL = imageURL;
|
||||
}
|
||||
|
||||
public string ModName { get; }
|
||||
public string ModDescription { get; }
|
||||
public string ModAuthor { get; }
|
||||
public bool IsNSFW { get; }
|
||||
public string ModURL { get; }
|
||||
public string ModID { get; }
|
||||
public BitmapImage Image { get; set; }
|
||||
public string ImageURL { get; }
|
||||
|
||||
}
|
||||
}
|
@ -103,7 +103,6 @@
|
||||
<Compile Include="NexusApi\NexusApiUtils.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ReportBuilder.cs" />
|
||||
<Compile Include="UI\Slide.cs" />
|
||||
<Compile Include="UI\UIUtils.cs" />
|
||||
<Compile Include="Updater\CheckForUpdates.cs" />
|
||||
<Compile Include="Validation\DTOs.cs" />
|
||||
|
36
Wabbajack/Extensions/EnumerableExt.cs
Normal file
36
Wabbajack/Extensions/EnumerableExt.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public static class EnumerableExt
|
||||
{
|
||||
#region Shuffle
|
||||
/// https://stackoverflow.com/questions/5807128/an-extension-method-on-ienumerable-needed-for-shuffling
|
||||
|
||||
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
|
||||
{
|
||||
if (source == null) throw new ArgumentNullException("source");
|
||||
if (rng == null) throw new ArgumentNullException("rng");
|
||||
|
||||
return source.ShuffleIterator(rng);
|
||||
}
|
||||
|
||||
private static IEnumerable<T> ShuffleIterator<T>(
|
||||
this IEnumerable<T> source, Random rng)
|
||||
{
|
||||
var buffer = source.ToList();
|
||||
for (int i = 0; i < buffer.Count; i++)
|
||||
{
|
||||
int j = rng.Next(i, buffer.Count);
|
||||
yield return buffer[j];
|
||||
|
||||
buffer[j] = buffer[i];
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -39,8 +39,8 @@ namespace Wabbajack
|
||||
|
||||
public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromResource("Wabbajack.Resources.Wabba_Mouth.png");
|
||||
|
||||
private readonly ObservableAsPropertyHelper<ModList> _ModList;
|
||||
public ModList ModList => _ModList.Value;
|
||||
private readonly ObservableAsPropertyHelper<ModListVM> _ModList;
|
||||
public ModListVM ModList => _ModList.Value;
|
||||
|
||||
[Reactive]
|
||||
public string ModListPath { get; set; }
|
||||
@ -115,9 +115,9 @@ namespace Wabbajack
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Select(source =>
|
||||
{
|
||||
if (source == null) return default;
|
||||
var modlist = Installer.LoadFromFile(source);
|
||||
if (modlist == null)
|
||||
if (source == null) return default(ModListVM);
|
||||
var modList = Installer.LoadFromFile(source);
|
||||
if (modList == null)
|
||||
{
|
||||
MessageBox.Show("Invalid Modlist, or file not found.", "Invalid Modlist", MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
@ -131,12 +131,12 @@ namespace Wabbajack
|
||||
window.Show();
|
||||
this.MWVM.MainWindow.Close();
|
||||
});
|
||||
return default;
|
||||
return default(ModListVM);
|
||||
}
|
||||
return modlist;
|
||||
return new ModListVM(modList, source);
|
||||
})
|
||||
.ObserveOnGuiThread()
|
||||
.StartWith(default(ModList))
|
||||
.StartWith(default(ModListVM))
|
||||
.ToProperty(this, nameof(this.ModList));
|
||||
this._HTMLReport = this.WhenAny(x => x.ModList)
|
||||
.Select(modList => modList?.ReportHTML)
|
||||
@ -153,57 +153,13 @@ namespace Wabbajack
|
||||
|
||||
this.Slideshow = new SlideShow(this);
|
||||
|
||||
// Locate and create modlist image if it exists
|
||||
var modListImage = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList),
|
||||
this.WhenAny(x => x.ModListPath),
|
||||
(modList, modListPath) => (modList, modListPath))
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Select(u =>
|
||||
{
|
||||
if (u.modList == null
|
||||
|| u.modListPath == null
|
||||
|| !File.Exists(u.modListPath)
|
||||
|| string.IsNullOrEmpty(u.modList.Image)
|
||||
|| u.modList.Image.Length != 36)
|
||||
{
|
||||
return WabbajackLogo;
|
||||
}
|
||||
try
|
||||
{
|
||||
using (var fs = new FileStream(u.modListPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
var entry = ar.GetEntry(u.modList.Image);
|
||||
using (var e = entry.Open())
|
||||
e.CopyTo(ms);
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = ms;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Log().Warn(ex, "Error loading modlist splash image.");
|
||||
return WabbajackLogo;
|
||||
}
|
||||
})
|
||||
.ObserveOnGuiThread()
|
||||
.StartWith(default(BitmapImage))
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
|
||||
// Set display items to modlist if configuring or complete,
|
||||
// or to the current slideshow data if installing
|
||||
this._Image = Observable.CombineLatest(
|
||||
modListImage
|
||||
.StartWith(default(BitmapImage)),
|
||||
this.WhenAny(x => x.ModList)
|
||||
.SelectMany(x => x?.ImageObservable ?? Observable.Empty<BitmapImage>())
|
||||
.NotNull()
|
||||
.StartWith(WabbajackLogo),
|
||||
this.WhenAny(x => x.Slideshow.Image)
|
||||
.StartWith(default(BitmapImage)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
@ -211,19 +167,22 @@ namespace Wabbajack
|
||||
.ToProperty(this, nameof(this.Image));
|
||||
this._TitleText = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList.Name),
|
||||
this.WhenAny(x => x.Slideshow.ModName),
|
||||
this.WhenAny(x => x.Slideshow.TargetMod.ModName)
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(this.TitleText));
|
||||
this._AuthorText = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList.Author),
|
||||
this.WhenAny(x => x.Slideshow.AuthorName),
|
||||
this.WhenAny(x => x.Slideshow.TargetMod.ModAuthor)
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(this.AuthorText));
|
||||
this._Description = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ModList.Description),
|
||||
this.WhenAny(x => x.Slideshow.Description),
|
||||
this.WhenAny(x => x.Slideshow.TargetMod.ModDescription)
|
||||
.StartWith(default(string)),
|
||||
this.WhenAny(x => x.Installing),
|
||||
resultSelector: (modList, mod, installing) => installing ? mod : modList)
|
||||
.ToProperty(this, nameof(this.Description));
|
||||
@ -312,7 +271,7 @@ namespace Wabbajack
|
||||
{
|
||||
this.Installing = true;
|
||||
this.InstallingMode = true;
|
||||
var installer = new Installer(this.ModListPath, this.ModList, Location)
|
||||
var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location)
|
||||
{
|
||||
DownloadFolder = DownloadLocation
|
||||
};
|
||||
|
89
Wabbajack/View Models/ModListVM.cs
Normal file
89
Wabbajack/View Models/ModListVM.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class ModListVM : ViewModel
|
||||
{
|
||||
public ModList SourceModList { get; }
|
||||
public string ModListPath { get; }
|
||||
public string Name => this.SourceModList.Name;
|
||||
public string ReportHTML => this.SourceModList.ReportHTML;
|
||||
public string Readme => this.SourceModList.Readme;
|
||||
public string ImageURL => this.SourceModList.Image;
|
||||
public string Author => this.SourceModList.Author;
|
||||
public string Description => this.SourceModList.Description;
|
||||
public string Website => this.SourceModList.Website;
|
||||
|
||||
// Image isn't exposed as a direct property, but as an observable.
|
||||
// This acts as a caching mechanism, as interested parties will trigger it to be created,
|
||||
// and the cached image will automatically be released when the last interested party is gone.
|
||||
public IObservable<BitmapImage> ImageObservable { get; }
|
||||
|
||||
public ModListVM(ModList sourceModList, string modListPath)
|
||||
{
|
||||
this.ModListPath = modListPath;
|
||||
this.SourceModList = sourceModList;
|
||||
|
||||
this.ImageObservable = Observable.Return(this.ImageURL)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Select(url =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(url)) return default(MemoryStream);
|
||||
if (string.IsNullOrWhiteSpace(sourceModList.Image)) return default(MemoryStream);
|
||||
if (sourceModList.Image.Length != 36) return default(MemoryStream);
|
||||
using (var fs = new FileStream(this.ModListPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var entry = ar.GetEntry(sourceModList.Image);
|
||||
using (var e = entry.Open())
|
||||
{
|
||||
e.CopyTo(ms);
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.LogToFile($"Exception while caching Mod List image {this.Name}\n{ex.ExceptionToString()}");
|
||||
return default(MemoryStream);
|
||||
}
|
||||
})
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Select(memStream =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = memStream;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.LogToFile($"Exception while caching Mod List image {this.Name}\n{ex.ExceptionToString()}");
|
||||
return default(BitmapImage);
|
||||
}
|
||||
})
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
}
|
||||
}
|
||||
}
|
97
Wabbajack/View Models/ModVM.cs
Normal file
97
Wabbajack/View Models/ModVM.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
using Wabbajack.Lib.NexusApi;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class ModVM : ViewModel
|
||||
{
|
||||
public string ModName { get; }
|
||||
|
||||
public string ModID { get; }
|
||||
|
||||
public string ModDescription { get; }
|
||||
|
||||
public string ModAuthor { get; }
|
||||
|
||||
public bool IsNSFW { get; }
|
||||
|
||||
public string ModURL { get; }
|
||||
|
||||
public string ImageURL { get; }
|
||||
|
||||
// Image isn't exposed as a direct property, but as an observable.
|
||||
// This acts as a caching mechanism, as interested parties will trigger it to be created,
|
||||
// and the cached image will automatically be released when the last interested party is gone.
|
||||
public IObservable<BitmapImage> ImageObservable { get; }
|
||||
|
||||
public ModVM(NexusDownloader.State m)
|
||||
{
|
||||
this.ModName = NexusApiUtils.FixupSummary(m.ModName);
|
||||
this.ModID = m.ModID;
|
||||
this.ModDescription = NexusApiUtils.FixupSummary(m.Summary);
|
||||
this.ModAuthor = NexusApiUtils.FixupSummary(m.Author);
|
||||
this.IsNSFW = m.Adult;
|
||||
this.ModURL = m.NexusURL;
|
||||
this.ImageURL = m.SlideShowPic;
|
||||
this.ImageObservable = Observable.Return(this.ImageURL)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.SelectTask(async url =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var ret = new MemoryStream();
|
||||
using (Stream stream = await new HttpClient().GetStreamAsync(url))
|
||||
{
|
||||
stream.CopyTo(ret);
|
||||
}
|
||||
|
||||
ret.Seek(0, SeekOrigin.Begin);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.LogToFile($"Exception while caching slide {this.ModName} ({this.ModID})\n{ex.ExceptionToString()}");
|
||||
return default(MemoryStream);
|
||||
}
|
||||
})
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Select(memStream =>
|
||||
{
|
||||
if (memStream == null) return default(BitmapImage);
|
||||
try
|
||||
{
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = memStream;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.LogToFile($"Exception while caching slide {this.ModName} ({this.ModID})\n{ex.ExceptionToString()}");
|
||||
return default(BitmapImage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
memStream?.Dispose();
|
||||
}
|
||||
})
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,261 +1,129 @@
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Splat;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
using Wabbajack.Lib.NexusApi;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class SlideShow : ViewModel
|
||||
{
|
||||
private readonly Random _random;
|
||||
private Slide _lastSlide;
|
||||
private const bool UseSync = false;
|
||||
private const int MaxCacheSize = 10;
|
||||
|
||||
public List<Slide> SlideShowElements { get; set; }
|
||||
|
||||
public Dictionary<string, Slide> CachedSlides { get; }
|
||||
|
||||
public Queue<Slide> SlidesQueue { get; }
|
||||
private readonly Random _random = new Random();
|
||||
|
||||
public InstallerVM Installer { get; }
|
||||
|
||||
public BitmapImage NextIcon { get; } = UIUtils.BitmapImageFromResource("Wabbajack.Resources.Icons.next.png");
|
||||
|
||||
[Reactive]
|
||||
public bool ShowNSFW { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public bool GCAfterUpdating { get; set; } = true;
|
||||
|
||||
[Reactive]
|
||||
public bool Enable { get; set; } = true;
|
||||
|
||||
[Reactive]
|
||||
public BitmapImage Image { get; set; }
|
||||
private readonly ObservableAsPropertyHelper<BitmapImage> _Image;
|
||||
public BitmapImage Image => _Image.Value;
|
||||
|
||||
[Reactive]
|
||||
public string ModName { get; set; } = "Wabbajack";
|
||||
|
||||
[Reactive]
|
||||
public string AuthorName { get; set; } = "Halgari & the Wabbajack Team";
|
||||
|
||||
[Reactive]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string NexusSiteURL { get; set; } = "https://github.com/wabbajack-tools/wabbajack";
|
||||
private readonly ObservableAsPropertyHelper<ModVM> _TargetMod;
|
||||
public ModVM TargetMod => _TargetMod.Value;
|
||||
|
||||
public IReactiveCommand SlideShowNextItemCommand { get; } = ReactiveCommand.Create(() => { });
|
||||
public IReactiveCommand VisitNexusSiteCommand { get; }
|
||||
|
||||
public SlideShow(InstallerVM appState)
|
||||
{
|
||||
SlideShowElements = NexusApiClient.CachedSlideShow.ToList();
|
||||
CachedSlides = new Dictionary<string, Slide>();
|
||||
SlidesQueue = new Queue<Slide>();
|
||||
_random = new Random();
|
||||
Installer = appState;
|
||||
this.Installer = appState;
|
||||
|
||||
// Wire target slideshow index
|
||||
var intervalSeconds = 10;
|
||||
// Compile all the sources that trigger a slideshow update, any of which trigger a counter update
|
||||
var selectedIndex = Observable.Merge(
|
||||
// If user requests one manually
|
||||
this.SlideShowNextItemCommand.StartingExecution(),
|
||||
// If the natural timer fires
|
||||
Observable.Merge(
|
||||
// Start with an initial timer
|
||||
Observable.Return(Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))),
|
||||
// but reset timer if user requests one
|
||||
this.SlideShowNextItemCommand.StartingExecution()
|
||||
.Select(_ => Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))))
|
||||
// When a new timer comes in, swap to it
|
||||
.Switch()
|
||||
.Unit())
|
||||
// When filter switch enabled, fire an initial signal
|
||||
.StartWith(Unit.Default)
|
||||
// Only subscribe to slideshow triggers if enabled and installing
|
||||
.FilterSwitch(
|
||||
Observable.CombineLatest(
|
||||
this.WhenAny(x => x.Enable),
|
||||
this.WhenAny(x => x.Installer.Installing),
|
||||
resultSelector: (enabled, installing) => enabled && installing))
|
||||
// Block spam
|
||||
.Debounce(TimeSpan.FromMilliseconds(250))
|
||||
.Scan(
|
||||
seed: 0,
|
||||
accumulator: (i, _) => i + 1)
|
||||
.Publish()
|
||||
.RefCount();
|
||||
|
||||
// Dynamic list changeset of mod VMs to display
|
||||
var modVMs = this.WhenAny(x => x.Installer.ModList)
|
||||
// Whenever modlist changes, grab the list of its slides
|
||||
.Select(modList =>
|
||||
{
|
||||
if (modList == null)
|
||||
{
|
||||
return Observable.Empty<ModVM>()
|
||||
.ToObservableChangeSet(x => x.ModID);
|
||||
}
|
||||
return modList.SourceModList.Archives
|
||||
.Select(m => m.State)
|
||||
.OfType<NexusDownloader.State>()
|
||||
.Select(nexus => new ModVM(nexus))
|
||||
// Shuffle it
|
||||
.Shuffle(this._random)
|
||||
.AsObservableChangeSet(x => x.ModID);
|
||||
})
|
||||
// Switch to the new list after every modlist change
|
||||
.Switch()
|
||||
// Filter out any NSFW slides if we don't want them
|
||||
.AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW))
|
||||
.Filter(slide => !slide.IsNSFW || this.ShowNSFW)
|
||||
.RefCount();
|
||||
|
||||
// Find target mod to display by combining dynamic list with currently desired index
|
||||
this._TargetMod = Observable.CombineLatest(
|
||||
modVMs.QueryWhenChanged(),
|
||||
selectedIndex,
|
||||
resultSelector: (query, selected) => query.Items.ElementAtOrDefault(selected % query.Count))
|
||||
.StartWith(default(ModVM))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.ToProperty(this, nameof(this.TargetMod));
|
||||
|
||||
// Mark interest and materialize image of target mod
|
||||
this._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(this.Image));
|
||||
|
||||
this.VisitNexusSiteCommand = ReactiveCommand.Create(
|
||||
execute: () => Process.Start(this.NexusSiteURL),
|
||||
canExecute: this.WhenAny(x => x.NexusSiteURL)
|
||||
execute: () => Process.Start(this.TargetMod.ModURL),
|
||||
canExecute: this.WhenAny(x => x.TargetMod.ModURL)
|
||||
.Select(x => x?.StartsWith("https://") ?? false)
|
||||
.ObserveOnGuiThread());
|
||||
|
||||
// Apply modlist properties when it changes
|
||||
this.WhenAny(x => x.Installer.ModList)
|
||||
.NotNull()
|
||||
.ObserveOnGuiThread()
|
||||
.Do(modList =>
|
||||
{
|
||||
this.SlideShowElements = modList.Archives
|
||||
.Select(m => m.State)
|
||||
.OfType<NexusDownloader.State>()
|
||||
.Select(m =>
|
||||
new Slide(NexusApiUtils.FixupSummary(m.ModName), m.ModID,
|
||||
NexusApiUtils.FixupSummary(m.Summary), NexusApiUtils.FixupSummary(m.Author),
|
||||
m.Adult, m.NexusURL, m.SlideShowPic)).ToList();
|
||||
})
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Do(modList =>
|
||||
{
|
||||
// This takes a while, and is currently blocking
|
||||
this.PreloadSlideShow();
|
||||
})
|
||||
.Subscribe()
|
||||
.DisposeWith(this.CompositeDisposable);
|
||||
|
||||
/// Wire slideshow updates
|
||||
// Merge all the sources that trigger a slideshow update
|
||||
Observable.Merge(
|
||||
// If the natural timer fires
|
||||
Observable.Interval(TimeSpan.FromSeconds(10))
|
||||
.Unit()
|
||||
// Only if enabled
|
||||
.FilterSwitch(this.WhenAny(x => x.Enable)),
|
||||
// If user requests one manually
|
||||
this.SlideShowNextItemCommand.StartingExecution())
|
||||
// When installing fire an initial signal
|
||||
.StartWith(Unit.Default)
|
||||
// Only subscribe to slideshow triggers if installing
|
||||
.FilterSwitch(this.WhenAny(x => x.Installer.Installing))
|
||||
// Don't ever update more than once every half second. ToDo: Update to debounce
|
||||
.Throttle(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => this.UpdateSlideShowItem())
|
||||
.DisposeWith(this.CompositeDisposable);
|
||||
}
|
||||
|
||||
public void PreloadSlideShow()
|
||||
{
|
||||
var turns = 0;
|
||||
for (var i = 0; i < SlideShowElements.Count; i++)
|
||||
{
|
||||
if (turns >= 3)
|
||||
break;
|
||||
|
||||
if (QueueRandomSlide(true, false))
|
||||
turns++;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSlideShowItem()
|
||||
{
|
||||
if (SlidesQueue.Count == 0) return;
|
||||
var slide = SlidesQueue.Peek();
|
||||
|
||||
while (CachedSlides.Count >= MaxCacheSize)
|
||||
{
|
||||
var idx = _random.Next(0, SlideShowElements.Count);
|
||||
var randomSlide = SlideShowElements[idx];
|
||||
while (!CachedSlides.ContainsKey(randomSlide.ModID) || SlidesQueue.Contains(randomSlide))
|
||||
{
|
||||
idx = _random.Next(0, SlideShowElements.Count);
|
||||
randomSlide = SlideShowElements[idx];
|
||||
}
|
||||
|
||||
//if (SlidesQueue.Contains(randomSlide)) continue;
|
||||
CachedSlides.Remove(randomSlide.ModID);
|
||||
if (this.GCAfterUpdating)
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
if (!slide.IsNSFW || (slide.IsNSFW && ShowNSFW))
|
||||
{
|
||||
this.Image = UIUtils.BitmapImageFromResource("Wabbajack.Resources.none.jpg");
|
||||
if (slide.ImageURL != null && slide.Image != null)
|
||||
{
|
||||
if (!CachedSlides.ContainsKey(slide.ModID)) return;
|
||||
this.Image = slide.Image;
|
||||
}
|
||||
|
||||
this.ModName = slide.ModName;
|
||||
this.AuthorName = slide.ModAuthor;
|
||||
this.Description = slide.ModDescription;
|
||||
this.NexusSiteURL = slide.ModURL;
|
||||
}
|
||||
|
||||
SlidesQueue.Dequeue();
|
||||
QueueRandomSlide(false, true);
|
||||
}
|
||||
|
||||
private void CacheImage(Slide slide)
|
||||
{
|
||||
Utils.LogToFile($"Caching slide for {slide.ModName} at {slide.ImageURL}");
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (UseSync)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
using (var stream = new HttpClient().GetStreamSync(slide.ImageURL))
|
||||
stream.CopyTo(ms);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Task<Stream> stream = new HttpClient().GetStreamAsync(slide.ImageURL))
|
||||
{
|
||||
stream.Wait();
|
||||
stream.Result.CopyTo(ms);
|
||||
}
|
||||
}
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = ms;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
|
||||
slide.Image = image;
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utils.LogToFile($"Exception while caching slide {slide.ModName} ({slide.ModID})\n{e.ExceptionToString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool QueueRandomSlide(bool init, bool checkLast)
|
||||
{
|
||||
var result = false;
|
||||
var idx = _random.Next(0, SlideShowElements.Count);
|
||||
var element = SlideShowElements[idx];
|
||||
|
||||
if (checkLast && SlideShowElements.Count > 1)
|
||||
{
|
||||
while (element == _lastSlide && (!element.IsNSFW || (element.IsNSFW && ShowNSFW)))
|
||||
{
|
||||
idx = _random.Next(0, SlideShowElements.Count);
|
||||
element = SlideShowElements[idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (element.ImageURL == null)
|
||||
{
|
||||
if (!init) SlidesQueue.Enqueue(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CachedSlides.ContainsKey(element.ModID))
|
||||
{
|
||||
CacheImage(element);
|
||||
CachedSlides.Add(element.ModID, element);
|
||||
SlidesQueue.Enqueue(element);
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!init) SlidesQueue.Enqueue(element);
|
||||
}
|
||||
|
||||
_lastSlide = element;
|
||||
}
|
||||
|
||||
return result;
|
||||
// ToDo
|
||||
// Can maybe add "preload" systems to prep upcoming images
|
||||
// This would entail subscribing to modVMs, narrowing it down to Top(X) or Page() somehow.
|
||||
// The result would not be used anywhere, just simply expressing interest in those mods'
|
||||
// images will implicitly cache them
|
||||
//
|
||||
// Page would be really clever to use, but it's not exactly right as its "window" won't follow the current index,
|
||||
// so at the boundary of a page, the next image won't be cached. Need like a Page() /w an offset parameter, or something.
|
||||
}
|
||||
}
|
||||
}
|
@ -162,6 +162,9 @@
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="Converters\BoolToVisibilityConverter.cs" />
|
||||
<Compile Include="Extensions\EnumerableExt.cs" />
|
||||
<Compile Include="View Models\ModListVM.cs" />
|
||||
<Compile Include="View Models\ModVM.cs" />
|
||||
<Compile Include="Views\CompilerView.xaml.cs">
|
||||
<DependentUpon>CompilerView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
Loading…
Reference in New Issue
Block a user