diff --git a/ARKBreedingStats/ARKBreedingStats.csproj b/ARKBreedingStats/ARKBreedingStats.csproj index b670c328..1b45828a 100644 --- a/ARKBreedingStats/ARKBreedingStats.csproj +++ b/ARKBreedingStats/ARKBreedingStats.csproj @@ -56,6 +56,7 @@ + @@ -121,6 +122,8 @@ True strings.resx + + @@ -151,8 +154,20 @@ ATImportFileLocationDialog.cs + + Form + + + FtpProgress.cs + + + Form + + + FtpCredentials.cs + UserControl @@ -591,6 +606,12 @@ customSoundChooser.cs + + FtpProgress.cs + + + FtpCredentials.cs + SpeciesSelector.cs @@ -756,6 +777,9 @@ + + 27.1.2 + 11.0.2 diff --git a/ARKBreedingStats/Form1.cs b/ARKBreedingStats/Form1.cs index 24b5c034..c2a759cb 100644 --- a/ARKBreedingStats/Form1.cs +++ b/ARKBreedingStats/Form1.cs @@ -1,20 +1,26 @@ using ARKBreedingStats.duplicates; using ARKBreedingStats.Library; +using ARKBreedingStats.miscClasses; using ARKBreedingStats.ocr; using ARKBreedingStats.settings; using ARKBreedingStats.species; using ARKBreedingStats.uiControls; using ARKBreedingStats.values; +using FluentFTP; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Serialization; @@ -806,9 +812,27 @@ private async void RunSavegameImport(object sender, EventArgs e) { workingCopyfilename = Path.GetTempPath(); } - workingCopyfilename = Path.Combine(workingCopyfilename, Path.GetFileName(atImportFileLocation.FileLocation)); - File.Copy(atImportFileLocation.FileLocation, workingCopyfilename, true); + + if (Uri.TryCreate(atImportFileLocation.FileLocation, UriKind.Absolute, out var uri)) + { + switch(uri.Scheme) + { + case "ftp": + workingCopyfilename = await CopyFtpFileAsync(uri, atImportFileLocation.ConvenientName, workingCopyfilename); + if (workingCopyfilename == null) + // the user didn't enter credentials + return; + break; + default: + throw new Exception($"Unsuppoerted uri scheme: {uri.Scheme}"); + } + } + else + { + workingCopyfilename = Path.Combine(workingCopyfilename, Path.GetFileName(atImportFileLocation.FileLocation)); + File.Copy(atImportFileLocation.FileLocation, workingCopyfilename, true); + } await ImportSavegame.ImportCollectionFromSavegame(creatureCollection, workingCopyfilename, atImportFileLocation.ServerName); @@ -845,6 +869,156 @@ private async void RunSavegameImport(object sender, EventArgs e) } } + private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, string workingCopyFolder) + { + var credentialsByServerName = LoadSavedCredentials(); + credentialsByServerName.TryGetValue(serverName, out var credentials); + + var dialogText = $"Ftp Credentials for {serverName}"; + + while (true) + { + if (credentials == null) + { + // get new credentials + using (var dialog = new FtpCredentialsForm { Text = dialogText }) + { + if (dialog.ShowDialog(this) == DialogResult.Cancel) + { + return null; + } + + credentials = dialog.Credentials; + + if (dialog.SaveCredentials) + { + credentialsByServerName[serverName] = credentials; + Properties.Settings.Default.SavedFtpCredentials = Encryption.Protect(JsonConvert.SerializeObject(credentialsByServerName)); + Properties.Settings.Default.Save(); + } + } + } + + var client = new FtpClient(ftpUri.Host, ftpUri.Port, credentials.Username, credentials.Password); + + var cancellationTokenSource = new CancellationTokenSource(); + using (var progressDialog = new FtpProgressForm(cancellationTokenSource)) + { + try + { + progressDialog.StatusText = $"Authenticating"; + progressDialog.Show(this); + + await client.ConnectAsync(); + + progressDialog.StatusText = $"Finding most recent file"; + await Task.Yield(); + + var ftpPath = ftpUri.AbsolutePath; + var lastSegment = ftpUri.Segments.Last(); + if (lastSegment.Contains("*")) + { + var mostRecentlyModifiedMatch = await GetLastModifiedFileAsync(client, ftpUri, cancellationTokenSource.Token); + if (mostRecentlyModifiedMatch == null) + { + throw new Exception($"No file found matching pattern '{lastSegment}'"); + } + + ftpPath = mostRecentlyModifiedMatch.FullName; + } + + var fileName = Path.GetFileName(ftpPath); + + progressDialog.FileName = fileName; + progressDialog.StatusText = $"Downloading {fileName}"; + await Task.Yield(); + + var filePath = Path.Combine(workingCopyFolder, Path.GetFileName(ftpPath)); + await client.DownloadFileAsync(filePath, ftpPath, FtpLocalExists.Overwrite, FtpVerify.Retry, progressDialog, token: cancellationTokenSource.Token); + await Task.Delay(500); + + if (filePath.EndsWith(".gz")) + { + progressDialog.StatusText = $"Decompressing {fileName}"; + await Task.Yield(); + + filePath = await DecompressGZippedFileAsync(filePath, cancellationTokenSource.Token); + } + + return filePath; + } + catch (FtpAuthenticationException ex) + { + // if auth fails, clear credentials, alert the user and loop until the either auth succeeds or the user cancels + progressDialog.StatusText = $"Authentication failed: {ex.Message}"; + credentials = null; + await Task.Delay(1000); + } + catch(OperationCanceledException) + { + return null; + } + catch(Exception ex) + { + progressDialog.StatusText = $"Unexpected error: {ex.Message}"; + } + } + } + } + + /// + /// Loads the encrypted ftp crednetials from settings, decrypts them, then returns them as a hostname to credentials dictionary + /// + private static Dictionary LoadSavedCredentials() + { + try + { + var savedCredentials = Encryption.Unprotect(Properties.Settings.Default.SavedFtpCredentials); + + if (!string.IsNullOrEmpty(savedCredentials)) + { + var savedDictionary = JsonConvert.DeserializeObject>(savedCredentials); + + // Ensure that the resulting dictionary is case insensitive on hostname + return new Dictionary(savedDictionary, StringComparer.OrdinalIgnoreCase); + } + } + catch (Exception ex) + { + MessageBox.Show($"An error occured while loading saved ftp credentials. Message: \n\n{ex.Message}", + "Settings Error", MessageBoxButtons.OK); + } + + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + private async Task DecompressGZippedFileAsync(string filePath, CancellationToken cancellationToken) + { + var newFileName = filePath.Remove(filePath.Length - 3); + + using (var originalFileStream = File.OpenRead(filePath)) + using (var decompressedFileStream = File.Create(newFileName)) + using (var decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress)) + { + await decompressionStream.CopyToAsync(decompressedFileStream, 81920, cancellationToken); + } + + return newFileName; + } + + public async Task GetLastModifiedFileAsync(FtpClient client, Uri ftpUri, CancellationToken cancellationToken) + { + var folderUri = new Uri(ftpUri, "."); + var listItems = await client.GetListingAsync(folderUri.AbsolutePath, cancellationToken); + + // Turn the wildcard into a regex pattern "super*.foo" -> "^super.*?\.foo$" + var nameRegex = new Regex("^" + Regex.Escape(ftpUri.Segments.Last()).Replace(@"\*", ".*?") + "$"); + + return listItems + .OrderByDescending(x => x.Modified) + .FirstOrDefault(x => nameRegex.IsMatch(x.Name)); + } + /// /// Checks each creature for tags and saves them in a list. /// diff --git a/ARKBreedingStats/ImportSavegame.cs b/ARKBreedingStats/ImportSavegame.cs index a8252fa4..c39f6316 100644 --- a/ARKBreedingStats/ImportSavegame.cs +++ b/ARKBreedingStats/ImportSavegame.cs @@ -116,7 +116,7 @@ private static void importCollection(CreatureCollection creatureCollection, List if (creatureCollection.changeCreatureStatusOnSavegameImport) { // mark creatures that are no longer present as unavailable - var removedCreatures = creatureCollection.creatures.Where(c => c.status == CreatureStatus.Available).Except(newCreatures); + var removedCreatures = creatureCollection.creatures.Where(c => c.status == CreatureStatus.Available && c.server == serverName).Except(newCreatures); foreach (var c in removedCreatures) c.status = CreatureStatus.Unavailable; @@ -130,6 +130,7 @@ private static void importCollection(CreatureCollection creatureCollection, List { creature.server = serverName; }); + creatureCollection.mergeCreatureList(newCreatures, true); } diff --git a/ARKBreedingStats/Properties/Settings.Designer.cs b/ARKBreedingStats/Properties/Settings.Designer.cs index efd8cad3..96cb8002 100644 --- a/ARKBreedingStats/Properties/Settings.Designer.cs +++ b/ARKBreedingStats/Properties/Settings.Designer.cs @@ -676,6 +676,18 @@ public bool IncludeCooldownsInBreedingPlan { } } + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string SavedFtpCredentials { + get { + return ((string)(this["SavedFtpCredentials"])); + } + set { + this["SavedFtpCredentials"] = value; + } + } + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("50")] @@ -697,6 +709,7 @@ public bool prettifyCollectionJson { } set { this["prettifyCollectionJson"] = value; + } } } diff --git a/ARKBreedingStats/Properties/Settings.settings b/ARKBreedingStats/Properties/Settings.settings index bee06cfa..2acdd81c 100644 --- a/ARKBreedingStats/Properties/Settings.settings +++ b/ARKBreedingStats/Properties/Settings.settings @@ -169,6 +169,8 @@ 50 + + False diff --git a/ARKBreedingStats/library/CreatureCollection.cs b/ARKBreedingStats/library/CreatureCollection.cs index 176ceed1..0effa626 100644 --- a/ARKBreedingStats/library/CreatureCollection.cs +++ b/ARKBreedingStats/library/CreatureCollection.cs @@ -218,6 +218,12 @@ public bool mergeCreatureList(List creaturesToMerge, bool update = fal creaturesWereAdded = true; } + if (old.server != creature.server) + { + old.server = creature.server; + creaturesWereAdded = true; + } + if (!old.levelsWild.SequenceEqual(creature.levelsWild)) { old.levelsWild = creature.levelsWild; diff --git a/ARKBreedingStats/miscClasses/Encryption.cs b/ARKBreedingStats/miscClasses/Encryption.cs new file mode 100644 index 00000000..b57dba85 --- /dev/null +++ b/ARKBreedingStats/miscClasses/Encryption.cs @@ -0,0 +1,63 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace ARKBreedingStats.miscClasses +{ + class Encryption + { + private static byte[] additionalEntropy = Encoding.UTF8.GetBytes("Additional Entropy"); + + /// + /// Converts a string into a base64 encoded ProdectedData encrypted byte array + /// + /// The string to encrypt + public static string Protect(string value) + { + try + { + var decryptedBytes = Encoding.UTF8.GetBytes(value); + + // Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted + // only by the same current user. + var encryptedBytes = ProtectedData.Protect(decryptedBytes, additionalEntropy, DataProtectionScope.CurrentUser); + + return Convert.ToBase64String(encryptedBytes); + } + catch (CryptographicException e) + { + Console.WriteLine("Data was not encrypted. An error occurred."); + Console.WriteLine(e.ToString()); + return null; + } + } + + /// + /// Converts base64 encoded ProdectedData encrypted byte array into a string + /// + /// The string to decrypt + public static string Unprotect(string value) + { + if (value == null) + { + return null; + } + + try + { + var encryptedBytes = Convert.FromBase64String(value); + + //Decrypt the data using DataProtectionScope.CurrentUser. + var decryptedBytes = ProtectedData.Unprotect(encryptedBytes, additionalEntropy, DataProtectionScope.CurrentUser); + + return Encoding.UTF8.GetString(decryptedBytes); + } + catch (CryptographicException e) + { + Console.WriteLine("Data was not decrypted. An error occurred."); + Console.WriteLine(e.ToString()); + return null; + } + } + } +} diff --git a/ARKBreedingStats/miscClasses/FtpCredentials.cs b/ARKBreedingStats/miscClasses/FtpCredentials.cs new file mode 100644 index 00000000..693c24fe --- /dev/null +++ b/ARKBreedingStats/miscClasses/FtpCredentials.cs @@ -0,0 +1,9 @@ +namespace ARKBreedingStats.miscClasses +{ + public class FtpCredentials + { + public string Username { get; set; } + + public string Password { get; set; } + } +} diff --git a/ARKBreedingStats/settings/FtpCredentials.Designer.cs b/ARKBreedingStats/settings/FtpCredentials.Designer.cs new file mode 100644 index 00000000..f86ad7ef --- /dev/null +++ b/ARKBreedingStats/settings/FtpCredentials.Designer.cs @@ -0,0 +1,142 @@ +namespace ARKBreedingStats.settings +{ + partial class FtpCredentialsForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.textBox_Username = new System.Windows.Forms.TextBox(); + this.label_Username = new System.Windows.Forms.Label(); + this.textBox_Password = new System.Windows.Forms.TextBox(); + this.label_Password = new System.Windows.Forms.Label(); + this.button_Ok = new System.Windows.Forms.Button(); + this.button_Cancel = new System.Windows.Forms.Button(); + this.checkBox_SaveCredentials = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // textBox_Username + // + this.textBox_Username.Location = new System.Drawing.Point(12, 25); + this.textBox_Username.Name = "textBox_Username"; + this.textBox_Username.Size = new System.Drawing.Size(203, 20); + this.textBox_Username.TabIndex = 3; + // + // label_Username + // + this.label_Username.AutoSize = true; + this.label_Username.Location = new System.Drawing.Point(12, 9); + this.label_Username.Name = "label_Username"; + this.label_Username.Size = new System.Drawing.Size(55, 13); + this.label_Username.TabIndex = 2; + this.label_Username.Text = "Username"; + // + // textBox_Password + // + this.textBox_Password.Location = new System.Drawing.Point(12, 64); + this.textBox_Password.Name = "textBox_Password"; + this.textBox_Password.Size = new System.Drawing.Size(203, 20); + this.textBox_Password.TabIndex = 5; + this.textBox_Password.UseSystemPasswordChar = true; + // + // label_Password + // + this.label_Password.AutoSize = true; + this.label_Password.Location = new System.Drawing.Point(12, 48); + this.label_Password.Name = "label_Password"; + this.label_Password.Size = new System.Drawing.Size(53, 13); + this.label_Password.TabIndex = 4; + this.label_Password.Text = "Password"; + // + // button_Ok + // + this.button_Ok.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.button_Ok.DialogResult = System.Windows.Forms.DialogResult.OK; + this.button_Ok.Location = new System.Drawing.Point(268, 103); + this.button_Ok.Name = "button_Ok"; + this.button_Ok.Size = new System.Drawing.Size(75, 23); + this.button_Ok.TabIndex = 10; + this.button_Ok.Text = "OK"; + this.button_Ok.UseVisualStyleBackColor = true; + // + // button_Cancel + // + this.button_Cancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.button_Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.button_Cancel.Location = new System.Drawing.Point(187, 103); + this.button_Cancel.Name = "button_Cancel"; + this.button_Cancel.Size = new System.Drawing.Size(75, 23); + this.button_Cancel.TabIndex = 9; + this.button_Cancel.Text = "Cancel"; + this.button_Cancel.UseVisualStyleBackColor = true; + // + // checkBox_SaveCredentials + // + this.checkBox_SaveCredentials.AutoSize = true; + this.checkBox_SaveCredentials.Location = new System.Drawing.Point(15, 90); + this.checkBox_SaveCredentials.Name = "checkBox_SaveCredentials"; + this.checkBox_SaveCredentials.Size = new System.Drawing.Size(106, 17); + this.checkBox_SaveCredentials.TabIndex = 11; + this.checkBox_SaveCredentials.Text = "Save Credentials"; + this.checkBox_SaveCredentials.UseVisualStyleBackColor = true; + // + // FtpCredentialsForm + // + this.AcceptButton = this.button_Ok; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.button_Cancel; + this.ClientSize = new System.Drawing.Size(355, 138); + this.Controls.Add(this.checkBox_SaveCredentials); + this.Controls.Add(this.button_Ok); + this.Controls.Add(this.button_Cancel); + this.Controls.Add(this.textBox_Password); + this.Controls.Add(this.label_Password); + this.Controls.Add(this.textBox_Username); + this.Controls.Add(this.label_Username); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "FtpCredentialsForm"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Ftp Credentials"; + this.TopMost = true; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TextBox textBox_Username; + private System.Windows.Forms.Label label_Username; + private System.Windows.Forms.TextBox textBox_Password; + private System.Windows.Forms.Label label_Password; + private System.Windows.Forms.Button button_Ok; + private System.Windows.Forms.Button button_Cancel; + private System.Windows.Forms.CheckBox checkBox_SaveCredentials; + } +} \ No newline at end of file diff --git a/ARKBreedingStats/settings/FtpCredentials.cs b/ARKBreedingStats/settings/FtpCredentials.cs new file mode 100644 index 00000000..4b43b3b1 --- /dev/null +++ b/ARKBreedingStats/settings/FtpCredentials.cs @@ -0,0 +1,21 @@ +using ARKBreedingStats.miscClasses; +using System.Windows.Forms; + +namespace ARKBreedingStats.settings +{ + public partial class FtpCredentialsForm : Form + { + public FtpCredentialsForm() + { + InitializeComponent(); + } + + public FtpCredentials Credentials => new FtpCredentials + { + Username = textBox_Username.Text, + Password = textBox_Password.Text + }; + + public bool SaveCredentials => checkBox_SaveCredentials.Checked; + } +} diff --git a/ARKBreedingStats/settings/FtpCredentials.resx b/ARKBreedingStats/settings/FtpCredentials.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/ARKBreedingStats/settings/FtpCredentials.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ARKBreedingStats/settings/FtpProgress.Designer.cs b/ARKBreedingStats/settings/FtpProgress.Designer.cs new file mode 100644 index 00000000..23c9417f --- /dev/null +++ b/ARKBreedingStats/settings/FtpProgress.Designer.cs @@ -0,0 +1,81 @@ +namespace ARKBreedingStats.settings +{ + partial class FtpProgressForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button_Cancel = new System.Windows.Forms.Button(); + this.StatusLabel = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // button_Cancel + // + this.button_Cancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom; + this.button_Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.button_Cancel.Location = new System.Drawing.Point(155, 71); + this.button_Cancel.Name = "button_Cancel"; + this.button_Cancel.Size = new System.Drawing.Size(75, 23); + this.button_Cancel.TabIndex = 9; + this.button_Cancel.Text = "Cancel"; + this.button_Cancel.UseVisualStyleBackColor = true; + // + // StatusLabel + // + this.StatusLabel.Anchor = System.Windows.Forms.AnchorStyles.Top; + this.StatusLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.StatusLabel.Location = new System.Drawing.Point(12, 9); + this.StatusLabel.Name = "StatusLabel"; + this.StatusLabel.Size = new System.Drawing.Size(360, 59); + this.StatusLabel.TabIndex = 10; + this.StatusLabel.Text = "Status Text"; + this.StatusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // FtpProgressForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.button_Cancel; + this.ClientSize = new System.Drawing.Size(384, 106); + this.Controls.Add(this.StatusLabel); + this.Controls.Add(this.button_Cancel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "FtpProgressForm"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Ftp Progress"; + this.TopMost = true; + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.Button button_Cancel; + private System.Windows.Forms.Label StatusLabel; + } +} \ No newline at end of file diff --git a/ARKBreedingStats/settings/FtpProgress.cs b/ARKBreedingStats/settings/FtpProgress.cs new file mode 100644 index 00000000..3391de8c --- /dev/null +++ b/ARKBreedingStats/settings/FtpProgress.cs @@ -0,0 +1,46 @@ +using ARKBreedingStats.miscClasses; +using FluentFTP; +using System; +using System.Diagnostics; +using System.Threading; +using System.Windows.Forms; + +namespace ARKBreedingStats.settings +{ + public partial class FtpProgressForm : Form, IProgress + { + public FtpProgressForm(CancellationTokenSource cancellationTokenSource) + { + InitializeComponent(); + FormClosing += (sender, args) => cancellationTokenSource.Cancel(); + } + + public string StatusText + { + get + { + return StatusLabel.Text; + } + set + { + StatusLabel.Text = value; + } + } + + public string FileName { get; set; } + private Stopwatch stopwatch = new Stopwatch(); + + public void Report(FtpProgress value) + { + if(value.Progress < 100 && stopwatch.IsRunning && stopwatch.ElapsedMilliseconds < 250) + { + // only update the progress every 250ms unless setting it to 100% + return; + } + + var statusText = $"Downloading {FileName}\r\n{value.Progress:F0}% complete\r\n{value.TransferSpeedToString()}"; + StatusLabel.Invoke(new Action(() => StatusLabel.Text = statusText)); + stopwatch.Restart(); + } + } +} diff --git a/ARKBreedingStats/settings/FtpProgress.resx b/ARKBreedingStats/settings/FtpProgress.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/ARKBreedingStats/settings/FtpProgress.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file