Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Flow.Launcher.Core/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
"resolved": "1.0.0",
"contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w=="
},
"ini-parser": {
"type": "Transitive",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Transitive",
"resolved": "1.0.4",
Expand Down Expand Up @@ -169,7 +174,7 @@
"resolved": "0.9.6.1",
"contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q=="
},
"Nerdbank.Streams": {

Check warning on line 177 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`Nerdbank` is not a recognized word. (unrecognized-spelling)
"type": "Transitive",
"resolved": "2.12.87",
"contentHash": "oDKOeKZ865I5X8qmU3IXMyrAnssYEiYWTobPGdrqubN3RtTzEHIv+D6fwhdcfrdhPJzHjCkK/ORztR/IsnmA6g==",
Expand All @@ -179,7 +184,7 @@
"System.IO.Pipelines": "8.0.0"
}
},
"Newtonsoft.Json": {

Check warning on line 187 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`Newtonsoft` is not a recognized word. (unrecognized-spelling)
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
Expand All @@ -189,12 +194,12 @@
"resolved": "3.0.0",
"contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A=="
},
"NHotkey.Wpf": {

Check warning on line 197 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NHotkey` is not a recognized word. (unrecognized-spelling)
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==",
"dependencies": {
"NHotkey": "3.0.0"

Check warning on line 202 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NHotkey` is not a recognized word. (unrecognized-spelling)
}
},
"NLog": {
Expand All @@ -202,12 +207,12 @@
"resolved": "6.0.4",
"contentHash": "Xr+lIk1ZlTTFXEqnxQVLxrDqZlt2tm5X+/AhJbaY2emb/dVtGDiU5QuEtj3gHtwV/SWlP/rJ922I/BPuOJXlRw=="
},
"NLog.OutputDebugString": {

Check warning on line 210 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NLog` is not a recognized word. (unrecognized-spelling)
"type": "Transitive",
"resolved": "6.0.4",
"contentHash": "TOP2Ap9BbE98B/l/TglnguowOD0rXo8B/20xAgvj9shO/kf6IJ5M4QMhVxq72mrneJ/ANhHY7Jcd+xJbzuI5PA==",
"dependencies": {
"NLog": "6.0.4"

Check warning on line 215 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NLog` is not a recognized word. (unrecognized-spelling)
}
},
"SharpVectors.Wpf": {
Expand Down Expand Up @@ -251,19 +256,20 @@
"flow.launcher.infrastructure": {
"type": "Project",
"dependencies": {
"Ben.Demystifier": "[0.4.1, )",

Check warning on line 259 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`Demystifier` is not a recognized word. (unrecognized-spelling)
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
"NHotkey.Wpf": "[3.0.0, )",

Check warning on line 266 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NHotkey` is not a recognized word. (unrecognized-spelling)
"NLog": "[6.0.4, )",

Check warning on line 267 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NLog` is not a recognized word. (unrecognized-spelling)
"NLog.OutputDebugString": "[6.0.4, )",

Check warning on line 268 in Flow.Launcher.Core/packages.lock.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`NLog` is not a recognized word. (unrecognized-spelling)
"SharpVectors.Wpf": "[1.8.5, )",
"System.Drawing.Common": "[7.0.0, )",
"ToolGood.Words.Pinyin": "[3.1.0.3, )"
"ToolGood.Words.Pinyin": "[3.1.0.3, )",
"ini-parser": "[2.5.2, )"
}
},
"flow.launcher.plugin": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.14.15" />
Expand Down
84 changes: 81 additions & 3 deletions Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using IniParser;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.Shell;

namespace Flow.Launcher.Infrastructure.Image
{
Expand Down Expand Up @@ -35,9 +36,32 @@ public class WindowsThumbnailProvider

private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;

private const string UrlExtension = ".url";

/// <summary>
/// Obtains a BitmapSource thumbnail for the specified file.
/// </summary>
/// <remarks>
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
HBITMAP hBitmap;

var extension = Path.GetExtension(fileName);
if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase))
{
hBitmap = GetHBitmapForUrlFile(fileName, width, height, options);
}
else
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}

try
{
Expand All @@ -50,6 +74,21 @@ public static BitmapSource GetThumbnail(string fileName, int width, int height,
}
}

/// <summary>
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
/// </summary>
/// <remarks>
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file to thumbnail.</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
var retCode = PInvoke.SHCreateItemFromParsingName(
Expand Down Expand Up @@ -108,5 +147,44 @@ private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height,

return hBitmap;
}

/// <summary>
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
/// </summary>
/// <remarks>
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
/// </remarks>
/// <param name="fileName">Path to the .url shortcut file.</param>
/// <param name="width">Requested thumbnail width (pixels).</param>
/// <param name="height">Requested thumbnail height (pixels).</param>
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap;

try
{
var parser = new FileIniDataParser();
var data = parser.ReadFile(fileName);
var urlSection = data["InternetShortcut"];

var iconPath = urlSection?["IconFile"];
if (!File.Exists(iconPath))
{
// If the IconFile is missing, throw exception to fallback to the default icon
throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file.");
}
hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options);
}
catch
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}

return hBitmap;
}
}
}
6 changes: 6 additions & 0 deletions Flow.Launcher.Infrastructure/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
"resolved": "6.9.3",
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
},
"ini-parser": {
"type": "Direct",
"requested": "[2.5.2, )",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Direct",
"requested": "[1.0.4, )",
Expand Down
8 changes: 7 additions & 1 deletion Flow.Launcher/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@
"resolved": "1.11.42",
"contentHash": "LDc1bEfF14EY2DZzak4xvzWvbpNXK3vi1u0KQbBpLUN4+cx/VrvXhgCAMSJhSU5vz0oMfW9JZIR20vj/PkDHPA=="
},
"ini-parser": {
"type": "Transitive",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Transitive",
"resolved": "1.0.4",
Expand Down Expand Up @@ -863,7 +868,8 @@
"NLog.OutputDebugString": "[6.0.4, )",
"SharpVectors.Wpf": "[1.8.5, )",
"System.Drawing.Common": "[7.0.0, )",
"ToolGood.Words.Pinyin": "[3.1.0.3, )"
"ToolGood.Words.Pinyin": "[3.1.0.3, )",
"ini-parser": "[2.5.2, )"
}
},
"flow.launcher.plugin": {
Expand Down
Loading