/ src / settings-ui / Settings.UI / SettingsXAML / OOBE / Views / ScoobeShellPage.xaml.cs
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  }