using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Media.Imaging; using Alphaleonis.Win32.Filesystem; using DynamicData; using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.Common; using Wabbajack.Downloaders; using Wabbajack.DTOs; using Wabbajack.Lib; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Extensions; using Wabbajack.Lib.ModListRegistry; using Wabbajack.Paths; using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; using Wabbajack.Services.OSIntegrated.Services; namespace Wabbajack { public struct ModListTag { public ModListTag(string name) { Name = name; } public string Name { get; } } public class ModListMetadataVM : ViewModel { public ModlistMetadata Metadata { get; } private ModListGalleryVM _parent; public ICommand OpenWebsiteCommand { get; } public ICommand ExecuteCommand { get; } public ICommand ModListContentsCommend { get; } private readonly ObservableAsPropertyHelper _Exists; public bool Exists => _Exists.Value; public AbsolutePath Location { get; } [Reactive] public List ModListTagList { get; private set; } [Reactive] public Percent ProgressPercent { get; private set; } [Reactive] public bool IsBroken { get; private set; } [Reactive] public bool IsDownloading { get; private set; } [Reactive] public string DownloadSizeText { get; private set; } [Reactive] public string InstallSizeText { get; private set; } [Reactive] public string VersionText { get; private set; } [Reactive] public IErrorResponse Error { get; private set; } private readonly ObservableAsPropertyHelper _Image; public BitmapImage Image => _Image.Value; private readonly ObservableAsPropertyHelper _LoadingImage; public bool LoadingImage => _LoadingImage.Value; private Subject IsLoadingIdle; private readonly ILogger _logger; private readonly ModListDownloadMaintainer _maintainer; public ModListMetadataVM(ILogger logger, ModListGalleryVM parent, ModlistMetadata metadata, ModListDownloadMaintainer maintainer) { _logger = logger; _parent = parent; _maintainer = maintainer; Metadata = metadata; Location = LauncherUpdater.CommonFolder.Value.Combine("downloaded_mod_lists", Metadata.Links.MachineURL).WithExtension(Ext.Wabbajack); ModListTagList = new List(); Metadata.tags.ForEach(tag => { ModListTagList.Add(new ModListTag(tag)); }); ModListTagList.Add(new ModListTag(metadata.Game.MetaData().HumanFriendlyGameName)); DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives); InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles); VersionText = "Modlist version : " + Metadata.Version; IsBroken = metadata.ValidationSummary.HasFailures || metadata.ForceDown; //https://www.wabbajack.org/#/modlists/info?machineURL=eldersouls OpenWebsiteCommand = ReactiveCommand.Create(() => UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/#/modlists/info?machineURL={Metadata.Links.MachineURL}"))); IsLoadingIdle = new Subject(); ModListContentsCommend = ReactiveCommand.Create(async () => { _parent.MWVM.ModListContentsVM.Value.Name = metadata.Title; IsLoadingIdle.OnNext(false); try { var status = await ClientAPIEx.GetDetailedStatus(metadata.Links.MachineURL); var coll = _parent.MWVM.ModListContentsVM.Value.Status; coll.Clear(); coll.AddRange(status.Archives); _parent.MWVM.NavigateTo(_parent.MWVM.ModListContentsVM.Value); } finally { IsLoadingIdle.OnNext(true); } }, IsLoadingIdle.StartWith(true)); ExecuteCommand = ReactiveCommand.CreateFromObservable( canExecute: this.WhenAny(x => x.IsBroken).Select(x => !x), execute: (unit) => Observable.Return(unit) .WithLatestFrom( this.WhenAny(x => x.Exists), (_, e) => e) // Do any download work on background thread .ObserveOn(RxApp.TaskpoolScheduler) .SelectTask(async (exists) => { if (!exists) { try { var success = await Download(); if (!success) { Error = ErrorResponse.Fail("Download was marked unsuccessful"); return false; } } catch (Exception ex) { Error = ErrorResponse.Fail(ex); return false; } // Return an updated check on exists return Location.FileExists(); } return exists; }) .Where(exists => exists) // Do any install page swap over on GUI thread .ObserveOnGuiThread() .Select(_ => { _parent.MWVM.OpenInstaller(Location); // Wait for modlist member to be filled, then open its readme return _parent.MWVM.Installer.Value.WhenAny(x => x.ModList) .NotNull() .Take(1) .Do(modList => { try { modList.OpenReadme(); } catch (Exception ex) { _logger.LogError(ex, "While opening modlist README"); } }); }) .Switch() .Unit()); _Exists = Observable.Interval(TimeSpan.FromSeconds(0.5)) .Unit() .StartWith(Unit.Default) .FlowSwitch(_parent.WhenAny(x => x.IsActive)) .SelectAsync(async _ => { try { return !IsDownloading && !(await maintainer.HaveModList(metadata)); } catch (Exception) { return true; } }) .ToGuiProperty(this, nameof(Exists)); var imageObs = Observable.Return(Metadata.Links.ImageUri) .DownloadBitmapImage((ex) => _logger.LogError("Error downloading modlist image {Title}", Metadata.Title)); _Image = imageObs .ToGuiProperty(this, nameof(Image)); _LoadingImage = imageObs .Select(x => false) .StartWith(true) .ToGuiProperty(this, nameof(LoadingImage)); } private async Task Download() { ProgressPercent = Percent.Zero; using (var queue = new WorkQueue(1)) using (queue.Status.Select(i => i.ProgressPercent) .ObserveOnGuiThread() .Subscribe(percent => ProgressPercent = percent)) { var tcs = new TaskCompletionSource(); queue.QueueTask(async () => { try { IsDownloading = true; _logger.LogInformation("Starting Download of {MachineUrl}", Metadata.Links.MachineURL); var downloader = await DownloadDispatcher.ResolveArchive(Metadata.Links.Download); var result = await downloader.Download( new Archive(state: null!) { Name = Metadata.Title, Size = Metadata.DownloadMetadata?.Size ?? 0 }, Location); Utils.Log($"Done downloading {Metadata.Links.MachineURL}"); // Want to rehash to current file, even if failed? await Location.FileHashCachedAsync(); Utils.Log($"Done hashing {Metadata.Links.MachineURL}"); await Metadata.ToJsonAsync(Location.WithExtension(Consts.ModlistMetadataExtension)); tcs.SetResult(result); } catch (Exception ex) { Utils.Error(ex, $"Error Downloading of {Metadata.Links.MachineURL}"); tcs.SetException(ex); } finally { IsDownloading = false; } }); Task.Run(async () => await Metrics.Send(Metrics.Downloading, Metadata.Title)) .FireAndForget(ex => Utils.Error(ex, "Error sending download metric")); return await tcs.Task; } } } }