From 82e91880a0f5b9ec45e0868502c3fdf6c12564a8 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Mon, 10 Aug 2026 17:11:18 -0400 Subject: [PATCH 1/7] Update submodules --- ColorThief | 2 +- H.NotifyIcon | 2 +- Hi3Helper.EncTool | 2 +- Hi3Helper.Http | 2 +- Hi3Helper.Plugin.Core | 2 +- Hi3Helper.SharpDiscordRPC | 2 +- Hi3Helper.SimpleZipArchiveReader | 2 +- Hi3Helper.Sophon | 2 +- Hi3Helper.Win32 | 2 +- ImageEx | 2 +- SevenZipExtractor | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ColorThief b/ColorThief index 5a04db74a..b568f2973 160000 --- a/ColorThief +++ b/ColorThief @@ -1 +1 @@ -Subproject commit 5a04db74a50573469e48d8ffae4ece35f8707308 +Subproject commit b568f2973aa050c4a5665362da665c71945da2bb diff --git a/H.NotifyIcon b/H.NotifyIcon index 7422382b4..8cadbd2a7 160000 --- a/H.NotifyIcon +++ b/H.NotifyIcon @@ -1 +1 @@ -Subproject commit 7422382b4da5a738f84b400b31d23df1c1c0c319 +Subproject commit 8cadbd2a7a5d06473083ec59174de0ca04427c15 diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 98d8b5786..47eeba101 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 98d8b57866ea969fbaf029c4491864af9b7332d8 +Subproject commit 47eeba101eb7a34d8eb94251ad022ad7215f1d04 diff --git a/Hi3Helper.Http b/Hi3Helper.Http index 887674ee6..2d4c209d4 160000 --- a/Hi3Helper.Http +++ b/Hi3Helper.Http @@ -1 +1 @@ -Subproject commit 887674ee6365c4907d4b45061b26857726d9df74 +Subproject commit 2d4c209d4d462ee94a5e6a13e3c45abcfbca848e diff --git a/Hi3Helper.Plugin.Core b/Hi3Helper.Plugin.Core index 55d05702c..14293ba31 160000 --- a/Hi3Helper.Plugin.Core +++ b/Hi3Helper.Plugin.Core @@ -1 +1 @@ -Subproject commit 55d05702ccc96eedb041c8d66f0bb3940b1ce08f +Subproject commit 14293ba31be02b513d3088d6866727504fa78da0 diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC index a2d1cb7c6..e80e57c3e 160000 --- a/Hi3Helper.SharpDiscordRPC +++ b/Hi3Helper.SharpDiscordRPC @@ -1 +1 @@ -Subproject commit a2d1cb7c6d4d39660bc557bec7296aafb6d48ad3 +Subproject commit e80e57c3e758bc7a28f9c69fb266019e977d1a1a diff --git a/Hi3Helper.SimpleZipArchiveReader b/Hi3Helper.SimpleZipArchiveReader index 345d80576..2abcc93d8 160000 --- a/Hi3Helper.SimpleZipArchiveReader +++ b/Hi3Helper.SimpleZipArchiveReader @@ -1 +1 @@ -Subproject commit 345d8057652890010e7e3600849d91c0acddfcf0 +Subproject commit 2abcc93d837fd53b8893988e3eab85b89356f74c diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon index 9189e990e..ea21b1883 160000 --- a/Hi3Helper.Sophon +++ b/Hi3Helper.Sophon @@ -1 +1 @@ -Subproject commit 9189e990e2d8ef6a9ee5b3dfd77b41e1874f9cac +Subproject commit ea21b18838579add755fe1edd709b2f3daf0fb7c diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32 index c061d517c..ff564bec9 160000 --- a/Hi3Helper.Win32 +++ b/Hi3Helper.Win32 @@ -1 +1 @@ -Subproject commit c061d517ca757ebaf8ceeca955aac1b73ef41087 +Subproject commit ff564bec95894ef8da5e4dfb4f08e3057080cdfc diff --git a/ImageEx b/ImageEx index f129c5ef9..4027757a4 160000 --- a/ImageEx +++ b/ImageEx @@ -1 +1 @@ -Subproject commit f129c5ef97209c9f343291b09d06639f692e97eb +Subproject commit 4027757a4890e68fe1d32306cee95310a19d231e diff --git a/SevenZipExtractor b/SevenZipExtractor index eb15009af..5e0f4de32 160000 --- a/SevenZipExtractor +++ b/SevenZipExtractor @@ -1 +1 @@ -Subproject commit eb15009af23925d56453415d75e51e3192160a44 +Subproject commit 5e0f4de32a5da3ae441fdd5c95d3951ef703480e From 7c1a411faf6cd43d2ee0339f0c31b446433e7297 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Mon, 10 Aug 2026 17:51:53 -0400 Subject: [PATCH 2/7] feat(settings): render plugin game settings pages --- .../Plugins/PluginPresetConfigWrapper.cs | 2 + .../XAMLs/MainApp/MainPage.Navigation.cs | 4 +- .../PluginGameSettingsPage.cs | 283 ++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs diff --git a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs index bd0578c38..6c8a77d52 100644 --- a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs @@ -25,6 +25,7 @@ public partial class PluginPresetConfigWrapper : PresetConfig, IDisposable { public DiscordPresenceExtension.DiscordPresenceContext DiscordPresenceContext { get; } public GameManagerExtension.RunGameFromGameManagerContext RunGameContext { get; } + public GameSettingsExtension.GameSettingsContext GameSettingsContext { get; } public readonly PluginInfo PluginInfo; public readonly IPlugin Plugin; private readonly IPluginPresetConfig _config; @@ -50,6 +51,7 @@ private unsafe PluginPresetConfigWrapper(PluginInfo pluginInfo, IPluginPresetCon }; DiscordPresenceContext = new DiscordPresenceExtension.DiscordPresenceContext(pluginInfo.Handle, config); + GameSettingsContext = new GameSettingsExtension.GameSettingsContext(pluginInfo.Handle, config); } public unsafe GameManagerExtension.RunGameFromGameManagerContext UseToggledGameLaunchContext() diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs index fb8913904..ad9a7a1cf 100644 --- a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs +++ b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs @@ -203,11 +203,13 @@ void Impl() GameNameType.StarRail => typeof(StarRailGameSettingsPage), GameNameType.Genshin => typeof(GenshinGameSettingsPage), GameNameType.Zenless => typeof(ZenlessGameSettingsPage), + GameNameType.Plugin when presetConfig is PluginPresetConfigWrapper + { GameSettingsContext.HasPage: true } => typeof(PluginGameSettingsPage), _ => null }; NavigationViewItemsContext.GameSettingsPage.Item.Tag = gspPageType; - NavigationViewItemsContext.GameSettingsPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; + NavigationViewItemsContext.GameSettingsPage.Item.Visibility = gspPageType == null ? Visibility.Collapsed : Visibility.Visible; NavigationViewItemsContext.FileCleanupPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; } } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs new file mode 100644 index 000000000..31d9f03a3 --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -0,0 +1,283 @@ +using CollapseLauncher.Plugins; +using CollapseLauncher.Helper; +using Hi3Helper.Plugin.Core.UI.Settings; +using Hi3Helper.Plugin.Core.Utility; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System; +using System.Globalization; +using static CollapseLauncher.Statics.GamePropertyVault; + +#nullable enable +namespace CollapseLauncher.Pages; + +/// +/// Renders the declarative game settings page exposed by a v0.1.6 plugin. +/// +public sealed class PluginGameSettingsPage : Page +{ + private readonly GameSettingsExtension.GameSettingsContext _context; + private readonly TextBlock _statusText = new() + { + Margin = new Thickness(16, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap + }; + + public PluginGameSettingsPage() + { + NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; + + if (GetCurrentGameProperty().GameVersion.GamePreset is not PluginPresetConfigWrapper preset) + { + throw new InvalidOperationException("The current game preset is not provided by a plugin"); + } + + _context = preset.GameSettingsContext; + Content = CreateContent(); + } + + private UIElement CreateContent() + { + if (!_context.TryGetPage(out GameSettingsPage? page, out Exception? error) || page == null) + { + return new TextBlock + { + Margin = new Thickness(32, 40, 32, 32), + Text = error?.Message ?? "This plugin did not provide a game settings page.", + TextWrapping = TextWrapping.Wrap + }; + } + + Grid root = new(); + root.RowDefinitions.Add(new RowDefinition()); + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + StackPanel sectionsPanel = new() { Margin = new Thickness(32, 40, 32, 32), Spacing = 24 }; + if (!string.IsNullOrWhiteSpace(page.Title)) + { + sectionsPanel.Children.Add(new TextBlock + { + Text = page.Title, + Style = Application.Current.Resources["TitleLargeTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + } + + foreach (GameSettingsSection section in page.Sections) + { + sectionsPanel.Children.Add(CreateSection(section)); + } + + ScrollViewer scrollViewer = new() + { + Content = sectionsPanel, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto + }; + root.Children.Add(scrollViewer); + + Grid applyPanel = new() + { + Padding = new Thickness(32, 16, 32, 16), + Background = Application.Current.Resources["GameSettingsApplyGridBrush"] as Brush + }; + applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); + applyPanel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + applyPanel.Children.Add(_statusText); + + Button applyButton = new() + { + Content = Locale.Current.Lang?._GameSettingsPage?.ApplyBtn ?? "Apply settings", + MinWidth = 144, + CornerRadius = new CornerRadius(16), + Style = Application.Current.Resources["AccentButtonStyle"] as Style + }; + applyButton.Click += OnApply; + Grid.SetColumn(applyButton, 1); + applyPanel.Children.Add(applyButton); + Grid.SetRow(applyPanel, 1); + root.Children.Add(applyPanel); + + return root; + } + + private FrameworkElement CreateSection(GameSettingsSection section) + { + StackPanel panel = new() { Spacing = 8 }; + panel.Children.Add(new TextBlock + { + Text = section.Title, + Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + + if (!string.IsNullOrWhiteSpace(section.Description)) + { + panel.Children.Add(new TextBlock + { + Text = section.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + } + + foreach (GameSettingEntry entry in section.Entries) + { + panel.Children.Add(CreateEntry(entry)); + } + + return panel; + } + + private FrameworkElement CreateEntry(GameSettingEntry entry) + { + Grid card = new() + { + Padding = new Thickness(16, 12, 16, 12), + ColumnSpacing = 24, + Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush, + CornerRadius = new CornerRadius(8) + }; + card.ColumnDefinitions.Add(new ColumnDefinition()); + card.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + StackPanel text = new() { VerticalAlignment = VerticalAlignment.Center, Spacing = 2 }; + text.Children.Add(new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }); + if (!string.IsNullOrWhiteSpace(entry.Description)) + { + text.Children.Add(new TextBlock + { + Text = entry.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + } + card.Children.Add(text); + + FrameworkElement editor = CreateEditor(entry); + editor.MinWidth = entry.Kind is GameSettingKind.Toggle ? 0 : 180; + editor.VerticalAlignment = VerticalAlignment.Center; + Grid.SetColumn(editor, 1); + card.Children.Add(editor); + return card; + } + + private FrameworkElement CreateEditor(GameSettingEntry entry) => entry.Kind switch + { + GameSettingKind.Toggle => CreateToggle(entry), + GameSettingKind.Text => CreateText(entry), + GameSettingKind.Number => CreateNumber(entry), + GameSettingKind.Slider => CreateSlider(entry), + GameSettingKind.Choice => CreateChoice(entry), + _ => throw new ArgumentOutOfRangeException(nameof(entry.Kind)) + }; + + private ToggleSwitch CreateToggle(GameSettingEntry entry) + { + ToggleSwitch control = new() { IsOn = bool.TryParse(entry.Value, out bool value) && value }; + control.Toggled += (_, _) => SetValue(entry.Key, control.IsOn ? bool.TrueString : bool.FalseString); + return control; + } + + private TextBox CreateText(GameSettingEntry entry) + { + TextBox control = new() { Text = entry.Value, PlaceholderText = entry.Placeholder }; + control.TextChanged += (_, _) => SetValue(entry.Key, control.Text); + return control; + } + + private NumberBox CreateNumber(GameSettingEntry entry) + { + NumberBox control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + SmallChange = entry.Step, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + Value = ParseNumber(entry.Value, entry.Minimum) + }; + control.ValueChanged += (_, args) => + { + if (!double.IsNaN(args.NewValue)) + { + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + } + }; + return control; + } + + private Slider CreateSlider(GameSettingEntry entry) + { + Slider control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + StepFrequency = entry.Step, + Value = ParseNumber(entry.Value, entry.Minimum), + Width = 220 + }; + control.ValueChanged += (_, args) => + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + return control; + } + + private ComboBox CreateChoice(GameSettingEntry entry) + { + ComboBox control = new(); + foreach (GameSettingChoice choice in entry.Choices ?? []) + { + ComboBoxItem item = new() { Content = choice.Title, Tag = choice.Value }; + control.Items.Add(item); + if (string.Equals(choice.Value, entry.Value, StringComparison.Ordinal)) + { + control.SelectedItem = item; + } + } + + control.SelectionChanged += (_, _) => + { + if (control.SelectedItem is ComboBoxItem { Tag: string value }) + { + SetValue(entry.Key, value); + } + }; + return control; + } + + private static double ParseNumber(string value, double fallback) => + double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) ? result : fallback; + + private void SetValue(string key, string value) + { + try + { + _context.SetValue(key, value); + SetStatus(null); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + } + } + + private void OnApply(object sender, RoutedEventArgs args) + { + try + { + _context.Apply(); + SetStatus(Locale.Current.Lang?._GameSettingsPage?.SettingsApplied ?? "Settings applied."); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + } + } + + private void SetStatus(string? text, bool isError = false) + { + _statusText.Text = text ?? string.Empty; + _statusText.Foreground = isError ? new SolidColorBrush(Colors.IndianRed) : null; + } +} From 847a75f715ac8fb45c094056fa6e25147df354ca Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 18 Aug 2026 02:39:05 -0400 Subject: [PATCH 3/7] Hydrate background activity notification state Expose `Progress` and `Status` on `IBackgroundActivity` and use them when wiring notification handlers in `BackgroundActivityManager`. This immediately initializes notification text/progress for already-running activities instead of waiting for the next event, preventing placeholder content from lingering. --- .../Classes/EventsManagement/BackgroundActivityManager.cs | 6 ++++++ CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs | 2 ++ 2 files changed, 8 insertions(+) diff --git a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs index 2ec01077c..d1d83fc86 100644 --- a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs +++ b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs @@ -259,6 +259,12 @@ private static void AttachEventToNotification(PresetConfig presetConfig, IBackgr activity!.ProgressChanged += progressChangedEventHandler; activity!.StatusChanged += statusChangedEventHandler; + // The notification can be attached after the operation has already + // started. Hydrate it immediately instead of waiting for another + // progress/status event and leaving the placeholder text visible. + progressChangedEventHandler(activity, activity.Progress); + statusChangedEventHandler(activity, activity.Status); + activity.FlushingTrigger += (_, _) => { activity.ProgressChanged -= progressChangedEventHandler; diff --git a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs index 3eeda615b..ba378bf23 100644 --- a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs +++ b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs @@ -11,6 +11,8 @@ internal interface IBackgroundActivity event EventHandler StatusChanged; event EventHandler FlushingTrigger; + TotalPerFileProgress Progress { get; } + TotalPerFileStatus Status { get; } bool IsRunning { get; } UIElement ParentUI { get; } void CancelRoutine(); From 6ddfd39fa12a7358558f501ba0c0fdc399088d43 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 16:05:37 -0400 Subject: [PATCH 4/7] Reset install speed baseline on state changes Add a reusable speed-calculator reset helper in `ProgressBase` and use it in plugin install progress updates to avoid bogus speed deltas after state transitions. The wrapper now resets its download baseline when install state changes, computes per-tick bytes only from monotonic progress, and clears speed/time-left values appropriately. It also ensures completed installs report zero time left and clamp per-file progress to 100%. --- .../Classes/Interfaces/Class/ProgressBase.cs | 7 ++++ .../Plugins/PluginGameInstallWrapper.cs | 37 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index 9ca682bfa..88f3d842a 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -467,6 +467,13 @@ protected virtual void UpdateProgressCopyStream(long currentPosition, int read, protected double CalculateSpeed(long receivedBytes) => CalculateSpeed(receivedBytes, ref _scLastSpeed, ref _scLastReceivedBytes, ref _scLastTick); + protected void ResetSpeedCalculator() + { + Interlocked.Exchange(ref _scLastReceivedBytes, 0); + Interlocked.Exchange(ref _scLastTick, Environment.TickCount64); + _scLastSpeed = 0; + } + protected static double CalculateSpeed(long receivedBytes, ref double lastSpeedToUse, ref long lastReceivedBytesToUse, ref long lastTickToUse) { long currentTick = Environment.TickCount64 - lastTickToUse + 1; diff --git a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs index f6fc94a65..c060d5371 100644 --- a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs @@ -73,6 +73,7 @@ public override string GamePath private InstallProgressState _currentInstallState = InstallProgressState.Idle; private string _lastActivityStatus = string.Empty; private float _lastLoggedPercentage = -1f; + private bool _resetSpeedBaseline = true; private PerFileProgressCallbackNative? _perFileProgressDelegate; private GCHandle _perFileProgressGcHandle; @@ -558,7 +559,18 @@ private void UpdateProgressCallback(in InstallProgress delegateProgress) long downloadedBytes = delegateProgress.DownloadedBytes; long downloadedBytesTotal = delegateProgress.TotalBytesToDownload; - long readDownload = delegateProgress.DownloadedBytes - _updateProgressProperty.LastDownloaded; + long readDownload = 0; + if (_resetSpeedBaseline) + { + _resetSpeedBaseline = false; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + else if (downloadedBytes >= _updateProgressProperty.LastDownloaded) + { + readDownload = downloadedBytes - _updateProgressProperty.LastDownloaded; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + double currentSpeed = CalculateSpeed(readDownload); Progress.ProgressAllSizeCurrent = downloadedBytes; @@ -605,8 +617,6 @@ or InstallProgressState.Verify : 0; } - _updateProgressProperty.LastDownloaded = downloadedBytes; - PublishProgressUi(updateProgressBar: true); } } @@ -623,7 +633,26 @@ private void UpdateStatusCallback(InstallProgressState delegateState) { using (_updateStatusLock.EnterScope()) { - _currentInstallState = delegateState; + if (_currentInstallState != delegateState) + { + _currentInstallState = delegateState; + _resetSpeedBaseline = true; + ResetSpeedCalculator(); + Progress.ProgressAllSpeed = 0; + } + + if (delegateState == InstallProgressState.Completed) + { + Progress.ProgressAllTimeLeft = TimeSpan.Zero; + if (Progress.ProgressPerFileSizeTotal > 0) + { + Progress.ProgressPerFilePercentage = Math.Min(100d, + ConverterTool.ToPercentage( + Progress.ProgressPerFileSizeTotal, + Progress.ProgressPerFileSizeCurrent)); + } + } + ApplyActivityStatusFromProperty(); PublishProgressUi(updateProgressBar: true); } From 3945b9dc2be5294cd8a5cb308fdee688f5d9c854 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 16:46:00 -0400 Subject: [PATCH 5/7] Bump submodules --- Hi3Helper.TaskScheduler/packages.lock.json | 101 ++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/Hi3Helper.TaskScheduler/packages.lock.json b/Hi3Helper.TaskScheduler/packages.lock.json index 0849386bc..9fed7d551 100644 --- a/Hi3Helper.TaskScheduler/packages.lock.json +++ b/Hi3Helper.TaskScheduler/packages.lock.json @@ -15,10 +15,105 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "hi3helper.win32": { - "type": "Project", + "Fody": { + "type": "Direct", + "requested": "[6.9.3, )", + "resolved": "6.9.3", + "contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3" + } + }, + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Direct", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==" + }, + "TaskScheduler": { + "type": "Direct", + "requested": "[2.12.2, )", + "resolved": "2.12.2", + "contentHash": "glpAb3VrwfdAofp6PIyAzL0ZeTV7XUJ8muu0oZoTeyU5jtk2sMJ6QAMRRuFbovcaj+SBJiEUGklxIWOqQoxshA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net462": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" + } + } + }, + ".NETFramework,Version=v4.6.2/win-x64": { + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.12, )" + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" } } } From 90c9c1a7f2feff63a9df045ac16b6b1115fd71d7 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 18:29:16 -0400 Subject: [PATCH 6/7] Fix opacity causing elements not to render correctly --- .../Pages/GameSettingsPages/PluginGameSettingsPage.cs | 9 ++++++++- .../Pages/GameSettingsPages/PluginGameSettingsPage.xaml | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 31d9f03a3..8778c0e9c 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -1,5 +1,6 @@ using CollapseLauncher.Plugins; using CollapseLauncher.Helper; +using CollapseLauncher.GameManagement.ImageBackground; using Hi3Helper.Plugin.Core.UI.Settings; using Hi3Helper.Plugin.Core.Utility; using Microsoft.UI; @@ -16,7 +17,7 @@ namespace CollapseLauncher.Pages; /// /// Renders the declarative game settings page exposed by a v0.1.6 plugin. /// -public sealed class PluginGameSettingsPage : Page +public sealed partial class PluginGameSettingsPage : Page { private readonly GameSettingsExtension.GameSettingsContext _context; private readonly TextBlock _statusText = new() @@ -28,6 +29,12 @@ public sealed class PluginGameSettingsPage : Page public PluginGameSettingsPage() { + InitializeComponent(); + + ImageBackgroundManager.Shared.IsBackgroundElevated = true; + ImageBackgroundManager.Shared.ForegroundOpacity = 0d; + ImageBackgroundManager.Shared.SmokeOpacity = 1d; + NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; if (GetCurrentGameProperty().GameVersion.GamePreset is not PluginPresetConfigWrapper preset) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml new file mode 100644 index 000000000..9b59cc711 --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml @@ -0,0 +1,5 @@ + + From e46deea69446eb1e9e901be2a5ec2a2179e8c6cf Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Sun, 6 Sep 2026 23:16:45 -0400 Subject: [PATCH 7/7] Improve plugin settings layout Refactor GSP into a responsive multi-column layout while keeping informational sections full width --- .../PluginGameSettingsPage.cs | 108 ++++++++++++------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 8778c0e9c..0c8dc08fc 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -6,9 +6,11 @@ using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Media; using System; using System.Globalization; +using Microsoft.UI.Text; using static CollapseLauncher.Statics.GamePropertyVault; #nullable enable @@ -73,11 +75,20 @@ private UIElement CreateContent() }); } + Grid sectionGrid = new() { ColumnSpacing = 32, RowSpacing = 28 }; foreach (GameSettingsSection section in page.Sections) { - sectionsPanel.Children.Add(CreateSection(section)); + // Informational sections, including plugin warnings, remain full width. + if (section.Entries.Count == 0) + sectionsPanel.Children.Add(CreateSection(section)); + else + sectionGrid.Children.Add(CreateSection(section)); } + sectionsPanel.Children.Add(sectionGrid); + sectionGrid.SizeChanged += (_, args) => ArrangeGrid(sectionGrid, args.NewSize.Width >= 900 ? 2 : 1); + ArrangeGrid(sectionGrid, 1); + ScrollViewer scrollViewer = new() { Content = sectionsPanel, @@ -90,8 +101,9 @@ private UIElement CreateContent() Padding = new Thickness(32, 16, 32, 16), Background = Application.Current.Resources["GameSettingsApplyGridBrush"] as Brush }; - applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); applyPanel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); + Grid.SetColumn(_statusText, 1); applyPanel.Children.Add(_statusText); Button applyButton = new() @@ -102,7 +114,7 @@ private UIElement CreateContent() Style = Application.Current.Resources["AccentButtonStyle"] as Style }; applyButton.Click += OnApply; - Grid.SetColumn(applyButton, 1); + Grid.SetColumn(applyButton, 0); applyPanel.Children.Add(applyButton); Grid.SetRow(applyPanel, 1); root.Children.Add(applyPanel); @@ -110,9 +122,29 @@ private UIElement CreateContent() return root; } + private static void ArrangeGrid(Grid grid, int columns) + { + int rows = (grid.Children.Count + columns - 1) / columns; + if (grid.ColumnDefinitions.Count == columns && grid.RowDefinitions.Count == rows) + return; + + grid.ColumnDefinitions.Clear(); + grid.RowDefinitions.Clear(); + for (int column = 0; column < columns; column++) + grid.ColumnDefinitions.Add(new ColumnDefinition()); + for (int row = 0; row < rows; row++) + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + for (int index = 0; index < grid.Children.Count; index++) + { + FrameworkElement child = (FrameworkElement)grid.Children[index]; + Grid.SetColumn(child, index % columns); + Grid.SetRow(child, index / columns); + } + } + private FrameworkElement CreateSection(GameSettingsSection section) { - StackPanel panel = new() { Spacing = 8 }; + StackPanel panel = new() { Spacing = 12, VerticalAlignment = VerticalAlignment.Top }; panel.Children.Add(new TextBlock { Text = section.Title, @@ -130,45 +162,50 @@ private FrameworkElement CreateSection(GameSettingsSection section) }); } + Grid entries = new() { ColumnSpacing = 16, RowSpacing = 16 }; foreach (GameSettingEntry entry in section.Entries) + entries.Children.Add(CreateEntry(entry)); + + if (entries.Children.Count > 0) { - panel.Children.Add(CreateEntry(entry)); + entries.SizeChanged += (_, args) => + ArrangeGrid(entries, args.NewSize.Width >= 520 ? 3 : args.NewSize.Width >= 340 ? 2 : 1); + ArrangeGrid(entries, 1); + panel.Children.Add(entries); } - return panel; } private FrameworkElement CreateEntry(GameSettingEntry entry) { - Grid card = new() - { - Padding = new Thickness(16, 12, 16, 12), - ColumnSpacing = 24, - Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush, - CornerRadius = new CornerRadius(8) - }; - card.ColumnDefinitions.Add(new ColumnDefinition()); - card.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + FrameworkElement editor = CreateEditor(entry); + editor.HorizontalAlignment = HorizontalAlignment.Stretch; + editor.VerticalAlignment = VerticalAlignment.Top; + AutomationProperties.SetName(editor, entry.Title); - StackPanel text = new() { VerticalAlignment = VerticalAlignment.Center, Spacing = 2 }; - text.Children.Add(new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }); + StackPanel panel = new() { Spacing = 6 }; + if (entry.Kind != GameSettingKind.Toggle) + { + panel.Children.Add(new TextBlock + { + Text = entry.Title, + FontWeight = FontWeights.SemiBold, + TextWrapping = TextWrapping.Wrap + }); + } + panel.Children.Add(editor); if (!string.IsNullOrWhiteSpace(entry.Description)) { - text.Children.Add(new TextBlock + panel.Children.Add(new TextBlock { Text = entry.Description, + FontSize = 12, Opacity = 0.72, TextWrapping = TextWrapping.Wrap }); + AutomationProperties.SetHelpText(editor, entry.Description); } - card.Children.Add(text); - - FrameworkElement editor = CreateEditor(entry); - editor.MinWidth = entry.Kind is GameSettingKind.Toggle ? 0 : 180; - editor.VerticalAlignment = VerticalAlignment.Center; - Grid.SetColumn(editor, 1); - card.Children.Add(editor); - return card; + return panel; } private FrameworkElement CreateEditor(GameSettingEntry entry) => entry.Kind switch @@ -181,10 +218,15 @@ private FrameworkElement CreateEntry(GameSettingEntry entry) _ => throw new ArgumentOutOfRangeException(nameof(entry.Kind)) }; - private ToggleSwitch CreateToggle(GameSettingEntry entry) + private CheckBox CreateToggle(GameSettingEntry entry) { - ToggleSwitch control = new() { IsOn = bool.TryParse(entry.Value, out bool value) && value }; - control.Toggled += (_, _) => SetValue(entry.Key, control.IsOn ? bool.TrueString : bool.FalseString); + CheckBox control = new() + { + Content = new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }, + IsChecked = bool.TryParse(entry.Value, out bool value) && value + }; + control.Checked += (_, _) => SetValue(entry.Key, bool.TrueString); + control.Unchecked += (_, _) => SetValue(entry.Key, bool.FalseString); return control; } @@ -222,8 +264,10 @@ private Slider CreateSlider(GameSettingEntry entry) Minimum = entry.Minimum, Maximum = entry.Maximum, StepFrequency = entry.Step, - Value = ParseNumber(entry.Value, entry.Minimum), - Width = 220 + Style = Application.Current.Resources["FatSliderStyle"] as Style, + TickFrequency = Math.Max(entry.Step, (entry.Maximum - entry.Minimum) / 10), + TickPlacement = Microsoft.UI.Xaml.Controls.Primitives.TickPlacement.Outside, + Value = ParseNumber(entry.Value, entry.Minimum) }; control.ValueChanged += (_, args) => SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); @@ -232,7 +276,7 @@ private Slider CreateSlider(GameSettingEntry entry) private ComboBox CreateChoice(GameSettingEntry entry) { - ComboBox control = new(); + ComboBox control = new() { CornerRadius = new CornerRadius(14) }; foreach (GameSettingChoice choice in entry.Choices ?? []) { ComboBoxItem item = new() { Content = choice.Title, Tag = choice.Value };