diff --git a/CreamInstaller/CreamInstaller.csproj b/CreamInstaller/CreamInstaller.csproj index 3e7e6aa..f0e5675 100644 --- a/CreamInstaller/CreamInstaller.csproj +++ b/CreamInstaller/CreamInstaller.csproj @@ -4,7 +4,7 @@ net7.0-windows True Resources\ini.ico - 4.7.1.3 + 4.8.0.0 2021, pointfeev (https://github.com/pointfeev) CreamInstaller Automatic DLC Unlocker Installer & Configuration Generator @@ -147,7 +147,6 @@ - diff --git a/CreamInstaller/Forms/MainForm.cs b/CreamInstaller/Forms/MainForm.cs deleted file mode 100644 index 57d2452..0000000 --- a/CreamInstaller/Forms/MainForm.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Windows.Forms; -using CreamInstaller.Components; -using CreamInstaller.Utility; -using HtmlAgilityPack; -using Onova; -using Onova.Models; -using Onova.Services; - -namespace CreamInstaller.Forms; - -internal sealed partial class MainForm : CustomForm -{ - private CancellationTokenSource cancellationTokenSource; - private Version latestVersion; - - private UpdateManager updateManager; - private IReadOnlyList versions; - - internal MainForm() - { - InitializeComponent(); - Text = Program.ApplicationNameShort; - } - - private void StartProgram() - { - if (cancellationTokenSource is not null) - { - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; - } -#pragma warning disable CA2000 // Dispose objects before losing scope - SelectForm form = new(); -#pragma warning restore CA2000 // Dispose objects before losing scope - form.InheritLocation(this); - form.FormClosing += (_, _) => Close(); - form.Show(); - Hide(); -#if DEBUG - DebugForm.Current.Attach(form); -#endif - } - - private async void OnLoad() - { - progressBar.Visible = false; - ignoreButton.Visible = true; - updateButton.Text = "Update"; - updateButton.Click -= OnUpdateCancel; - progressLabel.Text = "Checking for updates . . ."; - changelogTreeView.Visible = false; - changelogTreeView.Location = progressLabel.Location with { Y = progressLabel.Location.Y + progressLabel.Size.Height + 13 }; - Refresh(); -#if DEBUG - DebugForm.Current.Attach(this); -#endif - GithubPackageResolver resolver = new(Program.RepositoryOwner, Program.RepositoryName, Program.RepositoryPackage); - ZipPackageExtractor extractor = new(); - updateManager = new(AssemblyMetadata.FromAssembly(Program.EntryAssembly, Program.CurrentProcessFilePath), resolver, extractor); - if (latestVersion is null) - { - cancellationTokenSource = new(); - try - { - CheckForUpdatesResult checkForUpdatesResult = await updateManager.CheckForUpdatesAsync(cancellationTokenSource.Token); -#if !DEBUG - if (checkForUpdatesResult.CanUpdate) - { -#endif - latestVersion = checkForUpdatesResult.LastVersion; - versions = checkForUpdatesResult.Versions; -#if !DEBUG - } -#endif - } -#if DEBUG - catch (TaskCanceledException) { } - catch (Exception e) - { - DebugForm.Current.Log($"Exception while checking for updates: {e.GetType()} ({e.Message})", LogTextBox.Warning); - } -#else - catch - { - // ignored - } -#endif - finally - { - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; - } - } - if (latestVersion is null) - { - updateManager.Dispose(); - updateManager = null; - StartProgram(); - } - else - { - progressLabel.Text = $"An update is available: v{latestVersion}"; - ignoreButton.Enabled = true; - updateButton.Enabled = true; - updateButton.Click += OnUpdate; - changelogTreeView.Visible = true; - Version currentVersion = new(Program.Version); -#if DEBUG - foreach (Version version in versions.Where(v => (v > currentVersion || v == latestVersion) && !changelogTreeView.Nodes.ContainsKey(v.ToString()))) -#else - foreach (Version version in versions.Where(v => v > currentVersion && !changelogTreeView.Nodes.ContainsKey(v.ToString()))) -#endif - { - TreeNode root = new($"v{version}") { Name = version.ToString() }; - changelogTreeView.Nodes.Add(root); - if (changelogTreeView.Nodes.Count > 0) - changelogTreeView.Nodes[0].EnsureVisible(); - _ = Task.Run(async () => - { - HtmlNodeCollection nodes = await HttpClientManager.GetDocumentNodes( - $"https://github.com/{Program.RepositoryOwner}/{Program.RepositoryName}/releases/tag/v{version}", - "//div[@data-test-selector='body-content']/ul/li"); - if (nodes is null) - changelogTreeView.Nodes.Remove(root); - else - foreach (HtmlNode node in nodes) - changelogTreeView.Invoke(delegate - { - TreeNode change = new() { Text = HttpUtility.HtmlDecode(node.InnerText) ?? string.Empty }; - root.Nodes.Add(change); - root.Expand(); - if (changelogTreeView.Nodes.Count > 0) - changelogTreeView.Nodes[0].EnsureVisible(); - }); - }); - } - } - } - - private void OnLoad(object sender, EventArgs _) - { - retry: - try - { - string fileName = Path.GetFileName(Program.CurrentProcessFilePath); - if (fileName != Program.ApplicationExecutable) - { - using DialogForm form = new(this); - if (form.Show(SystemIcons.Warning, - "WARNING: " + Program.ApplicationExecutable + " was renamed!" + "\n\nThis will cause undesirable behavior when updating the program!", - "Ignore", "Abort") == DialogResult.Cancel) - { - Application.Exit(); - return; - } - } - OnLoad(); - } - catch (Exception e) - { - if (e.HandleException(this)) - goto retry; - Close(); - } - } - - private void OnIgnore(object sender, EventArgs e) => StartProgram(); - - private async void OnUpdate(object sender, EventArgs e) - { - progressBar.Visible = true; - ignoreButton.Visible = false; - updateButton.Text = "Cancel"; - updateButton.Click -= OnUpdate; - updateButton.Click += OnUpdateCancel; - changelogTreeView.Location = progressBar.Location with { Y = progressBar.Location.Y + progressBar.Size.Height + 6 }; - Refresh(); - Progress progress = new(); - progress.ProgressChanged += delegate(object _, double _progress) - { - progressLabel.Text = $"Updating . . . {(int)_progress}%"; - progressBar.Value = (int)_progress; - }; - progressLabel.Text = "Updating . . . "; - cancellationTokenSource = new(); - try - { - await updateManager.PrepareUpdateAsync(latestVersion, progress, cancellationTokenSource.Token); - } -#if DEBUG - catch (TaskCanceledException) { } - catch (Exception ex) - { - DebugForm.Current.Log($"Exception while preparing update: {ex.GetType()} ({ex.Message})", LogTextBox.Warning); - } -#else - catch - { - // ignored - } -#endif - finally - { - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; - } - if (updateManager is not null && updateManager.IsUpdatePrepared(latestVersion)) - { - updateManager.LaunchUpdater(latestVersion); - Application.Exit(); - return; - } - OnLoad(); - } - - private void OnUpdateCancel(object sender, EventArgs e) - { - cancellationTokenSource?.Cancel(); - updateManager?.Dispose(); - updateManager = null; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - components?.Dispose(); - base.Dispose(disposing); - cancellationTokenSource?.Dispose(); - updateManager?.Dispose(); - } -} \ No newline at end of file diff --git a/CreamInstaller/Forms/MainForm.Designer.cs b/CreamInstaller/Forms/UpdateForm.Designer.cs similarity index 97% rename from CreamInstaller/Forms/MainForm.Designer.cs rename to CreamInstaller/Forms/UpdateForm.Designer.cs index 946069b..267e1f7 100644 --- a/CreamInstaller/Forms/MainForm.Designer.cs +++ b/CreamInstaller/Forms/UpdateForm.Designer.cs @@ -4,7 +4,7 @@ using CreamInstaller.Components; namespace CreamInstaller.Forms { - partial class MainForm + partial class UpdateForm { private IContainer components = null; @@ -78,7 +78,7 @@ namespace CreamInstaller.Forms this.changelogTreeView.Sorted = true; this.changelogTreeView.TabIndex = 5; // - // MainForm + // UpdateForm // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; @@ -94,9 +94,9 @@ namespace CreamInstaller.Forms this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; this.MaximizeBox = false; this.MinimizeBox = false; - this.Name = "MainForm"; + this.Name = "UpdateForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "MainForm"; + this.Text = "UpdateForm"; this.Load += new System.EventHandler(this.OnLoad); this.ResumeLayout(false); diff --git a/CreamInstaller/Forms/UpdateForm.cs b/CreamInstaller/Forms/UpdateForm.cs new file mode 100644 index 0000000..ccbf6dc --- /dev/null +++ b/CreamInstaller/Forms/UpdateForm.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; +using CreamInstaller.Components; +using CreamInstaller.Utility; +using Newtonsoft.Json; + +namespace CreamInstaller.Forms; + +internal sealed partial class UpdateForm : CustomForm +{ + private Release latestRelease; + + internal UpdateForm() + { + InitializeComponent(); + Text = Program.ApplicationNameShort; + } + + private void StartProgram() + { + SelectForm form = new(); + form.InheritLocation(this); + form.FormClosing += (_, _) => Close(); + form.Show(); + Hide(); +#if DEBUG + DebugForm.Current.Attach(form); +#endif + } + + private async void OnLoad() + { + progressBar.Visible = false; + ignoreButton.Visible = true; + updateButton.Text = "Update"; + updateButton.Click -= OnUpdateCancel; + progressLabel.Text = "Checking for updates . . ."; + changelogTreeView.Visible = false; + changelogTreeView.Location = progressLabel.Location with { Y = progressLabel.Location.Y + progressLabel.Size.Height + 13 }; + Refresh(); +#if !DEBUG + Version currentVersion = new(Program.Version); +#endif + List releases = null; + string response = await HttpClientManager.EnsureGet($"https://api.github.com/repos/{Program.RepositoryOwner}/{Program.RepositoryName}/releases"); + if (response is not null) + releases = JsonConvert.DeserializeObject>(response) + ?.Where(release => !release.Draft && !release.Prerelease && release.Assets.Count > 0).ToList(); + latestRelease = releases?.FirstOrDefault(); +#if DEBUG + if (latestRelease?.Version is not { } latestVersion) +#else + if (latestRelease?.Version is not { } latestVersion || latestVersion <= currentVersion) +#endif + StartProgram(); + else + { + progressLabel.Text = $"An update is available: v{latestVersion}"; + ignoreButton.Enabled = true; + updateButton.Enabled = true; + updateButton.Click += OnUpdate; + changelogTreeView.Visible = true; + for (int r = releases!.Count - 1; r >= 0; r--) + { + Release release = releases[r]; +#if !DEBUG + if (release.Version <= currentVersion) + continue; +#endif + TreeNode root = new(release.Name) { Name = release.Name }; + changelogTreeView.Nodes.Add(root); + if (changelogTreeView.Nodes.Count > 0) + changelogTreeView.Nodes[0].EnsureVisible(); + for (int i = release.Changes.Length - 1; i >= 0; i--) + changelogTreeView.Invoke(delegate + { + string change = release.Changes[i]; + TreeNode changeNode = new() { Text = change }; + root.Nodes.Add(changeNode); + root.Expand(); + if (changelogTreeView.Nodes.Count > 0) + changelogTreeView.Nodes[0].EnsureVisible(); + }); + } + } + } + + private void OnLoad(object sender, EventArgs _) + { + retry: + try + { + string fileName = Path.GetFileName(Program.CurrentProcessFilePath); + if (fileName != Program.ApplicationExecutable) + { + using DialogForm form = new(this); + if (form.Show(SystemIcons.Warning, + "WARNING: " + Program.ApplicationExecutable + " was renamed!" + "\n\nThis will cause undesirable behavior when updating the program!", + "Ignore", "Abort") == DialogResult.Cancel) + { + Application.Exit(); + return; + } + } + OnLoad(); + } + catch (Exception e) + { + if (e.HandleException(this)) + goto retry; + Close(); + } + } + + private void OnIgnore(object sender, EventArgs e) => StartProgram(); + + private void OnUpdate(object sender, EventArgs e) + { + progressBar.Visible = true; + ignoreButton.Visible = false; + updateButton.Text = "Cancel"; + updateButton.Click -= OnUpdate; + updateButton.Click += OnUpdateCancel; + changelogTreeView.Location = progressBar.Location with { Y = progressBar.Location.Y + progressBar.Size.Height + 6 }; + Refresh(); + Progress progress = new(); + progress.ProgressChanged += delegate(object _, double _progress) + { + progressLabel.Text = $"Updating . . . {(int)_progress}%"; + progressBar.Value = (int)_progress; + }; + progressLabel.Text = "Updating . . . "; + // do update + OnLoad(); + } + + private void OnUpdateCancel(object sender, EventArgs e) + { + // cancel update + } + + protected override void Dispose(bool disposing) + { + if (disposing) + components?.Dispose(); + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/CreamInstaller/Forms/MainForm.resx b/CreamInstaller/Forms/UpdateForm.resx similarity index 100% rename from CreamInstaller/Forms/MainForm.resx rename to CreamInstaller/Forms/UpdateForm.resx diff --git a/CreamInstaller/Platforms/Steam/SteamStore.cs b/CreamInstaller/Platforms/Steam/SteamStore.cs index 0d38df4..db10308 100644 --- a/CreamInstaller/Platforms/Steam/SteamStore.cs +++ b/CreamInstaller/Platforms/Steam/SteamStore.cs @@ -40,7 +40,7 @@ internal static class SteamStore string response = await HttpClientManager.EnsureGet($"https://store.steampowered.com/api/appdetails?appids={appId}"); if (response is not null) { - IDictionary apps = (IDictionary)JsonConvert.DeserializeObject(response); + Dictionary apps = JsonConvert.DeserializeObject>(response); if (apps is not null) foreach (KeyValuePair app in apps) try diff --git a/CreamInstaller/Program.cs b/CreamInstaller/Program.cs index 1ccf65a..7f24d2f 100644 --- a/CreamInstaller/Program.cs +++ b/CreamInstaller/Program.cs @@ -86,7 +86,7 @@ internal static class Program try { HttpClientManager.Setup(); - using MainForm form = new(); + using UpdateForm form = new(); #if DEBUG DebugForm.Current.Attach(form); #endif diff --git a/CreamInstaller/Release.cs b/CreamInstaller/Release.cs new file mode 100644 index 0000000..3e4e8de --- /dev/null +++ b/CreamInstaller/Release.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CreamInstaller; + +public class Release +{ + private string[] changes; + + private Version version; + + [JsonProperty("tag_name", NullValueHandling = NullValueHandling.Ignore)] + public string TagName { get; set; } + + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("draft", NullValueHandling = NullValueHandling.Ignore)] + public bool Draft { get; set; } + + [JsonProperty("prerelease", NullValueHandling = NullValueHandling.Ignore)] + public bool Prerelease { get; set; } + + [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] + public List Assets { get; } = new(); + + [JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)] + public string Body { get; set; } + + public Version Version => version ??= new(TagName[1..]); + + public string[] Changes => changes ??= Body.Replace("- ", "").Split("\r\n"); +} + +public class Asset +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] + public string ContentType { get; set; } + + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public string State { get; set; } + + [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] + public int Size { get; set; } + + [JsonProperty("browser_download_url", NullValueHandling = NullValueHandling.Ignore)] + public string BrowserDownloadUrl { get; set; } +} \ No newline at end of file diff --git a/CreamInstaller/Utility/HttpClientManager.cs b/CreamInstaller/Utility/HttpClientManager.cs index 967a51f..16fa397 100644 --- a/CreamInstaller/Utility/HttpClientManager.cs +++ b/CreamInstaller/Utility/HttpClientManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Drawing; using System.Net; using System.Net.Http; @@ -14,6 +15,8 @@ internal static class HttpClientManager { internal static HttpClient HttpClient; + private static readonly Dictionary HttpContentCache = new(); + internal static void Setup() { HttpClient = new(); @@ -26,8 +29,12 @@ internal static class HttpClientManager { using HttpRequestMessage request = new(HttpMethod.Get, url); using HttpResponseMessage response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + if (response.StatusCode is HttpStatusCode.NotModified && HttpContentCache.TryGetValue(url, out string content)) + return content; _ = response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); + content = await response.Content.ReadAsStringAsync(); + HttpContentCache[url] = content; + return content; } catch (HttpRequestException e) {