ScoobeShellPage.xaml.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Linq; 8 using System.Net; 9 using System.Net.Http; 10 using System.Text.Json; 11 using System.Threading.Tasks; 12 using ManagedCommon; 13 using Microsoft.PowerToys.Settings.UI.Helpers; 14 using Microsoft.PowerToys.Settings.UI.SerializationContext; 15 using Microsoft.UI.Xaml; 16 using Microsoft.UI.Xaml.Controls; 17 18 namespace Microsoft.PowerToys.Settings.UI.OOBE.Views 19 { 20 public sealed partial class ScoobeShellPage : Page 21 { 22 public static Action<Type> OpenMainWindowCallback { get; set; } 23 24 public static void SetOpenMainWindowCallback(Action<Type> implementation) 25 { 26 OpenMainWindowCallback = implementation; 27 } 28 29 /// <summary> 30 /// Gets or sets a shell handler to be used to update contents of the shell dynamically from page within the frame. 31 /// </summary> 32 public static ScoobeShellPage ScoobeShellHandler { get; set; } 33 34 /// <summary> 35 /// Gets the list of release groups loaded from GitHub (grouped by major.minor version). 36 /// </summary> 37 public IList<IList<PowerToysReleaseInfo>> ReleaseGroups { get; private set; } 38 39 private bool _isLoading; 40 41 public ScoobeShellPage() 42 { 43 InitializeComponent(); 44 ScoobeShellHandler = this; 45 } 46 47 private async void ShellPage_Loaded(object sender, RoutedEventArgs e) 48 { 49 SetTitleBar(); 50 await LoadReleasesAsync(); 51 } 52 53 private async Task LoadReleasesAsync() 54 { 55 if (_isLoading) 56 { 57 return; 58 } 59 60 _isLoading = true; 61 LoadingProgressRing.Visibility = Visibility.Visible; 62 ErrorInfoBar.IsOpen = false; 63 navigationView.MenuItems.Clear(); 64 65 try 66 { 67 var releases = await FetchReleasesFromGitHubAsync(); 68 ReleaseGroups = GroupReleasesByMajorMinor(releases); 69 PopulateNavigationItems(); 70 } 71 catch (Exception ex) 72 { 73 Logger.LogError("Failed to load releases", ex); 74 ErrorInfoBar.IsOpen = true; 75 } 76 finally 77 { 78 LoadingProgressRing.Visibility = Visibility.Collapsed; 79 _isLoading = false; 80 } 81 } 82 83 private static async Task<IList<PowerToysReleaseInfo>> FetchReleasesFromGitHubAsync() 84 { 85 using var proxyClientHandler = new HttpClientHandler 86 { 87 DefaultProxyCredentials = CredentialCache.DefaultCredentials, 88 Proxy = WebRequest.GetSystemWebProxy(), 89 PreAuthenticate = true, 90 }; 91 92 using var httpClient = new HttpClient(proxyClientHandler); 93 httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys"); 94 95 string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=20"); 96 var allReleases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo); 97 98 return allReleases 99 .OrderByDescending(r => r.PublishedDate) 100 .ToList(); 101 } 102 103 private static IList<IList<PowerToysReleaseInfo>> GroupReleasesByMajorMinor(IList<PowerToysReleaseInfo> releases) 104 { 105 return releases 106 .GroupBy(r => GetMajorMinorVersion(r)) 107 .Select(g => g.OrderByDescending(r => r.PublishedDate).ToList() as IList<PowerToysReleaseInfo>) 108 .ToList(); 109 } 110 111 private static string GetMajorMinorVersion(PowerToysReleaseInfo release) 112 { 113 string version = GetVersionFromRelease(release); 114 var parts = version.Split('.'); 115 if (parts.Length >= 2) 116 { 117 return $"{parts[0]}.{parts[1]}"; 118 } 119 120 return version; 121 } 122 123 private static string GetVersionFromRelease(PowerToysReleaseInfo release) 124 { 125 // TagName is typically like "v0.96.0", Name might be "Release v0.96.0" 126 string version = release.TagName ?? release.Name ?? "Unknown"; 127 if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) 128 { 129 version = version.Substring(1); 130 } 131 132 return version; 133 } 134 135 private void PopulateNavigationItems() 136 { 137 if (ReleaseGroups == null || ReleaseGroups.Count == 0) 138 { 139 return; 140 } 141 142 foreach (var releaseGroup in ReleaseGroups) 143 { 144 var viewModel = new ScoobeReleaseGroupViewModel(releaseGroup); 145 navigationView.MenuItems.Add(viewModel); 146 } 147 148 // Select the first item to trigger navigation 149 navigationView.SelectedItem = navigationView.MenuItems[0]; 150 } 151 152 private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) 153 { 154 if (args.SelectedItem is ScoobeReleaseGroupViewModel viewModel) 155 { 156 NavigationFrame.Navigate(typeof(ScoobeReleaseNotesPage), viewModel.Releases); 157 } 158 } 159 160 private async void RetryButton_Click(object sender, RoutedEventArgs e) 161 { 162 await LoadReleasesAsync(); 163 } 164 165 private void SetTitleBar() 166 { 167 var window = App.GetScoobeWindow(); 168 if (window != null) 169 { 170 window.ExtendsContentIntoTitleBar = true; 171 window.SetTitleBar(AppTitleBar); 172 } 173 } 174 175 private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) 176 { 177 if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal) 178 { 179 TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment 180 AppTitleBar.IsPaneToggleButtonVisible = true; 181 } 182 else 183 { 184 TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment 185 AppTitleBar.IsPaneToggleButtonVisible = false; 186 } 187 } 188 189 private void TitleBar_PaneButtonClick(TitleBar sender, object args) 190 { 191 navigationView.IsPaneOpen = !navigationView.IsPaneOpen; 192 } 193 } 194 }