From d8fe4b50cb701ec2a79475e3eb0111497dbbfa73 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Thu, 9 Apr 2026 11:56:25 -0400 Subject: [PATCH 1/5] added the minimum age in avalonia --- .../Pages/SettingsPages/UpdatesViewModel.cs | 100 ++++++++++++++++ .../Views/Controls/Settings/TextboxCard.cs | 13 +++ .../SettingsPages/PackageManagerPage.axaml.cs | 110 ++++++++++++++++++ .../Views/Pages/SettingsPages/Updates.axaml | 22 ++++ .../Pages/SettingsPages/Updates.axaml.cs | 20 ++++ .../Assets/Languages/lang_en.json | 12 ++ .../SettingsEngine_Names.cs | 8 ++ .../UpgradablePackagesLoader.cs | 41 +++++++ 8 files changed, 326 insertions(+) diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs index 1111d8c036..37ea2a42c0 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs @@ -1,9 +1,15 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages.SettingsPages; using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine; using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; +using CornerRadius = global::Avalonia.CornerRadius; +using Thickness = global::Avalonia.Thickness; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; @@ -13,6 +19,23 @@ public partial class UpdatesViewModel : ViewModelBase public event EventHandler? NavigationRequested; [ObservableProperty] private bool _isAutoCheckEnabled; + [ObservableProperty] private bool _isCustomAgeSelected; + + private static readonly HashSet _managersWithoutUpdateDate = + new(StringComparer.OrdinalIgnoreCase) + { "Homebrew", "Scoop", "vcpkg" }; + + /// Items for the minimum update age ComboboxCard, in display/value pairs. + public IReadOnlyList<(string Name, string Value)> MinimumAgeItems { get; } = + [ + (CoreTools.Translate("No minimum age"), "0"), + (CoreTools.Translate("1 day"), "1"), + (CoreTools.Translate("{0} days", 3), "3"), + (CoreTools.Translate("{0} days", 7), "7"), + (CoreTools.Translate("{0} days", 14), "14"), + (CoreTools.Translate("{0} days", 30), "30"), + (CoreTools.Translate("Custom..."), "custom"), + ]; /// Items for the update interval ComboboxCard, in display/value pairs. public IReadOnlyList<(string Name, string Value)> IntervalItems { get; } = @@ -33,8 +56,85 @@ public partial class UpdatesViewModel : ViewModelBase public UpdatesViewModel() { _isAutoCheckEnabled = !CoreSettings.Get(CoreSettings.K.DisableAutoCheckforUpdates); + _isCustomAgeSelected = CoreSettings.GetValue(CoreSettings.K.MinimumUpdateAge) == "custom"; + } + + public Control BuildReleaseDateCompatTable() + { + string yesStr = CoreTools.Translate("Yes"); + string noStr = CoreTools.Translate("No"); + + var managers = PEInterface.Managers.ToList(); + + var table = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto"), + ColumnSpacing = 24, + RowSpacing = 8, + }; + for (int i = 0; i <= managers.Count; i++) + table.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + + var h1 = new TextBlock { Text = CoreTools.Translate("Package manager"), FontWeight = FontWeight.Bold }; + var h2 = new TextBlock { Text = CoreTools.Translate("Supports release dates"), FontWeight = FontWeight.Bold, HorizontalAlignment = HorizontalAlignment.Center }; + Grid.SetRow(h1, 0); Grid.SetColumn(h1, 0); + Grid.SetRow(h2, 0); Grid.SetColumn(h2, 1); + table.Children.Add(h1); + table.Children.Add(h2); + + for (int i = 0; i < managers.Count; i++) + { + var manager = managers[i]; + int row = i + 1; + + var name = new TextBlock { Text = manager.DisplayName, VerticalAlignment = VerticalAlignment.Center }; + Grid.SetRow(name, row); Grid.SetColumn(name, 0); + + bool supported = !_managersWithoutUpdateDate.Contains(manager.Name); + var badge = _statusBadge(supported ? yesStr : noStr, supported ? Colors.Green : Colors.Red); + Grid.SetRow(badge, row); Grid.SetColumn(badge, 1); + + table.Children.Add(name); + table.Children.Add(badge); + } + + var title = new TextBlock + { + Text = CoreTools.Translate("Release date support per package manager"), + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 12), + }; + + var centerWrapper = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto,*") }; + Grid.SetColumn(table, 1); + centerWrapper.Children.Add(table); + + var stack = new StackPanel { Orientation = Orientation.Vertical }; + stack.Children.Add(title); + stack.Children.Add(centerWrapper); + + var border = new Border + { + CornerRadius = new CornerRadius(8), + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Child = stack, + }; + border.Classes.Add("settings-card"); + return border; } + private static Border _statusBadge(string text, Color color) => new Border + { + CornerRadius = new CornerRadius(4), + Padding = new Thickness(4, 2), + BorderThickness = new Thickness(1), + HorizontalAlignment = HorizontalAlignment.Stretch, + Background = new SolidColorBrush(Color.FromArgb(60, color.R, color.G, color.B)), + BorderBrush = new SolidColorBrush(Color.FromArgb(120, color.R, color.G, color.B)), + Child = new TextBlock { Text = text, TextAlignment = TextAlignment.Center }, + }; + [RelayCommand] private void UpdateAutoCheckEnabled() { diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs index c7fc187fd6..d4880b833c 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs @@ -35,6 +35,8 @@ public CoreSettings.K SettingName } } + public bool IsNumericOnly { get; set; } + public string Placeholder { set => _textbox.Watermark = value; @@ -83,6 +85,17 @@ public void SaveValue() { string sanitizedText = _textbox.Text ?? ""; + if (IsNumericOnly) + { + string filtered = string.Concat(sanitizedText.Where(char.IsDigit)); + if (filtered != sanitizedText) + { + _textbox.Text = filtered; // triggers TextChanged → SaveValue again with clean text + return; + } + sanitizedText = filtered; + } + if (CoreSettings.ResolveKey(setting_name).Contains("File")) sanitizedText = CoreTools.MakeValidFileName(sanitizedText); diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index cbeaf89886..128bc4097b 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -19,6 +19,9 @@ namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; public sealed partial class PackageManagerPage : UserControl, ISettingsPage { + private static readonly HashSet _managersWithoutUpdateDate = + new(StringComparer.OrdinalIgnoreCase) + { "Homebrew", "Scoop", "vcpkg" }; private PackageManagerViewModel ViewModel => (PackageManagerViewModel)DataContext!; public bool CanGoBack => true; @@ -197,6 +200,113 @@ private void BuildPage() BuildExtraControls(disableNotifsCard); + // ── Per-manager minimum update age + ExtraControls.Children.Add(new TextBlock + { + Margin = new Thickness(44, 24, 4, 8), + FontWeight = FontWeight.SemiBold, + Text = CoreTools.Translate("Update security"), + }); + + (string Label, string Value)[] ageItems = + [ + (CoreTools.Translate("Use global setting"), ""), + (CoreTools.Translate("No minimum age"), "0"), + (CoreTools.Translate("1 day"), "1"), + (CoreTools.Translate("{0} days", 3), "3"), + (CoreTools.Translate("{0} days", 7), "7"), + (CoreTools.Translate("{0} days", 14), "14"), + (CoreTools.Translate("{0} days", 30), "30"), + (CoreTools.Translate("Custom..."), "custom"), + ]; + + var ageCombo = new ComboBox { MinWidth = 200 }; + foreach (var (label, _) in ageItems) + ageCombo.Items.Add(label); + + string? savedAge = CoreSettings.GetDictionaryItem( + CoreSettings.K.PerManagerMinimumUpdateAge, manager.Name); + int savedAgeIdx = Array.FindIndex(ageItems, i => i.Value == (savedAge ?? "")); + ageCombo.SelectedIndex = savedAgeIdx >= 0 ? savedAgeIdx : 0; + + var customAgeInput = new TextBox + { + MinWidth = 200, + Watermark = CoreTools.Translate("e.g. 10"), + Text = CoreSettings.GetDictionaryItem( + CoreSettings.K.PerManagerMinimumUpdateAgeCustom, manager.Name) ?? "", + }; + customAgeInput.TextChanged += (_, _) => + { + string current = customAgeInput.Text ?? ""; + string filtered = string.Concat(current.Where(char.IsDigit)); + if (filtered != current) + { + customAgeInput.Text = filtered; + return; + } + if (filtered.Length > 0) + CoreSettings.SetDictionaryItem( + CoreSettings.K.PerManagerMinimumUpdateAgeCustom, manager.Name, filtered); + else + CoreSettings.RemoveDictionaryKey( + CoreSettings.K.PerManagerMinimumUpdateAgeCustom, manager.Name); + }; + + bool initiallyCustom = savedAge == "custom"; + bool ageSupported = !_managersWithoutUpdateDate.Contains(manager.Name); + object ageDescription = !ageSupported + ? new TextBlock + { + Text = CoreTools.Translate("{pm} does not provide release dates for its packages, so this setting will have no effect") + .Replace("{pm}", manager.DisplayName), + Foreground = new SolidColorBrush(Color.Parse("#e05252")), + TextWrapping = TextWrapping.Wrap, + FontSize = 12, + } + : CoreTools.Translate("Override the global minimum update age for this package manager"); + + ageCombo.IsEnabled = ageSupported; + customAgeInput.IsEnabled = ageSupported; + + var minimumAgeCard = new SettingsCard + { + Header = CoreTools.Translate("Minimum age for updates"), + Description = ageDescription, + Content = ageCombo, + CornerRadius = initiallyCustom ? new CornerRadius(8, 8, 0, 0) : new CornerRadius(8), + BorderThickness = new Thickness(1), + }; + var customAgeCard = new SettingsCard + { + Header = CoreTools.Translate("Custom minimum age (days)"), + Content = customAgeInput, + IsVisible = initiallyCustom, + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + + ageCombo.SelectionChanged += (_, _) => + { + int idx = ageCombo.SelectedIndex; + if (idx < 0) return; + string val = ageItems[idx].Value; + + bool isCustom = val == "custom"; + customAgeCard.IsVisible = isCustom; + minimumAgeCard.CornerRadius = isCustom ? new CornerRadius(8, 8, 0, 0) : new CornerRadius(8); + + if (string.IsNullOrEmpty(val)) + CoreSettings.RemoveDictionaryKey( + CoreSettings.K.PerManagerMinimumUpdateAge, manager.Name); + else + CoreSettings.SetDictionaryItem( + CoreSettings.K.PerManagerMinimumUpdateAge, manager.Name, val); + }; + + ExtraControls.Children.Add(minimumAgeCard); + ExtraControls.Children.Add(customAgeCard); + // ── Logs card ManagerLogs.Text = CoreTools.Translate("View {0} logs", manager.DisplayName); ManagerLogs.Icon = UniGetUI.Interface.Enums.IconType.Console; diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml index 6cf5449a75..5e396e7680 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml @@ -58,6 +58,28 @@ CornerRadius="0,0,8,8" IsEnabled="{Binding IsAutoCheckEnabled}"/> + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml.cs index f12c9e0c10..7d8333f535 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml.cs @@ -1,6 +1,8 @@ using Avalonia.Controls; using UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; using UniGetUI.Core.Tools; +using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; +using CornerRadius = global::Avalonia.CornerRadius; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; @@ -25,5 +27,23 @@ public Updates() foreach (var (name, val) in VM.IntervalItems) UpdatesCheckIntervalSelector.AddItem(name, val, false); UpdatesCheckIntervalSelector.ShowAddedItems(); + + foreach (var (name, val) in VM.MinimumAgeItems) + MinimumUpdateAgeSelector.AddItem(name, val, false); + MinimumUpdateAgeSelector.ShowAddedItems(); + + MinimumUpdateAgeSelector.ValueChanged += (_, _) => RefreshMinimumAgeLayout(); + RefreshMinimumAgeLayout(); + + ReleaseDateCompatTableHolder.Content = VM.BuildReleaseDateCompatTable(); + } + + private void RefreshMinimumAgeLayout() + { + bool isCustom = CoreSettings.GetValue(CoreSettings.K.MinimumUpdateAge) == "custom"; + VM.IsCustomAgeSelected = isCustom; + MinimumUpdateAgeSelector.CornerRadius = isCustom + ? new CornerRadius(8, 8, 0, 0) + : new CornerRadius(8); } } diff --git a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json index 1ebb678c3e..96cef3e6b6 100644 --- a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json +++ b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json @@ -66,6 +66,7 @@ "Allow parallel installs (NOT RECOMMENDED)": "Allow parallel installs (NOT RECOMMENDED)", "Allow pre-release versions": "Allow pre-release versions", "Allow {pm} operations to be performed in parallel": "Allow {pm} operations to be performed in parallel", + "{pm} does not provide release dates for its packages, so this setting will have no effect": "{pm} does not provide release dates for its packages, so this setting will have no effect", "Alternatively, you can also install {0} by running the following command in a Windows PowerShell prompt:": "Alternatively, you can also install {0} by running the following command in a Windows PowerShell prompt:", "Always elevate {pm} installations by default": "Always elevate {pm} installations by default", "Always run {pm} operations with administrator rights": "Always run {pm} operations with administrator rights", @@ -203,6 +204,8 @@ "Current status: Not logged in": "Current status: Not logged in", "Current user": "Current user", "Custom arguments:": "Custom arguments:", + "Custom minimum age (days)": "Custom minimum age (days)", + "Custom...": "Custom...", "Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control. Using custom command-lines can break packages. Proceed with caution.": "Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control. Using custom command-lines can break packages. Proceed with caution.", "Custom command-line arguments:": "Custom command-line arguments:", "Custom install arguments:": "Custom install arguments:", @@ -267,6 +270,7 @@ "Downloading backup...": "Downloading backup...", "Downloading installer for {package}": "Downloading installer for {package}", "Downloading package metadata...": "Downloading package metadata...", + "e.g. 10": "e.g. 10", "Enable Scoop cleanup on launch": "Enable Scoop cleanup on launch", "Enable WingetUI notifications": "Enable UniGetUI notifications", "Enable an [experimental] improved WinGet troubleshooter": "Enable an [experimental] improved WinGet troubleshooter", @@ -324,6 +328,7 @@ "Here you can change UniGetUI's behaviour regarding the following shortcuts. Checking a shortcut will make UniGetUI delete it if if gets created on a future upgrade. Unchecking it will keep the shortcut intact": "Here you can change UniGetUI's behaviour regarding the following shortcuts. Checking a shortcut will make UniGetUI delete it if gets created on a future upgrade. Unchecking it will keep the shortcut intact", "Hi, my name is Martí, and i am the developer of WingetUI. WingetUI has been entirely made on my free time!": "Hi, my name is Martí, and i am the developer of UniGetUI. UniGetUI has been entirely made on my free time!", "Hide details": "Hide details", + "Only show updates that are at least the specified number of days old": "Only show updates that are at least the specified number of days old", "Homepage": "Homepage", "Hooray! No updates were found.": "Hooray! No updates were found.", "How should installations that require administrator privileges be treated?": "How should installations that require administrator privileges be treated?", @@ -447,6 +452,7 @@ "Manual scan": "Manual scan", "Microsoft's official package manager. Full of well-known and verified packages
Contains: General Software, Microsoft Store apps": "Microsoft's official package manager. Full of well-known and verified packages
Contains: General Software, Microsoft Store apps", "Missing dependency": "Missing dependency", + "Minimum age for updates": "Minimum age for updates", "More": "More", "More details": "More details", "More details about the shared data and how it will be processed": "More details about the shared data and how it will be processed", @@ -461,6 +467,7 @@ "Nice! Backups will be uploaded to a private gist on your account": "Nice! Backups will be uploaded to a private gist on your account", "No": "No", "No applicable installer was found for the package {0}": "No applicable installer was found for the package {0}", + "No minimum age": "No minimum age", "No dependencies specified": "No dependencies specified", "No new shortcuts were found during the scan.": "No new shortcuts were found during the scan.", "No package was selected": "No package was selected", @@ -488,6 +495,7 @@ "Ok": "Ok", "Open": "Open", "Open GitHub": "Open GitHub", + "Override the global minimum update age for this package manager": "Override the global minimum update age for this package manager", "Open UniGetUI": "Open UniGetUI", "Open UniGetUI security settings": "Open UniGetUI security settings", "Open WingetUI": "Open UniGetUI", @@ -584,6 +592,7 @@ "Reinstall": "Reinstall", "Reinstall package": "Reinstall package", "Related settings": "Related settings", + "Release date support per package manager": "Release date support per package manager", "Release notes": "Release notes", "Release notes URL": "Release notes URL", "Release notes URL:": "Release notes URL:", @@ -733,6 +742,7 @@ "Suport the developer": "Support the developer", "Support me": "Support me", "Support the developer": "Support the developer", + "Supports release dates": "Supports release dates", "Systems are now ready to go!": "Systems are now ready to go!", "Telemetry": "Telemetry", "Text": "Text", @@ -863,6 +873,7 @@ "Update as administrator": "Update as administrator", "Update check frequency, automatically install updates, etc.": "Update check frequency, automatically install updates, etc.", "Update checking": "Update checking", + "Update security": "Update security", "Update date": "Update date", "Update failed": "Update failed", "Update found!": "Update found!", @@ -885,6 +896,7 @@ "Updating WingetUI": "Updating UniGetUI", "Url": "URL", "Use Legacy bundled WinGet instead of PowerShell CMDLets": "Use Legacy bundled WinGet instead of PowerShell CMDLets", + "Use global setting": "Use global setting", "Use a custom icon and screenshot database URL": "Use a custom icon and screenshot database URL", "Use bundled WinGet instead of PowerShell CMDlets": "Use bundled WinGet instead of PowerShell CMDlets", "Use bundled WinGet instead of system WinGet": "Use bundled WinGet instead of system WinGet", diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index dc09e46940..bb5142101e 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -85,6 +85,10 @@ public enum K DisableIntegrityChecks, UseLegacyElevator, WinGetForceLocationOnUpdate, + MinimumUpdateAge, + MinimumUpdateAgeCustom, + PerManagerMinimumUpdateAge, + PerManagerMinimumUpdateAgeCustom, Test1, Test2, @@ -181,6 +185,10 @@ public static string ResolveKey(K key) K.DisableIntegrityChecks => "DisableIntegrityChecks", K.UseLegacyElevator => "UseLegacyElevator", K.WinGetForceLocationOnUpdate => "WinGetForceLocationOnUpdate", + K.MinimumUpdateAge => "MinimumUpdateAge", + K.MinimumUpdateAgeCustom => "MinimumUpdateAgeCustom", + K.PerManagerMinimumUpdateAge => "PerManagerMinimumUpdateAge", + K.PerManagerMinimumUpdateAgeCustom => "PerManagerMinimumUpdateAgeCustom", K.Test1 => "TestSetting1", K.Test2 => "TestSetting2", diff --git a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs index 80008139e7..1ab8529a48 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Globalization; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Interface.Enums; @@ -52,6 +53,46 @@ protected override async Task IsPackageValid(IPackage package) return false; } + string? perManagerVal = Settings.GetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAge, package.Manager.Name); + + int minimumAge = 0; + if (perManagerVal is { Length: > 0 }) + { + if (perManagerVal == "custom") + int.TryParse( + Settings.GetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAgeCustom, package.Manager.Name), + out minimumAge); + else + int.TryParse(perManagerVal, out minimumAge); + } + else + { + string globalVal = Settings.GetValue(Settings.K.MinimumUpdateAge); + if (globalVal == "custom") + int.TryParse( + Settings.GetValue(Settings.K.MinimumUpdateAgeCustom), + out minimumAge); + else + int.TryParse(globalVal, out minimumAge); + } + + if (minimumAge > 0) + { + await package.Details.Load(); + string? dateStr = package.Details.UpdateDate; + if (dateStr is { Length: > 0 } && + DateTime.TryParse(dateStr, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime releaseDate) && + (DateTime.UtcNow - releaseDate.ToUniversalTime()).TotalDays < minimumAge) + { + Logger.Info( + $"Suppressing update for {package.Id}: released {releaseDate:yyyy-MM-dd}, minimum age is {minimumAge} days" + ); + return false; + } + } + return true; } From 63acc00b115329a53996bcfabc4e3934318ff3b7 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Thu, 9 Apr 2026 16:26:06 -0400 Subject: [PATCH 2/5] added the settings in windows --- .../SettingsPages/GeneralPages/Updates.xaml | 34 +++++ .../GeneralPages/Updates.xaml.cs | 140 ++++++++++++++++++ .../ManagersPages/PackageManager.xaml.cs | 115 ++++++++++++++ 3 files changed, 289 insertions(+) diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml index ba1ae92bee..0863133e97 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml @@ -80,6 +80,40 @@ Text="Do not automatically install updates when the battery saver is on" /> + + + + + + + + + + + + + + + public sealed partial class Updates : Page, ISettingsPage { + private static readonly HashSet _managersWithoutUpdateDate = + new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg" }; + public Updates() { this.InitializeComponent(); @@ -37,6 +46,137 @@ public Updates() } UpdatesCheckIntervalSelector.ShowAddedItems(); + + // Minimum age for updates + MinimumUpdateAgeSelector.Description = CoreTools.Translate( + "Only show updates that are at least the specified number of days old"); + + Dictionary minimum_age_dict = new() + { + { CoreTools.Translate("No minimum age"), "0" }, + { CoreTools.Translate("1 day"), "1" }, + { CoreTools.Translate("{0} days", 3), "3" }, + { CoreTools.Translate("{0} days", 7), "7" }, + { CoreTools.Translate("{0} days", 14), "14" }, + { CoreTools.Translate("{0} days", 30), "30" }, + { CoreTools.Translate("Custom..."), "custom" }, + }; + + foreach (KeyValuePair entry in minimum_age_dict) + MinimumUpdateAgeSelector.AddItem(entry.Key, entry.Value, false); + + MinimumUpdateAgeSelector.ShowAddedItems(); + MinimumUpdateAgeSelector.ValueChanged += (_, _) => RefreshMinimumAgeLayout(); + RefreshMinimumAgeLayout(); + + MinimumUpdateAgeCustomInput.PlaceholderText = CoreTools.Translate("e.g. 10"); + MinimumUpdateAgeCustomInput.Text = Settings.GetValue(Settings.K.MinimumUpdateAgeCustom); + MinimumUpdateAgeCustomInput.TextChanged += (_, _) => + { + string current = MinimumUpdateAgeCustomInput.Text ?? ""; + string filtered = new string(current.Where(char.IsDigit).ToArray()); + if (filtered != current) + { + MinimumUpdateAgeCustomInput.Text = filtered; + return; + } + if (filtered.Length > 0) + Settings.SetValue(Settings.K.MinimumUpdateAgeCustom, filtered); + else + Settings.Set(Settings.K.MinimumUpdateAgeCustom, false); + }; + + ReleaseDateCompatTableHolder.Content = BuildReleaseDateCompatTable(); + } + + private void RefreshMinimumAgeLayout() + { + bool isCustom = Settings.GetValue(Settings.K.MinimumUpdateAge) == "custom"; + MinimumUpdateAgeCustomCard.Visibility = isCustom ? Visibility.Visible : Visibility.Collapsed; + MinimumUpdateAgeSelector.CornerRadius = isCustom + ? new CornerRadius(8, 8, 0, 0) + : new CornerRadius(8); + } + + private UIElement BuildReleaseDateCompatTable() + { + string yesStr = CoreTools.Translate("Yes"); + string noStr = CoreTools.Translate("No"); + + var managers = PEInterface.Managers.ToList(); + + var table = new Grid { ColumnSpacing = 24, RowSpacing = 8 }; + table.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + table.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + for (int i = 0; i <= managers.Count; i++) + table.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + var h1 = new TextBlock + { + Text = CoreTools.Translate("Package manager"), + FontWeight = new FontWeight(600), + }; + var h2 = new TextBlock + { + Text = CoreTools.Translate("Supports release dates"), + FontWeight = new FontWeight(600), + HorizontalAlignment = HorizontalAlignment.Center, + }; + Grid.SetRow(h1, 0); Grid.SetColumn(h1, 0); + Grid.SetRow(h2, 0); Grid.SetColumn(h2, 1); + table.Children.Add(h1); + table.Children.Add(h2); + + for (int i = 0; i < managers.Count; i++) + { + var manager = managers[i]; + int row = i + 1; + var name = new TextBlock { Text = manager.DisplayName, VerticalAlignment = VerticalAlignment.Center }; + Grid.SetRow(name, row); Grid.SetColumn(name, 0); + bool supported = !_managersWithoutUpdateDate.Contains(manager.Name); + var badge = MakeStatusBadge(supported ? yesStr : noStr, supported); + Grid.SetRow(badge, row); Grid.SetColumn(badge, 1); + table.Children.Add(name); + table.Children.Add(badge); + } + + var centerPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + }; + centerPanel.Children.Add(table); + + var card = new SettingsCard + { + CornerRadius = new CornerRadius(8), + HorizontalAlignment = HorizontalAlignment.Stretch, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }; + card.Header = CoreTools.Translate("Release date support per package manager"); + card.Description = centerPanel; + return card; + } + + private static Border MakeStatusBadge(string text, bool isSupported) + { + var bgColor = isSupported + ? Color.FromArgb(60, 0, 180, 0) + : Color.FromArgb(60, 224, 82, 82); + var borderColor = isSupported + ? Color.FromArgb(120, 0, 180, 0) + : Color.FromArgb(120, 224, 82, 82); + + return new Border + { + CornerRadius = new CornerRadius(4), + Padding = new Thickness(4, 2, 4, 2), + BorderThickness = new Thickness(1), + HorizontalAlignment = HorizontalAlignment.Stretch, + Background = new SolidColorBrush(bgColor), + BorderBrush = new SolidColorBrush(borderColor), + Child = new TextBlock { Text = text, TextAlignment = TextAlignment.Center }, + }; } public bool CanGoBack => true; diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs index 757e640bca..6c14e57261 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs @@ -34,6 +34,9 @@ namespace UniGetUI.Pages.SettingsPages.GeneralPages /// public sealed partial class PackageManagerPage : Page, ISettingsPage { + private static readonly HashSet _managersWithoutUpdateDate = + new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg" }; + IPackageManager? Manager; public event EventHandler? RestartRequired; public event EventHandler? NavigationRequested @@ -424,6 +427,118 @@ protected override void OnNavigatedTo(NavigationEventArgs e) ExtraControls.Children.Add(DisableNotifsCard); } + // ── Per-manager minimum update age + ExtraControls.Children.Add(new TextBlock + { + Margin = new Thickness(4, 24, 4, 8), + FontWeight = new Windows.UI.Text.FontWeight(600), + Text = CoreTools.Translate("Update security"), + }); + + (string Label, string Value)[] ageItems = + [ + (CoreTools.Translate("Use global setting"), ""), + (CoreTools.Translate("No minimum age"), "0"), + (CoreTools.Translate("1 day"), "1"), + (CoreTools.Translate("{0} days", 3), "3"), + (CoreTools.Translate("{0} days", 7), "7"), + (CoreTools.Translate("{0} days", 14), "14"), + (CoreTools.Translate("{0} days", 30), "30"), + (CoreTools.Translate("Custom..."), "custom"), + ]; + + var ageCombo = new ComboBox { MinWidth = 200 }; + foreach (var (label, _) in ageItems) + ageCombo.Items.Add(label); + + string? savedAgeVal = Settings.GetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAge, Manager.Name); + int savedAgeIdx = Array.FindIndex(ageItems, i => i.Value == (savedAgeVal ?? "")); + ageCombo.SelectedIndex = savedAgeIdx >= 0 ? savedAgeIdx : 0; + + var customAgeInput = new TextBox + { + MinWidth = 200, + PlaceholderText = CoreTools.Translate("e.g. 10"), + Text = Settings.GetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAgeCustom, Manager.Name) ?? "", + }; + customAgeInput.TextChanged += (_, _) => + { + string current = customAgeInput.Text ?? ""; + string filtered = new string(current.Where(char.IsDigit).ToArray()); + if (filtered != current) + { + customAgeInput.Text = filtered; + return; + } + if (filtered.Length > 0) + Settings.SetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAgeCustom, Manager.Name, filtered); + else + Settings.RemoveDictionaryKey( + Settings.K.PerManagerMinimumUpdateAgeCustom, Manager.Name); + }; + + bool initiallyCustomAge = savedAgeVal == "custom"; + bool ageSupported = !_managersWithoutUpdateDate.Contains(Manager.Name); + + object ageCardDescription = !ageSupported + ? new TextBlock + { + Text = CoreTools.Translate( + "{pm} does not provide release dates for its packages, so this setting will have no effect") + .Replace("{pm}", Manager.DisplayName), + Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush( + Windows.UI.Color.FromArgb(255, 224, 82, 82)), + TextWrapping = TextWrapping.Wrap, + FontSize = 12, + } + : (object)CoreTools.Translate("Override the global minimum update age for this package manager"); + + ageCombo.IsEnabled = ageSupported; + customAgeInput.IsEnabled = ageSupported; + + var minimumAgeCard = new SettingsCard + { + Header = CoreTools.Translate("Minimum age for updates"), + Description = ageCardDescription, + Content = ageCombo, + CornerRadius = initiallyCustomAge ? new CornerRadius(8, 8, 0, 0) : new CornerRadius(8), + BorderThickness = new Thickness(1), + }; + var customAgeCard = new SettingsCard + { + Header = CoreTools.Translate("Custom minimum age (days)"), + Content = customAgeInput, + Visibility = initiallyCustomAge ? Visibility.Visible : Visibility.Collapsed, + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + }; + + ageCombo.SelectionChanged += (_, _) => + { + int idx = ageCombo.SelectedIndex; + if (idx < 0) return; + string val = ageItems[idx].Value; + + bool isCustom = val == "custom"; + customAgeCard.Visibility = isCustom ? Visibility.Visible : Visibility.Collapsed; + minimumAgeCard.CornerRadius = isCustom + ? new CornerRadius(8, 8, 0, 0) + : new CornerRadius(8); + + if (string.IsNullOrEmpty(val)) + Settings.RemoveDictionaryKey( + Settings.K.PerManagerMinimumUpdateAge, Manager.Name); + else + Settings.SetDictionaryItem( + Settings.K.PerManagerMinimumUpdateAge, Manager.Name, val); + }; + + ExtraControls.Children.Add(minimumAgeCard); + ExtraControls.Children.Add(customAgeCard); + // Hide the AppExecutionAliasWarning element if Manager is not Pip if (Manager is Pip) { From 809b3b0361429a296e9647c13b5a6775ed7e882a Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Fri, 10 Apr 2026 09:16:18 -0400 Subject: [PATCH 3/5] Added winget to unsuported manager --- src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs | 3 ++- .../Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs index 3fd014bc14..60592e2fda 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs @@ -19,7 +19,7 @@ namespace UniGetUI.Pages.SettingsPages.GeneralPages public sealed partial class Updates : Page, ISettingsPage { private static readonly HashSet _managersWithoutUpdateDate = - new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg" }; + new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg", "WinGet" }; public Updates() { @@ -144,6 +144,7 @@ private UIElement BuildReleaseDateCompatTable() { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, + Margin = new Thickness(0, 16, 0, 0), }; centerPanel.Children.Add(table); diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs index 6c14e57261..a17d60af8f 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs @@ -35,7 +35,7 @@ namespace UniGetUI.Pages.SettingsPages.GeneralPages public sealed partial class PackageManagerPage : Page, ISettingsPage { private static readonly HashSet _managersWithoutUpdateDate = - new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg" }; + new(StringComparer.OrdinalIgnoreCase) { "Homebrew", "Scoop", "vcpkg", "WinGet" }; IPackageManager? Manager; public event EventHandler? RestartRequired; From e4e51f76103d24dcea9e4f6f98d92da5926ae4c7 Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Fri, 10 Apr 2026 09:46:17 -0400 Subject: [PATCH 4/5] removed unused import and added a missing text --- .../ViewModels/Pages/SettingsPages/UpdatesViewModel.cs | 9 ++++----- .../Assets/Languages/lang_en.json | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs index 37ea2a42c0..9eca37b3bf 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/UpdatesViewModel.cs @@ -3,13 +3,12 @@ using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages.SettingsPages; using UniGetUI.Core.Tools; using UniGetUI.PackageEngine; -using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; -using CornerRadius = global::Avalonia.CornerRadius; -using Thickness = global::Avalonia.Thickness; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; +using CornerRadius = Avalonia.CornerRadius; +using Thickness = Avalonia.Thickness; namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; @@ -62,7 +61,7 @@ public UpdatesViewModel() public Control BuildReleaseDateCompatTable() { string yesStr = CoreTools.Translate("Yes"); - string noStr = CoreTools.Translate("No"); + string noStr = CoreTools.Translate("No"); var managers = PEInterface.Managers.ToList(); diff --git a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json index 96cef3e6b6..0ed3e8094b 100644 --- a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json +++ b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json @@ -67,6 +67,7 @@ "Allow pre-release versions": "Allow pre-release versions", "Allow {pm} operations to be performed in parallel": "Allow {pm} operations to be performed in parallel", "{pm} does not provide release dates for its packages, so this setting will have no effect": "{pm} does not provide release dates for its packages, so this setting will have no effect", + "{pm} does not provide release dates for its packages, so this setting will have no effect": "{pm} does not provide release dates for its packages, so this setting will have no effect", "Alternatively, you can also install {0} by running the following command in a Windows PowerShell prompt:": "Alternatively, you can also install {0} by running the following command in a Windows PowerShell prompt:", "Always elevate {pm} installations by default": "Always elevate {pm} installations by default", "Always run {pm} operations with administrator rights": "Always run {pm} operations with administrator rights", From 8ab5bf85329aa2f31d481824b1bfb02bf624676c Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Fri, 10 Apr 2026 10:14:04 -0400 Subject: [PATCH 5/5] removed the unnecessary value --- .../UpgradablePackagesLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs index 1ab8529a48..70ef99b327 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs @@ -56,7 +56,7 @@ protected override async Task IsPackageValid(IPackage package) string? perManagerVal = Settings.GetDictionaryItem( Settings.K.PerManagerMinimumUpdateAge, package.Manager.Name); - int minimumAge = 0; + int minimumAge; if (perManagerVal is { Length: > 0 }) { if (perManagerVal == "custom")