From 3e1623c47c5ffbec99115318098e996737a6fcbf Mon Sep 17 00:00:00 2001 From: Maxime Landry Date: Fri, 16 Oct 2026 14:35:26 -0400 Subject: [PATCH 1/2] Fixed the swapped found and matching counts in the package page subtitle and drew its numbers stronger than the words Co-Authored-By: Claude Opus 5.5 --- .../SoftwarePages/PackagesPageViewModel.cs | 71 +++++++++++++++---- .../SoftwarePages/AbstractPackagesPage.axaml | 8 +-- .../AbstractPackagesPage.axaml.cs | 29 ++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index d4cde5ac28..a17fa212aa 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -169,6 +169,46 @@ partial void OnIsFilterPaneOpenChanged(bool value) [ObservableProperty] private string _pageTitle = ""; [ObservableProperty] private string _pageIconPath = ""; [ObservableProperty] private string _subtitle = ""; + + // Shown after the subtitle in a secondary tone: when the list was last checked, then the page suffix. + [ObservableProperty] private string _subtitleDetail = ""; + + // The whole subtitle as one sentence, for screen readers. + [ObservableProperty] private string _subtitleAccessibleText = ""; + + /// The subtitle cut around its numbers, so the page can draw them stronger than the words. + [ObservableProperty] private IReadOnlyList _subtitleSegments = []; + + // Private-use marks around each number: the translated sentence decides where the numbers go, the marks find them. + private const char VALUE_START = '\uE000'; + private const char VALUE_END = '\uE001'; + + private static string MarkValue(int value) => $"{VALUE_START}{value}{VALUE_END}"; + + private void SetSubtitle(string marked) + { + var segments = new List(); + var text = new System.Text.StringBuilder(); + foreach (string part in marked.Split(VALUE_START)) + { + int end = part.IndexOf(VALUE_END); + if (end >= 0) + { + segments.Add(new SubtitleSegment(part[..end], true)); + text.Append(part, 0, end); + } + + string words = end >= 0 ? part[(end + 1)..] : part; + if (words.Length > 0) + { + segments.Add(new SubtitleSegment(words, false)); + text.Append(words); + } + } + + SubtitleSegments = segments; + Subtitle = text.ToString(); + } [ObservableProperty] private bool _isLoading; [ObservableProperty] private bool _backgroundTextVisible; [ObservableProperty] private bool _loadingImageVisible; @@ -1059,30 +1099,32 @@ public void UpdateSubtitle() { if (Loader.IsLoading || (LoadsOnStart && Loader.HasPendingInitialLoad)) { - Subtitle = _stillLoadingSubtitle; + SetSubtitle(_stillLoadingSubtitle); + SubtitleDetail = ""; + SubtitleAccessibleText = Subtitle; return; } + SubtitleDetail = (_showLastCheckedTime + ? " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture)) + : "") + SubtitleSuffix; + if (Loader.Any()) { + // {0} is everything the managers returned, {1} what the filters keep, as the sentence reads int selected = FilteredPackages.GetCheckedPackages().Count; - string r = CoreTools.Translate( + SetSubtitle(CoreTools.Translate( "{0} packages were found, {1} of which match the specified filters.", - FilteredPackages.Count, - _wrappedPackages.Count - ) + " (" + CoreTools.Translate("{0} selected", selected) + ")"; - - if (_showLastCheckedTime) - r += " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture)); - - Subtitle = r + SubtitleSuffix; + MarkValue(_wrappedPackages.Count), + MarkValue(FilteredPackages.Count) + ) + " (" + CoreTools.Translate("{0} selected", MarkValue(selected)) + ")"); } else { - Subtitle = _noPackagesSubtitleBase + (_showLastCheckedTime - ? " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture)) - : "") + SubtitleSuffix; + SetSubtitle(_noPackagesSubtitleBase); } + + SubtitleAccessibleText = Subtitle + SubtitleDetail; } // ─── Commands ───────────────────────────────────────────────────────────── @@ -1202,3 +1244,6 @@ public static bool NameOrIdExactMatch(IPackage pkg, string q, ListA piece of the page subtitle; marks a number. +public readonly record struct SubtitleSegment(string Text, bool IsValue); diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index 17043f8cbf..aeb79c8d64 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -270,14 +270,14 @@ FontFamily="Segoe UI Variable Display" TextWrapping="Wrap" Text="{Binding PageTitle}"/> + + Foreground="{DynamicResource TextFillColorSecondaryBrush}" + TextWrapping="Wrap"/> @@ -717,7 +717,7 @@ ItemsSource="{Binding FilteredPackages}" automation:AutomationProperties.AccessibilityView="Control" automation:AutomationProperties.Name="{Binding PageTitle}" - automation:AutomationProperties.HelpText="{Binding Subtitle}" + automation:AutomationProperties.HelpText="{Binding SubtitleAccessibleText}" AutoGenerateColumns="False" CanUserReorderColumns="True" CanUserResizeColumns="True" diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs index e32752db8c..3bb7b73b58 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Documents; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; @@ -84,7 +85,13 @@ or nameof(PackagesPageViewModel.SortAscending)) SyncFiltersButtonName(); UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen, animate: true); } + // Set last by UpdateSubtitle, so the segments and the detail are both current by then + if (args.PropertyName is nameof(PackagesPageViewModel.SubtitleAccessibleText)) + { + UpdateSubtitleInlines(); + } }; + UpdateSubtitleInlines(); SyncFiltersButtonName(); SyncOrderByButtonName(); @@ -411,6 +418,28 @@ private void SyncOrderByButtonName() CoreTools.Translate("{0}: {1}, {2}", CoreTools.Translate("Order by"), ViewModel.SortFieldName, direction)); } + // The numbers are the information that changes: semibold in the body text colour, the words keep the secondary tone + private void UpdateSubtitleInlines() + { + var inlines = new InlineCollection(); + foreach (SubtitleSegment segment in ViewModel.SubtitleSegments) + { + var run = new Run(segment.Text); + if (segment.IsValue) + { + run.FontWeight = FontWeight.SemiBold; + run.Bind(TextElement.ForegroundProperty, this.GetResourceObservable("TextFillColorPrimaryBrush")); + } + inlines.Add(run); + } + + if (ViewModel.SubtitleDetail.Length > 0) + inlines.Add(new Run(ViewModel.SubtitleDetail)); + + MainSubtitle.Inlines = inlines; + AutomationProperties.SetName(MainSubtitle, ViewModel.SubtitleAccessibleText); + } + private void UpdateSortMenuChecks() { OrderByName_Menu.Icon = Check(ViewModel.SortFieldIndex == 0); From dbe70f664e4aafa70a1b8d013dba813e71c92fff Mon Sep 17 00:00:00 2001 From: Maxime Landry Date: Fri, 16 Oct 2026 15:13:28 -0400 Subject: [PATCH 2/2] Formatted the subtitle with the real counts so plural rules keep working, and swapped the placeholders in the five translations that compensated for the old order Co-Authored-By: Claude Opus 5.5 --- src/Languages/lang_bn.json | 2 +- src/Languages/lang_ca.json | 2 +- src/Languages/lang_gu.json | 2 +- src/Languages/lang_nb.json | 2 +- src/Languages/lang_sv.json | 2 +- .../SoftwarePages/PackagesPageViewModel.cs | 50 ++++++++++--------- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/Languages/lang_bn.json b/src/Languages/lang_bn.json index dc73d82fd3..ffec542b62 100644 --- a/src/Languages/lang_bn.json +++ b/src/Languages/lang_bn.json @@ -302,7 +302,7 @@ "Local": "স্থানীয়", "OK": "ঠিক আছে", "Last checked: {0}": "সর্বশেষ পরীক্ষা করা হয়েছে: {0}", - "{0} packages were found, {1} of which match the specified filters.": "{1} প্যাকেজ পাওয়া গেছে যার মধ্যে {0}টি নির্দিষ্ট ফিল্টারগুলির সাথে মেলে।", + "{0} packages were found, {1} of which match the specified filters.": "{0} প্যাকেজ পাওয়া গেছে যার মধ্যে {1}টি নির্দিষ্ট ফিল্টারগুলির সাথে মেলে।", "{0} selected": "{0} নির্বাচিত", "(Last checked: {0})": "(শেষ পরীক্ষা করা হয়েছেঃ {0})", "All packages selected": "সমস্ত প্যাকেজ নির্বাচিত", diff --git a/src/Languages/lang_ca.json b/src/Languages/lang_ca.json index 2621606a48..2b640226ae 100644 --- a/src/Languages/lang_ca.json +++ b/src/Languages/lang_ca.json @@ -302,7 +302,7 @@ "Local": "Local", "OK": "D'acord", "Last checked: {0}": "Última comprovació: {0}", - "{0} packages were found, {1} of which match the specified filters.": "S'han trobat {1} paquets, {0} dels quals s'ajusten als filtres establerts.", + "{0} packages were found, {1} of which match the specified filters.": "S'han trobat {0} paquets, {1} dels quals s'ajusten als filtres establerts.", "{0} selected": "{0} seleccionats", "(Last checked: {0})": "(Comprovat per darrer cop: {0})", "All packages selected": "Tots els paquets seleccionats", diff --git a/src/Languages/lang_gu.json b/src/Languages/lang_gu.json index e7fb08c27b..df80e9eb35 100644 --- a/src/Languages/lang_gu.json +++ b/src/Languages/lang_gu.json @@ -302,7 +302,7 @@ "Local": "સ્થાનિક", "OK": "ઠીક છે", "Last checked: {0}": "છેલ્લે તપાસ્યું: {0}", - "{0} packages were found, {1} of which match the specified filters.": "{1} packages મળ્યા, જેમાંથી {0} નિર્દિષ્ટ filters સાથે મેળ ખાતા હતા.", + "{0} packages were found, {1} of which match the specified filters.": "{0} packages મળ્યા, જેમાંથી {1} નિર્દિષ્ટ filters સાથે મેળ ખાતા હતા.", "{0} selected": "{0} પસંદ થયું", "(Last checked: {0})": "(છેલ્લે ચકાસાયેલ: {0})", "All packages selected": "બધા પેકેજો પસંદ કરવામાં આવ્યા", diff --git a/src/Languages/lang_nb.json b/src/Languages/lang_nb.json index be5c0d6014..30af8334b6 100644 --- a/src/Languages/lang_nb.json +++ b/src/Languages/lang_nb.json @@ -302,7 +302,7 @@ "Local": "Lokal", "OK": "Greit", "Last checked: {0}": "Sist sjekket: {0}", - "{0} packages were found, {1} of which match the specified filters.": "{1} pakker ble funnet, hvorav {0} samsvarte med filtrene dine.", + "{0} packages were found, {1} of which match the specified filters.": "{0} pakker ble funnet, hvorav {1} samsvarte med filtrene dine.", "{0} selected": "{0} valgt", "(Last checked: {0})": "(Sist sjekket: {0})", "All packages selected": "Alle pakker valgt", diff --git a/src/Languages/lang_sv.json b/src/Languages/lang_sv.json index f96f443241..777668dae6 100644 --- a/src/Languages/lang_sv.json +++ b/src/Languages/lang_sv.json @@ -302,7 +302,7 @@ "Local": "Lokal", "OK": "OK", "Last checked: {0}": "Senast kontrollerad: {0}", - "{0} packages were found, {1} of which match the specified filters.": "{1} paket hittades, varav {0} matchade filtret", + "{0} packages were found, {1} of which match the specified filters.": "{0} paket hittades, varav {1} matchade filtret", "{0} selected": "{0} valda", "(Last checked: {0})": "(Kontrollerades senast: {0})", "All packages selected": "Alla paket valda", diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index a17fa212aa..bcd3afa473 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -179,35 +179,35 @@ partial void OnIsFilterPaneOpenChanged(bool value) /// The subtitle cut around its numbers, so the page can draw them stronger than the words. [ObservableProperty] private IReadOnlyList _subtitleSegments = []; - // Private-use marks around each number: the translated sentence decides where the numbers go, the marks find them. - private const char VALUE_START = '\uE000'; - private const char VALUE_END = '\uE001'; - - private static string MarkValue(int value) => $"{VALUE_START}{value}{VALUE_END}"; - - private void SetSubtitle(string marked) + // The sentence is formatted with the real integers, so ICU plural selectors keep working (ru, lt, et, sq); + // the numbers are then found in the formatted text, wherever the translation placed them. + private void SetSubtitle(string text, params int[] values) { var segments = new List(); - var text = new System.Text.StringBuilder(); - foreach (string part in marked.Split(VALUE_START)) + if (values.Length > 0) { - int end = part.IndexOf(VALUE_END); - if (end >= 0) - { - segments.Add(new SubtitleSegment(part[..end], true)); - text.Append(part, 0, end); - } - - string words = end >= 0 ? part[(end + 1)..] : part; - if (words.Length > 0) + string alternatives = string.Join('|', values + .Select(v => v.ToString(CultureInfo.CurrentCulture)) + .Distinct() + .OrderByDescending(v => v.Length) + .Select(System.Text.RegularExpressions.Regex.Escape)); + int last = 0; + foreach (System.Text.RegularExpressions.Match match in + System.Text.RegularExpressions.Regex.Matches(text, $@"(? last) segments.Add(new SubtitleSegment(text[last..match.Index], false)); + segments.Add(new SubtitleSegment(match.Value, true)); + last = match.Index + match.Length; } + if (last < text.Length) segments.Add(new SubtitleSegment(text[last..], false)); + } + else + { + segments.Add(new SubtitleSegment(text, false)); } SubtitleSegments = segments; - Subtitle = text.ToString(); + Subtitle = text; } [ObservableProperty] private bool _isLoading; [ObservableProperty] private bool _backgroundTextVisible; @@ -1113,11 +1113,13 @@ public void UpdateSubtitle() { // {0} is everything the managers returned, {1} what the filters keep, as the sentence reads int selected = FilteredPackages.GetCheckedPackages().Count; + int found = _wrappedPackages.Count; + int matching = FilteredPackages.Count; SetSubtitle(CoreTools.Translate( "{0} packages were found, {1} of which match the specified filters.", - MarkValue(_wrappedPackages.Count), - MarkValue(FilteredPackages.Count) - ) + " (" + CoreTools.Translate("{0} selected", MarkValue(selected)) + ")"); + found, + matching + ) + " (" + CoreTools.Translate("{0} selected", selected) + ")", found, matching, selected); } else {