diff --git a/Flow.Launcher.Core/packages.lock.json b/Flow.Launcher.Core/packages.lock.json
index b7a00d94d..373cdcd15 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 5b4eaf893..d0c9ebdda 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/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs
index 4ce0df026..86f757eb8 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,32 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
+ 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 = 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
{
@@ -50,6 +74,21 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
+ ///
+ /// 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(
@@ -108,5 +147,44 @@ namespace Flow.Launcher.Infrastructure.Image
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;
+
+ 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;
+ }
}
}
diff --git a/Flow.Launcher.Infrastructure/packages.lock.json b/Flow.Launcher.Infrastructure/packages.lock.json
index 47c94d5f6..a1aea7f88 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.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
index 6c506cfc0..3af57f00d 100644
--- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
@@ -150,6 +150,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
return File.Exists(filePath);
}
+ ///
+ /// Checks if a file or directory exists
+ ///
+ ///
+ ///
+ public static bool FileOrLocationExists(this string path)
+ {
+ return LocationExists(path) || FileExists(path);
+ }
+
///
/// Open a directory window (using the OS's default handler, usually explorer)
///
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 03fa1e84c..eb0edac4b 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -595,6 +595,7 @@
Error
An error occurred while opening the folder. {0}
An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window
+ File or directory not found: {0}
Please wait...
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index d68da4a77..b7c735eed 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -182,7 +182,7 @@ namespace Flow.Launcher
Clipboard.SetFileDropList(paths);
});
-
+
if (exception == null)
{
if (showDefaultNotification)
@@ -221,7 +221,7 @@ namespace Flow.Launcher
{
LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception);
ShowMsgError(GetTranslation("failedToCopy"));
- }
+ }
}
}
@@ -330,7 +330,7 @@ namespace Flow.Launcher
((PluginJsonStorage)_pluginJsonStorages[type]).Save();
}
-
+
public void OpenDirectory(string directoryPath, string fileNameOrFilePath = null)
{
try
@@ -415,6 +415,12 @@ namespace Flow.Launcher
private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false)
{
+ if (uri.IsFile && !FilesFolders.FileOrLocationExists(uri.LocalPath))
+ {
+ ShowMsgError(GetTranslation("errorTitle"), string.Format(GetTranslation("fileNotFoundError"), uri.LocalPath));
+ return;
+ }
+
if (forceBrowser || uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
var browserInfo = _settings.CustomBrowser;
@@ -444,7 +450,15 @@ namespace Flow.Launcher
}
else
{
- StartProcess(uri.AbsoluteUri, arguments: string.Empty, useShellExecute: true);
+ try
+ {
+ StartProcess(uri.AbsoluteUri, arguments: string.Empty, useShellExecute: true);
+ }
+ catch (Exception e)
+ {
+ LogException(ClassName, $"Failed to open: {uri.AbsoluteUri}", e);
+ ShowMsgError(GetTranslation("errorTitle"), e.Message);
+ }
}
}
@@ -478,7 +492,7 @@ namespace Flow.Launcher
OpenUri(appUri);
}
- public void ToggleGameMode()
+ public void ToggleGameMode()
{
_mainVM.ToggleGameMode();
}
diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json
index c90db6b0c..4af9894ce 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": {
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index 8a9162e62..8392a0dbe 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -106,8 +106,8 @@
-
-
+
+