AppListItem.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.Threading.Tasks; 8 using ManagedCommon; 9 using Microsoft.CmdPal.Core.Common.Helpers; 10 using Microsoft.CmdPal.Ext.Apps.Commands; 11 using Microsoft.CmdPal.Ext.Apps.Helpers; 12 using Microsoft.CommandPalette.Extensions; 13 using Microsoft.CommandPalette.Extensions.Toolkit; 14 15 namespace Microsoft.CmdPal.Ext.Apps.Programs; 16 17 public sealed partial class AppListItem : ListItem 18 { 19 private readonly AppCommand _appCommand; 20 private readonly AppItem _app; 21 22 private readonly Lazy<Task<IconInfo?>> _iconLoadTask; 23 private readonly Lazy<Task<Details>> _detailsLoadTask; 24 25 private InterlockedBoolean _isLoadingIcon; 26 private InterlockedBoolean _isLoadingDetails; 27 28 public override IDetails? Details 29 { 30 get 31 { 32 if (_isLoadingDetails.Set()) 33 { 34 _ = LoadDetailsAsync(); 35 } 36 37 return base.Details; 38 } 39 set => base.Details = value; 40 } 41 42 public override IIconInfo? Icon 43 { 44 get 45 { 46 if (_isLoadingIcon.Set()) 47 { 48 _ = LoadIconAsync(); 49 } 50 51 return base.Icon; 52 } 53 set => base.Icon = value; 54 } 55 56 public string AppIdentifier => _app.AppIdentifier; 57 58 public AppItem App => _app; 59 60 public AppListItem(AppItem app, bool useThumbnails, bool isPinned) 61 { 62 Command = _appCommand = new AppCommand(app); 63 _app = app; 64 Title = app.Name; 65 Subtitle = app.Subtitle; 66 Icon = Icons.GenericAppIcon; 67 68 MoreCommands = AddPinCommands(_app.Commands!, isPinned); 69 70 _detailsLoadTask = new Lazy<Task<Details>>(BuildDetails); 71 _iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false)); 72 } 73 74 private async Task LoadDetailsAsync() 75 { 76 try 77 { 78 Details = await _detailsLoadTask.Value; 79 } 80 catch (Exception ex) 81 { 82 Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}"); 83 } 84 } 85 86 private async Task LoadIconAsync() 87 { 88 try 89 { 90 Icon = _appCommand.Icon = CoalesceIcon(await _iconLoadTask.Value); 91 } 92 catch (Exception ex) 93 { 94 Logger.LogWarning($"Failed to load icon for {AppIdentifier}\n{ex}"); 95 } 96 } 97 98 private static IconInfo CoalesceIcon(IconInfo? value) 99 { 100 return CoalesceIcon(value, Icons.GenericAppIcon)!; 101 } 102 103 private static IconInfo? CoalesceIcon(IconInfo? value, IconInfo? replacement) 104 { 105 return IconIsNullOrEmpty(value) ? replacement : value; 106 } 107 108 private static bool IconIsNullOrEmpty(IconInfo? value) 109 { 110 return value == null || (string.IsNullOrEmpty(value.Light?.Icon) && value.Light?.Data is null) || (string.IsNullOrEmpty(value.Dark?.Icon) && value.Dark?.Data is null); 111 } 112 113 private async Task<Details> BuildDetails() 114 { 115 // Build metadata, with app type, path, etc. 116 var metadata = new List<DetailsElement>(); 117 metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } }); 118 if (!_app.IsPackaged) 119 { 120 metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } }); 121 } 122 123 #if DEBUG 124 metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } }); 125 metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } }); 126 metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } }); 127 metadata.Add(new DetailsElement() { Key = "[DEBUG] JumboIconPath", Data = new DetailsLink() { Text = _app.JumboIconPath ?? "(null)" } }); 128 #endif 129 130 // Icon 131 IconInfo? heroImage = null; 132 if (_app.IsPackaged) 133 { 134 heroImage = new IconInfo(_app.JumboIconPath ?? _app.IcoPath); 135 } 136 else 137 { 138 // Get the icon from the system 139 if (!string.IsNullOrEmpty(_app.JumboIconPath)) 140 { 141 var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.JumboIconPath, 64); 142 if (randomAccessStream != null) 143 { 144 heroImage = IconInfo.FromStream(randomAccessStream); 145 } 146 } 147 148 if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath)) 149 { 150 var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.IcoPath, 64); 151 if (randomAccessStream != null) 152 { 153 heroImage = IconInfo.FromStream(randomAccessStream); 154 } 155 } 156 157 // do nothing if we fail to load an icon. 158 // Logging it would be too NOISY, there's really no need. 159 if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.JumboIconPath)) 160 { 161 heroImage = await TryLoadThumbnail(_app.JumboIconPath, jumbo: true, logOnFailure: false); 162 } 163 164 if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath)) 165 { 166 heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false); 167 } 168 169 if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.ExePath)) 170 { 171 heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false); 172 } 173 } 174 175 return new Details() 176 { 177 Title = this.Title, 178 HeroImage = CoalesceIcon(CoalesceIcon(heroImage, this.Icon as IconInfo)), 179 Metadata = [..metadata], 180 }; 181 } 182 183 private async Task<IconInfo> FetchIcon(bool useThumbnails) 184 { 185 IconInfo? icon = null; 186 if (_app.IsPackaged) 187 { 188 icon = new IconInfo(_app.IcoPath); 189 return icon; 190 } 191 192 if (useThumbnails) 193 { 194 if (!string.IsNullOrEmpty(_app.IcoPath)) 195 { 196 icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true); 197 } 198 199 if (IconIsNullOrEmpty(icon) && !string.IsNullOrEmpty(_app.ExePath)) 200 { 201 icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true); 202 } 203 } 204 205 icon ??= new IconInfo(_app.IcoPath); 206 207 return icon; 208 } 209 210 private IContextItem[] AddPinCommands(List<IContextItem> commands, bool isPinned) 211 { 212 var newCommands = new List<IContextItem>(); 213 newCommands.AddRange(commands); 214 215 newCommands.Add(new Separator()); 216 217 if (isPinned) 218 { 219 newCommands.Add( 220 new CommandContextItem( 221 new UnpinAppCommand(this.AppIdentifier)) 222 { 223 RequestedShortcut = KeyChords.TogglePin, 224 }); 225 } 226 else 227 { 228 newCommands.Add( 229 new CommandContextItem( 230 new PinAppCommand(this.AppIdentifier)) 231 { 232 RequestedShortcut = KeyChords.TogglePin, 233 }); 234 } 235 236 return newCommands.ToArray(); 237 } 238 239 private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure) 240 { 241 return await Task.Run(async () => 242 { 243 try 244 { 245 var stream = await ThumbnailHelper.GetThumbnail(path, jumbo).ConfigureAwait(false); 246 if (stream is not null) 247 { 248 return IconInfo.FromStream(stream); 249 } 250 } 251 catch (Exception ex) 252 { 253 if (logOnFailure) 254 { 255 Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}"); 256 } 257 } 258 259 return null; 260 }).ConfigureAwait(false); 261 } 262 }