From fd48127fc4d2c30dff30c2836dacde2e4bbd8ef1 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 21 Oct 2024 17:07:23 -0500 Subject: [PATCH] Cache migration and other fixes --- Cmdline/Action/Cache.cs | 9 +- Core/CKANPathUtils.cs | 9 +- Core/GameInstanceManager.cs | 77 +++- Core/ModuleInstaller.cs | 6 +- Core/Net/NetAsyncModulesDownloader.cs | 2 +- Core/Net/NetFileCache.cs | 157 ++++--- Core/Net/NetModuleCache.cs | 39 +- Core/Properties/Resources.resx | 8 + GUI/Controls/ManageMods.cs | 15 +- GUI/Controls/ModInfo.cs | 5 + GUI/Dialogs/SelectionDialog.Designer.cs | 96 ++-- GUI/Dialogs/SelectionDialog.cs | 162 ++++--- GUI/Dialogs/SelectionDialog.resx | 2 +- GUI/Dialogs/SettingsDialog.Designer.cs | 143 ++++-- GUI/Dialogs/SettingsDialog.cs | 421 ++++++++++++------ GUI/Dialogs/SettingsDialog.resx | 3 + GUI/Dialogs/YesNoDialog.Designer.cs | 2 +- GUI/Dialogs/YesNoDialog.cs | 5 - GUI/GUIUser.cs | 11 +- .../de-DE/SelectionDialog.de-DE.resx | 2 +- .../fr-FR/SelectionDialog.fr-FR.resx | 2 +- .../it-IT/SelectionDialog.it-IT.resx | 2 +- .../ja-JP/SelectionDialog.ja-JP.resx | 2 +- .../ko-KR/SelectionDialog.ko-KR.resx | 2 +- .../nl-NL/SelectionDialog.nl-NL.resx | 2 +- .../pl-PL/SelectionDialog.pl-PL.resx | 2 +- .../pt-BR/SelectionDialog.pt-BR.resx | 2 +- .../ru-RU/SelectionDialog.ru-RU.resx | 2 +- .../zh-CN/SelectionDialog.zh-CN.resx | 2 +- GUI/Main/Main.cs | 7 +- GUI/Main/MainDialogs.cs | 14 +- GUI/Main/MainDownload.cs | 20 +- GUI/Properties/Resources.resx | 5 + Netkan/Processors/Inflator.cs | 2 +- Netkan/Services/ModuleService.cs | 4 +- 35 files changed, 811 insertions(+), 433 deletions(-) diff --git a/Cmdline/Action/Cache.cs b/Cmdline/Action/Cache.cs index 5d446772f4..b8e5190ceb 100644 --- a/Cmdline/Action/Cache.cs +++ b/Cmdline/Action/Cache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using CommandLine; @@ -177,7 +178,9 @@ private int SetCacheDirectory(SetOptions options) if (manager != null) { - if (manager.TrySetupCache(options.Path, out string? failReason)) + if (manager.TrySetupCache(options.Path, + new Progress(p => {}), + out string? failReason)) { IConfiguration cfg = ServiceLocator.Container.Resolve(); user?.RaiseMessage(Properties.Resources.CacheSet, cfg.DownloadCacheDir ?? ""); @@ -205,7 +208,9 @@ private int ResetCacheDirectory() { if (manager != null) { - if (manager.TrySetupCache("", out string? failReason)) + if (manager.TrySetupCache("", + new Progress(p => {}), + out string? failReason)) { IConfiguration cfg = ServiceLocator.Container.Resolve(); user?.RaiseMessage(Properties.Resources.CacheReset, cfg.DownloadCacheDir ?? ""); diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs index 72e82abc9e..9a1c94b7a0 100644 --- a/Core/CKANPathUtils.cs +++ b/Core/CKANPathUtils.cs @@ -114,7 +114,9 @@ public static string ToAbsolute(string path, string root) return NormalizePath(Path.Combine(root, path)); } - public static void CheckFreeSpace(DirectoryInfo where, long bytesToStore, string errorDescription) + public static void CheckFreeSpace(DirectoryInfo where, + long bytesToStore, + string errorDescription) { if (bytesToStore > 0) { @@ -131,5 +133,10 @@ public static void CheckFreeSpace(DirectoryInfo where, long bytesToStore, string } } + public static bool PathEquals(this FileSystemInfo a, + FileSystemInfo b) + => NormalizePath(a.FullName).Equals(NormalizePath(b.FullName), + Platform.PathComparison); + } } diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index c605b7cbcb..28cb510fcd 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -33,6 +33,7 @@ public class GameInstanceManager : IDisposable public GameInstance? CurrentInstance { get; set; } public NetModuleCache? Cache { get; private set; } + public event Action? CacheChanged; public readonly SteamLibrary SteamLibrary = new SteamLibrary(); @@ -580,11 +581,14 @@ private void LoadCacheSettings() } } - if (!TrySetupCache(Configuration.DownloadCacheDir, out string? failReason)) + var progress = new Progress(p => {}); + if (!TrySetupCache(Configuration.DownloadCacheDir, + progress, + out string? failReason)) { log.ErrorFormat("Cache not found at configured path {0}: {1}", Configuration.DownloadCacheDir, failReason); // Fall back to default path to minimize chance of ending up in an invalid state at startup - TrySetupCache("", out _); + TrySetupCache("", progress, out _); } } @@ -597,9 +601,11 @@ private void LoadCacheSettings() /// true if successful, false otherwise /// public bool TrySetupCache(string? path, + IProgress progress, [NotNullWhen(returnValue: false)] out string? failureReason) { - var origPath = Configuration.DownloadCacheDir; + var origPath = Configuration.DownloadCacheDir; + var origCache = Cache; try { if (path == null || string.IsNullOrEmpty(path)) @@ -614,17 +620,81 @@ public bool TrySetupCache(string? path, Cache = new NetModuleCache(this, path); Configuration.DownloadCacheDir = path; } + if (origPath != null && origCache != null) + { + origCache.GetSizeInfo(out _, out long oldNumBytes, out _); + Cache.GetSizeInfo(out _, out _, out long bytesFree); + + if (oldNumBytes > 0) + { + switch (User.RaiseSelectionDialog( + string.Format(Properties.Resources.GameInstanceManagerCacheMigrationPrompt, + CkanModule.FmtSize(oldNumBytes), + CkanModule.FmtSize(bytesFree)), + oldNumBytes < bytesFree ? 0 : 2, + Properties.Resources.GameInstanceManagerCacheMigrationMove, + Properties.Resources.GameInstanceManagerCacheMigrationDelete, + Properties.Resources.GameInstanceManagerCacheMigrationOpen, + Properties.Resources.GameInstanceManagerCacheMigrationNothing, + Properties.Resources.GameInstanceManagerCacheMigrationRevert)) + { + case 0: + if (oldNumBytes < bytesFree) + { + Cache.MoveFrom(new DirectoryInfo(origPath), progress); + CacheChanged?.Invoke(origCache); + } + else + { + User.RaiseError(Properties.Resources.GameInstanceManagerCacheMigrationNotEnoughFreeSpace); + // Abort since the user picked an option that doesn't work + Cache = origCache; + Configuration.DownloadCacheDir = origPath; + failureReason = ""; + } + break; + + case 1: + origCache.RemoveAll(); + CacheChanged?.Invoke(origCache); + break; + + case 2: + Utilities.OpenFileBrowser(origPath); + Utilities.OpenFileBrowser(Configuration.DownloadCacheDir); + CacheChanged?.Invoke(origCache); + break; + + case 3: + CacheChanged?.Invoke(origCache); + break; + + case -1: + case 4: + Cache = origCache; + Configuration.DownloadCacheDir = origPath; + failureReason = ""; + return false; + } + } + else + { + CacheChanged?.Invoke(origCache); + } + } failureReason = null; return true; } catch (DirectoryNotFoundKraken) { + Cache = origCache; Configuration.DownloadCacheDir = origPath; failureReason = string.Format(Properties.Resources.GameInstancePathNotFound, path); return false; } catch (Exception ex) { + Cache = origCache; Configuration.DownloadCacheDir = origPath; failureReason = ex.Message; return false; @@ -647,6 +717,7 @@ public void Dispose() } // Attempting to dispose of the related RegistryManager object here is a bad idea, it cause loads of failures + GC.SuppressFinalize(this); } public static bool IsGameInstanceDir(DirectoryInfo path) diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 4f4cd2a297..1de0f7aabf 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1210,9 +1210,8 @@ public void Upgrade(ICollection modules, if (installed_mod == null) { if (!Cache.IsMaybeCachedZip(module) - && Cache.GetInProgressFileName(module) is string p) + && Cache.GetInProgressFileName(module) is FileInfo inProgressFile) { - var inProgressFile = new FileInfo(p); if (inProgressFile.Exists) { User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeInstallingResuming, @@ -1253,9 +1252,8 @@ public void Upgrade(ICollection modules, else { if (!Cache.IsMaybeCachedZip(module) - && Cache.GetInProgressFileName(module) is string p) + && Cache.GetInProgressFileName(module) is FileInfo inProgressFile) { - var inProgressFile = new FileInfo(p); if (inProgressFile.Exists) { User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeUpgradingResuming, diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index ed0da2a1ef..97c5bd8fab 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -60,7 +60,7 @@ private NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup( .OrderBy(u => u, new PreferredHostUriComparer(preferredHosts)) .ToList(), - cache.GetInProgressFileName(first), + cache.GetInProgressFileName(first)?.FullName, first.download_size, string.IsNullOrEmpty(first.download_content_type) ? defaultMimeType diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 9fbd10d08a..24cf02d900 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -35,7 +35,9 @@ public class NetFileCache : IDisposable private readonly FileSystemWatcher watcher; // hash => full file path private Dictionary? cachedFiles; - private readonly string cachePath; + private readonly DirectoryInfo cachePath; + // Files go here while they're downloading + private readonly DirectoryInfo inProgressPath; private readonly GameInstanceManager? manager; private static readonly Regex cacheFileRegex = new Regex("^[0-9A-F]{8}-", RegexOptions.Compiled); private static readonly ILog log = LogManager.GetLogger(typeof (NetFileCache)); @@ -44,7 +46,7 @@ public class NetFileCache : IDisposable /// Initialize a cache given a GameInstanceManager /// /// GameInstanceManager object containing the Instances that might have old caches - public NetFileCache(GameInstanceManager? mgr, string path) + public NetFileCache(GameInstanceManager mgr, string path) : this(path) { manager = mgr; @@ -56,21 +58,23 @@ public NetFileCache(GameInstanceManager? mgr, string path) /// Location of folder to use for caching public NetFileCache(string path) { - cachePath = path; - + cachePath = new DirectoryInfo(path); // Basic validation, our cache has to exist. - if (!Directory.Exists(cachePath)) + if (!cachePath.Exists) { - throw new DirectoryNotFoundKraken(cachePath, string.Format( - Properties.Resources.NetFileCacheCannotFind, cachePath)); + throw new DirectoryNotFoundKraken( + path, + string.Format(Properties.Resources.NetFileCacheCannotFind, + path)); } + inProgressPath = new DirectoryInfo(Path.Combine(path, "downloading")); // Make sure we can access it - var bytesFree = new DirectoryInfo(cachePath)?.GetDrive()?.AvailableFreeSpace ?? 0; + var bytesFree = cachePath.GetDrive().AvailableFreeSpace; // Establish a watch on our cache. This means we can cache the directory contents, // and discard that cache if we spot changes. - watcher = new FileSystemWatcher(cachePath, "*.zip") + watcher = new FileSystemWatcher(cachePath.FullName, "*.zip") { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess @@ -102,30 +106,29 @@ public void Dispose() // We disable its event raising capabilities first for good measure. watcher.EnableRaisingEvents = false; watcher.Dispose(); + GC.SuppressFinalize(this); } - // Files go here while they're downloading - private string InProgressPath => Path.Combine(cachePath, "downloading"); - - private string GetInProgressFileName(string hash, string description) + private FileInfo GetInProgressFileName(string hash, string description) { - Directory.CreateDirectory(InProgressPath); - return Directory.EnumerateFiles(InProgressPath) - .FirstOrDefault(path => new FileInfo(path).Name.StartsWith(hash)) - // If not found, return the name to create - ?? Path.Combine(InProgressPath, $"{hash}-{description}"); + inProgressPath.Create(); + return inProgressPath.EnumerateFiles() + .FirstOrDefault(path => path.Name.StartsWith(hash)) + // If not found, return the name to create + ?? new FileInfo(Path.Combine(inProgressPath.FullName, + $"{hash}-{description}")); } - public string GetInProgressFileName(Uri url, string description) + public FileInfo GetInProgressFileName(Uri url, string description) => GetInProgressFileName(CreateURLHash(url), description); - public string? GetInProgressFileName(List urls, string description) + public FileInfo? GetInProgressFileName(List urls, string description) { - var filenames = urls?.Select(url => GetInProgressFileName(CreateURLHash(url), description)) - .ToArray(); - return filenames?.FirstOrDefault(File.Exists) - ?? filenames?.FirstOrDefault(); + var filenames = urls.Select(url => GetInProgressFileName(CreateURLHash(url), description)) + .Memoize(); + return filenames.FirstOrDefault(fi => fi.Exists) + ?? filenames.FirstOrDefault(); } /// @@ -256,40 +259,36 @@ public bool IsMaybeCachedZip(Uri url, DateTime? remoteTimestamp = null) /// Output parameter set to number of bytes free public void GetSizeInfo(out int numFiles, out long numBytes, out long bytesFree) { - numFiles = 0; - numBytes = 0; - GetSizeInfo(cachePath, ref numFiles, ref numBytes); - bytesFree = new DirectoryInfo(cachePath)?.GetDrive()?.AvailableFreeSpace ?? 0; - foreach (var legacyDir in legacyDirs()) - { - GetSizeInfo(legacyDir, ref numFiles, ref numBytes); - } + bytesFree = cachePath.GetDrive().AvailableFreeSpace; + (numFiles, numBytes) = Enumerable.Repeat(cachePath, 1) + .Concat(legacyDirs()) + .Select(GetDirSizeInfo) + .Aggregate((numFiles: 0, + numBytes: 0L), + (total, next) => (numFiles: total.numFiles + next.numFiles, + numBytes: total.numBytes + next.numBytes)); } - private static void GetSizeInfo(string path, ref int numFiles, ref long numBytes) - { - DirectoryInfo cacheDir = new DirectoryInfo(path); - foreach (var file in cacheDir.EnumerateFiles("*", SearchOption.AllDirectories)) - { - ++numFiles; - numBytes += file.Length; - } - } + private static (int numFiles, long numBytes) GetDirSizeInfo(DirectoryInfo cacheDir) + => cacheDir.EnumerateFiles("*", SearchOption.AllDirectories) + .Aggregate((numFiles: 0, + numBytes: 0L), + (tuple, fi) => (numFiles: tuple.numFiles + 1, + numBytes: tuple.numBytes + fi.Length)); public void CheckFreeSpace(long bytesToStore) { - CKANPathUtils.CheckFreeSpace(new DirectoryInfo(cachePath), + CKANPathUtils.CheckFreeSpace(cachePath, bytesToStore, Properties.Resources.NotEnoughSpaceToCache); } - private HashSet legacyDirs() + private IEnumerable legacyDirs() => manager?.Instances.Values - .Where(ksp => ksp.Valid) - .Select(ksp => ksp.DownloadCacheDir()) - .Where(Directory.Exists) - .ToHashSet() - ?? new HashSet(); + .Where(ksp => ksp.Valid) + .Select(ksp => new DirectoryInfo(ksp.DownloadCacheDir())) + .Where(dir => dir.Exists) + ?? Enumerable.Empty(); public void EnforceSizeLimit(long bytes, Registry registry) { @@ -379,14 +378,12 @@ private static int compareFiles(IReadOnlyDictionary> ha private List allFiles(bool includeInProgress = false) { - DirectoryInfo mainDir = new DirectoryInfo(cachePath); - var files = mainDir.EnumerateFiles("*", - includeInProgress ? SearchOption.AllDirectories - : SearchOption.TopDirectoryOnly); - foreach (string legacyDir in legacyDirs()) + var files = cachePath.EnumerateFiles("*", + includeInProgress ? SearchOption.AllDirectories + : SearchOption.TopDirectoryOnly); + foreach (var legacyDir in legacyDirs()) { - DirectoryInfo legDir = new DirectoryInfo(legacyDir); - files = files.Union(legDir.EnumerateFiles()); + files = files.Union(legacyDir.EnumerateFiles()); } return files.Where(fi => // Require 8 digit hex prefix followed by dash; any else was not put there by CKAN @@ -427,7 +424,7 @@ public string Store(Uri url, $"description {description} isn't as filesystem safe as we thought... (#1266)"); string fullName = string.Format("{0}-{1}", hash, Path.GetFileName(description)); - string targetPath = Path.Combine(cachePath, fullName); + string targetPath = Path.Combine(cachePath.FullName, fullName); // Purge hashes associated with the new file PurgeHashes(tx_file, targetPath); @@ -459,7 +456,8 @@ public string Store(Uri url, /// public bool Remove(Uri url) { - if (GetCachedFilename(url) is string file) + if (GetCachedFilename(url) is string file + && File.Exists(file)) { TxFileManager tx_file = new TxFileManager(); tx_file.Delete(file); @@ -471,6 +469,12 @@ public bool Remove(Uri url) return false; } + public bool Remove(IEnumerable urls) + => urls.Select(Remove) + // Force all elements to be evaluated + .ToArray() + .Any(found => found); + private void PurgeHashes(TxFileManager? tx_file, string file) { try @@ -493,15 +497,15 @@ private void PurgeHashes(TxFileManager? tx_file, string file) public void RemoveAll() { var dirs = Enumerable.Repeat(cachePath, 1) - .Concat(Enumerable.Repeat(InProgressPath, 1)) + .Concat(Enumerable.Repeat(inProgressPath, 1)) .Concat(legacyDirs()); - foreach (string dir in dirs) + foreach (var dir in dirs) { - foreach (string file in Directory.EnumerateFiles(dir)) + foreach (var file in dir.EnumerateFiles()) { try { - File.Delete(file); + file.Delete(); } catch { } } @@ -516,17 +520,26 @@ public void RemoveAll() /// May throw an IOException if disk is full! /// /// Path from which to move files - public void MoveFrom(string fromDir) + public void MoveFrom(DirectoryInfo fromDir, + IProgress percentProgress) { - if (cachePath != fromDir && Directory.Exists(fromDir)) + if (fromDir.Exists && !cachePath.PathEquals(fromDir)) { + var files = fromDir.GetFiles("*", SearchOption.AllDirectories); + var bytesProgress = new ProgressScalePercentsByFileSizes( + percentProgress, + files.Select(f => f.Length)); bool hasAny = false; - foreach (string fromFile in Directory.EnumerateFiles(fromDir)) + foreach (var fromFile in files) { - string toFile = Path.Combine(cachePath, Path.GetFileName(fromFile)); + bytesProgress.Report(0); + + var toFile = Path.Combine(cachePath.FullName, + CKANPathUtils.ToRelative(fromFile.FullName, + fromDir.FullName)); if (File.Exists(toFile)) { - if (File.GetCreationTime(fromFile) == File.GetCreationTime(toFile)) + if (fromFile.CreationTimeUtc == File.GetCreationTimeUtc(toFile)) { // Same filename with same timestamp, almost certainly the same // actual file on disk via different paths thanks to symlinks. @@ -536,14 +549,20 @@ public void MoveFrom(string fromDir) else { // Don't need multiple copies of the same file - File.Delete(fromFile); + fromFile.Delete(); } } else { - File.Move(fromFile, toFile); + if (Path.GetDirectoryName(toFile) is string parent) + { + Directory.CreateDirectory(parent); + } + fromFile.MoveTo(toFile); hasAny = true; } + bytesProgress.Report(100); + bytesProgress.NextFile(); } if (hasAny) { @@ -631,7 +650,7 @@ private string GetFileHash(string filePath, { hash = BitConverter.ToString(hasher.ComputeHash(bs, progress, cancelToken)).Replace("-", ""); cache.Add(filePath, hash); - if (Path.GetDirectoryName(hashFile) == Path.GetFullPath(cachePath)) + if (Path.GetDirectoryName(hashFile) == cachePath.FullName) { File.WriteAllText(hashFile, hash); } diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs index 1cbfc4e953..ee0d03be42 100644 --- a/Core/Net/NetModuleCache.cs +++ b/Core/Net/NetModuleCache.cs @@ -2,6 +2,7 @@ using System.Linq; using System.IO; using System.Threading; +using System.Collections.Generic; using ICSharpCode.SharpZipLib.Zip; @@ -49,15 +50,16 @@ public NetModuleCache(string path) public void Dispose() { cache.Dispose(); + GC.SuppressFinalize(this); } public void RemoveAll() { cache.RemoveAll(); ModPurged?.Invoke(null); } - public void MoveFrom(string fromDir) + public void MoveFrom(DirectoryInfo fromDir, IProgress progress) { - cache.MoveFrom(fromDir); + cache.MoveFrom(fromDir, progress); } public bool IsCached(CkanModule m) => m.download?.Any(cache.IsCached) @@ -96,7 +98,7 @@ public void CheckFreeSpace(long bytesToStore) cache.CheckFreeSpace(bytesToStore); } - public string? GetInProgressFileName(CkanModule m) + public FileInfo? GetInProgressFileName(CkanModule m) => m.download == null ? null : cache.GetInProgressFileName(m.download, m.StandardName()); @@ -117,9 +119,7 @@ public string DescribeAvailability(CkanModule m) ? string.Format(Properties.Resources.NetModuleCacheMetapackage, m.name, m.version) : IsMaybeCachedZip(m) ? string.Format(Properties.Resources.NetModuleCacheModuleCached, m.name, m.version) - : DescribeUncachedAvailability(m, - GetInProgressFileName(m) is string s - ? new FileInfo(s) : null); + : DescribeUncachedAvailability(m, GetInProgressFileName(m)); /// /// Calculate the SHA1 hash of a file @@ -340,18 +340,25 @@ public static bool ZipValid(string filename, /// public bool Purge(CkanModule module) { - if (module.download != null) + if (module.download != null + && cache.Remove(module.download)) { - foreach (var dlUri in module.download) - { - if (!cache.Remove(dlUri)) - { - return false; - } - } + ModPurged?.Invoke(module); + return true; } - ModPurged?.Invoke(module); - return true; + return false; + } + + public bool Purge(ICollection modules) + { + if (modules.Select(m => cache.Remove(m.download ?? Enumerable.Empty())) + .ToArray() + .Any(removed => removed)) + { + ModPurged?.Invoke(modules.First()); + return true; + } + return false; } private readonly NetFileCache cache; diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index cb1c435912..2f574c3c60 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -205,6 +205,14 @@ Install the `mono-complete` package or equivalent for your operating system.portable Auto {0} Please select the game that is installed at {0} + Old cache folder contains {0}. New cache folder has {1} free. +What would you like to do? + Move old cached files to new cache folder + Delete cached files + Open old and new cache folders in file browsers + Do nothing + Revert back to using the old cache folder + Not enough space to move files! The specified instance is not a valid {0} instance The specified {0} version is not a known version: {1} The specified folder already exists and is not empty diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 9accd3eb49..5d516557f4 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -1308,14 +1308,15 @@ private void purgeContentsToolStripMenuItem_Click(object? sender, EventArgs? e) // Purge other versions as well since the user is likely to want that // and has no other way to achieve it var selected = SelectedModule; - if (selected != null && currentInstance != null) + if (selected != null + && currentInstance != null + && manager?.Cache != null) { - IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; - var allAvail = registry.AvailableByIdentifier(selected.Identifier); - foreach (CkanModule mod in allAvail) - { - manager?.Cache?.Purge(mod); - } + manager.Cache.Purge( + RegistryManager.Instance(currentInstance, repoData) + .registry + .AvailableByIdentifier(selected.Identifier) + .ToArray()); } } diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index c1fe525e89..0b0f931b58 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -53,6 +53,11 @@ public void RefreshModContentsTree() Contents.RefreshModContentsTree(); } + public void SwitchTab(string name) + { + ModInfoTabControl.SelectedTab = ModInfoTabControl.TabPages[name]; + } + public event Action? OnDownloadClick; public event Action? OnChangeFilter; public event Action? ModuleDoubleClicked; diff --git a/GUI/Dialogs/SelectionDialog.Designer.cs b/GUI/Dialogs/SelectionDialog.Designer.cs index bb94fae490..2fbea479e6 100644 --- a/GUI/Dialogs/SelectionDialog.Designer.cs +++ b/GUI/Dialogs/SelectionDialog.Designer.cs @@ -34,50 +34,57 @@ private void InitializeComponent() System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(SelectionDialog)); this.panel1 = new System.Windows.Forms.Panel(); this.MessageLabel = new System.Windows.Forms.Label(); - this.SelectButton = new System.Windows.Forms.Button(); - this.DefaultButton = new System.Windows.Forms.Button(); - this.CancelButton = new System.Windows.Forms.Button(); this.OptionsList = new System.Windows.Forms.ListBox(); + this.CancelSelectionButton = new System.Windows.Forms.Button(); + this.DefaultButton = new System.Windows.Forms.Button(); + this.SelectButton = new System.Windows.Forms.Button(); this.panel1.SuspendLayout(); this.SuspendLayout(); // - // panel1 - // - this.panel1.Controls.Add(this.MessageLabel); - this.panel1.Controls.Add(this.OptionsList); - this.panel1.Controls.Add(this.CancelButton); - this.panel1.Controls.Add(this.DefaultButton); - this.panel1.Controls.Add(this.SelectButton); - this.panel1.Location = new System.Drawing.Point(10, 10); - this.panel1.Size = new System.Drawing.Size(400, 400); - this.panel1.Name = "panel1"; - this.OptionsList.TabStop = false; - this.DefaultButton.UseVisualStyleBackColor = true; - // // MessageLabel // + this.MessageLabel.Dock = System.Windows.Forms.DockStyle.Top; this.MessageLabel.Location = new System.Drawing.Point(5, 5); this.MessageLabel.Size = new System.Drawing.Size(390, 40); this.MessageLabel.Name = "MessageLabel"; - this.OptionsList.TabStop = false; + this.MessageLabel.TabStop = false; this.MessageLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.DefaultButton.UseVisualStyleBackColor = true; resources.ApplyResources(this.MessageLabel, "MessageLabel"); // + // panel1 + // + this.panel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.panel1.Controls.Add(this.OptionsList); + this.panel1.Controls.Add(this.CancelSelectionButton); + this.panel1.Controls.Add(this.DefaultButton); + this.panel1.Controls.Add(this.SelectButton); + this.panel1.Margin = new System.Windows.Forms.Padding(10); + this.panel1.Padding = new System.Windows.Forms.Padding(10); + this.panel1.Location = new System.Drawing.Point(10, 10); + this.panel1.Size = new System.Drawing.Size(400, 300); + this.panel1.Name = "panel1"; + this.panel1.TabStop = false; + // // OptionsList // - this.OptionsList.Location = new System.Drawing.Point(5, 55); - this.OptionsList.Size = new System.Drawing.Size(390, 315); + this.OptionsList.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.OptionsList.Location = new System.Drawing.Point(5, 5); + this.OptionsList.Size = new System.Drawing.Size(390, 265); this.OptionsList.SelectionMode = System.Windows.Forms.SelectionMode.One; this.OptionsList.MultiColumn = false; - this.OptionsList.SelectedIndexChanged += new System.EventHandler(OptionsList_SelectedIndexChanged); this.OptionsList.Name = "OptionsList"; - this.DefaultButton.UseVisualStyleBackColor = true; + this.OptionsList.SelectedIndexChanged += new System.EventHandler(OptionsList_SelectedIndexChanged); + this.OptionsList.DoubleClick += new System.EventHandler(this.OptionsList_DoubleClick); + this.OptionsList.KeyDown += new System.Windows.Forms.KeyEventHandler(this.OptionsList_KeyDown); + resources.ApplyResources(this.OptionsList, "OptionsList"); // // SelectButton // + this.SelectButton.AutoSize = true; + this.SelectButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.SelectButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; this.SelectButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.SelectButton.Location = new System.Drawing.Point(325, 375); + this.SelectButton.Location = new System.Drawing.Point(325, 275); this.SelectButton.Size = new System.Drawing.Size(60, 20); this.SelectButton.DialogResult = System.Windows.Forms.DialogResult.OK; this.SelectButton.Name = "SelectButton"; @@ -87,8 +94,11 @@ private void InitializeComponent() // // DefaultButton // + this.DefaultButton.AutoSize = true; + this.DefaultButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.DefaultButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom; this.DefaultButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.DefaultButton.Location = new System.Drawing.Point(160, 375); + this.DefaultButton.Location = new System.Drawing.Point(175, 275); this.DefaultButton.Size = new System.Drawing.Size(60, 20); this.DefaultButton.DialogResult = System.Windows.Forms.DialogResult.Yes; this.DefaultButton.Name = "SelectButton"; @@ -96,27 +106,35 @@ private void InitializeComponent() this.DefaultButton.UseVisualStyleBackColor = true; resources.ApplyResources(this.DefaultButton, "DefaultButton"); // - // CancelButton + // CancelSelectionButton // - this.CancelButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.CancelButton.Location = new System.Drawing.Point(5, 375); - this.CancelButton.Size = new System.Drawing.Size(60, 20); - this.CancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.CancelButton.Name = "CancelButton"; - this.CancelButton.TabIndex = 2; - this.CancelButton.UseVisualStyleBackColor = true; - resources.ApplyResources(this.CancelButton, "CancelButton"); + this.CancelSelectionButton.AutoSize = true; + this.CancelSelectionButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.CancelSelectionButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + this.CancelSelectionButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CancelSelectionButton.Location = new System.Drawing.Point(5, 275); + this.CancelSelectionButton.Size = new System.Drawing.Size(60, 20); + this.CancelSelectionButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelSelectionButton.Name = "CancelSelectionButton"; + this.CancelSelectionButton.TabIndex = 2; + this.CancelSelectionButton.UseVisualStyleBackColor = true; + resources.ApplyResources(this.CancelSelectionButton, "CancelSelectionButton"); // // SelectionDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(420, 420); + this.MinimumSize = new System.Drawing.Size(300, 200); + this.AcceptButton = this.SelectButton; + this.CancelButton = this.CancelSelectionButton; + this.ControlBox = false; this.Controls.Add(this.panel1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Controls.Add(this.MessageLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.Icon = EmbeddedImages.AppIcon; this.Name = "SelectionDialog"; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; resources.ApplyResources(this, "$this"); this.panel1.ResumeLayout(false); this.ResumeLayout(false); @@ -125,11 +143,11 @@ private void InitializeComponent() #endregion - private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Label MessageLabel; - private System.Windows.Forms.Button SelectButton; - private System.Windows.Forms.Button DefaultButton; - private new System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.Panel panel1; private System.Windows.Forms.ListBox OptionsList; + private System.Windows.Forms.Button CancelSelectionButton; + private System.Windows.Forms.Button DefaultButton; + private System.Windows.Forms.Button SelectButton; } } diff --git a/GUI/Dialogs/SelectionDialog.cs b/GUI/Dialogs/SelectionDialog.cs index 8e24487215..8323414453 100644 --- a/GUI/Dialogs/SelectionDialog.cs +++ b/GUI/Dialogs/SelectionDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Windows.Forms; #if NET5_0_OR_GREATER using System.Runtime.Versioning; @@ -13,121 +14,148 @@ namespace CKAN.GUI #endif public partial class SelectionDialog : Form { - private int currentSelected; - - public SelectionDialog () + public SelectionDialog() { InitializeComponent(); currentSelected = 0; } /// - /// Shows the selection dialog. + /// Shows the selection dialog /// - /// The selected index, -1 if canceled. + /// The selected index, -1 if canceled /// Message. - /// Array of items to select from. + /// Array of items to select from. If first is an int, it will be interpreted as the index of the default option. [ForbidGUICalls] - public int ShowSelectionDialog (string message, params object[] args) + public int ShowSelectionDialog(string message, params object[] args) { int defaultSelection = -1; - int return_cancel = -1; + int return_cancel = -1; - // Validate input. + // Validate input if (string.IsNullOrWhiteSpace(message)) { throw new Kraken("Passed message string must be non-empty."); } - if (args.Length == 0) - { - throw new Kraken("Passed list of selection candidates must be non-empty."); - } - - // Hide the default button unless we have a default option - Util.Invoke(DefaultButton, DefaultButton.Hide); - // Clear the item list. - Util.Invoke(OptionsList, OptionsList.Items.Clear); - - // Check if we have a default option. + // Check if we have a default option if (//args is [int v, ..] args.Length > 0 && args[0] is int v) { - // Check that the default selection makes sense. + // Check that the default selection makes sense defaultSelection = v; if (defaultSelection < 0 || defaultSelection > args.Length - 1) { - throw new Kraken("Passed default arguments is out of range of the selection candidates."); + throw new Kraken("Passed default argument is out of range of the selection candidates."); } // Extract the relevant arguments. - object[] newArgs = new object[args.Length - 1]; - - for (int i = 1; i < args.Length; i++) - { - newArgs[i - 1] = args[i]; - } - - args = newArgs; + args = args.Skip(1).ToArray(); - // Show the defaultButton. - Util.Invoke(DefaultButton, DefaultButton.Show); + // Show the default button + Util.Invoke(this, DefaultButton.Show); + } + else + { + // Hide the default button unless we have a default option + Util.Invoke(this, DefaultButton.Hide); } - // Further data validation. - foreach (object argument in args) + if (args.Length == 0) { - if (string.IsNullOrWhiteSpace(argument.ToString())) - { - throw new Kraken("Candidate may not be empty."); - } + throw new Kraken("Passed list of selection candidates must be non-empty."); } - // Add all items to the OptionsList. - for (int i = 0; i < args.Length; i++) + // Further data validation + var argStrs = args.Select(arg => arg.ToString()).ToArray(); + if (argStrs.Any(string.IsNullOrWhiteSpace)) { - if (defaultSelection == i) - { - Util.Invoke(OptionsList, () => OptionsList.Items.Add(string.Concat(args[i].ToString(), " -- Default"))); + throw new Kraken("Candidate may not be empty."); + } - } - else + DialogResult result = DialogResult.Cancel; + + // Validation completed, set up the UI + Util.Invoke(this, () => + { + // Write the message to the label + MessageLabel.Text = message; + + // Clear the item list. + OptionsList.Items.Clear(); + + // Add all items to the OptionsList + OptionsList.Items.AddRange( + argStrs.Select((arg, i) => + defaultSelection == i + ? string.Format(Properties.Resources.SelectionDialogDefault, + arg) + : arg) + .ToArray()); + + if (defaultSelection >= 0) { - Util.Invoke(OptionsList, () => OptionsList.Items.Add(args[i].ToString() ?? "")); + OptionsList.SetSelected(defaultSelection, true); } - } - // Write the message to the label. - Util.Invoke(MessageLabel, () => MessageLabel.Text = message); + Height = Height - OptionsList.Height + + (OptionsList.ItemHeight * (OptionsList.Items.Count + 2)); - // Now show the dialog and get the return values. - DialogResult result = ShowDialog(); - if (result == DialogResult.Yes) - { - // If pressed Defaultbutton - return defaultSelection; - } - else if (result == DialogResult.Cancel) - { - // If pressed CancelButton - return return_cancel; - } - else + result = ShowDialog(ActiveForm); + }); + + // Show the dialog and get the return values + return result switch { - return currentSelected; - } + // Lots of dialog results we don't care about + DialogResult.Abort or DialogResult.Retry or DialogResult.Ignore + or DialogResult.No or DialogResult.None + => throw new NotImplementedException(), + + // If pressed Default button + DialogResult.Yes => defaultSelection, + + // If pressed Cancel button + DialogResult.Cancel => return_cancel, + + // If pressed Select button or double clicked + DialogResult.OK or _ => currentSelected, + }; } - public void HideYesNoDialog () + private void OptionsList_SelectedIndexChanged(object? sender, EventArgs? e) { - Util.Invoke(this, Close); + currentSelected = OptionsList.SelectedIndex; } - private void OptionsList_SelectedIndexChanged(object? sender, EventArgs? e) + private void OptionsList_DoubleClick(object? sender, EventArgs? e) { currentSelected = OptionsList.SelectedIndex; + DialogResult = SelectButton.DialogResult; + Close(); } + + private void OptionsList_KeyDown(object? sender, KeyEventArgs? e) + { + switch (e?.KeyCode) + { + case Keys.Enter: + e.Handled = true; + currentSelected = OptionsList.SelectedIndex; + DialogResult = SelectButton.DialogResult; + Close(); + break; + + case Keys.Escape: + e.Handled = true; + DialogResult = CancelSelectionButton.DialogResult; + Close(); + break; + } + } + + private int currentSelected; } } diff --git a/GUI/Dialogs/SelectionDialog.resx b/GUI/Dialogs/SelectionDialog.resx index 627fdffaf8..bbbaf20e53 100644 --- a/GUI/Dialogs/SelectionDialog.resx +++ b/GUI/Dialogs/SelectionDialog.resx @@ -120,6 +120,6 @@ Please select: Select Default - Cancel + Cancel CKAN Selection Dialog diff --git a/GUI/Dialogs/SettingsDialog.Designer.cs b/GUI/Dialogs/SettingsDialog.Designer.cs index 44bfe16edd..756e5089aa 100644 --- a/GUI/Dialogs/SettingsDialog.Designer.cs +++ b/GUI/Dialogs/SettingsDialog.Designer.cs @@ -30,8 +30,9 @@ private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(SettingsDialog)); + this.ToolTip = new System.Windows.Forms.ToolTip(this.components); this.RepositoryGroupBox = new System.Windows.Forms.GroupBox(); - this.ReposListBox = new ThemedListView(); + this.ReposListBox = new CKAN.GUI.ThemedListView(); this.RepoNameHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.RepoURLHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.NewRepoButton = new System.Windows.Forms.Button(); @@ -39,13 +40,16 @@ private void InitializeComponent() this.DownRepoButton = new System.Windows.Forms.Button(); this.DeleteRepoButton = new System.Windows.Forms.Button(); this.AuthTokensGroupBox = new System.Windows.Forms.GroupBox(); - this.AuthTokensListBox = new ThemedListView(); + this.AuthTokensListBox = new CKAN.GUI.ThemedListView(); this.AuthHostHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.AuthTokenHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.NewAuthTokenButton = new System.Windows.Forms.Button(); this.DeleteAuthTokenButton = new System.Windows.Forms.Button(); this.CacheGroupBox = new System.Windows.Forms.GroupBox(); - this.CachePath = new System.Windows.Forms.TextBox(); + this.CachePathTextBox = new System.Windows.Forms.TextBox(); + this.CachePathEditButton = new System.Windows.Forms.Button(); + this.CachePathSaveButton = new System.Windows.Forms.Button(); + this.CachePathCancelButton = new System.Windows.Forms.Button(); this.CacheSummary = new System.Windows.Forms.Label(); this.CacheLimitPreLabel = new System.Windows.Forms.Label(); this.CacheLimit = new System.Windows.Forms.TextBox(); @@ -57,6 +61,7 @@ private void InitializeComponent() this.PurgeAllMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ResetCacheButton = new System.Windows.Forms.Button(); this.OpenCacheButton = new System.Windows.Forms.Button(); + this.MoveCacheProgressBar = new System.Windows.Forms.ProgressBar(); this.AutoUpdateGroupBox = new System.Windows.Forms.GroupBox(); this.LocalVersionPreLabel = new System.Windows.Forms.Label(); this.LocalVersionLabel = new System.Windows.Forms.Label(); @@ -80,7 +85,6 @@ private void InitializeComponent() this.HideEpochsCheckbox = new System.Windows.Forms.CheckBox(); this.HideVCheckbox = new System.Windows.Forms.CheckBox(); this.AutoSortUpdateCheckBox = new System.Windows.Forms.CheckBox(); - this.ToolTip = new System.Windows.Forms.ToolTip(this.components); this.RepositoryGroupBox.SuspendLayout(); this.AuthTokensGroupBox.SuspendLayout(); this.CacheGroupBox.SuspendLayout(); @@ -341,7 +345,10 @@ private void InitializeComponent() // // CacheGroupBox // - this.CacheGroupBox.Controls.Add(this.CachePath); + this.CacheGroupBox.Controls.Add(this.CachePathTextBox); + this.CacheGroupBox.Controls.Add(this.CachePathEditButton); + this.CacheGroupBox.Controls.Add(this.CachePathSaveButton); + this.CacheGroupBox.Controls.Add(this.CachePathCancelButton); this.CacheGroupBox.Controls.Add(this.CacheSummary); this.CacheGroupBox.Controls.Add(this.CacheLimitPreLabel); this.CacheGroupBox.Controls.Add(this.CacheLimit); @@ -350,6 +357,7 @@ private void InitializeComponent() this.CacheGroupBox.Controls.Add(this.ClearCacheButton); this.CacheGroupBox.Controls.Add(this.ResetCacheButton); this.CacheGroupBox.Controls.Add(this.OpenCacheButton); + this.CacheGroupBox.Controls.Add(this.MoveCacheProgressBar); this.CacheGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.CacheGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CacheGroupBox.Location = new System.Drawing.Point(280, 144); @@ -359,17 +367,59 @@ private void InitializeComponent() this.CacheGroupBox.TabStop = false; resources.ApplyResources(this.CacheGroupBox, "CacheGroupBox"); // - // CachePath - // - this.CachePath.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; - this.CachePath.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource; - this.CachePath.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.CachePath.Location = new System.Drawing.Point(12, 18); - this.CachePath.Margin = new System.Windows.Forms.Padding(2); - this.CachePath.Name = "CachePath"; - this.CachePath.Size = new System.Drawing.Size(452, 20); - this.CachePath.TabIndex = 20; - this.CachePath.TextChanged += new System.EventHandler(this.CachePath_TextChanged); + // CachePathTextBox + // + this.CachePathTextBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; + this.CachePathTextBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource; + this.CachePathTextBox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.CachePathTextBox.Enabled = false; + this.CachePathTextBox.Location = new System.Drawing.Point(12, 18); + this.CachePathTextBox.Name = "CachePathTextBox"; + this.CachePathTextBox.Size = new System.Drawing.Size(402, 20); + this.CachePathTextBox.TabIndex = 20; + this.CachePathTextBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.CachePathTextBox_KeyDown); + // + // CachePathEditButton + // + this.CachePathEditButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.CachePathEditButton.AutoSize = true; + this.CachePathEditButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.CachePathEditButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CachePathEditButton.Location = new System.Drawing.Point(414, 18); + this.CachePathEditButton.Padding = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.CachePathEditButton.Name = "CachePathEditButton"; + this.CachePathEditButton.Size = new System.Drawing.Size(50, 12); + this.CachePathEditButton.TabIndex = 21; + this.CachePathEditButton.Click += new System.EventHandler(this.CachePathEditButton_Click); + resources.ApplyResources(this.CachePathEditButton, "CachePathEditButton"); + // + // CachePathSaveButton + // + this.CachePathSaveButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.CachePathSaveButton.AutoSize = true; + this.CachePathSaveButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.CachePathSaveButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CachePathSaveButton.Location = new System.Drawing.Point(414, 18); + this.CachePathSaveButton.Name = "CachePathSaveButton"; + this.CachePathSaveButton.Padding = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.CachePathSaveButton.Size = new System.Drawing.Size(50, 12); + this.CachePathSaveButton.TabIndex = 22; + this.CachePathSaveButton.Click += new System.EventHandler(this.CachePathSaveButton_Click); + resources.ApplyResources(this.CachePathSaveButton, "CachePathSaveButton"); + // + // CachePathCancelButton + // + this.CachePathCancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.CachePathCancelButton.AutoSize = true; + this.CachePathCancelButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.CachePathCancelButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CachePathCancelButton.Location = new System.Drawing.Point(364, 18); + this.CachePathCancelButton.Name = "CachePathCancelButton"; + this.CachePathCancelButton.Padding = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.CachePathCancelButton.Size = new System.Drawing.Size(50, 12); + this.CachePathCancelButton.TabIndex = 23; + this.CachePathCancelButton.Click += new System.EventHandler(this.CachePathCancelButton_Click); + resources.ApplyResources(this.CachePathCancelButton, "CachePathCancelButton"); // // CacheSummary // @@ -377,7 +427,7 @@ private void InitializeComponent() this.CacheSummary.Location = new System.Drawing.Point(9, 44); this.CacheSummary.Name = "CacheSummary"; this.CacheSummary.Size = new System.Drawing.Size(70, 13); - this.CacheSummary.TabIndex = 21; + this.CacheSummary.TabIndex = 24; resources.ApplyResources(this.CacheSummary, "CacheSummary"); // // CacheLimitPreLabel @@ -386,7 +436,7 @@ private void InitializeComponent() this.CacheLimitPreLabel.Location = new System.Drawing.Point(9, 65); this.CacheLimitPreLabel.Name = "CacheLimitPreLabel"; this.CacheLimitPreLabel.Size = new System.Drawing.Size(108, 13); - this.CacheLimitPreLabel.TabIndex = 22; + this.CacheLimitPreLabel.TabIndex = 25; resources.ApplyResources(this.CacheLimitPreLabel, "CacheLimitPreLabel"); // // CacheLimit @@ -398,7 +448,7 @@ private void InitializeComponent() this.CacheLimit.Margin = new System.Windows.Forms.Padding(2); this.CacheLimit.Name = "CacheLimit"; this.CacheLimit.Size = new System.Drawing.Size(50, 20); - this.CacheLimit.TabIndex = 23; + this.CacheLimit.TabIndex = 26; this.CacheLimit.TextChanged += new System.EventHandler(this.CacheLimit_TextChanged); this.CacheLimit.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.CacheLimit_KeyPress); // @@ -408,7 +458,7 @@ private void InitializeComponent() this.CacheLimitPostLabel.Location = new System.Drawing.Point(167, 65); this.CacheLimitPostLabel.Name = "CacheLimitPostLabel"; this.CacheLimitPostLabel.Size = new System.Drawing.Size(119, 13); - this.CacheLimitPostLabel.TabIndex = 24; + this.CacheLimitPostLabel.TabIndex = 27; resources.ApplyResources(this.CacheLimitPostLabel, "CacheLimitPostLabel"); // // ChangeCacheButton @@ -417,7 +467,7 @@ private void InitializeComponent() this.ChangeCacheButton.Location = new System.Drawing.Point(12, 89); this.ChangeCacheButton.Name = "ChangeCacheButton"; this.ChangeCacheButton.Size = new System.Drawing.Size(75, 25); - this.ChangeCacheButton.TabIndex = 25; + this.ChangeCacheButton.TabIndex = 28; this.ChangeCacheButton.Click += new System.EventHandler(this.ChangeCacheButton_Click); resources.ApplyResources(this.ChangeCacheButton, "ChangeCacheButton"); // @@ -428,7 +478,7 @@ private void InitializeComponent() this.ClearCacheButton.Menu = this.ClearCacheMenu; this.ClearCacheButton.Name = "ClearCacheButton"; this.ClearCacheButton.Size = new System.Drawing.Size(75, 25); - this.ClearCacheButton.TabIndex = 26; + this.ClearCacheButton.TabIndex = 29; resources.ApplyResources(this.ClearCacheButton, "ClearCacheButton"); // // ClearCacheMenu @@ -459,7 +509,7 @@ private void InitializeComponent() this.ResetCacheButton.Location = new System.Drawing.Point(174, 89); this.ResetCacheButton.Name = "ResetCacheButton"; this.ResetCacheButton.Size = new System.Drawing.Size(75, 25); - this.ResetCacheButton.TabIndex = 27; + this.ResetCacheButton.TabIndex = 30; this.ResetCacheButton.Click += new System.EventHandler(this.ResetCacheButton_Click); resources.ApplyResources(this.ResetCacheButton, "ResetCacheButton"); // @@ -469,10 +519,21 @@ private void InitializeComponent() this.OpenCacheButton.Location = new System.Drawing.Point(255, 89); this.OpenCacheButton.Name = "OpenCacheButton"; this.OpenCacheButton.Size = new System.Drawing.Size(75, 25); - this.OpenCacheButton.TabIndex = 28; + this.OpenCacheButton.TabIndex = 31; this.OpenCacheButton.Click += new System.EventHandler(this.OpenCacheButton_Click); resources.ApplyResources(this.OpenCacheButton, "OpenCacheButton"); // + // MoveCacheProgressBar + // + this.MoveCacheProgressBar.Location = new System.Drawing.Point(12, 123); + this.MoveCacheProgressBar.Name = "MoveCacheProgressBar"; + this.MoveCacheProgressBar.Size = new System.Drawing.Size(452, 20); + this.MoveCacheProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.MoveCacheProgressBar.TabStop = false; + this.MoveCacheProgressBar.Minimum = 0; + this.MoveCacheProgressBar.Maximum = 100; + this.MoveCacheProgressBar.Visible = false; + // // BehaviourGroupBox // this.BehaviourGroupBox.Controls.Add(this.EnableTrayIconCheckBox); @@ -486,7 +547,7 @@ private void InitializeComponent() this.BehaviourGroupBox.Location = new System.Drawing.Point(12, 310); this.BehaviourGroupBox.Name = "BehaviourGroupBox"; this.BehaviourGroupBox.Size = new System.Drawing.Size(254, 150); - this.BehaviourGroupBox.TabIndex = 29; + this.BehaviourGroupBox.TabIndex = 32; this.BehaviourGroupBox.TabStop = false; resources.ApplyResources(this.BehaviourGroupBox, "BehaviourGroupBox"); // @@ -497,7 +558,7 @@ private void InitializeComponent() this.EnableTrayIconCheckBox.Location = new System.Drawing.Point(12, 18); this.EnableTrayIconCheckBox.Name = "EnableTrayIconCheckBox"; this.EnableTrayIconCheckBox.Size = new System.Drawing.Size(102, 17); - this.EnableTrayIconCheckBox.TabIndex = 30; + this.EnableTrayIconCheckBox.TabIndex = 33; this.EnableTrayIconCheckBox.CheckedChanged += new System.EventHandler(this.EnableTrayIconCheckBox_CheckedChanged); resources.ApplyResources(this.EnableTrayIconCheckBox, "EnableTrayIconCheckBox"); // @@ -508,7 +569,7 @@ private void InitializeComponent() this.MinimizeToTrayCheckBox.Location = new System.Drawing.Point(12, 41); this.MinimizeToTrayCheckBox.Name = "MinimizeToTrayCheckBox"; this.MinimizeToTrayCheckBox.Size = new System.Drawing.Size(98, 17); - this.MinimizeToTrayCheckBox.TabIndex = 31; + this.MinimizeToTrayCheckBox.TabIndex = 34; this.MinimizeToTrayCheckBox.CheckedChanged += new System.EventHandler(this.MinimizeToTrayCheckBox_CheckedChanged); resources.ApplyResources(this.MinimizeToTrayCheckBox, "MinimizeToTrayCheckBox"); // @@ -518,7 +579,7 @@ private void InitializeComponent() this.RefreshPreLabel.Location = new System.Drawing.Point(9, 66); this.RefreshPreLabel.Name = "RefreshPreLabel"; this.RefreshPreLabel.Size = new System.Drawing.Size(114, 26); - this.RefreshPreLabel.TabIndex = 32; + this.RefreshPreLabel.TabIndex = 35; resources.ApplyResources(this.RefreshPreLabel, "RefreshPreLabel"); // // RefreshTextBox @@ -529,7 +590,7 @@ private void InitializeComponent() this.RefreshTextBox.Location = new System.Drawing.Point(125, 64); this.RefreshTextBox.Name = "RefreshTextBox"; this.RefreshTextBox.Size = new System.Drawing.Size(25, 20); - this.RefreshTextBox.TabIndex = 33; + this.RefreshTextBox.TabIndex = 36; this.RefreshTextBox.TextChanged += new System.EventHandler(this.RefreshTextBox_TextChanged); this.RefreshTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.RefreshTextBox_KeyPress); // @@ -539,7 +600,7 @@ private void InitializeComponent() this.RefreshPostLabel.Location = new System.Drawing.Point(153, 66); this.RefreshPostLabel.Name = "RefreshPostLabel"; this.RefreshPostLabel.Size = new System.Drawing.Size(49, 13); - this.RefreshPostLabel.TabIndex = 34; + this.RefreshPostLabel.TabIndex = 37; resources.ApplyResources(this.RefreshPostLabel, "RefreshPostLabel"); // // PauseRefreshCheckBox @@ -549,7 +610,7 @@ private void InitializeComponent() this.PauseRefreshCheckBox.Location = new System.Drawing.Point(12, 103); this.PauseRefreshCheckBox.Name = "PauseRefreshCheckBox"; this.PauseRefreshCheckBox.Size = new System.Drawing.Size(105, 17); - this.PauseRefreshCheckBox.TabIndex = 35; + this.PauseRefreshCheckBox.TabIndex = 38; this.PauseRefreshCheckBox.CheckedChanged += new System.EventHandler(this.PauseRefreshCheckBox_CheckedChanged); resources.ApplyResources(this.PauseRefreshCheckBox, "PauseRefreshCheckBox"); // @@ -566,7 +627,7 @@ private void InitializeComponent() this.MoreSettingsGroupBox.Location = new System.Drawing.Point(280, 310); this.MoreSettingsGroupBox.Name = "MoreSettingsGroupBox"; this.MoreSettingsGroupBox.Size = new System.Drawing.Size(476, 150); - this.MoreSettingsGroupBox.TabIndex = 36; + this.MoreSettingsGroupBox.TabIndex = 39; this.MoreSettingsGroupBox.TabStop = false; resources.ApplyResources(this.MoreSettingsGroupBox, "MoreSettingsGroupBox"); // @@ -585,7 +646,7 @@ private void InitializeComponent() this.LanguageSelectionComboBox.Location = new System.Drawing.Point(244, 18); this.LanguageSelectionComboBox.Name = "LanguageSelectionComboBox"; this.LanguageSelectionComboBox.Size = new System.Drawing.Size(220, 17); - this.LanguageSelectionComboBox.TabIndex = 37; + this.LanguageSelectionComboBox.TabIndex = 40; this.LanguageSelectionComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.LanguageSelectionComboBox.SelectionChangeCommitted += new System.EventHandler(this.LanguageSelectionComboBox_SelectionChanged); this.LanguageSelectionComboBox.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.LanguageSelectionComboBox_MouseWheel); @@ -597,7 +658,7 @@ private void InitializeComponent() this.RefreshOnStartupCheckbox.Location = new System.Drawing.Point(12, 41); this.RefreshOnStartupCheckbox.Name = "RefreshOnStartupCheckbox"; this.RefreshOnStartupCheckbox.Size = new System.Drawing.Size(167, 17); - this.RefreshOnStartupCheckbox.TabIndex = 38; + this.RefreshOnStartupCheckbox.TabIndex = 41; this.RefreshOnStartupCheckbox.CheckedChanged += new System.EventHandler(this.RefreshOnStartupCheckbox_CheckedChanged); resources.ApplyResources(this.RefreshOnStartupCheckbox, "RefreshOnStartupCheckbox"); // @@ -608,7 +669,7 @@ private void InitializeComponent() this.HideEpochsCheckbox.Location = new System.Drawing.Point(12, 64); this.HideEpochsCheckbox.Name = "HideEpochsCheckbox"; this.HideEpochsCheckbox.Size = new System.Drawing.Size(261, 17); - this.HideEpochsCheckbox.TabIndex = 39; + this.HideEpochsCheckbox.TabIndex = 42; this.HideEpochsCheckbox.CheckedChanged += new System.EventHandler(this.HideEpochsCheckbox_CheckedChanged); resources.ApplyResources(this.HideEpochsCheckbox, "HideEpochsCheckbox"); // @@ -619,7 +680,7 @@ private void InitializeComponent() this.HideVCheckbox.Location = new System.Drawing.Point(12, 87); this.HideVCheckbox.Name = "HideVCheckbox"; this.HideVCheckbox.Size = new System.Drawing.Size(204, 17); - this.HideVCheckbox.TabIndex = 40; + this.HideVCheckbox.TabIndex = 43; this.HideVCheckbox.CheckedChanged += new System.EventHandler(this.HideVCheckbox_CheckedChanged); resources.ApplyResources(this.HideVCheckbox, "HideVCheckbox"); // @@ -632,7 +693,7 @@ private void InitializeComponent() this.AutoSortUpdateCheckBox.Location = new System.Drawing.Point(12, 110); this.AutoSortUpdateCheckBox.Name = "AutoSortUpdateCheckBox"; this.AutoSortUpdateCheckBox.Size = new System.Drawing.Size(452, 32); - this.AutoSortUpdateCheckBox.TabIndex = 41; + this.AutoSortUpdateCheckBox.TabIndex = 44; this.AutoSortUpdateCheckBox.CheckedChanged += new System.EventHandler(this.AutoSortUpdateCheckBox_CheckedChanged); resources.ApplyResources(this.AutoSortUpdateCheckBox, "AutoSortUpdateCheckBox"); // @@ -672,6 +733,7 @@ private void InitializeComponent() #endregion + private System.Windows.Forms.ToolTip ToolTip; private System.Windows.Forms.GroupBox RepositoryGroupBox; private System.Windows.Forms.ListView ReposListBox; private System.Windows.Forms.ColumnHeader RepoNameHeader; @@ -687,7 +749,10 @@ private void InitializeComponent() private System.Windows.Forms.Button NewAuthTokenButton; private System.Windows.Forms.Button DeleteAuthTokenButton; private System.Windows.Forms.GroupBox CacheGroupBox; - private System.Windows.Forms.TextBox CachePath; + private System.Windows.Forms.TextBox CachePathTextBox; + private System.Windows.Forms.Button CachePathEditButton; + private System.Windows.Forms.Button CachePathSaveButton; + private System.Windows.Forms.Button CachePathCancelButton; private System.Windows.Forms.Label CacheSummary; private System.Windows.Forms.Label CacheLimitPreLabel; private System.Windows.Forms.TextBox CacheLimit; @@ -699,6 +764,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem PurgeAllMenuItem; private System.Windows.Forms.Button ResetCacheButton; private System.Windows.Forms.Button OpenCacheButton; + private System.Windows.Forms.ProgressBar MoveCacheProgressBar; private System.Windows.Forms.GroupBox AutoUpdateGroupBox; private System.Windows.Forms.Label LocalVersionPreLabel; private System.Windows.Forms.Label LocalVersionLabel; @@ -722,6 +788,5 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox HideEpochsCheckbox; private System.Windows.Forms.CheckBox HideVCheckbox; private System.Windows.Forms.CheckBox AutoSortUpdateCheckBox; - private System.Windows.Forms.ToolTip ToolTip; } } diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index 26c7f97f40..5588aadff0 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -19,21 +19,6 @@ namespace CKAN.GUI #endif public partial class SettingsDialog : Form { - private static readonly ILog log = LogManager.GetLogger(typeof(SettingsDialog)); - - public bool RepositoryAdded { get; private set; } = false; - public bool RepositoryRemoved { get; private set; } = false; - public bool RepositoryMoved { get; private set; } = false; - - private static GameInstanceManager? manager => Main.Instance?.Manager; - - private readonly IConfiguration coreConfig; - private readonly GUIConfiguration guiConfig; - private readonly RegistryManager regMgr; - private readonly AutoUpdate updater; - private readonly IUser user; - private readonly string? userAgent; - /// /// Initialize a settings window /// @@ -46,7 +31,11 @@ public SettingsDialog(IConfiguration coreConfig, { InitializeComponent(); - ToolTip.SetToolTip(RefreshTextBox, Properties.Resources.SettingsToolTipRefreshTextBox); + ToolTip.SetToolTip(RefreshTextBox, Properties.Resources.SettingsToolTipRefreshTextBox); + ToolTip.SetToolTip(ChangeCacheButton, Properties.Resources.SettingsToolTipChangeCacheButton); + ToolTip.SetToolTip(ResetCacheButton, Properties.Resources.SettingsToolTipResetCacheButton); + ToolTip.SetToolTip(OpenCacheButton, Properties.Resources.SettingsToolTipOpenCacheButton); + ToolTip.SetToolTip(ClearCacheButton, Properties.Resources.SettingsToolTipClearCacheButton); this.coreConfig = coreConfig; this.guiConfig = guiConfig; @@ -58,6 +47,8 @@ public SettingsDialog(IConfiguration coreConfig, { ClearCacheMenu.Renderer = new FlatToolStripRenderer(); } + CachePathEditButton.Height = CachePathSaveButton.Height = + CachePathCancelButton.Height = CachePathTextBox.Height; } private void SettingsDialog_Load(object? sender, EventArgs? e) @@ -84,40 +75,14 @@ public void UpdateDialog() UpdateRefreshRate(); - if (coreConfig.DownloadCacheDir != null) - { - UpdateCacheInfo(coreConfig.DownloadCacheDir); - } - } - - private void UpdateAutoUpdate() - { - LocalVersionLabel.Text = Meta.GetVersion(); - try - { - var latestVersion = updater.GetUpdate(coreConfig.DevBuilds ?? false, userAgent) - .Version; - LatestVersionLabel.Text = latestVersion?.ToString() ?? ""; - // Allow downgrading in case they want to stop using dev builds - InstallUpdateButton.Enabled = !latestVersion?.Equals(new ModuleVersion(Meta.GetVersion())) ?? false; - } - catch - { - // Can't get the version, reset the label - var resources = new SingleAssemblyComponentResourceManager(typeof(SettingsDialog)); - resources.ApplyResources(LatestVersionLabel, - LatestVersionLabel.Name); - InstallUpdateButton.Enabled = false; - } + UpdateCacheInfo(coreConfig.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); } protected override void OnFormClosing(FormClosingEventArgs e) { - if (CachePath.Text != coreConfig.DownloadCacheDir - && manager != null - && !manager.TrySetupCache(CachePath.Text, out string? failReason)) + if (!RepositoryGroupBox.Enabled) { - user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + // Don't close the window if still editing the cache path e.Cancel = true; } else @@ -126,110 +91,166 @@ protected override void OnFormClosing(FormClosingEventArgs e) } } - private void UpdateRefreshRate() + #region Cache path and limit + + private void UpdateCacheInfo(string newPath) { - if (Main.Instance != null) + CachePathTextBox.Text = newPath; + EnableDisableCachePath(false); + if (manager?.Cache != null) { - int rate = coreConfig.RefreshRate; - RefreshTextBox.Text = rate.ToString(); - PauseRefreshCheckBox.Enabled = rate != 0; - Main.Instance.pauseToolStripMenuItem.Enabled = coreConfig.RefreshRate != 0; - Main.Instance.UpdateRefreshTimer(); + // Background thread in case GetSizeInfo takes a while + Task.Factory.StartNew(() => + { + try + { + manager.Cache.GetSizeInfo(out int cacheFileCount, + out long cacheSize, + out long cacheFreeSpace); + + Util.Invoke(this, () => + { + if (coreConfig.CacheSizeLimit.HasValue) + { + // Show setting in MiB + CacheLimit.Text = (coreConfig.CacheSizeLimit.Value / 1024 / 1024).ToString(); + } + CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummmary, + cacheFileCount, + CkanModule.FmtSize(cacheSize), + CkanModule.FmtSize(cacheFreeSpace)); + CacheSummary.ForeColor = SystemColors.ControlText; + OpenCacheButton.Enabled = true; + ClearCacheButton.Enabled = (cacheSize > 0); + PurgeToLimitMenuItem.Enabled = (coreConfig.CacheSizeLimit.HasValue + && cacheSize > coreConfig.CacheSizeLimit.Value); + }); + } + catch (Exception ex) + { + Util.Invoke(this, () => + { + CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummaryInvalid, + ex.Message); + CacheSummary.ForeColor = Color.Red; + OpenCacheButton.Enabled = false; + ClearCacheButton.Enabled = false; + }); + } + }); } } - private void RefreshReposListBox(bool saveChanges = true, - Repository? toSelect = null) + private async void CachePathTextBox_KeyDown(object? sender, KeyEventArgs e) { - ReposListBox.BeginUpdate(); - ReposListBox.Items.Clear(); - ReposListBox.Items.AddRange(regMgr.registry.Repositories.Values - // SortedDictionary just sorts by name - .OrderBy(r => r.priority) - .Select(r => new ListViewItem(new string[] - { - r.name, r.uri?.ToString() ?? "", - }) - { - Tag = r, - Selected = toSelect == r, - }) - .ToArray()); - ReposListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); - ReposListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); - ReposListBox.Focus(); - ReposListBox.EndUpdate(); - EnableDisableRepoButtons(); - - if (saveChanges) + switch (e.KeyCode) { - UseWaitCursor = true; - // Save registry in background thread to keep GUI responsive - Task.Factory.StartNew(() => - { - // Visual cue that we're doing something - regMgr.Save(); - Util.Invoke(this, () => UseWaitCursor = false); - }); + case Keys.Escape: + e.Handled = true; + e.SuppressKeyPress = true; + UpdateCacheInfo(coreConfig.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); + break; + + case Keys.Enter: + e.Handled = true; + e.SuppressKeyPress = true; + await TrySaveCachePath(CachePathTextBox.Text); + break; } } - private void UpdateLanguageSelectionComboBox() + private void CachePathEditButton_Click(object? sender, EventArgs? e) { - LanguageSelectionComboBox.Items.Clear(); - LanguageSelectionComboBox.Items.AddRange(Utilities.AvailableLanguages); - // If the current language is supported by CKAN, set is as selected. - // Else display a blank field. - LanguageSelectionComboBox.SelectedIndex = LanguageSelectionComboBox.FindStringExact(coreConfig.Language); + EnableDisableCachePath(true); + CachePathTextBox.Focus(); } - private void UpdateCacheInfo(string newPath) + private async void CachePathSaveButton_Click(object? sender, EventArgs? e) { - CachePath.Text = newPath; - // Background thread in case GetSizeInfo takes a while - Task.Factory.StartNew(() => + await TrySaveCachePath(CachePathTextBox.Text); + } + + private void CachePathCancelButton_Click(object? sender, EventArgs? e) + { + UpdateCacheInfo(coreConfig.DownloadCacheDir + ?? JsonConfiguration.DefaultDownloadCacheDir); + } + + private async Task TrySaveCachePath(string newPath) + { + CachePathTextBox.Enabled = false; + CachePathSaveButton.Enabled = false; + CachePathCancelButton.Enabled = false; + return await Task.Run(() => { - try + if (newPath != coreConfig.DownloadCacheDir + && manager != null + && !manager.TrySetupCache(newPath, + new Progress(UpdateCacheProgress), + out string? failReason)) { - // Make a temporary cache object to validate the path without changing the setting till close - var cache = new NetModuleCache(newPath); - cache.GetSizeInfo(out int cacheFileCount, out long cacheSize, out long cacheFreeSpace); - Util.Invoke(this, () => { - if (coreConfig.CacheSizeLimit.HasValue) + MoveCacheProgressBar.Visible = false; + if (failReason.Length > 0) { - // Show setting in MiB - CacheLimit.Text = (coreConfig.CacheSizeLimit.Value / 1024 / 1024).ToString(); + user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, + failReason); + } + else + { + // User cancelled the choice popup, reset UI + UpdateCacheInfo(coreConfig.DownloadCacheDir + ?? JsonConfiguration.DefaultDownloadCacheDir); } - CacheSummary.Text = string.Format( - Properties.Resources.SettingsDialogSummmary, - cacheFileCount, CkanModule.FmtSize(cacheSize), CkanModule.FmtSize(cacheFreeSpace)); - CacheSummary.ForeColor = SystemColors.ControlText; - OpenCacheButton.Enabled = true; - ClearCacheButton.Enabled = (cacheSize > 0); - PurgeToLimitMenuItem.Enabled = (coreConfig.CacheSizeLimit.HasValue - && cacheSize > coreConfig.CacheSizeLimit.Value); }); - + return false; } - catch (Exception ex) + else { Util.Invoke(this, () => { - CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummaryInvalid, - ex.Message); - CacheSummary.ForeColor = Color.Red; - OpenCacheButton.Enabled = false; - ClearCacheButton.Enabled = false; + MoveCacheProgressBar.Visible = false; + UpdateCacheInfo(newPath); }); + return true; } }); } - private void CachePath_TextChanged(object? sender, EventArgs? e) + private void UpdateCacheProgress(int percent) { - UpdateCacheInfo(CachePath.Text); + Util.Invoke(CachePathTextBox, () => + { + MoveCacheProgressBar.Visible = true; + MoveCacheProgressBar.Value = percent; + }); + } + + private void EnableDisableCachePath(bool enable) + { + ControlBox = !enable; + BehaviourGroupBox.Enabled = !enable; + AuthTokensGroupBox.Enabled = !enable; + AutoUpdateGroupBox.Enabled = !enable; + RepositoryGroupBox.Enabled = !enable; + MoreSettingsGroupBox.Enabled = !enable; + CachePathTextBox.Enabled = enable; + CachePathEditButton.Visible = !enable; + CachePathSaveButton.Enabled = true; + CachePathSaveButton.Visible = enable; + CachePathCancelButton.Enabled = true; + CachePathCancelButton.Visible = enable; + ChangeCacheButton.Enabled = !enable; + ResetCacheButton.Enabled = !enable + && CachePathTextBox.Text != JsonConfiguration.DefaultDownloadCacheDir; + ClearCacheButton.Enabled = !enable; + if (enable) + { + CachePathCancelButton.Left = CachePathSaveButton.Left - CachePathCancelButton.Width; + } + CachePathTextBox.Width = enable ? CachePathCancelButton.Left - CachePathTextBox.Left + : CachePathEditButton.Left - CachePathTextBox.Left; } private void CacheLimit_TextChanged(object? sender, EventArgs? e) @@ -243,7 +264,7 @@ private void CacheLimit_TextChanged(object? sender, EventArgs? e) // Translate from MB to bytes coreConfig.CacheSizeLimit = Convert.ToInt64(CacheLimit.Text) * 1024 * 1024; } - UpdateCacheInfo(CachePath.Text); + UpdateCacheInfo(CachePathTextBox.Text); } private void CacheLimit_KeyPress(object sender, KeyPressEventArgs e) @@ -254,7 +275,7 @@ private void CacheLimit_KeyPress(object sender, KeyPressEventArgs e) } } - private void ChangeCacheButton_Click(object? sender, EventArgs? e) + private async void ChangeCacheButton_Click(object? sender, EventArgs? e) { var cacheChooser = new FolderBrowserDialog() { @@ -266,26 +287,19 @@ private void ChangeCacheButton_Click(object? sender, EventArgs? e) }; if (cacheChooser.ShowDialog(this) == DialogResult.OK) { - UpdateCacheInfo(cacheChooser.SelectedPath); + await TrySaveCachePath(cacheChooser.SelectedPath); } } private void PurgeToLimitMenuItem_Click(object? sender, EventArgs? e) { // Purge old downloads if we're over the limit - if (coreConfig.CacheSizeLimit.HasValue && manager != null && coreConfig.DownloadCacheDir != null) + if (coreConfig.CacheSizeLimit.HasValue + && manager?.Cache != null + && coreConfig.DownloadCacheDir != null) { - // Switch main cache since user seems committed to this path - if (CachePath.Text != coreConfig.DownloadCacheDir - && !manager.TrySetupCache(CachePath.Text, out string? failReason)) - { - user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); - return; - } - - manager.Cache?.EnforceSizeLimit( - coreConfig.CacheSizeLimit.Value, - regMgr.registry); + manager.Cache.EnforceSizeLimit(coreConfig.CacheSizeLimit.Value, + regMgr.registry); UpdateCacheInfo(coreConfig.DownloadCacheDir); } } @@ -294,19 +308,12 @@ private void PurgeAllMenuItem_Click(object? sender, EventArgs? e) { if (manager?.Cache != null) { - // Switch main cache since user seems committed to this path - if (CachePath.Text != coreConfig.DownloadCacheDir - && !manager.TrySetupCache(CachePath.Text, out string? failReason)) - { - user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); - return; - } + manager.Cache.GetSizeInfo(out int cacheFileCount, + out long cacheSize, + out _); - manager.Cache.GetSizeInfo( - out int cacheFileCount, out long cacheSize, out _); - - YesNoDialog deleteConfirmationDialog = new YesNoDialog(); - string confirmationText = string.Format( + var deleteConfirmationDialog = new YesNoDialog(); + var confirmationText = string.Format( Properties.Resources.SettingsDialogDeleteConfirm, cacheFileCount, CkanModule.FmtSize(cacheSize)); @@ -324,15 +331,60 @@ private void PurgeAllMenuItem_Click(object? sender, EventArgs? e) } } - private void ResetCacheButton_Click(object? sender, EventArgs? e) + private async void ResetCacheButton_Click(object? sender, EventArgs? e) { // Reset to default cache path - UpdateCacheInfo(JsonConfiguration.DefaultDownloadCacheDir); + await TrySaveCachePath(JsonConfiguration.DefaultDownloadCacheDir); } private void OpenCacheButton_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(coreConfig.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); + Utilities.ProcessStartURL(coreConfig.DownloadCacheDir + ?? JsonConfiguration.DefaultDownloadCacheDir); + } + + #endregion + + #region Repositories + + public bool RepositoryAdded { get; private set; } = false; + public bool RepositoryRemoved { get; private set; } = false; + public bool RepositoryMoved { get; private set; } = false; + + private void RefreshReposListBox(bool saveChanges = true, + Repository? toSelect = null) + { + ReposListBox.BeginUpdate(); + ReposListBox.Items.Clear(); + ReposListBox.Items.AddRange(regMgr.registry.Repositories.Values + // SortedDictionary just sorts by name + .OrderBy(r => r.priority) + .Select(r => new ListViewItem(new string[] + { + r.name, r.uri?.ToString() ?? "", + }) + { + Tag = r, + Selected = toSelect == r, + }) + .ToArray()); + ReposListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + ReposListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); + ReposListBox.Focus(); + ReposListBox.EndUpdate(); + EnableDisableRepoButtons(); + + if (saveChanges) + { + UseWaitCursor = true; + // Save registry in background thread to keep GUI responsive + Task.Factory.StartNew(() => + { + // Visual cue that we're doing something + regMgr.Save(); + Util.Invoke(this, () => UseWaitCursor = false); + }); + } } private void ReposListBox_SelectedIndexChanged(object? sender, EventArgs? e) @@ -462,6 +514,10 @@ private void DownRepoButton_Click(object? sender, EventArgs? e) } } + #endregion + + #region Auth tokens + private void RefreshAuthTokensListBox() { AuthTokensListBox.Items.Clear(); @@ -476,6 +532,8 @@ private void RefreshAuthTokensListBox() }); } } + AuthTokensListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + AuthTokensListBox.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); } private void AuthTokensListBox_SelectedIndexChanged(object? sender, EventArgs? e) @@ -611,6 +669,31 @@ private void DeleteAuthTokenButton_Click(object? sender, EventArgs? e) } } + #endregion + + #region CKAN updates + + private void UpdateAutoUpdate() + { + LocalVersionLabel.Text = Meta.GetVersion(); + try + { + var latestVersion = updater.GetUpdate(coreConfig.DevBuilds ?? false, userAgent) + .Version; + LatestVersionLabel.Text = latestVersion?.ToString() ?? ""; + // Allow downgrading in case they want to stop using dev builds + InstallUpdateButton.Enabled = !latestVersion?.Equals(new ModuleVersion(Meta.GetVersion())) ?? false; + } + catch + { + // Can't get the version, reset the label + var resources = new SingleAssemblyComponentResourceManager(typeof(SettingsDialog)); + resources.ApplyResources(LatestVersionLabel, + LatestVersionLabel.Name); + InstallUpdateButton.Enabled = false; + } + } + private void CheckForUpdatesButton_Click(object? sender, EventArgs? e) { try @@ -647,6 +730,19 @@ private void DevBuildsCheckbox_CheckedChanged(object? sender, EventArgs? e) UpdateAutoUpdate(); } + #endregion + + #region More settings + + private void UpdateLanguageSelectionComboBox() + { + LanguageSelectionComboBox.Items.Clear(); + LanguageSelectionComboBox.Items.AddRange(Utilities.AvailableLanguages); + // If the current language is supported by CKAN, set is as selected. + // Else display a blank field. + LanguageSelectionComboBox.SelectedIndex = LanguageSelectionComboBox.FindStringExact(coreConfig.Language); + } + private void RefreshOnStartupCheckbox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.RefreshOnStartup = RefreshOnStartupCheckbox.Checked; @@ -681,6 +777,22 @@ private void AutoSortUpdateCheckBox_CheckedChanged(object? sender, EventArgs? e) guiConfig.AutoSortByUpdate = AutoSortUpdateCheckBox.Checked; } + #endregion + + #region Tray icon + + private void UpdateRefreshRate() + { + if (Main.Instance != null) + { + int rate = coreConfig.RefreshRate; + RefreshTextBox.Text = rate.ToString(); + PauseRefreshCheckBox.Enabled = rate != 0; + Main.Instance.pauseToolStripMenuItem.Enabled = coreConfig.RefreshRate != 0; + Main.Instance.UpdateRefreshTimer(); + } + } + private void EnableTrayIconCheckBox_CheckedChanged(object? sender, EventArgs? e) { MinimizeToTrayCheckBox.Enabled = guiConfig.EnableTrayIcon = EnableTrayIconCheckBox.Checked; @@ -720,5 +832,18 @@ private void PauseRefreshCheckBox_CheckedChanged(object? sender, EventArgs? e) Main.Instance?.refreshTimer?.Start(); } } + + #endregion + + private static GameInstanceManager? manager => Main.Instance?.Manager; + + private readonly IConfiguration coreConfig; + private readonly GUIConfiguration guiConfig; + private readonly RegistryManager regMgr; + private readonly AutoUpdate updater; + private readonly IUser user; + private readonly string? userAgent; + + private static readonly ILog log = LogManager.GetLogger(typeof(SettingsDialog)); } } diff --git a/GUI/Dialogs/SettingsDialog.resx b/GUI/Dialogs/SettingsDialog.resx index 72e7c02d00..8061ee3fce 100644 --- a/GUI/Dialogs/SettingsDialog.resx +++ b/GUI/Dialogs/SettingsDialog.resx @@ -129,6 +129,9 @@ New Delete Download Cache + Edit + Save + Cancel N files, M MiB Maximum cache size: MiB (empty for unlimited) diff --git a/GUI/Dialogs/YesNoDialog.Designer.cs b/GUI/Dialogs/YesNoDialog.Designer.cs index f138f08f5d..c3be4ec4e8 100644 --- a/GUI/Dialogs/YesNoDialog.Designer.cs +++ b/GUI/Dialogs/YesNoDialog.Designer.cs @@ -131,7 +131,7 @@ private void InitializeComponent() this.panel1.ResumeLayout(false); this.BottomButtonPanel.ResumeLayout(false); this.BottomButtonPanel.PerformLayout(); - this.ResumeLayout(false); + this.ResumeLayout(false); this.PerformLayout(); } diff --git a/GUI/Dialogs/YesNoDialog.cs b/GUI/Dialogs/YesNoDialog.cs index 34a1d042cf..2358dc2060 100644 --- a/GUI/Dialogs/YesNoDialog.cs +++ b/GUI/Dialogs/YesNoDialog.cs @@ -78,11 +78,6 @@ private void SetupSuppressable(string text, string? yesText, string? noText, str SuppressCheckbox.Visible = true; } - public void HideYesNoDialog() - { - Util.Invoke(this, Close); - } - private const int maxHeight = 600; private TaskCompletionSource>? task; private readonly string defaultYes; diff --git a/GUI/GUIUser.cs b/GUI/GUIUser.cs index 6f63b1056b..dc8f519c20 100644 --- a/GUI/GUIUser.cs +++ b/GUI/GUIUser.cs @@ -56,7 +56,16 @@ public bool RaiseYesNoDialog(string question) /// Array of offered options. [ForbidGUICalls] public int RaiseSelectionDialog(string message, params object[] args) - => main.SelectionDialog(message, args); + { + int result = 0; + Util.Invoke(main, () => + { + var dlg = new SelectionDialog(); + result = dlg.ShowSelectionDialog(message, args); + dlg.Dispose(); + }); + return result; + } /// /// Shows a message box containing the formatted error message. diff --git a/GUI/Localization/de-DE/SelectionDialog.de-DE.resx b/GUI/Localization/de-DE/SelectionDialog.de-DE.resx index 91b23817e6..54001cbe2a 100644 --- a/GUI/Localization/de-DE/SelectionDialog.de-DE.resx +++ b/GUI/Localization/de-DE/SelectionDialog.de-DE.resx @@ -126,7 +126,7 @@ Standard - + Abbrechen diff --git a/GUI/Localization/fr-FR/SelectionDialog.fr-FR.resx b/GUI/Localization/fr-FR/SelectionDialog.fr-FR.resx index b6d2ea90a6..20a4160e94 100644 --- a/GUI/Localization/fr-FR/SelectionDialog.fr-FR.resx +++ b/GUI/Localization/fr-FR/SelectionDialog.fr-FR.resx @@ -126,7 +126,7 @@ Défaut - + Annuler diff --git a/GUI/Localization/it-IT/SelectionDialog.it-IT.resx b/GUI/Localization/it-IT/SelectionDialog.it-IT.resx index 85d519ed13..a1b6a64ce3 100644 --- a/GUI/Localization/it-IT/SelectionDialog.it-IT.resx +++ b/GUI/Localization/it-IT/SelectionDialog.it-IT.resx @@ -126,7 +126,7 @@ Predefinito - + Annulla diff --git a/GUI/Localization/ja-JP/SelectionDialog.ja-JP.resx b/GUI/Localization/ja-JP/SelectionDialog.ja-JP.resx index b932dd4c7e..b2bbf49bdd 100644 --- a/GUI/Localization/ja-JP/SelectionDialog.ja-JP.resx +++ b/GUI/Localization/ja-JP/SelectionDialog.ja-JP.resx @@ -126,7 +126,7 @@ デフォルト - + 取消 diff --git a/GUI/Localization/ko-KR/SelectionDialog.ko-KR.resx b/GUI/Localization/ko-KR/SelectionDialog.ko-KR.resx index 60c307a6fa..17c757b021 100644 --- a/GUI/Localization/ko-KR/SelectionDialog.ko-KR.resx +++ b/GUI/Localization/ko-KR/SelectionDialog.ko-KR.resx @@ -126,7 +126,7 @@ 기본 - + 취소하기 diff --git a/GUI/Localization/nl-NL/SelectionDialog.nl-NL.resx b/GUI/Localization/nl-NL/SelectionDialog.nl-NL.resx index d29f72a947..7b53b00c8a 100644 --- a/GUI/Localization/nl-NL/SelectionDialog.nl-NL.resx +++ b/GUI/Localization/nl-NL/SelectionDialog.nl-NL.resx @@ -126,7 +126,7 @@ Standaard - + Annuleer diff --git a/GUI/Localization/pl-PL/SelectionDialog.pl-PL.resx b/GUI/Localization/pl-PL/SelectionDialog.pl-PL.resx index 8644dda07b..c2dd514aff 100644 --- a/GUI/Localization/pl-PL/SelectionDialog.pl-PL.resx +++ b/GUI/Localization/pl-PL/SelectionDialog.pl-PL.resx @@ -126,7 +126,7 @@ Domyślny - + Anuluj diff --git a/GUI/Localization/pt-BR/SelectionDialog.pt-BR.resx b/GUI/Localization/pt-BR/SelectionDialog.pt-BR.resx index 4f7b8e8753..0b289a76a1 100644 --- a/GUI/Localization/pt-BR/SelectionDialog.pt-BR.resx +++ b/GUI/Localization/pt-BR/SelectionDialog.pt-BR.resx @@ -126,7 +126,7 @@ Padrão - + Cancelar diff --git a/GUI/Localization/ru-RU/SelectionDialog.ru-RU.resx b/GUI/Localization/ru-RU/SelectionDialog.ru-RU.resx index e58b99a64f..31d4897a82 100644 --- a/GUI/Localization/ru-RU/SelectionDialog.ru-RU.resx +++ b/GUI/Localization/ru-RU/SelectionDialog.ru-RU.resx @@ -126,7 +126,7 @@ По умолчанию - + Отмена diff --git a/GUI/Localization/zh-CN/SelectionDialog.zh-CN.resx b/GUI/Localization/zh-CN/SelectionDialog.zh-CN.resx index 23cb418044..202df100a8 100644 --- a/GUI/Localization/zh-CN/SelectionDialog.zh-CN.resx +++ b/GUI/Localization/zh-CN/SelectionDialog.zh-CN.resx @@ -126,7 +126,7 @@ 默认 - + 取消 diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 274c0e1e29..fbfb829e0a 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -158,11 +158,8 @@ public Main(string[] cmdlineArgs, Manager = new GameInstanceManager(currentUser); } - if (Manager.Cache != null) - { - Manager.Cache.ModStored += OnModStoredOrPurged; - Manager.Cache.ModPurged += OnModStoredOrPurged; - } + Manager.CacheChanged += OnCacheChanged; + OnCacheChanged(null); tabController = new TabController(MainTabControl); tabController.ShowTab("ManageModsTabPage"); diff --git a/GUI/Main/MainDialogs.cs b/GUI/Main/MainDialogs.cs index 1a5ff3059e..4ecbb1c073 100644 --- a/GUI/Main/MainDialogs.cs +++ b/GUI/Main/MainDialogs.cs @@ -8,21 +8,19 @@ namespace CKAN.GUI { public partial class Main { - public ControlFactory controlFactory; + public ControlFactory controlFactory; private ErrorDialog errorDialog; private PluginsDialog pluginsDialog; private YesNoDialog yesNoDialog; - private SelectionDialog selectionDialog; [MemberNotNull(nameof(controlFactory), nameof(errorDialog), nameof(pluginsDialog), - nameof(yesNoDialog), nameof(selectionDialog))] + nameof(yesNoDialog))] public void RecreateDialogs() { controlFactory ??= new ControlFactory(); - errorDialog = controlFactory.CreateControl(); + errorDialog = controlFactory.CreateControl(); pluginsDialog = controlFactory.CreateControl(); - yesNoDialog = controlFactory.CreateControl(); - selectionDialog = controlFactory.CreateControl(); + yesNoDialog = controlFactory.CreateControl(); } [ForbidGUICalls] @@ -43,9 +41,5 @@ public bool YesNoDialog(string text, string? yesText = null, string? noText = nu [ForbidGUICalls] public Tuple SuppressableYesNoDialog(string text, string suppressText, string? yesText = null, string? noText = null) => yesNoDialog.ShowSuppressableYesNoDialog(this, text, suppressText, yesText, noText); - - [ForbidGUICalls] - public int SelectionDialog(string message, params object[] args) - => selectionDialog.ShowSelectionDialog(message, args); } } diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index 58b6943a16..2b29559e58 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -91,6 +91,7 @@ public void PostModCaching(object? sender, RunWorkerCompletedEventArgs? e) // Close progress tab and switch back to mod list HideWaitDialog(); EnableMainWindow(); + ModInfo.SwitchTab("ContentTabPage"); } } @@ -103,13 +104,30 @@ private void UpdateCachedByDownloads(CkanModule? module) .Select(guiMod => guiMod.ToModule()) .OfType()) .Select(other => allGuiMods[other.identifier]) - ?? allGuiMods.Values; + ?? allGuiMods.Values; foreach (var otherMod in affectedMods) { otherMod.UpdateIsCached(); } } + [ForbidGUICalls] + private void OnCacheChanged(NetModuleCache? prev) + { + if (prev != null) + { + prev.ModStored -= OnModStoredOrPurged; + prev.ModPurged -= OnModStoredOrPurged; + } + if (Manager.Cache != null) + { + Manager.Cache.ModStored += OnModStoredOrPurged; + Manager.Cache.ModPurged += OnModStoredOrPurged; + } + UpdateCachedByDownloads(null); + ModInfo.RefreshModContentsTree(); + } + [ForbidGUICalls] private void OnModStoredOrPurged(CkanModule? module) { diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index f8a7364636..5d5091b295 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -360,6 +360,10 @@ Find the folder where your game is installed and choose one of these files: Failed to fetch master list. CKAN Plugins (*.dll)|*.dll Setting to 0 will not refresh modlist + Choose a new download cache folder + Use the default download cache folder + Browse the cache folder + Delete files from the cache {0} files, {1}, {2} free Invalid path: {0} Choose a folder for storing CKAN's mod downloads: @@ -513,4 +517,5 @@ This action cannot be undone! Remove selected host from preferences list Make selected host higher priority Make selected host lower priority + {0} (DEFAULT) diff --git a/Netkan/Processors/Inflator.cs b/Netkan/Processors/Inflator.cs index 96f596194f..f8a5edb993 100644 --- a/Netkan/Processors/Inflator.cs +++ b/Netkan/Processors/Inflator.cs @@ -106,7 +106,7 @@ private static NetFileCache FindCache(IConfiguration cfg, string? cacheDir) { log.InfoFormat("Using main CKAN meta-cache at {0}", cfg.DownloadCacheDir); // Create a new file cache in the same location so NetKAN can download pure URLs not sourced from CkanModules - return new NetFileCache(null, cfg.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); + return new NetFileCache(cfg.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); } catch { diff --git a/Netkan/Services/ModuleService.cs b/Netkan/Services/ModuleService.cs index b1935cf636..7608202e15 100644 --- a/Netkan/Services/ModuleService.cs +++ b/Netkan/Services/ModuleService.cs @@ -64,8 +64,8 @@ public ModuleService(IGame game) ? GetFilesBySuffix(module, zip, ".ckan", inst) .Select(instF => instF.source) // Find embedded .ckan files anywhere in the ZIP - : zip.Cast() - .Where(entry => entry.Name.EndsWith(".ckan", StringComparison.InvariantCultureIgnoreCase))) + : zip.OfType() + .Where(entry => entry.Name.EndsWith(".ckan", StringComparison.InvariantCultureIgnoreCase))) .Select(entry => DeserializeFromStream( zip.GetInputStream(entry))) .FirstOrDefault();