/ src / modules / cmdpal / ext / Microsoft.CmdPal.Ext.WinGet / Pages / InstallPackageListItem.cs
InstallPackageListItem.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.Diagnostics;
  8  using System.Linq;
  9  using System.Runtime.InteropServices;
 10  using System.Threading.Tasks;
 11  using ManagedCommon;
 12  using Microsoft.CommandPalette.Extensions;
 13  using Microsoft.CommandPalette.Extensions.Toolkit;
 14  using Microsoft.Management.Deployment;
 15  using Windows.Foundation.Metadata;
 16  
 17  namespace Microsoft.CmdPal.Ext.WinGet.Pages;
 18  
 19  public partial class InstallPackageListItem : ListItem
 20  {
 21      private readonly CatalogPackage _package;
 22  
 23      // Lazy-init the details
 24      private readonly Lazy<Details?> _details;
 25  
 26      public override IDetails? Details { get => _details.Value; set => base.Details = value; }
 27  
 28      private InstallPackageCommand? _installCommand;
 29  
 30      public InstallPackageListItem(CatalogPackage package)
 31          : base(new NoOpCommand())
 32      {
 33          _package = package;
 34  
 35          PackageVersionInfo? version = null;
 36          try
 37          {
 38              version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
 39          }
 40          catch (Exception e)
 41          {
 42              Logger.LogError("Could not get package version", e);
 43          }
 44  
 45          var versionTagText = "Unknown";
 46          if (version is not null)
 47          {
 48              versionTagText = version.Version == "Unknown" && version.PackageCatalog.Info.Id == "StoreEdgeFD" ? "msstore" : version.Version;
 49          }
 50  
 51          Title = _package.Name;
 52          Subtitle = _package.Id;
 53          Tags = [new Tag() { Text = versionTagText }];
 54  
 55          _details = new Lazy<Details?>(() => BuildDetails(version));
 56  
 57          _ = Task.Run(UpdatedInstalledStatus);
 58      }
 59  
 60      private Details? BuildDetails(PackageVersionInfo? version)
 61      {
 62          CatalogPackageMetadata? metadata = null;
 63          try
 64          {
 65              metadata = version?.GetCatalogPackageMetadata();
 66          }
 67          catch (COMException ex)
 68          {
 69              Logger.LogWarning($"GetCatalogPackageMetadata error {ex.ErrorCode}");
 70          }
 71  
 72          if (metadata is not null)
 73          {
 74              for (var i = 0; i < metadata.Tags.Count; i++)
 75              {
 76                  if (metadata.Tags[i].Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase))
 77                  {
 78                      if (_installCommand is not null)
 79                      {
 80                          _installCommand.SkipDependencies = true;
 81                      }
 82  
 83                      break;
 84                  }
 85              }
 86  
 87              var description = string.IsNullOrEmpty(metadata.Description) ?
 88                  metadata.ShortDescription :
 89                  metadata.Description;
 90              var detailsBody = $"""
 91  
 92  {description}
 93  """;
 94              IconInfo heroIcon = new(string.Empty);
 95              var icons = metadata.Icons;
 96              if (icons.Count > 0)
 97              {
 98                  // There's also a .Theme property we could probably use to
 99                  // switch between default or individual icons.
100                  heroIcon = new IconInfo(icons[0].Url);
101              }
102  
103              return new Details()
104              {
105                  Body = detailsBody,
106                  Title = metadata.PackageName,
107                  HeroImage = heroIcon,
108                  Metadata = GetDetailsMetadata(metadata).ToArray(),
109              };
110          }
111  
112          return null;
113      }
114  
115      private List<IDetailsElement> GetDetailsMetadata(CatalogPackageMetadata metadata)
116      {
117          List<IDetailsElement> detailsElements = [];
118  
119          // key -> {text, url}
120          Dictionary<string, (string, string)> simpleData = new()
121          {
122              { Properties.Resources.winget_author, (metadata.Author, string.Empty) },
123              { Properties.Resources.winget_publisher, (metadata.Publisher, metadata.PublisherUrl) },
124              { Properties.Resources.winget_copyright, (metadata.Copyright, metadata.CopyrightUrl) },
125              { Properties.Resources.winget_license, (metadata.License, metadata.LicenseUrl) },
126              { Properties.Resources.winget_publisher_support, (string.Empty, metadata.PublisherSupportUrl) },
127  
128              // The link to the release notes will only show up if there is an
129              // actual URL for the release notes
130              { Properties.Resources.winget_view_release_notes, (string.IsNullOrEmpty(metadata.ReleaseNotesUrl) ? string.Empty : Properties.Resources.winget_view_online, metadata.ReleaseNotesUrl) },
131  
132              // These can be l o n g
133              { Properties.Resources.winget_release_notes, (metadata.ReleaseNotes, string.Empty) },
134          };
135  
136          try
137          {
138              var docs = metadata.Documentations;
139              var count = docs.Count;
140              for (var i = 0; i < count; i++)
141              {
142                  var item = docs[i];
143                  simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl));
144              }
145          }
146          catch (Exception ex)
147          {
148              Logger.LogWarning($"Failed to retrieve documentations from metadata: {ex.Message}");
149          }
150  
151          UriCreationOptions options = default;
152          foreach (var kv in simpleData)
153          {
154              var text = string.IsNullOrEmpty(kv.Value.Item1) ? kv.Value.Item2 : kv.Value.Item1;
155              var target = kv.Value.Item2;
156              if (!string.IsNullOrEmpty(text))
157              {
158                  Uri? uri = null;
159                  Uri.TryCreate(target, options, out uri);
160  
161                  DetailsElement pair = new()
162                  {
163                      Key = kv.Key,
164                      Data = new DetailsLink() { Link = uri, Text = text },
165                  };
166                  detailsElements.Add(pair);
167              }
168          }
169  
170          try
171          {
172              if (metadata.Tags.Count > 0)
173              {
174                  DetailsElement pair = new()
175                  {
176                      Key = "Tags",
177                      Data = new DetailsTags() { Tags = metadata.Tags.Select(t => new Tag(t)).ToArray() },
178                  };
179                  detailsElements.Add(pair);
180              }
181          }
182          catch (Exception ex)
183          {
184              Logger.LogWarning($"Failed to retrieve tags from metadata: {ex.Message}");
185          }
186  
187          return detailsElements;
188      }
189  
190      private async void UpdatedInstalledStatus()
191      {
192          try
193          {
194              var status = await _package.CheckInstalledStatusAsync();
195          }
196          catch (OperationCanceledException)
197          {
198              // DO NOTHING HERE
199              return;
200          }
201          catch (Exception ex)
202          {
203              // Handle other exceptions
204              ExtensionHost.LogMessage($"[WinGet] UpdatedInstalledStatus throw exception: {ex.Message}");
205              Logger.LogError($"[WinGet] UpdatedInstalledStatus throw exception", ex);
206              return;
207          }
208  
209          var isInstalled = _package.InstalledVersion is not null;
210  
211          var installedState = isInstalled ?
212              (_package.IsUpdateAvailable ? PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
213              PackageInstallCommandState.Install;
214  
215          // might be an uninstall command
216          InstallPackageCommand installCommand = new(_package, installedState);
217  
218          if (_package.InstalledVersion is not null)
219          {
220  #if DEBUG
221              var installerType = _package.InstalledVersion.GetMetadata(PackageVersionMetadataField.InstallerType);
222              Subtitle = installerType + " | " + Subtitle;
223  #endif
224  
225              List<IContextItem> contextMenu = [];
226              Command = installCommand;
227              Icon = installedState switch
228              {
229                  PackageInstallCommandState.Install => Icons.DownloadIcon,
230                  PackageInstallCommandState.Update => Icons.UpdateIcon,
231                  PackageInstallCommandState.Uninstall => Icons.CompletedIcon,
232                  _ => Icons.DownloadIcon,
233              };
234  
235              TryLocateAndAppendActionForApp(contextMenu);
236  
237              MoreCommands = contextMenu.ToArray();
238          }
239          else
240          {
241              _installCommand = new InstallPackageCommand(_package, installedState);
242              _installCommand.InstallStateChanged += InstallStateChangedHandler;
243              Command = _installCommand;
244              Icon = _installCommand.Icon;
245          }
246      }
247  
248      private void TryLocateAndAppendActionForApp(List<IContextItem> contextMenu)
249      {
250          try
251          {
252              // Let's try to connect it to an installed app if possible
253              // This is a bit of dark magic, since there's no direct link between
254              // WinGet packages and installed apps.
255              var lookupByPackageName = WinGetStatics.AppSearchByPackageFamilyNameCallback;
256              if (lookupByPackageName is not null)
257              {
258                  var names = _package.InstalledVersion.PackageFamilyNames;
259                  for (var i = 0; i < names.Count; i++)
260                  {
261                      var installedAppByPfn = lookupByPackageName(names[i]);
262                      if (installedAppByPfn is not null)
263                      {
264                          contextMenu.Add(new Separator());
265                          contextMenu.Add(new CommandContextItem(installedAppByPfn.Command));
266                          foreach (var item in installedAppByPfn.MoreCommands)
267                          {
268                              contextMenu.Add(item);
269                          }
270  
271                          return;
272                      }
273                  }
274              }
275  
276              var lookupByProductCode = WinGetStatics.AppSearchByProductCodeCallback;
277              if (lookupByProductCode is not null)
278              {
279                  var productCodes = _package.InstalledVersion.ProductCodes;
280                  for (var i = 0; i < productCodes.Count; i++)
281                  {
282                      var installedAppByProductCode = lookupByProductCode(productCodes[i]);
283                      if (installedAppByProductCode is not null)
284                      {
285                          contextMenu.Add(new Separator());
286                          contextMenu.Add(new CommandContextItem(installedAppByProductCode.Command));
287                          foreach (var item in installedAppByProductCode.MoreCommands)
288                          {
289                              contextMenu.Add(item);
290                          }
291  
292                          return;
293                      }
294                  }
295              }
296          }
297          catch (Exception ex)
298          {
299              Logger.LogError($"Failed to retrieve app context menu items for package '{_package?.Name ?? "Unknown"}'", ex);
300          }
301      }
302  
303      private void InstallStateChangedHandler(object? sender, InstallPackageCommand e)
304      {
305          if (!ApiInformation.IsApiContractPresent("Microsoft.Management.Deployment.WindowsPackageManagerContract", 12))
306          {
307              Logger.LogError($"RefreshPackageCatalogAsync isn't available");
308              e.FakeChangeStatus();
309              Command = e;
310              Icon = (IconInfo?)Command.Icon;
311              return;
312          }
313  
314          _ = Task.Run(() =>
315          {
316              Stopwatch s = new();
317              Logger.LogDebug($"Starting RefreshPackageCatalogAsync");
318              s.Start();
319              var refs = WinGetStatics.AvailableCatalogs;
320              for (var i = 0; i < refs.Count; i++)
321              {
322                  var catalog = refs[i];
323                  var operation = catalog.RefreshPackageCatalogAsync();
324                  operation.Wait();
325              }
326  
327              s.Stop();
328              Logger.LogDebug($"RefreshPackageCatalogAsync took {s.ElapsedMilliseconds}ms");
329          }).ContinueWith((previous) =>
330          {
331              if (previous.IsCompletedSuccessfully)
332              {
333                  Logger.LogDebug($"Updating InstalledStatus");
334                  UpdatedInstalledStatus();
335              }
336          });
337      }
338  }