From fbc88bb4cdea088751578e5c749d2a20c499a722 Mon Sep 17 00:00:00 2001 From: Spencer Stream Date: Mon, 22 Sep 2025 19:40:01 -0500 Subject: [PATCH 1/8] Add ini-parser package to Infrastructure project --- Flow.Launcher.Core/packages.lock.json | 8 +++++++- .../Flow.Launcher.Infrastructure.csproj | 1 + Flow.Launcher.Infrastructure/packages.lock.json | 6 ++++++ Flow.Launcher/packages.lock.json | 8 +++++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/packages.lock.json b/Flow.Launcher.Core/packages.lock.json index b7a00d94d10..373cdcd15ed 100644 --- a/Flow.Launcher.Core/packages.lock.json +++ b/Flow.Launcher.Core/packages.lock.json @@ -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", @@ -263,7 +268,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": { diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 5b4eaf89394..d0c9ebddaa6 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -60,6 +60,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Flow.Launcher.Infrastructure/packages.lock.json b/Flow.Launcher.Infrastructure/packages.lock.json index 47c94d5f6ab..a1aea7f884b 100644 --- a/Flow.Launcher.Infrastructure/packages.lock.json +++ b/Flow.Launcher.Infrastructure/packages.lock.json @@ -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, )", diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index c90db6b0cdd..4af9894cebc 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -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", @@ -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": { From 90c73e5e3df07bd1fcee73d9e423017456d541ab Mon Sep 17 00:00:00 2001 From: Spencer Stream Date: Mon, 22 Sep 2025 19:40:58 -0500 Subject: [PATCH 2/8] Support .url file icons --- .../Image/ThumbnailReader.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 4ce0df0260d..dbe6a694b00 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -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 { @@ -35,9 +36,21 @@ public class WindowsThumbnailProvider private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205; + private const string UrlExtension = ".url"; + 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)?.ToLowerInvariant(); + if (extension is UrlExtension) + { + hBitmap = GetHBitmapForUrlFile(fileName, width, height, options); + } + else + { + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + } try { @@ -108,5 +121,30 @@ private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, return hBitmap; } + + 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 (string.IsNullOrEmpty(iconPath)) + { + throw new FileNotFoundException(); + } + hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options); + } + catch + { + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + } + + return hBitmap; + } } } From c293a273ca5b7839d715e7a94cd436c4a01ab7c1 Mon Sep 17 00:00:00 2001 From: Spencer Stream Date: Mon, 22 Sep 2025 21:53:54 -0500 Subject: [PATCH 3/8] Fix nitpick on extension string comparison --- Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index dbe6a694b00..68940ff6a9c 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -42,8 +42,8 @@ public static BitmapSource GetThumbnail(string fileName, int width, int height, { HBITMAP hBitmap; - var extension = Path.GetExtension(fileName)?.ToLowerInvariant(); - if (extension is UrlExtension) + var extension = Path.GetExtension(fileName); + if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase)) { hBitmap = GetHBitmapForUrlFile(fileName, width, height, options); } From 130033cf4b8f302537c969fba4a4529b2b93f0d2 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Tue, 23 Sep 2025 12:22:46 +0800 Subject: [PATCH 4/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 68940ff6a9c..c942488c46a 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -135,7 +135,7 @@ private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, i var iconPath = urlSection?["IconFile"]; if (string.IsNullOrEmpty(iconPath)) { - throw new FileNotFoundException(); + throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file."); } hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options); } From 49d5cd36df9bb788e1899cfbe61dbd60fa0b17c1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 23 Sep 2025 12:24:44 +0800 Subject: [PATCH 5/8] Catch exception --- Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index c942488c46a..9f38a64dfe7 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -141,7 +141,15 @@ private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, i } catch { - hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + try + { + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + } + catch (System.Exception ex) + { + // Handle other exceptions + throw new InvalidOperationException("Failed to get thumbnail", ex); + } } return hBitmap; From 3bd6906c800932c944bd037fa20ad8e0474645b0 Mon Sep 17 00:00:00 2001 From: Spencer Stream Date: Tue, 23 Sep 2025 07:19:39 -0500 Subject: [PATCH 6/8] Validate iconPath exists --- Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 9f38a64dfe7..011f01ca59b 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -133,8 +133,9 @@ private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, i var urlSection = data["InternetShortcut"]; var iconPath = urlSection?["IconFile"]; - if (string.IsNullOrEmpty(iconPath)) + 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); From 9b2d36b78a79ba6493555eda12ef1595c732a5d6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 23 Sep 2025 20:48:41 +0800 Subject: [PATCH 7/8] Revert "Catch exception" This reverts commit 49d5cd36df9bb788e1899cfbe61dbd60fa0b17c1. --- Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 9f38a64dfe7..c942488c46a 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -141,15 +141,7 @@ private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, i } catch { - try - { - hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); - } - catch (System.Exception ex) - { - // Handle other exceptions - throw new InvalidOperationException("Failed to get thumbnail", ex); - } + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); } return hBitmap; From ba7de5d33d4b8ae6a35f587ebd2ae022267a665a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 23 Sep 2025 21:39:42 +0800 Subject: [PATCH 8/8] Add documents --- .../Image/ThumbnailReader.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 3b6f66bcaa8..86f757eb887 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -38,6 +38,17 @@ public class WindowsThumbnailProvider private const string UrlExtension = ".url"; + /// + /// Obtains a BitmapSource thumbnail for the specified file. + /// + /// + /// 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. + /// + /// Path to the file (can be a regular file or a ".url" shortcut). + /// Requested thumbnail width in pixels. + /// Requested thumbnail height in pixels. + /// Thumbnail extraction options (flags) controlling fallback and caching behavior. + /// A BitmapSource representing the requested thumbnail. public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { HBITMAP hBitmap; @@ -63,6 +74,21 @@ public static BitmapSource GetThumbnail(string fileName, int width, int height, } } + /// + /// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory. + /// + /// + /// If is and thumbnail extraction fails + /// due to extraction errors or a missing path, the method falls back to requesting an icon (). + /// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks. + /// + /// Path to the file to thumbnail. + /// Requested thumbnail width in pixels. + /// Requested thumbnail height in pixels. + /// Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly). + /// An HBITMAP handle containing the image. Caller must free the handle when finished. + /// If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName). + /// If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image. private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) { var retCode = PInvoke.SHCreateItemFromParsingName( @@ -122,6 +148,19 @@ private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, return hBitmap; } + /// + /// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap. + /// + /// + /// 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. + /// + /// Path to the .url shortcut file. + /// Requested thumbnail width (pixels). + /// Requested thumbnail height (pixels). + /// ThumbnailOptions flags controlling extraction behavior. + /// An HBITMAP containing the requested image; callers are responsible for freeing the native handle. private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options) { HBITMAP hBitmap;