diff --git a/source/Reloaded.Mod.Launcher.Lib/Lib.cs b/source/Reloaded.Mod.Launcher.Lib/Lib.cs index cd6255df..2194ed2e 100644 --- a/source/Reloaded.Mod.Launcher.Lib/Lib.cs +++ b/source/Reloaded.Mod.Launcher.Lib/Lib.cs @@ -62,7 +62,7 @@ public static void Init(IDictionaryResourceProvider provider, SynchronizationCon Actions.ShowModLoaderUpdateDialogDelegate showModLoaderUpdate, Actions.ShowModUpdateDialogDelegate showModUpdate, Actions.ConfigureNuGetFeedsDialogDelegate configureNuGetFeeds, Actions.ConfigureModDialogDelegate configureModDialog, Actions.ShowMissingCoreDependencyDialogDelegate showMissingCoreDependency, Actions.EditModDialogDelegate editModDialog, Actions.PublishModDialogDelegate publishModDialog, - Actions.ShowEditModUserConfigDialogDelegate showEditModUserConfig, Actions.ShowFetchPackageDialogDelegate showFetchPackageDialog, + Actions.ShowEditModUserConfigDialogDelegate showEditModUserConfig, Actions.ShowFetchPackageDialogDelegate showFetchPackageDialog, Actions.ShowInstallPackageDialogDelegate showInstallPackageDialog, Actions.ShowSelectAddedGameDialogDelegate showSelectAddedGameDialog, Actions.ShowAddAppHashMismatchDialogDelegate showAddAppMismatchDialog, Actions.ShowApplicationWarningDialogDelegate showApplicationWarningDialog, Actions.ShowRunAppViaWineDialogDelegate showRunAppViaWineDialog, Actions.ShowEditPackDialogDelegate showEditPackDialog, Actions.ShowInstallModPackDialogDelegate showInstallModPackDialog, Action initControllerSupport) @@ -85,6 +85,7 @@ public static void Init(IDictionaryResourceProvider provider, SynchronizationCon Actions.PublishModDialog = publishModDialog; Actions.ShowEditModUserConfig = showEditModUserConfig; Actions.ShowFetchPackageDialog = showFetchPackageDialog; + Actions.ShowInstallPackageDialog = showInstallPackageDialog; Actions.ShowSelectAddedGameDialog = showSelectAddedGameDialog; Actions.ShowAddAppHashMismatchDialog = showAddAppMismatchDialog; Actions.ShowApplicationWarningDialog = showApplicationWarningDialog; diff --git a/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/CreateModViewModel.cs b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/CreateModViewModel.cs index 4ed5187d..2f239cdb 100644 --- a/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/CreateModViewModel.cs +++ b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/CreateModViewModel.cs @@ -33,6 +33,8 @@ public CreateModViewModel(ModConfigService modConfigService) ReleaseMetadataFileName = $"{ModId}.ReleaseMetadata.json" }; + config.IgnoreRegexes.Add($"{Regex.Escape($@"{config.ModId}.nuspec")}"); + config.IncludeRegexes.Add(Regex.Escape(ModConfig.ConfigFileName)); var modDirectory = Path.Combine(IoC.Get().GetModConfigDirectory(), IOEx.ForceValidFilePath(ModId)); var filePath = Path.Combine(modDirectory, ModConfig.ConfigFileName); await IConfig.ToPathAsync(config, filePath); diff --git a/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/InstallPackageViewModel.cs b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/InstallPackageViewModel.cs new file mode 100644 index 00000000..dbc6bd12 --- /dev/null +++ b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/InstallPackageViewModel.cs @@ -0,0 +1,15 @@ +namespace Reloaded.Mod.Launcher.Lib.Models.ViewModel.Dialog; + +/// +/// ViewModel for downloading an individual package. +/// +[AddINotifyPropertyChangedInterface] +public class InstallPackageViewModel : INotifyPropertyChanged +{ + public string Text { get; set; } + public string Title { get; set; } + public double Progress { get; set; } + public bool IsComplete { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; +} \ No newline at end of file diff --git a/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/PublishModDialogViewModel.cs b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/PublishModDialogViewModel.cs index 5373ada0..e19e7c94 100644 --- a/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/PublishModDialogViewModel.cs +++ b/source/Reloaded.Mod.Launcher.Lib/Models/ViewModel/Dialog/PublishModDialogViewModel.cs @@ -103,23 +103,47 @@ public PublishModDialogViewModel(PathTuple modTuple) _modTuple = modTuple; PackageName = IOEx.ForceValidFilePath(_modTuple.Config.ModName.Replace(' ', '_')); OutputFolder = Path.Combine(Path.GetTempPath(), $"{IOEx.ForceValidFilePath(_modTuple.Config.ModId)}.Publish"); - - // Set default Regexes. - IgnoreRegexes = new ObservableCollection() + IgnoreRegexes = new ObservableCollection( + _modTuple.Config.IgnoreRegexes.Select(x => new StringWrapper { Value = x }) + ); + foreach (StringWrapper item in IgnoreRegexes) + item.PropertyChanged += (_, __) => UpdateConfig(_modTuple.Config.IgnoreRegexes, IgnoreRegexes); + IgnoreRegexes.CollectionChanged += (s, e) => { - @".*\.json", // Config files - $"{Regex.Escape($@"{_modTuple.Config.ModId}.nuspec")}" + if (e.NewItems != null) + foreach (StringWrapper item in e.NewItems) + item.PropertyChanged += (_, __) => UpdateConfig(_modTuple.Config.IgnoreRegexes, IgnoreRegexes); + + if (e.OldItems != null) + foreach (StringWrapper item in e.OldItems) + item.PropertyChanged -= (_, __) => UpdateConfig(_modTuple.Config.IgnoreRegexes, IgnoreRegexes); + _modTuple.Config.IgnoreRegexes.Clear(); + _modTuple.Config.IgnoreRegexes.AddRange(IgnoreRegexes.Select(x => x.Value)); }; - - IncludeRegexes = new ObservableCollection() + IncludeRegexes = new ObservableCollection( + _modTuple.Config.IncludeRegexes.Select(x => new StringWrapper { Value = x }) + ); + foreach (StringWrapper item in IncludeRegexes) + item.PropertyChanged += (_, __) => UpdateConfig(_modTuple.Config.IncludeRegexes, IncludeRegexes); + IncludeRegexes.CollectionChanged += (s, e) => { - Regex.Escape(ModConfig.ConfigFileName), - @"\.deps\.json", - @"\.runtimeconfig\.json", + if (e.NewItems != null) + foreach (StringWrapper item in e.NewItems) + item.PropertyChanged += (_, __) => UpdateConfig(_modTuple.Config.IncludeRegexes, IncludeRegexes); + + if (e.OldItems != null) + foreach (StringWrapper item in e.OldItems) + item.PropertyChanged -= (_, __) => UpdateConfig(_modTuple.Config.IncludeRegexes, IncludeRegexes); + _modTuple.Config.IncludeRegexes.Clear(); + _modTuple.Config.IncludeRegexes.AddRange(IncludeRegexes.Select(x => x.Value)); }; + } - // Set notifications - PropertyChanged += ChangeUiVisbilityOnPropertyChanged; + void UpdateConfig(List list, ObservableCollection collection) + { + list.Clear(); + list.AddRange(collection.Select(x => x.Value)); + _modTuple.SaveAsync(); } /// @@ -148,8 +172,8 @@ await PublishAsync(new PublishArgs() PublishTarget = PublishTarget, OutputFolder = OutputFolder, ModTuple = _modTuple, - IgnoreRegexes = IgnoreRegexes.Select(x => x.Value).ToList(), - IncludeRegexes = IncludeRegexes.Select(x => x.Value).ToList(), + IgnoreRegexes = _modTuple.Config.IgnoreRegexes, + IncludeRegexes = _modTuple.Config.IncludeRegexes, Progress = new Progress(d => BuildProgress = d * 100), AutomaticDelta = AutomaticDelta, CompressionLevel = CompressionLevel, @@ -286,6 +310,11 @@ public void SetOutputFolder() /// public void SetReadmePath() => ReadmePath = FileSelectors.SelectMarkdownFile(); + /// + /// Lets the user save all changes to the mod config. + /// + public Task SaveAsync() => _modTuple.SaveAsync(); + private string GetModFolder() => Path.GetDirectoryName(_modTuple.Path)!; diff --git a/source/Reloaded.Mod.Launcher.Lib/Static/Actions.cs b/source/Reloaded.Mod.Launcher.Lib/Static/Actions.cs index ca952f98..af850eb0 100644 --- a/source/Reloaded.Mod.Launcher.Lib/Static/Actions.cs +++ b/source/Reloaded.Mod.Launcher.Lib/Static/Actions.cs @@ -77,6 +77,9 @@ public static class Actions /// public static ShowFetchPackageDialogDelegate ShowFetchPackageDialog { get; set; } = null!; + public static ShowInstallPackageDialogDelegate ShowInstallPackageDialog { get; set; } = null!; + + /// /// Shows a dialog that can be used to select the added game. /// @@ -254,6 +257,12 @@ public enum MessageBoxType /// The ViewModel used for downloading the individual package. public delegate bool ShowFetchPackageDialogDelegate(DownloadPackageViewModel viewModel); + /// + /// Shows a dialog that can be used to download an individual package. + /// + /// The ViewModel used for downloading the individual package. + public delegate bool ShowInstallPackageDialogDelegate(InstallPackageViewModel viewModel); + /// /// Shows a dialog that can be used to select the added game. /// diff --git a/source/Reloaded.Mod.Launcher.Lib/Static/Resources.cs b/source/Reloaded.Mod.Launcher.Lib/Static/Resources.cs index 1b329244..00014d0b 100644 --- a/source/Reloaded.Mod.Launcher.Lib/Static/Resources.cs +++ b/source/Reloaded.Mod.Launcher.Lib/Static/Resources.cs @@ -181,7 +181,7 @@ public static void Init(IDictionaryResourceProvider provider) // Update 1.21.0: Mod Packs Install public static IDictionaryResource InstallModPackDownloading { get; set; } public static IDictionaryResource InstallModPackErrorDownloadFail { get; set; } - + // Update 1.21.6: Mod Packs Install public static IDictionaryResource ErrorAddApplicationGeneral { get; set; } public static IDictionaryResource ErrorAddApplicationCantReadSymlink { get; set; } @@ -219,4 +219,10 @@ public static void Init(IDictionaryResourceProvider provider) public static IDictionaryResource ErrorViewDetails { get; set; } public static IDictionaryResource ErrorStacktraceTitle { get; set; } public static IDictionaryResource ErrorStacktraceSubtitle { get; set; } + + // Update 1.X.X: New Progress Window for Local Mod Installation (UPDATE LAUNCHER VER BEFORE RELEASE) + public static IDictionaryResource InstallModArchiveTitle { get; set; } + public static IDictionaryResource InstalledModName { get; set; } + public static IDictionaryResource InstallingModWait { get; set; } + public static IDictionaryResource ExtractingLocalModArchive { get; set; } } \ No newline at end of file diff --git a/source/Reloaded.Mod.Launcher/Assets/Languages/en-GB.xaml b/source/Reloaded.Mod.Launcher/Assets/Languages/en-GB.xaml index bdce0f65..7318188a 100644 --- a/source/Reloaded.Mod.Launcher/Assets/Languages/en-GB.xaml +++ b/source/Reloaded.Mod.Launcher/Assets/Languages/en-GB.xaml @@ -197,6 +197,12 @@ Download Mod Archive File Name + + + Installing Mod Archive + Mod Name + Please wait while we install the mod! + Extracting a local mod, please wait! You need to run this application as administrator. Administrative privileges are needed to receive application launch/exit events from Windows Management Instrumentation (WMI). Developers: Run your favourite IDE e.g. Visual Studio as Admin. diff --git a/source/Reloaded.Mod.Launcher/LibraryBindings.cs b/source/Reloaded.Mod.Launcher/LibraryBindings.cs index 2b623ef9..638db1b4 100644 --- a/source/Reloaded.Mod.Launcher/LibraryBindings.cs +++ b/source/Reloaded.Mod.Launcher/LibraryBindings.cs @@ -27,6 +27,7 @@ public static void Init(IResourceFileSelector? languageSelector, IResourceFileSe publishModDialog: PublishModDialog, showEditModUserConfig: ShowEditModUserConfig, showFetchPackageDialog: ShowFetchPackageDialog, + showInstallPackageDialog: ShowInstallPackageDialog, showSelectAddedGameDialog: ShowSelectAddedGameDialog, showAddAppMismatchDialog: ShowAddAppMismatchDialog, showApplicationWarningDialog: ShowApplicationWarningDialog, @@ -106,6 +107,7 @@ private static bool EditModDialog(EditModDialogViewModel viewmodel, object? owne private static bool ShowEditModUserConfig(EditModUserConfigDialogViewModel viewmodel) => ShowDialogAndGetResult(new EditModUserConfigDialog(viewmodel)); private static bool PublishModDialog(PublishModDialogViewModel viewmodel) => ShowDialogAndGetResult(new PublishModDialog(viewmodel)); private static bool ShowFetchPackageDialog(DownloadPackageViewModel viewmodel) => ShowDialogAndGetResult(new DownloadPackageDialog(viewmodel)); + private static bool ShowInstallPackageDialog(InstallPackageViewModel viewmodel) => ShowDialogAndGetResult(new InstallPackageDialog(viewmodel)); private static bool ShowAddAppMismatchDialog(AddAppHashMismatchDialogViewModel viewmodel) => ShowDialogAndGetResult(new AddAppHashMismatchDialog(viewmodel)); private static bool ShowApplicationWarningDialog(AddApplicationWarningDialogViewModel viewmodel) => ShowDialogAndGetResult(new ShowApplicationWarningDialog(viewmodel)); private static bool ShowInstallModPackDialog(InstallModPackDialogViewModel viewmodel) => ShowDialogAndGetResult(new InstallModPackDialog(viewmodel)); diff --git a/source/Reloaded.Mod.Launcher/MainWindow.xaml.cs b/source/Reloaded.Mod.Launcher/MainWindow.xaml.cs index eb9c0816..1bb80223 100644 --- a/source/Reloaded.Mod.Launcher/MainWindow.xaml.cs +++ b/source/Reloaded.Mod.Launcher/MainWindow.xaml.cs @@ -1,7 +1,9 @@ -using System.Text; +using NuGet.Common; using Reloaded.Mod.Loader.Update.Providers.Web; using Sewer56.DeltaPatchGenerator.Lib.Utility; using Sewer56.Update.Extractors.SevenZipSharp; +using System.Text; +using System.Windows.Threading; using static Reloaded.Mod.Launcher.Lib.Static.Resources; namespace Reloaded.Mod.Launcher; @@ -88,10 +90,40 @@ private async void InstallMod_Drop(object sender, DragEventArgs e) /* Extract to Temp Directory */ using var tempFolder = new TemporaryFolderAllocation(); var archiveExtractor = new SevenZipSharpExtractor(); - await archiveExtractor.ExtractPackageAsync(file, tempFolder.FolderPath, new Progress(), default); - /* Get name of package. */ - WebDownloadablePackage.CopyPackagesFromExtractFolderToTargetDir(modsFolder!, tempFolder.FolderPath, default); + var installVM = new InstallPackageViewModel + { + Title = InstallModArchiveTitle.Get(), + Text = ExtractingLocalModArchive.Get(), + Progress = 0 + }; + + var progress = new Progress(value => + { + installVM.Progress = value * 100; + }); + + //Waits for 3 seconds before showing the install dialog, if the installation is not complete by then. + var timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(3) + }; + + timer.Tick += (s, e) => + { + timer.Stop(); + if (installVM.Progress != 100) + { + Actions.ShowInstallPackageDialog.Invoke(installVM); + } + }; + timer.Start(); + + await archiveExtractor.ExtractPackageAsync(file, tempFolder.FolderPath, progress, default); + + installVM.Text = InstallingModWait.Get(); + await WebDownloadablePackage.CopyPackagesFromExtractFolderToTargetDir(modsFolder!, tempFolder.FolderPath, default); + installVM.IsComplete = true; } // Find the new mods diff --git a/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml b/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml new file mode 100644 index 00000000..f9cb59b1 --- /dev/null +++ b/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml.cs b/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml.cs new file mode 100644 index 00000000..3de571cb --- /dev/null +++ b/source/Reloaded.Mod.Launcher/Pages/Dialogs/InstallPackageDialog.xaml.cs @@ -0,0 +1,26 @@ +using Button = System.Windows.Controls.Button; + +namespace Reloaded.Mod.Launcher.Pages.Dialogs; + +/// +/// Interaction logic for DownloadPackageDialog.xaml +/// +public partial class InstallPackageDialog : ReloadedWindow +{ + public new InstallPackageViewModel ViewModel { get; set; } + + /// + public InstallPackageDialog(InstallPackageViewModel viewModel) + { + InitializeComponent(); + ViewModel = viewModel; + DataContext = viewModel; + viewModel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(InstallPackageViewModel.IsComplete) && viewModel.IsComplete) + { + ActionWrappers.ExecuteWithApplicationDispatcher(this.Close); + } + }; + } +} \ No newline at end of file diff --git a/source/Reloaded.Mod.Launcher/Pages/Dialogs/PublishModDialog.xaml.cs b/source/Reloaded.Mod.Launcher/Pages/Dialogs/PublishModDialog.xaml.cs index 6901ade2..235ac004 100644 --- a/source/Reloaded.Mod.Launcher/Pages/Dialogs/PublishModDialog.xaml.cs +++ b/source/Reloaded.Mod.Launcher/Pages/Dialogs/PublishModDialog.xaml.cs @@ -20,7 +20,10 @@ public PublishModDialog(PublishModDialogViewModel viewModel) this.Closing += OnClosing; } - private void OnClosing(object? sender, CancelEventArgs e) => _cancellationTokenSource.Cancel(); + private async void OnClosing(object? sender, CancelEventArgs e) { + await ViewModel.SaveAsync(); + _cancellationTokenSource.Cancel(); + } private async void Publish_Click(object sender, System.Windows.RoutedEventArgs e) { diff --git a/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs b/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs index b63e36b7..f583de42 100644 --- a/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs +++ b/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs @@ -1,4 +1,5 @@ using Reloaded.Memory.Extensions; +using System.Text.RegularExpressions; namespace Reloaded.Mod.Loader.IO.Config; @@ -36,6 +37,10 @@ public class ModConfig : ObservableObject, IConfig, IModConfig public bool IsLibrary { get; set; } = false; public string ReleaseMetadataFileName { get; set; } = "Sewer56.Update.ReleaseMetadata.json"; + /// Publishing + public List IgnoreRegexes { get; set; } = [@".*\.json"]; + public List IncludeRegexes { get; set; } = [@"\.deps\.json", @"\.runtimeconfig\.json"]; + [JsonIgnore] public string ModSubDirs { get; set; } = string.Empty; diff --git a/source/Reloaded.Mod.Loader.Update/Providers/Update/UpdateDownloadablePackage.cs b/source/Reloaded.Mod.Loader.Update/Providers/Update/UpdateDownloadablePackage.cs index 17a743ea..e22b2013 100644 --- a/source/Reloaded.Mod.Loader.Update/Providers/Update/UpdateDownloadablePackage.cs +++ b/source/Reloaded.Mod.Loader.Update/Providers/Update/UpdateDownloadablePackage.cs @@ -116,7 +116,7 @@ await retryPolicy.ExecuteAsync(async () => await archiveExtractor.ExtractPackageAsync(tempDownloadPath, tempExtractDir.FolderPath, extractSlice, token); // Copy all packages from download. - return WebDownloadablePackage.CopyPackagesFromExtractFolderToTargetDir(packageFolder, tempExtractDir.FolderPath, token); + return await WebDownloadablePackage.CopyPackagesFromExtractFolderToTargetDir(packageFolder, tempExtractDir.FolderPath, token); } #pragma warning disable CS0067 // Event never used diff --git a/source/Reloaded.Mod.Loader.Update/Providers/Web/WebDownloadablePackage.cs b/source/Reloaded.Mod.Loader.Update/Providers/Web/WebDownloadablePackage.cs index 679ab373..d254934b 100644 --- a/source/Reloaded.Mod.Loader.Update/Providers/Web/WebDownloadablePackage.cs +++ b/source/Reloaded.Mod.Loader.Update/Providers/Web/WebDownloadablePackage.cs @@ -129,7 +129,7 @@ await retryPolicy.ExecuteAsync(async () => await archiveExtractor.ExtractPackageAsync(tempFilePath, tempExtractDirectory.FolderPath, extractProgress, token); /* Get name of package. */ - return CopyPackagesFromExtractFolderToTargetDir(packageFolder, tempExtractDirectory.FolderPath, token); + return await CopyPackagesFromExtractFolderToTargetDir(packageFolder, tempExtractDirectory.FolderPath, token); } /// @@ -139,7 +139,7 @@ await retryPolicy.ExecuteAsync(async () => /// Finds all mods in and copies them to appropriate subfolders in . /// /// Path to last folder copied. - public static string CopyPackagesFromExtractFolderToTargetDir(string packageFolder, string tempExtractDir, CancellationToken token) + public async static Task CopyPackagesFromExtractFolderToTargetDir(string packageFolder, string tempExtractDir, CancellationToken token) { var configs = ConfigReader.ReadConfigurations(tempExtractDir, ModConfig.ConfigFileName, token, int.MaxValue, 0); var returnResult = ""; diff --git a/source/Reloaded.Mod.Loader.Update/Utilities/StringWrapper.cs b/source/Reloaded.Mod.Loader.Update/Utilities/StringWrapper.cs index 65689ee8..8d2c2331 100644 --- a/source/Reloaded.Mod.Loader.Update/Utilities/StringWrapper.cs +++ b/source/Reloaded.Mod.Loader.Update/Utilities/StringWrapper.cs @@ -1,8 +1,11 @@ +using PropertyChanged; + namespace Reloaded.Mod.Loader.Update.Utilities; /// /// Class that wraps a string. Used for data binding. /// +[AddINotifyPropertyChangedInterface] [JsonConverter(typeof(StringWrapperConverter))] public class StringWrapper : ObservableObject {