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 }