From 5caf9a1e55d7abc134e1f71a37ce4c47fde81477 Mon Sep 17 00:00:00 2001 From: S <18078548+lotsofs@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:48:46 +0200 Subject: [PATCH 1/4] Add option to read upload key from file --- UI/Components/CollectorComponent.cs | 19 +- UI/Components/CollectorSettings.Designer.cs | 203 ++++++++++++-------- UI/Components/CollectorSettings.cs | 19 +- UI/Components/CollectorSettings.resx | 3 + 4 files changed, 154 insertions(+), 90 deletions(-) diff --git a/UI/Components/CollectorComponent.cs b/UI/Components/CollectorComponent.cs index 8f16b85..a1df72c 100644 --- a/UI/Components/CollectorComponent.cs +++ b/UI/Components/CollectorComponent.cs @@ -1,6 +1,7 @@ using LiveSplit.Model; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; @@ -32,6 +33,8 @@ public class CollectorComponent : LogicComponent private TimeSpan CurrentPausedTime = TimeSpan.Zero; private TimeSpan TimePausedBeforeResume = TimeSpan.Zero; + private string UploadKey = ""; + public CollectorComponent(LiveSplitState state) { State = state; @@ -120,7 +123,7 @@ private object buildLiveRunData() currentDuration = State.CurrentAttemptDuration.TotalMilliseconds, startTime = State.AttemptStarted.Time.ToUniversalTime(), endTime = State.AttemptEnded.Time.ToUniversalTime(), - uploadKey = Settings.Path, + uploadKey = GetUploadKey(), isPaused = TimerPaused, isGameTimePaused = State.IsGameTimePaused, gameTimePauseTime = State.GameTimePauseTime, @@ -195,16 +198,26 @@ public async void HandleReset(object sender, TimerPhase value) catch { } } + private string GetUploadKey() + { + string uploadKey = Settings.Path; + if (!string.IsNullOrEmpty(uploadKey)) return uploadKey; + + if (!File.Exists(Settings.FilePath)) return ""; + uploadKey = File.ReadAllText(Settings.FilePath).Trim(); + return uploadKey; + } + private bool AreSplitsValid() { - return GameName != "" && CategoryName != "" && Settings.Path.Length == 36; + return GameName != "" && CategoryName != "" && GetUploadKey().Length == 36; } public async Task UploadSplits() { if (!Settings.IsStatsUploadingEnabled) return; - string UploadKey = Settings.Path; + UploadKey = GetUploadKey(); string FileName = HttpUtility.UrlEncode(GameName) + "-" + HttpUtility.UrlEncode(CategoryName) + ".lss"; string FileUploadUrl = FileUploadBaseUrl + "?filename=" + FileName + "&uploadKey=" + UploadKey; diff --git a/UI/Components/CollectorSettings.Designer.cs b/UI/Components/CollectorSettings.Designer.cs index ba4cf8d..f7216b9 100644 --- a/UI/Components/CollectorSettings.Designer.cs +++ b/UI/Components/CollectorSettings.Designer.cs @@ -28,88 +28,118 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.txtPath = new System.Windows.Forms.TextBox(); - this.label1 = new System.Windows.Forms.Label(); - this.chkStatsUploadEnabled = new System.Windows.Forms.CheckBox(); - this.chkLiveTrackingEnabled = new System.Windows.Forms.CheckBox(); - this.tableLayoutPanel1.SuspendLayout(); - this.SuspendLayout(); - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.ColumnCount = 2; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 139F)); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.txtPath, 1, 0); - this.tableLayoutPanel1.Controls.Add(this.label1, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.chkLiveTrackingEnabled, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.chkStatsUploadEnabled, 0, 1); - this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 3; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 29F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(476, 141); - this.tableLayoutPanel1.TabIndex = 0; - // - // txtPath - // - this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.txtPath.Location = new System.Drawing.Point(142, 17); - this.txtPath.Name = "txtPath"; - this.txtPath.Size = new System.Drawing.Size(331, 20); - this.txtPath.TabIndex = 2; - this.txtPath.UseSystemPasswordChar = true; - this.txtPath.TextChanged += new System.EventHandler(this.txtPath_TextChanged); - // - // label1 - // - this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(3, 20); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(133, 13); - this.label1.TabIndex = 1; - this.label1.Text = "Upload Key"; - this.label1.Click += new System.EventHandler(this.label1_Click); - // - // chkStatsUploadEnabled - // - this.chkStatsUploadEnabled.AutoSize = true; - this.chkStatsUploadEnabled.Checked = true; - this.chkStatsUploadEnabled.CheckState = System.Windows.Forms.CheckState.Checked; - this.chkStatsUploadEnabled.Location = new System.Drawing.Point(3, 57); - this.chkStatsUploadEnabled.Name = "chkStatsUploadEnabled"; - this.chkStatsUploadEnabled.Size = new System.Drawing.Size(123, 17); - this.chkStatsUploadEnabled.TabIndex = 3; - this.chkStatsUploadEnabled.Text = "Enable Stats Upload"; - this.chkStatsUploadEnabled.UseVisualStyleBackColor = true; - // - // chkLiveTrackingEnabled - // - this.chkLiveTrackingEnabled.AutoSize = true; - this.chkLiveTrackingEnabled.Checked = true; - this.chkLiveTrackingEnabled.CheckState = System.Windows.Forms.CheckState.Checked; - this.chkLiveTrackingEnabled.Location = new System.Drawing.Point(3, 86); - this.chkLiveTrackingEnabled.Name = "chkLiveTrackingEnabled"; - this.chkLiveTrackingEnabled.Size = new System.Drawing.Size(127, 17); - this.chkLiveTrackingEnabled.TabIndex = 4; - this.chkLiveTrackingEnabled.Text = "Enable Live Tracking"; - this.chkLiveTrackingEnabled.UseVisualStyleBackColor = true; - // - // CollectorSettings - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.Controls.Add(this.tableLayoutPanel1); - this.Name = "CollectorSettings"; - this.Padding = new System.Windows.Forms.Padding(7); - this.Size = new System.Drawing.Size(476, 141); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.ResumeLayout(false); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.txtPath = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.chkStatsUploadEnabled = new System.Windows.Forms.CheckBox(); + this.chkLiveTrackingEnabled = new System.Windows.Forms.CheckBox(); + this.txtFile = new System.Windows.Forms.TextBox(); + this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); + this.buttonSelectFile = new System.Windows.Forms.Button(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.ColumnCount = 2; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 139F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Controls.Add(this.txtPath, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.label1, 0, 0); + this.tableLayoutPanel1.Controls.Add(this.chkLiveTrackingEnabled, 0, 3); + this.tableLayoutPanel1.Controls.Add(this.chkStatsUploadEnabled, 0, 2); + this.tableLayoutPanel1.Controls.Add(this.txtFile, 1, 1); + this.tableLayoutPanel1.Controls.Add(this.buttonSelectFile, 0, 1); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 4; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 29F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 33F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 45F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(476, 141); + this.tableLayoutPanel1.TabIndex = 0; + // + // txtPath + // + this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.txtPath.Location = new System.Drawing.Point(142, 7); + this.txtPath.Name = "txtPath"; + this.txtPath.Size = new System.Drawing.Size(331, 20); + this.txtPath.TabIndex = 2; + this.txtPath.UseSystemPasswordChar = true; + this.txtPath.TextChanged += new System.EventHandler(this.txtPath_TextChanged); + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, 10); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(133, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Upload Key"; + this.label1.Click += new System.EventHandler(this.label1_Click); + // + // chkStatsUploadEnabled + // + this.chkStatsUploadEnabled.AutoSize = true; + this.chkStatsUploadEnabled.Checked = true; + this.chkStatsUploadEnabled.CheckState = System.Windows.Forms.CheckState.Checked; + this.chkStatsUploadEnabled.Location = new System.Drawing.Point(3, 66); + this.chkStatsUploadEnabled.Name = "chkStatsUploadEnabled"; + this.chkStatsUploadEnabled.Size = new System.Drawing.Size(123, 17); + this.chkStatsUploadEnabled.TabIndex = 3; + this.chkStatsUploadEnabled.Text = "Enable Stats Upload"; + this.chkStatsUploadEnabled.UseVisualStyleBackColor = true; + // + // chkLiveTrackingEnabled + // + this.chkLiveTrackingEnabled.AutoSize = true; + this.chkLiveTrackingEnabled.Checked = true; + this.chkLiveTrackingEnabled.CheckState = System.Windows.Forms.CheckState.Checked; + this.chkLiveTrackingEnabled.Location = new System.Drawing.Point(3, 99); + this.chkLiveTrackingEnabled.Name = "chkLiveTrackingEnabled"; + this.chkLiveTrackingEnabled.Size = new System.Drawing.Size(127, 17); + this.chkLiveTrackingEnabled.TabIndex = 4; + this.chkLiveTrackingEnabled.Text = "Enable Live Tracking"; + this.chkLiveTrackingEnabled.UseVisualStyleBackColor = true; + // + // txtFile + // + this.txtFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.txtFile.Location = new System.Drawing.Point(142, 38); + this.txtFile.Name = "txtFile"; + this.txtFile.Size = new System.Drawing.Size(331, 20); + this.txtFile.TabIndex = 6; + this.txtFile.UseSystemPasswordChar = true; + // + // openFileDialog1 + // + this.openFileDialog1.FileName = "openFileDialog1"; + // + // buttonSelectFile + // + this.buttonSelectFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.buttonSelectFile.Location = new System.Drawing.Point(3, 37); + this.buttonSelectFile.Name = "buttonSelectFile"; + this.buttonSelectFile.Size = new System.Drawing.Size(133, 23); + this.buttonSelectFile.TabIndex = 7; + this.buttonSelectFile.Text = "OR Read Key from File"; + this.buttonSelectFile.UseVisualStyleBackColor = true; + this.buttonSelectFile.Click += new System.EventHandler(this.buttonSelectFile_Click); + // + // CollectorSettings + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.tableLayoutPanel1); + this.Name = "CollectorSettings"; + this.Padding = new System.Windows.Forms.Padding(7); + this.Size = new System.Drawing.Size(476, 141); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); } @@ -118,7 +148,10 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; private System.Windows.Forms.Label label1; public System.Windows.Forms.TextBox txtPath; - private System.Windows.Forms.CheckBox chkLiveTrackingEnabled; - private System.Windows.Forms.CheckBox chkStatsUploadEnabled; - } + private System.Windows.Forms.CheckBox chkLiveTrackingEnabled; + private System.Windows.Forms.CheckBox chkStatsUploadEnabled; + public System.Windows.Forms.TextBox txtFile; + private System.Windows.Forms.Button buttonSelectFile; + private System.Windows.Forms.OpenFileDialog openFileDialog1; + } } diff --git a/UI/Components/CollectorSettings.cs b/UI/Components/CollectorSettings.cs index cd921c3..1dff264 100644 --- a/UI/Components/CollectorSettings.cs +++ b/UI/Components/CollectorSettings.cs @@ -1,6 +1,7 @@ using System; using System.Xml; using System.Windows.Forms; +using System.IO; namespace LiveSplit.UI.Components { @@ -10,6 +11,7 @@ public partial class CollectorSettings : UserControl public LayoutMode Mode { get; set; } public string Path { get; set; } + public string FilePath { get; set; } public bool IsStatsUploadingEnabled { get; set; } public bool IsLiveTrackingEnabled { get; set; } @@ -18,12 +20,14 @@ public CollectorSettings() InitializeComponent(); txtPath.DataBindings.Add("Text", this, "Path", false, DataSourceUpdateMode.OnPropertyChanged); + txtFile.DataBindings.Add("Text", this, "FilePath", false, DataSourceUpdateMode.OnPropertyChanged); chkStatsUploadEnabled.DataBindings.Add("Checked", this, "IsStatsUploadingEnabled", false, DataSourceUpdateMode.OnPropertyChanged); chkLiveTrackingEnabled.DataBindings.Add("Checked", this, "IsLiveTrackingEnabled", false, DataSourceUpdateMode.OnPropertyChanged); Path = ""; + FilePath = ""; IsStatsUploadingEnabled = true; IsLiveTrackingEnabled = true; } @@ -34,6 +38,7 @@ public void SetSettings(XmlNode node) Version version = SettingsHelper.ParseVersion(element["Version"]); Path = SettingsHelper.ParseString(element["Path"]); + FilePath = SettingsHelper.ParseString(element["FilePath"]); IsStatsUploadingEnabled = element["IsStatsUploadingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsStatsUploadingEnabled"]); IsLiveTrackingEnabled = element["IsLiveTrackingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsLiveTrackingEnabled"]); } @@ -54,7 +59,8 @@ private int CreateSettingsNode(XmlDocument document, XmlElement parent) { return SettingsHelper.CreateSetting(document, parent, "Version", "1.0.0") ^ SettingsHelper.CreateSetting(document, parent, "Path", Path) ^ - SettingsHelper.CreateSetting(document, parent, + SettingsHelper.CreateSetting(document, parent, "FilePath", FilePath) ^ + SettingsHelper.CreateSetting(document, parent, "IsStatsUploadingEnabled", IsStatsUploadingEnabled) ^ SettingsHelper.CreateSetting(document, parent, "IsLiveTrackingEnabled", IsLiveTrackingEnabled); @@ -69,5 +75,14 @@ private void label1_Click(object sender, EventArgs e) { } - } + + private void buttonSelectFile_Click(object sender, EventArgs e) { + if (File.Exists(FilePath)) { + openFileDialog1.FileName = FilePath; + } + if (openFileDialog1.ShowDialog() == DialogResult.OK) { + FilePath = txtFile.Text = openFileDialog1.FileName; + } + } + } } diff --git a/UI/Components/CollectorSettings.resx b/UI/Components/CollectorSettings.resx index 1af7de1..9bad2f5 100644 --- a/UI/Components/CollectorSettings.resx +++ b/UI/Components/CollectorSettings.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + \ No newline at end of file From f0b0c99ae3646c3300181b74c120a2edd08439e9 Mon Sep 17 00:00:00 2001 From: S <18078548+lotsofs@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:51:08 +0200 Subject: [PATCH 2/4] Don't use pass censor for file directory. --- UI/Components/CollectorSettings.Designer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/UI/Components/CollectorSettings.Designer.cs b/UI/Components/CollectorSettings.Designer.cs index f7216b9..d7567d7 100644 --- a/UI/Components/CollectorSettings.Designer.cs +++ b/UI/Components/CollectorSettings.Designer.cs @@ -112,7 +112,6 @@ private void InitializeComponent() this.txtFile.Name = "txtFile"; this.txtFile.Size = new System.Drawing.Size(331, 20); this.txtFile.TabIndex = 6; - this.txtFile.UseSystemPasswordChar = true; // // openFileDialog1 // From 7e3525a7e5c37ff3e3113b8c0b07b810d8d513cc Mon Sep 17 00:00:00 2001 From: S <18078548+lotsofs@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:33:30 +0200 Subject: [PATCH 3/4] Make the written file the default and under-the-hood so there is no practical difference to the end user --- Components/LiveSplit.TheRun.dll | Bin 35328 -> 33280 bytes UI/Components/CollectorComponent.cs | 496 ++++++++++---------- UI/Components/CollectorFactory.cs | 2 +- UI/Components/CollectorSettings.Designer.cs | 77 +-- UI/Components/CollectorSettings.cs | 174 ++++--- UI/Components/CollectorSettings.resx | 3 - 6 files changed, 363 insertions(+), 389 deletions(-) diff --git a/Components/LiveSplit.TheRun.dll b/Components/LiveSplit.TheRun.dll index 4626b2fdc371cfb6f7b35d1f32b55c23ed82d655..834cfb375c050acfd673ef9dfaf72478a8260f4e 100644 GIT binary patch literal 33280 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 literal 35328 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 "therun.gg"; - - private LiveSplitState State { get; set; } - private CollectorSettings Settings { get; set; } - - private readonly HttpClient httpClient; - - private string SplitWebhookUrl => "https://dspc6ekj2gjkfp44cjaffhjeue0fbswr.lambda-url.eu-west-1.on.aws/"; - private string FileUploadBaseUrl => "https://2uxp372ks6nwrjnk6t7lqov4zu0solno.lambda-url.eu-west-1.on.aws/"; - - private string GameName = ""; - private string CategoryName = ""; - - private bool TimerPaused = false; - private bool WasJustResumed = false; - private TimeSpan CurrentPausedTime = TimeSpan.Zero; - private TimeSpan TimePausedBeforeResume = TimeSpan.Zero; - - private string UploadKey = ""; - - public CollectorComponent(LiveSplitState state) - { - State = state; - Settings = new CollectorSettings(); - - httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("Accept", "*/*"); - httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Site", "cross-site"); - httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Disposition", "attachment"); - - SetGameAndCategory(); - - State.OnStart += HandleSplit; - State.OnSplit += HandleSplit; - State.OnSkipSplit += HandleSplit; - State.OnUndoSplit += HandleSplit; - State.OnUndoAllPauses += HandleSplit; - - State.OnPause += HandlePause; - State.OnResume += HandleResume; - State.OnReset += HandleReset; - } - - public async Task UpdateSplitsState() - { - object returnData = buildLiveRunData(); - - JavaScriptSerializer serializer = new JavaScriptSerializer(); - var content = new StringContent(serializer.Serialize(returnData)); - - await httpClient.PostAsync(SplitWebhookUrl, content); - } - - private void SetGameAndCategory() - { - GameName = State.Run.GameName; - CategoryName = State.Run.CategoryName; - } - - private object buildLiveRunData() - { - var run = State.Run; - TimeSpan? CurrentTime = State.CurrentTime[State.CurrentTimingMethod]; - List runData = new List(); - - var MetaData = new - { - game = GameName, - category = CategoryName, - platform = run.Metadata.PlatformName, - region = run.Metadata.RegionName, - emulator = run.Metadata.UsesEmulator, - variables = run.Metadata.VariableValueNames - }; - - foreach (var segment in run) - { - List comparisons = new List(); - - foreach (string key in segment.Comparisons.Keys) - { - comparisons.Add(new - { - name = key, - time = ConvertTime(segment.Comparisons[key]) - }); - } - - runData.Add(new - { - name = segment.Name, - splitTime = ConvertTime(segment.SplitTime), - pbSplitTime = ConvertTime(segment.PersonalBestSplitTime), - bestPossible = ConvertTime(segment.BestSegmentTime), - comparisons = comparisons - }); - } - - return new - { - metadata = MetaData, - currentTime = ConvertTime(State.CurrentTime), - currentSplitName = State.CurrentSplit != null ? State.CurrentSplit.Name : "", - currentSplitIndex = State.CurrentSplitIndex, - timingMethod = State.CurrentTimingMethod, - currentDuration = State.CurrentAttemptDuration.TotalMilliseconds, - startTime = State.AttemptStarted.Time.ToUniversalTime(), - endTime = State.AttemptEnded.Time.ToUniversalTime(), - uploadKey = GetUploadKey(), - isPaused = TimerPaused, - isGameTimePaused = State.IsGameTimePaused, - gameTimePauseTime = State.GameTimePauseTime, - totalPauseTime = State.PauseTime, - currentPauseTime = TimePausedBeforeResume, - timePausedAt = State.TimePausedAt.TotalMilliseconds, - wasJustResumed = WasJustResumed, - currentComparison = State.CurrentComparison, - runData = runData - }; - } - - private double? ConvertTime(Time time) - { - if (time[State.CurrentTimingMethod] == null) return null; - - TimeSpan timeSpan = (TimeSpan) time[State.CurrentTimingMethod]; - - return timeSpan.TotalMilliseconds; - } - - public async void HandlePause(object sender, object e) - { - TimerPaused = true; - HandleSplit(sender, e); - } - - public async void HandleResume(object sender, object e) - { - - TimePausedBeforeResume = (TimeSpan)(State.PauseTime - CurrentPausedTime); - CurrentPausedTime = (TimeSpan) State.PauseTime; - TimerPaused = false; - WasJustResumed = true; - - HandleSplit(sender, e); - } - - - // TODO: Log or tell user when splits are invalid or when an error occurs. Don't just continue silently. - public async void HandleSplit(object sender, object e) - { - if (!AreSplitsValid() || !Settings.IsLiveTrackingEnabled) return; - - try - { - SetGameAndCategory(); - await UpdateSplitsState(); - - if (State.CurrentSplitIndex == State.Run.Count) - { - await UploadSplits(); - } - } - catch { } - - WasJustResumed = false; - } - - public async void HandleReset(object sender, TimerPhase value) - { - if (!AreSplitsValid()) return; - - try - { - SetGameAndCategory(); - if (Settings.IsLiveTrackingEnabled) - await UpdateSplitsState(); - - await UploadSplits(); - } - catch { } - } - - private string GetUploadKey() - { - string uploadKey = Settings.Path; - if (!string.IsNullOrEmpty(uploadKey)) return uploadKey; - - if (!File.Exists(Settings.FilePath)) return ""; - uploadKey = File.ReadAllText(Settings.FilePath).Trim(); - return uploadKey; + public class CollectorComponent : LogicComponent + { + public override string ComponentName => "therun.gg"; + + private LiveSplitState State { get; set; } + private CollectorSettings Settings { get; set; } + + private readonly HttpClient httpClient; + + private string SplitWebhookUrl => "https://dspc6ekj2gjkfp44cjaffhjeue0fbswr.lambda-url.eu-west-1.on.aws/"; + private string FileUploadBaseUrl => "https://2uxp372ks6nwrjnk6t7lqov4zu0solno.lambda-url.eu-west-1.on.aws/"; + + private string GameName = ""; + private string CategoryName = ""; + + private bool TimerPaused = false; + private bool WasJustResumed = false; + private TimeSpan CurrentPausedTime = TimeSpan.Zero; + private TimeSpan TimePausedBeforeResume = TimeSpan.Zero; + + public CollectorComponent(LiveSplitState state) + { + State = state; + Settings = new CollectorSettings(); + + httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Accept", "*/*"); + httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Site", "cross-site"); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Disposition", "attachment"); + + SetGameAndCategory(); + + State.OnStart += HandleSplit; + State.OnSplit += HandleSplit; + State.OnSkipSplit += HandleSplit; + State.OnUndoSplit += HandleSplit; + State.OnUndoAllPauses += HandleSplit; + + State.OnPause += HandlePause; + State.OnResume += HandleResume; + State.OnReset += HandleReset; } - private bool AreSplitsValid() - { - return GameName != "" && CategoryName != "" && GetUploadKey().Length == 36; - } + public async Task UpdateSplitsState() + { + object returnData = buildLiveRunData(); - public async Task UploadSplits() - { - if (!Settings.IsStatsUploadingEnabled) return; + JavaScriptSerializer serializer = new JavaScriptSerializer(); + var content = new StringContent(serializer.Serialize(returnData)); - UploadKey = GetUploadKey(); - string FileName = HttpUtility.UrlEncode(GameName) + "-" + HttpUtility.UrlEncode(CategoryName) + ".lss"; - string FileUploadUrl = FileUploadBaseUrl + "?filename=" + FileName + "&uploadKey=" + UploadKey; + await httpClient.PostAsync(SplitWebhookUrl, content); + } - var result = await httpClient.GetAsync(FileUploadUrl); - var responseBody = await result.Content.ReadAsStringAsync(); + private void SetGameAndCategory() + { + GameName = State.Run.GameName; + CategoryName = State.Run.CategoryName; + } - // Something went wrong, but the backend will handle the error, LiveSplit should just keep going. - // Probably the upload key was not filled in. - if (!result.IsSuccessStatusCode) return; + private object buildLiveRunData() + { + var run = State.Run; + TimeSpan? CurrentTime = State.CurrentTime[State.CurrentTimingMethod]; + List runData = new List(); + + var MetaData = new + { + game = GameName, + category = CategoryName, + platform = run.Metadata.PlatformName, + region = run.Metadata.RegionName, + emulator = run.Metadata.UsesEmulator, + variables = run.Metadata.VariableValueNames + }; + + foreach (var segment in run) + { + List comparisons = new List(); + + foreach (string key in segment.Comparisons.Keys) + { + comparisons.Add(new + { + name = key, + time = ConvertTime(segment.Comparisons[key]) + }); + } + + runData.Add(new + { + name = segment.Name, + splitTime = ConvertTime(segment.SplitTime), + pbSplitTime = ConvertTime(segment.PersonalBestSplitTime), + bestPossible = ConvertTime(segment.BestSegmentTime), + comparisons = comparisons + }); + } + + return new + { + metadata = MetaData, + currentTime = ConvertTime(State.CurrentTime), + currentSplitName = State.CurrentSplit != null ? State.CurrentSplit.Name : "", + currentSplitIndex = State.CurrentSplitIndex, + timingMethod = State.CurrentTimingMethod, + currentDuration = State.CurrentAttemptDuration.TotalMilliseconds, + startTime = State.AttemptStarted.Time.ToUniversalTime(), + endTime = State.AttemptEnded.Time.ToUniversalTime(), + uploadKey = Settings.UploadKey, + isPaused = TimerPaused, + isGameTimePaused = State.IsGameTimePaused, + gameTimePauseTime = State.GameTimePauseTime, + totalPauseTime = State.PauseTime, + currentPauseTime = TimePausedBeforeResume, + timePausedAt = State.TimePausedAt.TotalMilliseconds, + wasJustResumed = WasJustResumed, + currentComparison = State.CurrentComparison, + runData = runData + }; + } + + private double? ConvertTime(Time time) + { + if (time[State.CurrentTimingMethod] == null) return null; - JavaScriptSerializer ser = new JavaScriptSerializer(); - var JSONObj = ser.Deserialize>(responseBody); + TimeSpan timeSpan = (TimeSpan)time[State.CurrentTimingMethod]; - string url = HttpUtility.UrlDecode(JSONObj["url"]); - string correctlyEncodedUrl = EncodeUrl(url); + return timeSpan.TotalMilliseconds; + } - StringContent content = new StringContent(XmlRunAsString()); - content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment"); + public async void HandlePause(object sender, object e) + { + TimerPaused = true; + HandleSplit(sender, e); + } - await httpClient.PutAsync(correctlyEncodedUrl, content); - } + public async void HandleResume(object sender, object e) + { - private string EncodeUrl(string url) - { - string[] urlParts = url.Split('&').Select(urlPart => urlPart.StartsWith("X-Amz-Credential") || urlPart.StartsWith("X-Amz-Security-Token") || urlPart.StartsWith("X-Amz-SignedHeaders") ? HttpUtility.UrlEncode(urlPart).Replace("%3d", "=") : urlPart).ToArray(); + TimePausedBeforeResume = (TimeSpan)(State.PauseTime - CurrentPausedTime); + CurrentPausedTime = (TimeSpan)State.PauseTime; + TimerPaused = false; + WasJustResumed = true; - string newUrl = string.Join("&", urlParts).Replace(GameName, HttpUtility.UrlEncode(GameName)).Replace(CategoryName, HttpUtility.UrlEncode(CategoryName)); - string username = newUrl.Replace("https://splits-bucket-main.s3.eu-west-1.amazonaws.com/", "").Split('/')[0]; + HandleSplit(sender, e); + } - return newUrl.Replace(username, HttpUtility.UrlEncode(username)); - } + // TODO: Log or tell user when splits are invalid or when an error occurs. Don't just continue silently. + public async void HandleSplit(object sender, object e) + { + if (!AreSplitsValid() || !Settings.IsLiveTrackingEnabled) return; - private string XmlRunAsString() - { - Model.RunSavers.XMLRunSaver runSaver = new Model.RunSavers.XMLRunSaver(); - System.IO.MemoryStream stream = new System.IO.MemoryStream(); + try + { + SetGameAndCategory(); + await UpdateSplitsState(); - runSaver.Save(State.Run, stream); + if (State.CurrentSplitIndex == State.Run.Count) + { + await UploadSplits(); + } + } + catch { } - return Encoding.UTF8.GetString(stream.ToArray()); - } + WasJustResumed = false; + } - public override void Dispose() - { - State.OnStart -= HandleSplit; - State.OnSplit -= HandleSplit; - State.OnSkipSplit -= HandleSplit; - State.OnUndoSplit -= HandleSplit; - State.OnReset -= HandleReset; + public async void HandleReset(object sender, TimerPhase value) + { + if (!AreSplitsValid()) return; + + try + { + SetGameAndCategory(); + if (Settings.IsLiveTrackingEnabled) + await UpdateSplitsState(); + + await UploadSplits(); + } + catch { } + } + + private bool AreSplitsValid() + { + return GameName != "" && CategoryName != "" && Settings.UploadKey.Length == 36; + } - httpClient.Dispose(); - } + public async Task UploadSplits() + { + if (!Settings.IsStatsUploadingEnabled) return; - public override XmlNode GetSettings(XmlDocument document) - { - return Settings.GetSettings(document); - } + string FileName = HttpUtility.UrlEncode(GameName) + "-" + HttpUtility.UrlEncode(CategoryName) + ".lss"; + string FileUploadUrl = FileUploadBaseUrl + "?filename=" + FileName + "&uploadKey=" + Settings.UploadKey; - public override Control GetSettingsControl(LayoutMode mode) - { - Settings.Mode = mode; - return Settings; - } + var result = await httpClient.GetAsync(FileUploadUrl); + var responseBody = await result.Content.ReadAsStringAsync(); - public override void SetSettings(XmlNode settings) - { - Settings.SetSettings(settings); - } + // Something went wrong, but the backend will handle the error, LiveSplit should just keep going. + // Probably the upload key was not filled in. + if (!result.IsSuccessStatusCode) return; + + JavaScriptSerializer ser = new JavaScriptSerializer(); + var JSONObj = ser.Deserialize>(responseBody); + + string url = HttpUtility.UrlDecode(JSONObj["url"]); + string correctlyEncodedUrl = EncodeUrl(url); + + StringContent content = new StringContent(XmlRunAsString()); + content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment"); + + await httpClient.PutAsync(correctlyEncodedUrl, content); + } + + private string EncodeUrl(string url) + { + string[] urlParts = url.Split('&').Select(urlPart => urlPart.StartsWith("X-Amz-Credential") || urlPart.StartsWith("X-Amz-Security-Token") || urlPart.StartsWith("X-Amz-SignedHeaders") ? HttpUtility.UrlEncode(urlPart).Replace("%3d", "=") : urlPart).ToArray(); + + string newUrl = string.Join("&", urlParts).Replace(GameName, HttpUtility.UrlEncode(GameName)).Replace(CategoryName, HttpUtility.UrlEncode(CategoryName)); + string username = newUrl.Replace("https://splits-bucket-main.s3.eu-west-1.amazonaws.com/", "").Split('/')[0]; + + return newUrl.Replace(username, HttpUtility.UrlEncode(username)); + } + + private string XmlRunAsString() + { + Model.RunSavers.XMLRunSaver runSaver = new Model.RunSavers.XMLRunSaver(); + System.IO.MemoryStream stream = new System.IO.MemoryStream(); + + runSaver.Save(State.Run, stream); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + + public override void Dispose() + { + State.OnStart -= HandleSplit; + State.OnSplit -= HandleSplit; + State.OnSkipSplit -= HandleSplit; + State.OnUndoSplit -= HandleSplit; + State.OnReset -= HandleReset; + + httpClient.Dispose(); + } + + public override XmlNode GetSettings(XmlDocument document) + { + return Settings.GetSettings(document); + } + + public override Control GetSettingsControl(LayoutMode mode) + { + Settings.Mode = mode; + return Settings; + } + + public override void SetSettings(XmlNode settings) + { + Settings.SetSettings(settings); + } - public override void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode) { } - } + public override void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode) { } + } } diff --git a/UI/Components/CollectorFactory.cs b/UI/Components/CollectorFactory.cs index bcc73a8..b91c004 100644 --- a/UI/Components/CollectorFactory.cs +++ b/UI/Components/CollectorFactory.cs @@ -21,6 +21,6 @@ public class CollectorFactory : IComponentFactory public string UpdateURL => "https://raw.githubusercontent.com/therungg/LiveSplit.TheRun/main/"; public string XMLURL => UpdateURL + "update.LiveSplit.TheRun.xml"; - public Version Version => Version.Parse("0.2.2"); + public Version Version => Version.Parse("0.3.0"); } } \ No newline at end of file diff --git a/UI/Components/CollectorSettings.Designer.cs b/UI/Components/CollectorSettings.Designer.cs index d7567d7..dd69604 100644 --- a/UI/Components/CollectorSettings.Designer.cs +++ b/UI/Components/CollectorSettings.Designer.cs @@ -31,11 +31,8 @@ private void InitializeComponent() this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); this.txtPath = new System.Windows.Forms.TextBox(); this.label1 = new System.Windows.Forms.Label(); - this.chkStatsUploadEnabled = new System.Windows.Forms.CheckBox(); this.chkLiveTrackingEnabled = new System.Windows.Forms.CheckBox(); - this.txtFile = new System.Windows.Forms.TextBox(); - this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); - this.buttonSelectFile = new System.Windows.Forms.Button(); + this.chkStatsUploadEnabled = new System.Windows.Forms.CheckBox(); this.tableLayoutPanel1.SuspendLayout(); this.SuspendLayout(); // @@ -46,87 +43,62 @@ private void InitializeComponent() this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanel1.Controls.Add(this.txtPath, 1, 0); this.tableLayoutPanel1.Controls.Add(this.label1, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.chkLiveTrackingEnabled, 0, 3); - this.tableLayoutPanel1.Controls.Add(this.chkStatsUploadEnabled, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.txtFile, 1, 1); - this.tableLayoutPanel1.Controls.Add(this.buttonSelectFile, 0, 1); + this.tableLayoutPanel1.Controls.Add(this.chkLiveTrackingEnabled, 0, 2); + this.tableLayoutPanel1.Controls.Add(this.chkStatsUploadEnabled, 0, 1); this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 4; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.RowCount = 3; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 29F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 33F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 45F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58F)); this.tableLayoutPanel1.Size = new System.Drawing.Size(476, 141); this.tableLayoutPanel1.TabIndex = 0; // // txtPath // this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.txtPath.Location = new System.Drawing.Point(142, 7); + this.txtPath.Location = new System.Drawing.Point(142, 17); this.txtPath.Name = "txtPath"; this.txtPath.Size = new System.Drawing.Size(331, 20); this.txtPath.TabIndex = 2; this.txtPath.UseSystemPasswordChar = true; this.txtPath.TextChanged += new System.EventHandler(this.txtPath_TextChanged); + this.txtPath.Leave += new System.EventHandler(this.txtPath_Leave); // // label1 // this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(3, 10); + this.label1.Location = new System.Drawing.Point(3, 20); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(133, 13); this.label1.TabIndex = 1; this.label1.Text = "Upload Key"; this.label1.Click += new System.EventHandler(this.label1_Click); // - // chkStatsUploadEnabled - // - this.chkStatsUploadEnabled.AutoSize = true; - this.chkStatsUploadEnabled.Checked = true; - this.chkStatsUploadEnabled.CheckState = System.Windows.Forms.CheckState.Checked; - this.chkStatsUploadEnabled.Location = new System.Drawing.Point(3, 66); - this.chkStatsUploadEnabled.Name = "chkStatsUploadEnabled"; - this.chkStatsUploadEnabled.Size = new System.Drawing.Size(123, 17); - this.chkStatsUploadEnabled.TabIndex = 3; - this.chkStatsUploadEnabled.Text = "Enable Stats Upload"; - this.chkStatsUploadEnabled.UseVisualStyleBackColor = true; - // // chkLiveTrackingEnabled // this.chkLiveTrackingEnabled.AutoSize = true; this.chkLiveTrackingEnabled.Checked = true; this.chkLiveTrackingEnabled.CheckState = System.Windows.Forms.CheckState.Checked; - this.chkLiveTrackingEnabled.Location = new System.Drawing.Point(3, 99); + this.chkLiveTrackingEnabled.Location = new System.Drawing.Point(3, 86); this.chkLiveTrackingEnabled.Name = "chkLiveTrackingEnabled"; this.chkLiveTrackingEnabled.Size = new System.Drawing.Size(127, 17); this.chkLiveTrackingEnabled.TabIndex = 4; this.chkLiveTrackingEnabled.Text = "Enable Live Tracking"; this.chkLiveTrackingEnabled.UseVisualStyleBackColor = true; // - // txtFile - // - this.txtFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.txtFile.Location = new System.Drawing.Point(142, 38); - this.txtFile.Name = "txtFile"; - this.txtFile.Size = new System.Drawing.Size(331, 20); - this.txtFile.TabIndex = 6; - // - // openFileDialog1 - // - this.openFileDialog1.FileName = "openFileDialog1"; - // - // buttonSelectFile + // chkStatsUploadEnabled // - this.buttonSelectFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.buttonSelectFile.Location = new System.Drawing.Point(3, 37); - this.buttonSelectFile.Name = "buttonSelectFile"; - this.buttonSelectFile.Size = new System.Drawing.Size(133, 23); - this.buttonSelectFile.TabIndex = 7; - this.buttonSelectFile.Text = "OR Read Key from File"; - this.buttonSelectFile.UseVisualStyleBackColor = true; - this.buttonSelectFile.Click += new System.EventHandler(this.buttonSelectFile_Click); + this.chkStatsUploadEnabled.AutoSize = true; + this.chkStatsUploadEnabled.Checked = true; + this.chkStatsUploadEnabled.CheckState = System.Windows.Forms.CheckState.Checked; + this.chkStatsUploadEnabled.Location = new System.Drawing.Point(3, 57); + this.chkStatsUploadEnabled.Name = "chkStatsUploadEnabled"; + this.chkStatsUploadEnabled.Size = new System.Drawing.Size(123, 17); + this.chkStatsUploadEnabled.TabIndex = 3; + this.chkStatsUploadEnabled.Text = "Enable Stats Upload"; + this.chkStatsUploadEnabled.UseVisualStyleBackColor = true; // // CollectorSettings // @@ -147,10 +119,7 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; private System.Windows.Forms.Label label1; public System.Windows.Forms.TextBox txtPath; - private System.Windows.Forms.CheckBox chkLiveTrackingEnabled; - private System.Windows.Forms.CheckBox chkStatsUploadEnabled; - public System.Windows.Forms.TextBox txtFile; - private System.Windows.Forms.Button buttonSelectFile; - private System.Windows.Forms.OpenFileDialog openFileDialog1; - } + private System.Windows.Forms.CheckBox chkLiveTrackingEnabled; + private System.Windows.Forms.CheckBox chkStatsUploadEnabled; + } } diff --git a/UI/Components/CollectorSettings.cs b/UI/Components/CollectorSettings.cs index 1dff264..6d8f2f4 100644 --- a/UI/Components/CollectorSettings.cs +++ b/UI/Components/CollectorSettings.cs @@ -5,84 +5,104 @@ namespace LiveSplit.UI.Components { - public partial class CollectorSettings : UserControl - { - - public LayoutMode Mode { get; set; } - - public string Path { get; set; } - public string FilePath { get; set; } - public bool IsStatsUploadingEnabled { get; set; } - public bool IsLiveTrackingEnabled { get; set; } - - public CollectorSettings() - { - InitializeComponent(); - - txtPath.DataBindings.Add("Text", this, "Path", false, DataSourceUpdateMode.OnPropertyChanged); - txtFile.DataBindings.Add("Text", this, "FilePath", false, DataSourceUpdateMode.OnPropertyChanged); - chkStatsUploadEnabled.DataBindings.Add("Checked", this, "IsStatsUploadingEnabled", - false, DataSourceUpdateMode.OnPropertyChanged); - chkLiveTrackingEnabled.DataBindings.Add("Checked", this, "IsLiveTrackingEnabled", - false, DataSourceUpdateMode.OnPropertyChanged); - - Path = ""; - FilePath = ""; - IsStatsUploadingEnabled = true; - IsLiveTrackingEnabled = true; - } - - public void SetSettings(XmlNode node) - { - var element = (XmlElement)node; - - Version version = SettingsHelper.ParseVersion(element["Version"]); - Path = SettingsHelper.ParseString(element["Path"]); - FilePath = SettingsHelper.ParseString(element["FilePath"]); - IsStatsUploadingEnabled = element["IsStatsUploadingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsStatsUploadingEnabled"]); - IsLiveTrackingEnabled = element["IsLiveTrackingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsLiveTrackingEnabled"]); - } - - public XmlNode GetSettings(XmlDocument document) - { - var parent = document.CreateElement("Settings"); - CreateSettingsNode(document, parent); - return parent; - } - - public int GetSettingsHashCode() - { - return CreateSettingsNode(null, null); - } - - private int CreateSettingsNode(XmlDocument document, XmlElement parent) - { - return SettingsHelper.CreateSetting(document, parent, "Version", "1.0.0") ^ - SettingsHelper.CreateSetting(document, parent, "Path", Path) ^ - SettingsHelper.CreateSetting(document, parent, "FilePath", FilePath) ^ + public partial class CollectorSettings : UserControl + { + + public LayoutMode Mode { get; set; } + + private string UploadKeyFile = "Livesplit.TheRun/uploadkey.txt"; + private string UploadKeyFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + public string UploadKey { get { return GetUploadKey(); } } + + public string Path { get; set; } + public bool IsStatsUploadingEnabled { get; set; } + public bool IsLiveTrackingEnabled { get; set; } + + public CollectorSettings() + { + InitializeComponent(); + + chkStatsUploadEnabled.DataBindings.Add("Checked", this, "IsStatsUploadingEnabled", + false, DataSourceUpdateMode.OnPropertyChanged); + chkLiveTrackingEnabled.DataBindings.Add("Checked", this, "IsLiveTrackingEnabled", + false, DataSourceUpdateMode.OnPropertyChanged); + + Path = ""; + IsStatsUploadingEnabled = true; + IsLiveTrackingEnabled = true; + } + + public void SetSettings(XmlNode node) + { + var element = (XmlElement)node; + + Version version = SettingsHelper.ParseVersion(element["Version"]); + Path = SettingsHelper.ParseString(element["Path"]); + txtPath.Text = GetUploadKey(); + IsStatsUploadingEnabled = element["IsStatsUploadingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsStatsUploadingEnabled"]); + IsLiveTrackingEnabled = element["IsLiveTrackingEnabled"] == null ? true : SettingsHelper.ParseBool(element["IsLiveTrackingEnabled"]); + } + + private string GetUploadKey() + { + if (!string.IsNullOrEmpty(this.Path)) + { + string key = this.Path; + SaveUploadKey(key); + return key; + } + if (!string.IsNullOrEmpty(txtPath.Text)) + { + return txtPath.Text; + } + string filePath = System.IO.Path.Combine(UploadKeyFolder, UploadKeyFile); + if (!File.Exists(filePath)) return ""; + return File.ReadAllText(filePath).Trim(); + } + + private void SaveUploadKey(string key) + { + string filePath = System.IO.Path.Combine(UploadKeyFolder, UploadKeyFile); + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(filePath)); + File.WriteAllText(filePath, key); + this.Path = ""; + } + + public XmlNode GetSettings(XmlDocument document) + { + var parent = document.CreateElement("Settings"); + CreateSettingsNode(document, parent); + return parent; + } + + public int GetSettingsHashCode() + { + return CreateSettingsNode(null, null); + } + + private int CreateSettingsNode(XmlDocument document, XmlElement parent) + { + return SettingsHelper.CreateSetting(document, parent, "Version", "1.0.0") ^ + SettingsHelper.CreateSetting(document, parent, + "IsStatsUploadingEnabled", IsStatsUploadingEnabled) ^ SettingsHelper.CreateSetting(document, parent, - "IsStatsUploadingEnabled", IsStatsUploadingEnabled) ^ - SettingsHelper.CreateSetting(document, parent, - "IsLiveTrackingEnabled", IsLiveTrackingEnabled); - } - - private void txtPath_TextChanged(object sender, EventArgs e) - { - - } - - private void label1_Click(object sender, EventArgs e) - { - - } - - private void buttonSelectFile_Click(object sender, EventArgs e) { - if (File.Exists(FilePath)) { - openFileDialog1.FileName = FilePath; - } - if (openFileDialog1.ShowDialog() == DialogResult.OK) { - FilePath = txtFile.Text = openFileDialog1.FileName; - } + "IsLiveTrackingEnabled", IsLiveTrackingEnabled); + } + + private void txtPath_TextChanged(object sender, EventArgs e) + { + + } + + private void label1_Click(object sender, EventArgs e) + { + + } + + private void txtPath_Leave(object sender, EventArgs e) + { + SaveUploadKey(txtPath.Text); } } } diff --git a/UI/Components/CollectorSettings.resx b/UI/Components/CollectorSettings.resx index 9bad2f5..1af7de1 100644 --- a/UI/Components/CollectorSettings.resx +++ b/UI/Components/CollectorSettings.resx @@ -117,7 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 17, 17 - \ No newline at end of file From 24421ac900ad1ceed2d09723fbe28570cc3f150b Mon Sep 17 00:00:00 2001 From: S <18078548+lotsofs@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:39:01 +0200 Subject: [PATCH 4/4] 0.3.0 changelog entry --- update.LiveSplit.TheRun.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/update.LiveSplit.TheRun.xml b/update.LiveSplit.TheRun.xml index e812c39..3be1725 100644 --- a/update.LiveSplit.TheRun.xml +++ b/update.LiveSplit.TheRun.xml @@ -1,4 +1,12 @@  + + + + + + Store the Upload Key separately from the LiveSplit layout file (.lsl). Save and overwrite existing layout files to erase the key from it. + +