Add non-Nexus mods to the Slideshow (#574)

* Created AbstractMetaState

* Added IAbstractMetaState to NexusDownloader.State

Slideshow is fully working with this setup and nothing changed
functionally.

* Renamed IAbstractMetaState to IMetaState

* Changed modVMs in SlideShow from type NexusDownloader.State to IMetaState

* Simplified IMetaState and ModVM

* Removed Setter from IMetaState and added to LoversLabDownloader

* Throw exception when the modlist could not be loaded

* Created AbstractMetaState

AbstractMetaState implements AbstractDownloadState and indicates that a
State from a specific Download contains meta information. This is used
for the Slideshow and can also be used for the Manifest.

* Created GatherMetaData function

* Implemented new AbstractMetaState for LoversLab

* Implemented new AbstractMetaState for NexusMods

* Replaced Utils.Log with Utils.Error

* Slideshow fixes

* Replaced AbstractMetaState with IMetaState

* Updated CHANGELOG

Co-authored-by: Timothy Baldridge <tbaldridge@gmail.com>
This commit is contained in:
erri120 2020-03-04 13:10:49 +01:00 committed by GitHub
parent a1e911669a
commit 1ce640ba2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 158 additions and 99 deletions

View File

@ -1,5 +1,9 @@
### Changelog ### Changelog
#### Version - 1.0.1.0 - 3/xx/2020
* Added download support for YouTube
* Slideshow can now display mods from non-Nexus sites
#### Verison - 1.0.0.0 - 2/29/2020 #### Verison - 1.0.0.0 - 2/29/2020
* 1.0, first non-beta release * 1.0, first non-beta release

View File

@ -223,7 +223,7 @@ namespace Wabbajack.Common
return sha.Hash.ToBase64(); return sha.Hash.ToBase64();
} }
public static string StringSHA256Hex(this string s) public static string StringSHA256Hex(this string s)
{ {
var sha = new SHA256Managed(); var sha = new SHA256Managed();

View File

@ -78,6 +78,27 @@ namespace Wabbajack.Lib
return id; return id;
} }
public async Task<bool> GatherMetaData()
{
Utils.Log($"Getting meta data for {SelectedArchives.Count} archives");
await SelectedArchives.PMap(Queue, async a =>
{
if (a.State is IMetaState metaState)
{
var b = await metaState.LoadMetaData();
Utils.Log(b
? $"Getting meta data for {a.Name} was successful!"
: $"Getting meta data for {a.Name} failed!");
}
else
{
Utils.Log($"Archive {a.Name} is not an AbstractMetaState!");
}
});
return true;
}
public void ExportModList() public void ExportModList()
{ {
Utils.Log($"Exporting ModList to {ModListOutputFile}"); Utils.Log($"Exporting ModList to {ModListOutputFile}");

View File

@ -30,7 +30,7 @@ namespace Wabbajack.Lib
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State), typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State), typeof(VectorPlexusDownloader.State), typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State), typeof(VectorPlexusDownloader.State),
typeof(DeadlyStreamDownloader.State), typeof(AFKModsDownloader.State), typeof(TESAllianceDownloader.State), typeof(DeadlyStreamDownloader.State), typeof(AFKModsDownloader.State), typeof(TESAllianceDownloader.State),
typeof(TES3ArchiveState), typeof(TES3FileState), typeof(BethesdaNetDownloader.State), typeof(YouTubeDownloader) typeof(TES3ArchiveState), typeof(TES3FileState), typeof(BethesdaNetDownloader.State), typeof(YouTubeDownloader), typeof(IMetaState)
}, },
}; };
Config.VersionTolerance.Mode = VersionToleranceMode.Standard; Config.VersionTolerance.Mode = VersionToleranceMode.Standard;

View File

@ -7,10 +7,23 @@ using Wabbajack.Lib.Validation;
namespace Wabbajack.Lib.Downloaders namespace Wabbajack.Lib.Downloaders
{ {
public interface IMetaState
{
string URL { get; }
string Name { get; set; }
string Author { get; set; }
string Version { get; set; }
string ImageURL { get; set; }
bool IsNSFW { get; set; }
string Description { get; set; }
Task<bool> LoadMetaData();
}
public abstract class AbstractDownloadState public abstract class AbstractDownloadState
{ {
public static List<Type> KnownSubTypes = new List<Type>() public static List<Type> KnownSubTypes = new List<Type>
{ {
typeof(HTTPDownloader.State), typeof(HTTPDownloader.State),
typeof(GameFileSourceDownloader.State), typeof(GameFileSourceDownloader.State),

View File

@ -5,6 +5,7 @@ using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using HtmlAgilityPack;
using Newtonsoft.Json; using Newtonsoft.Json;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
@ -15,8 +16,8 @@ namespace Wabbajack.Lib.Downloaders
// IPS4 is the site used by LoversLab, VectorPlexus, etc. the general mechanics of each site are the // IPS4 is the site used by LoversLab, VectorPlexus, etc. the general mechanics of each site are the
// same, so we can fairly easily abstract the state. // same, so we can fairly easily abstract the state.
// Pass in the state type via TState // Pass in the state type via TState
public abstract class AbstractIPS4Downloader<TDownloader, TState> : AbstractNeedsLoginDownloader, IDownloader public abstract class AbstractIPS4Downloader<TDownloader, TState> : AbstractNeedsLoginDownloader, IDownloader
where TState : AbstractIPS4Downloader<TDownloader, TState>.State<TDownloader>, new() where TState : AbstractIPS4Downloader<TDownloader, TState>.State<TDownloader>, new()
where TDownloader : IDownloader where TDownloader : IDownloader
{ {
public override string SiteName { get; } public override string SiteName { get; }
@ -61,7 +62,7 @@ namespace Wabbajack.Lib.Downloaders
} }
public class State<TDownloader> : AbstractDownloadState where TDownloader : IDownloader public class State<TDownloader> : AbstractDownloadState, IMetaState where TDownloader : IDownloader
{ {
public string FileID { get; set; } public string FileID { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
@ -69,10 +70,12 @@ namespace Wabbajack.Lib.Downloaders
private static bool IsHTTPS => Downloader.SiteURL.AbsolutePath.StartsWith("https://"); private static bool IsHTTPS => Downloader.SiteURL.AbsolutePath.StartsWith("https://");
private static string URLPrefix => IsHTTPS ? "https://" : "http://"; private static string URLPrefix => IsHTTPS ? "https://" : "http://";
private static string Site => string.IsNullOrWhiteSpace(Downloader.SiteURL.Query) public static string Site => string.IsNullOrWhiteSpace(Downloader.SiteURL.Query)
? $"{URLPrefix}{Downloader.SiteURL.Host}" ? $"{URLPrefix}{Downloader.SiteURL.Host}"
: Downloader.SiteURL.ToString(); : Downloader.SiteURL.ToString();
public static AbstractNeedsLoginDownloader Downloader => (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>();
public override object[] PrimaryKey public override object[] PrimaryKey
{ {
get get
@ -100,13 +103,13 @@ namespace Wabbajack.Lib.Downloaders
private async Task<Stream> ResolveDownloadStream() private async Task<Stream> ResolveDownloadStream()
{ {
var downloader = (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>(); //var downloader = (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>();
TOP: TOP:
var csrfurl = FileID == null var csrfurl = FileID == null
? $"{Site}/files/file/{FileName}/?do=download" ? $"{Site}/files/file/{FileName}/?do=download"
: $"{Site}/files/file/{FileName}/?do=download&r={FileID}"; : $"{Site}/files/file/{FileName}/?do=download&r={FileID}";
var html = await downloader.AuthedClient.GetStringAsync(csrfurl); var html = await Downloader.AuthedClient.GetStringAsync(csrfurl);
var pattern = new Regex("(?<=csrfKey=).*(?=[&\"\'])|(?<=csrfKey: \").*(?=[&\"\'])"); var pattern = new Regex("(?<=csrfKey=).*(?=[&\"\'])|(?<=csrfKey: \").*(?=[&\"\'])");
var matches = pattern.Matches(html).Cast<Match>(); var matches = pattern.Matches(html).Cast<Match>();
@ -122,10 +125,10 @@ namespace Wabbajack.Lib.Downloaders
: $"{Site}/files/file/{FileName}/{sep}do=download&r={FileID}&confirm=1&t=1&csrfKey={csrfKey}"; : $"{Site}/files/file/{FileName}/{sep}do=download&r={FileID}&confirm=1&t=1&csrfKey={csrfKey}";
var streamResult = await downloader.AuthedClient.GetAsync(url); var streamResult = await Downloader.AuthedClient.GetAsync(url);
if (streamResult.StatusCode != HttpStatusCode.OK) if (streamResult.StatusCode != HttpStatusCode.OK)
{ {
Utils.ErrorThrow(new InvalidOperationException(), $"{downloader.SiteName} servers reported an error for file: {FileID}"); Utils.ErrorThrow(new InvalidOperationException(), $"{Downloader.SiteName} servers reported an error for file: {FileID}");
} }
var contentType = streamResult.Content.Headers.ContentType; var contentType = streamResult.Content.Headers.ContentType;
@ -138,7 +141,7 @@ namespace Wabbajack.Lib.Downloaders
var secs = times.Download - times.CurrentTime; var secs = times.Download - times.CurrentTime;
for (int x = 0; x < secs; x++) for (int x = 0; x < secs; x++)
{ {
Utils.Status($"Waiting for {secs} at the request of {downloader.SiteName}", Percent.FactoryPutInRange(x, secs)); Utils.Status($"Waiting for {secs} at the request of {Downloader.SiteName}", Percent.FactoryPutInRange(x, secs));
await Task.Delay(1000); await Task.Delay(1000);
} }
streamResult.Dispose(); streamResult.Dispose();
@ -200,7 +203,19 @@ namespace Wabbajack.Lib.Downloaders
}; };
} }
private static AbstractNeedsLoginDownloader Downloader => (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>(); // from IMetaState
public string URL => $"{Site}/files/file/{FileName}";
public string Name { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string ImageURL { get; set; }
public virtual bool IsNSFW { get; set; }
public string Description { get; set; }
public virtual async Task<bool> LoadMetaData()
{
return false;
}
} }
protected AbstractIPS4Downloader(Uri loginUri, string encryptedKeyName, string cookieDomain) : protected AbstractIPS4Downloader(Uri loginUri, string encryptedKeyName, string cookieDomain) :

View File

@ -1,24 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using HtmlAgilityPack;
using System.Windows.Input;
using CefSharp;
using ReactiveUI;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation;
using Wabbajack.Lib.WebAutomation; using Wabbajack.Lib.WebAutomation;
using File = Alphaleonis.Win32.Filesystem.File;
namespace Wabbajack.Lib.Downloaders namespace Wabbajack.Lib.Downloaders
{ {
@ -29,7 +14,6 @@ namespace Wabbajack.Lib.Downloaders
public override Uri SiteURL => new Uri("https://www.loverslab.com"); public override Uri SiteURL => new Uri("https://www.loverslab.com");
public override Uri IconUri => new Uri("https://www.loverslab.com/favicon.ico"); public override Uri IconUri => new Uri("https://www.loverslab.com/favicon.ico");
#endregion #endregion
public LoversLabDownloader() : base(new Uri("https://www.loverslab.com/login"), public LoversLabDownloader() : base(new Uri("https://www.loverslab.com/login"),
"loverslabcookies", "loverslab.com") "loverslabcookies", "loverslab.com")
{ {
@ -46,8 +30,31 @@ namespace Wabbajack.Lib.Downloaders
Utils.Error(ex); Utils.Error(ex);
} }
} }
public class State : State<LoversLabDownloader> public class State : State<LoversLabDownloader>
{ {
public override bool IsNSFW => true;
public override async Task<bool> LoadMetaData()
{
var html = await Downloader.AuthedClient.GetStringAsync(URL);
var doc = new HtmlDocument();
doc.LoadHtml(html);
var node = doc.DocumentNode;
Name = node.SelectNodes("//h1[@class='ipsType_pageTitle ipsContained_container']/span")?.First().InnerHtml;
Author = node
.SelectNodes(
"//div[@class='ipsBox_alt']/div[@class='ipsPhotoPanel ipsPhotoPanel_tiny ipsClearfix ipsSpacer_bottom']/div/p[@class='ipsType_reset ipsType_large ipsType_blendLinks']/a")
?.First().InnerHtml;
Version = node.SelectNodes("//section/h2[@class='ipsType_sectionHead']/span[@data-role='versionTitle']")
?
.First().InnerHtml;
ImageURL = node
.SelectNodes(
"//div[@class='ipsBox ipsSpacer_top ipsSpacer_double']/section/div[@class='ipsPad ipsAreaBackground']/div[@class='ipsCarousel ipsClearfix']/div[@class='ipsCarousel_inner']/ul[@class='cDownloadsCarousel ipsClearfix']/li[@class='ipsCarousel_item ipsAreaBackground_reset ipsPad_half']/span[@class='ipsThumb ipsThumb_medium ipsThumb_bg ipsCursor_pointer']")
?.First().GetAttributeValue("data-fullurl", "none");
return true;
}
} }
} }
} }

View File

@ -6,11 +6,13 @@ using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Ceras;
using ReactiveUI; using ReactiveUI;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Common.StatusFeed.Errors; using Wabbajack.Common.StatusFeed.Errors;
using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.Validation; using Wabbajack.Lib.Validation;
using Game = Wabbajack.Common.Game;
namespace Wabbajack.Lib.Downloaders namespace Wabbajack.Lib.Downloaders
{ {
@ -69,21 +71,18 @@ namespace Wabbajack.Lib.Downloaders
Utils.Error($"Error getting mod info for Nexus mod with {general.modID}"); Utils.Error($"Error getting mod info for Nexus mod with {general.modID}");
throw; throw;
} }
return new State return new State
{ {
GameName = general.gameName, Name = NexusApiUtils.FixupSummary(info.name),
FileID = general.fileID, Author = NexusApiUtils.FixupSummary(info.author),
ModID = general.modID,
Version = general.version ?? "0.0.0.0", Version = general.version ?? "0.0.0.0",
Author = info.author, ImageURL = info.picture_url,
UploadedBy = info.uploaded_by, IsNSFW = info.contains_adult_content,
UploaderProfile = info.uploaded_users_profile_url, Description = NexusApiUtils.FixupSummary(info.summary),
ModName = info.name, GameName = general.gameName,
SlideShowPic = info.picture_url, ModID = general.modID,
NexusURL = NexusApiUtils.GetModURL(game, info.mod_id), FileID = general.fileID
Summary = info.summary,
Adult = info.contains_adult_content
}; };
} }
@ -123,20 +122,30 @@ namespace Wabbajack.Lib.Downloaders
} }
} }
public class State : AbstractDownloadState public class State : AbstractDownloadState, IMetaState
{ {
public string URL => $"http://nexusmods.com/{NexusApiUtils.ConvertGameName(GameName)}/mods/{ModID}";
public string Name { get; set; }
public string Author { get; set; } public string Author { get; set; }
public string FileID { get; set; }
public string Version { get; set; }
public string ImageURL { get; set; }
public bool IsNSFW { get; set; }
public string Description { get; set; }
public async Task<bool> LoadMetaData()
{
return true;
}
public string GameName { get; set; } public string GameName { get; set; }
public string ModID { get; set; } public string ModID { get; set; }
public string UploadedBy { get; set; } public string FileID { get; set; }
public string UploaderProfile { get; set; }
public string Version { get; set; }
public string SlideShowPic { get; set; }
public string ModName { get; set; }
public string NexusURL { get; set; }
public string Summary { get; set; }
public bool Adult { get; set; }
public override object[] PrimaryKey { get => new object[]{GameName, ModID, FileID};} public override object[] PrimaryKey { get => new object[]{GameName, ModID, FileID};}
@ -192,7 +201,7 @@ namespace Wabbajack.Lib.Downloaders
} }
catch (Exception ex) catch (Exception ex)
{ {
Utils.Log($"{ModName} - {GameName} - {ModID} - {FileID} - Error getting Nexus download URL - {ex}"); Utils.Log($"{Name} - {GameName} - {ModID} - {FileID} - Error getting Nexus download URL - {ex}");
return false; return false;
} }

View File

@ -87,7 +87,7 @@ namespace Wabbajack.Lib
protected override async Task<bool> _Begin(CancellationToken cancel) protected override async Task<bool> _Begin(CancellationToken cancel)
{ {
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested) return false;
ConfigureProcessor(19, ConstructDynamicNumThreads(await RecommendQueueSize())); ConfigureProcessor(20, ConstructDynamicNumThreads(await RecommendQueueSize()));
UpdateTracker.Reset(); UpdateTracker.Reset();
UpdateTracker.NextStep("Gathering information"); UpdateTracker.NextStep("Gathering information");
Info("Looking for other profiles"); Info("Looking for other profiles");
@ -279,6 +279,9 @@ namespace Wabbajack.Lib
UpdateTracker.NextStep("Building Patches"); UpdateTracker.NextStep("Building Patches");
await BuildPatches(); await BuildPatches();
UpdateTracker.NextStep("Gathering Metadata");
await GatherMetaData();
ModList = new ModList ModList = new ModList
{ {
GameType = CompilingGame.Game, GameType = CompilingGame.Game,

View File

@ -284,7 +284,7 @@ namespace Wabbajack.Lib.NexusApi
try try
{ {
Utils.Log($"Requesting manual download for {archive.ModName}"); Utils.Log($"Requesting manual download for {archive.Name}");
return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString(); return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString();
} }
catch (TaskCanceledException ex) catch (TaskCanceledException ex)

View File

@ -113,7 +113,7 @@ namespace Wabbajack.Lib.Validation
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive)) if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{ {
var ext = Path.GetExtension(p.ArchiveHashPath.Last()); var ext = Path.GetExtension(p.ArchiveHashPath.Last());
var url = (archive.archive.State as NexusDownloader.State).NexusURL; var url = (archive.archive.State as NexusDownloader.State).URL;
if (Consts.AssetFileExtensions.Contains(ext) && !(archive.permissions.CanModifyAssets ?? true)) if (Consts.AssetFileExtensions.Contains(ext) && !(archive.permissions.CanModifyAssets ?? true))
{ {
ValidationErrors.Push($"{p.To} from {url} is set to disallow asset modification"); ValidationErrors.Push($"{p.To} from {url} is set to disallow asset modification");
@ -131,7 +131,7 @@ namespace Wabbajack.Lib.Validation
{ {
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive)) if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{ {
var url = (archive.archive.State as NexusDownloader.State).NexusURL; var url = (archive.archive.State as NexusDownloader.State).URL;
if (!(archive.permissions.CanExtractBSAs ?? true) && if (!(archive.permissions.CanExtractBSAs ?? true) &&
p.ArchiveHashPath.Skip(1).ButLast().Any(a => Consts.SupportedBSAs.Contains(Path.GetExtension(a).ToLower()))) p.ArchiveHashPath.Skip(1).ButLast().Any(a => Consts.SupportedBSAs.Contains(Path.GetExtension(a).ToLower())))
{ {

View File

@ -274,7 +274,7 @@ namespace Wabbajack
_titleText = Observable.CombineLatest( _titleText = Observable.CombineLatest(
this.WhenAny(x => x.ModList) this.WhenAny(x => x.ModList)
.Select(modList => modList?.Name ?? string.Empty), .Select(modList => modList?.Name ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.ModName) this.WhenAny(x => x.Slideshow.TargetMod.State.Name)
.StartWith(default(string)), .StartWith(default(string)),
this.WhenAny(x => x.Installing), this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList) resultSelector: (modList, mod, installing) => installing ? mod : modList)
@ -282,7 +282,7 @@ namespace Wabbajack
_authorText = Observable.CombineLatest( _authorText = Observable.CombineLatest(
this.WhenAny(x => x.ModList) this.WhenAny(x => x.ModList)
.Select(modList => modList?.Author ?? string.Empty), .Select(modList => modList?.Author ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.ModAuthor) this.WhenAny(x => x.Slideshow.TargetMod.State.Author)
.StartWith(default(string)), .StartWith(default(string)),
this.WhenAny(x => x.Installing), this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList) resultSelector: (modList, mod, installing) => installing ? mod : modList)
@ -290,7 +290,7 @@ namespace Wabbajack
_description = Observable.CombineLatest( _description = Observable.CombineLatest(
this.WhenAny(x => x.ModList) this.WhenAny(x => x.ModList)
.Select(modList => modList?.Description ?? string.Empty), .Select(modList => modList?.Description ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.ModDescription) this.WhenAny(x => x.Slideshow.TargetMod.State.Description)
.StartWith(default(string)), .StartWith(default(string)),
this.WhenAny(x => x.Installing), this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList) resultSelector: (modList, mod, installing) => installing ? mod : modList)

View File

@ -38,6 +38,7 @@ namespace Wabbajack
catch (Exception ex) catch (Exception ex)
{ {
Error = ex; Error = ex;
Utils.Error(ex, "Exception while loading the modlist!");
} }
ImageObservable = Observable.Return(Unit.Default) ImageObservable = Observable.Return(Unit.Default)

View File

@ -1,49 +1,29 @@
using ReactiveUI; using ReactiveUI;
using System; using System;
using System.IO;
using System.Net.Http;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib; using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
namespace Wabbajack namespace Wabbajack
{ {
public class ModVM : ViewModel public class ModVM : ViewModel
{ {
public string ModName { get; } public IMetaState State { 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. // 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, // 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. // and the cached image will automatically be released when the last interested party is gone.
public IObservable<BitmapImage> ImageObservable { get; } public IObservable<BitmapImage> ImageObservable { get; }
public ModVM(NexusDownloader.State m) public ModVM(IMetaState state)
{ {
ModName = NexusApiUtils.FixupSummary(m.ModName); State = state;
ModID = m.ModID;
ModDescription = NexusApiUtils.FixupSummary(m.Summary); ImageObservable = Observable.Return(State.ImageURL)
ModAuthor = NexusApiUtils.FixupSummary(m.Author);
IsNSFW = m.Adult;
ModURL = m.NexusURL;
ImageURL = m.SlideShowPic;
ImageObservable = Observable.Return(ImageURL)
.ObserveOn(RxApp.TaskpoolScheduler) .ObserveOn(RxApp.TaskpoolScheduler)
.DownloadBitmapImage((ex) => Utils.Log($"Skipping slide for mod {ModName} ({ModID})")) .DownloadBitmapImage((ex) => Utils.Log($"Skipping slide for mod {State.Name}"))
.Replay(1) .Replay(1)
.RefCount(TimeSpan.FromMilliseconds(5000)); .RefCount(TimeSpan.FromMilliseconds(5000));
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib; using Wabbajack.Lib;
@ -33,7 +34,7 @@ namespace Wabbajack
public ModVM TargetMod => _targetMod.Value; public ModVM TargetMod => _targetMod.Value;
public ReactiveCommand<Unit, Unit> SlideShowNextItemCommand { get; } = ReactiveCommand.Create(() => { }); public ReactiveCommand<Unit, Unit> SlideShowNextItemCommand { get; } = ReactiveCommand.Create(() => { });
public ReactiveCommand<Unit, Unit> VisitNexusSiteCommand { get; } public ReactiveCommand<Unit, Unit> VisitURLCommand { get; }
public const int PreloadAmount = 4; public const int PreloadAmount = 4;
@ -82,23 +83,24 @@ namespace Wabbajack
{ {
if (modList?.SourceModList?.Archives == null) if (modList?.SourceModList?.Archives == null)
{ {
return Observable.Empty<NexusDownloader.State>() return Observable.Empty<IMetaState>()
.ToObservableChangeSet(x => x.ModID); .ToObservableChangeSet(x => x.URL);
} }
return modList.SourceModList.Archives return modList.SourceModList.Archives
.Select(m => m.State) .Select(m => m.State)
.OfType<NexusDownloader.State>() .OfType<IMetaState>()
.DistinctBy(x => x.URL)
// Shuffle it // Shuffle it
.Shuffle(_random) .Shuffle(_random)
.AsObservableChangeSet(x => x.ModID); .AsObservableChangeSet(x => x.URL);
}) })
// Switch to the new list after every ModList change // Switch to the new list after every ModList change
.Switch() .Switch()
.Transform(nexus => new ModVM(nexus)) .Transform(mod => new ModVM(mod))
.DisposeMany() .DisposeMany()
// Filter out any NSFW slides if we don't want them // Filter out any NSFW slides if we don't want them
.AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW)) .AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW))
.Filter(slide => !slide.IsNSFW || ShowNSFW) .Filter(slide => !slide.State.IsNSFW || ShowNSFW)
.RefCount(); .RefCount();
// Find target mod to display by combining dynamic list with currently desired index // Find target mod to display by combining dynamic list with currently desired index
@ -120,14 +122,18 @@ namespace Wabbajack
.Switch() .Switch()
.ToGuiProperty(this, nameof(Image)); .ToGuiProperty(this, nameof(Image));
VisitNexusSiteCommand = ReactiveCommand.Create( VisitURLCommand = ReactiveCommand.Create(
execute: () => execute: () =>
{ {
Utils.OpenWebsite(TargetMod.ModURL); Utils.OpenWebsite(TargetMod.State.URL);
return Unit.Default; return Unit.Default;
}, },
canExecute: this.WhenAny(x => x.TargetMod.ModURL) canExecute: this.WhenAny(x => x.TargetMod.State.URL)
.Select(x => x?.StartsWith("https://") ?? false) .Select(x =>
{
var regex = new Regex("^(http|https):\\/\\/");
return x != null && regex.Match(x).Success;
})
.ObserveOnGuiThread()); .ObserveOnGuiThread());
// Preload upcoming images // Preload upcoming images

View File

@ -157,7 +157,7 @@ namespace Wabbajack
}; };
await vm.Driver.WaitForInitialized(); await vm.Driver.WaitForInitialized();
IWebDriver browser = new CefSharpWrapper(vm.Browser); IWebDriver browser = new CefSharpWrapper(vm.Browser);
vm.Instructions = $"Please Download {state.ModName} - {state.ModID} - {state.FileID}"; vm.Instructions = $"Please Download {state.Name} - {state.ModID} - {state.FileID}";
browser.DownloadHandler = uri => browser.DownloadHandler = uri =>
{ {
manuallyDownloadNexusFile.Resume(uri); manuallyDownloadNexusFile.Resume(uri);

View File

@ -73,7 +73,7 @@ namespace Wabbajack
}) })
.BindToStrict(this, x => x.PlayPauseButton.ToolTip) .BindToStrict(this, x => x.PlayPauseButton.ToolTip)
.DisposeWith(dispose); .DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Slideshow.VisitNexusSiteCommand) this.WhenAny(x => x.ViewModel.Slideshow.VisitURLCommand)
.BindToStrict(this, x => x.OpenWebsite.Command) .BindToStrict(this, x => x.OpenWebsite.Command)
.DisposeWith(dispose); .DisposeWith(dispose);
this.BindStrict(this.ViewModel, x => x.Slideshow.ShowNSFW, x => x.ShowNSFWButton.IsChecked, this.BindStrict(this.ViewModel, x => x.Slideshow.ShowNSFW, x => x.ShowNSFWButton.IsChecked,