diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index 6e6b2e5f4ad..bf75440e2bf 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; -using Microsoft.Data.Sqlite; namespace Flow.Launcher.Plugin.BrowserBookmark; @@ -27,28 +26,15 @@ protected List LoadBookmarks(string browserDataPath, string name) { var bookmarks = new List(); if (!Directory.Exists(browserDataPath)) return bookmarks; + + // Watch the entire user data directory for changes to catch journal file writes. + Main.RegisterBrowserDataDirectory(browserDataPath); + var paths = Directory.GetDirectories(browserDataPath); foreach (var profile in paths) { var bookmarkPath = Path.Combine(profile, "Bookmarks"); - if (!File.Exists(bookmarkPath)) - continue; - - // Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile) - try - { - if (File.Exists(bookmarkPath)) - { - Main.RegisterBookmarkFile(bookmarkPath); - } - } - catch (Exception ex) - { - Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex); - continue; - } - var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source); @@ -78,10 +64,18 @@ protected static List LoadBookmarksFromFile(string path, string source if (!File.Exists(path)) return bookmarks; - using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); - if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement)) - return bookmarks; - EnumerateRoot(rootElement, bookmarks, source); + try + { + using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); + if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement)) + return bookmarks; + EnumerateRoot(rootElement, bookmarks, source); + } + catch (JsonException e) + { + Main._context.API.LogException(ClassName, $"Failed to parse bookmarks file: {path}", e); + } + return bookmarks; } @@ -115,24 +109,27 @@ private static void EnumerateFolderBookmark(JsonElement folderElement, ICollecti case "workspace": // Edge Workspace EnumerateFolderBookmark(subElement, bookmarks, source); break; - default: - bookmarks.Add(new Bookmark( - subElement.GetProperty("name").GetString(), - subElement.GetProperty("url").GetString(), - source)); + case "url": + if (subElement.TryGetProperty("name", out var name) && + subElement.TryGetProperty("url", out var url)) + { + bookmarks.Add(new Bookmark(name.GetString(), url.GetString(), source)); + } break; } } else { - Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString()}"); + Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString() ?? string.Empty}"); } } } private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - FaviconHelper.LoadFaviconsFromDb(_faviconCacheDir, dbPath, (tempDbPath) => + if (!File.Exists(dbPath)) return; + + FaviconHelper.ExecuteWithTempDb(_faviconCacheDir, dbPath, tempDbPath => { // Since some bookmarks may have same favicon id, we need to record them to avoid duplicates var savedPaths = new ConcurrentDictionary(); @@ -142,62 +139,58 @@ private void LoadFaviconsFromDb(string dbPath, List bookmarks) { // Use read-only connection to avoid locking issues // Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580 - var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); + if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri)) + return; + + using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); connection.Open(); try { - var url = bookmark.Url; - if (string.IsNullOrEmpty(url)) return; - - // Extract domain from URL - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - return; - - var domain = uri.Host; - using var cmd = connection.CreateCommand(); cmd.CommandText = @" SELECT f.id, b.image_data FROM favicons f JOIN favicon_bitmaps b ON f.id = b.icon_id JOIN icon_mapping m ON f.id = m.icon_id - WHERE m.page_url LIKE @url + WHERE m.page_url GLOB @pattern ORDER BY b.width DESC LIMIT 1"; - cmd.Parameters.AddWithValue("@url", $"%{domain}%"); + cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*"); using var reader = cmd.ExecuteReader(); - if (!reader.Read() || reader.IsDBNull(1)) + if (!reader.Read()) return; - var iconId = reader.GetInt64(0).ToString(); + var id = reader.GetInt64(0).ToString(); var imageData = (byte[])reader["image_data"]; if (imageData is not { Length: > 0 }) return; - var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png"); + var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png"); - // Filter out duplicate favicons if (savedPaths.TryAdd(faviconPath, true)) { - FaviconHelper.SaveBitmapData(imageData, faviconPath); + if (FaviconHelper.SaveBitmapData(imageData, faviconPath)) + bookmark.FaviconPath = faviconPath; + } + else + { + bookmark.FaviconPath = faviconPath; } - - bookmark.FaviconPath = faviconPath; } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex); + Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex); } finally { // Cache connection and clear pool after all operations to avoid issue: // ObjectDisposedException: Safe handle has been closed. + SqliteConnection.ClearPool(connection); connection.Close(); - connection.Dispose(); } }); }); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs index 005c83992bf..3636afd551f 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs @@ -1,5 +1,6 @@ using Flow.Launcher.Plugin.BrowserBookmark.Models; using System.Collections.Generic; +using System.IO; namespace Flow.Launcher.Plugin.BrowserBookmark; diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index ec3b867ea81..9f28bb9a216 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -42,16 +42,9 @@ protected List GetBookmarksFromPath(string placesPath) if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath)) return bookmarks; - // Try to register file monitoring - try - { - Main.RegisterBookmarkFile(placesPath); - } - catch (Exception ex) - { - Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex); - return bookmarks; - } + // DO NOT watch Firefox files, as places.sqlite is updated on every navigation, + // which would cause constant, performance-killing reloads. + // A periodic check on query is used instead. var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite"); @@ -65,18 +58,18 @@ protected List GetBookmarksFromPath(string placesPath) // Open connection to the database file and execute the query dbConnection.Open(); - var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader(); - - // Get results in List format - bookmarks = reader - .Select( - x => new Bookmark( - x["title"] is DBNull ? string.Empty : x["title"].ToString(), - x["url"].ToString(), - "Firefox" - ) - ) - .ToList(); + using var command = new SqliteCommand(QueryAllBookmarks, dbConnection); + using var reader = command.ExecuteReader(); + + // Put results in list + while (reader.Read()) + { + bookmarks.Add(new Bookmark( + reader["title"] is DBNull ? string.Empty : reader["title"].ToString(), + reader["url"].ToString(), + "Firefox" + )); + } // Load favicons after loading bookmarks if (Main._settings.EnableFavicons) @@ -119,81 +112,69 @@ protected List GetBookmarksFromPath(string placesPath) private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - FaviconHelper.LoadFaviconsFromDb(_faviconCacheDir, dbPath, (tempDbPath) => + if (!File.Exists(dbPath)) return; + + FaviconHelper.ExecuteWithTempDb(_faviconCacheDir, dbPath, tempDbPath => { - // Since some bookmarks may have same favicon id, we need to record them to avoid duplicates var savedPaths = new ConcurrentDictionary(); // Get favicons based on bookmarks concurrently Parallel.ForEach(bookmarks, bookmark => { - // Use read-only connection to avoid locking issues - // Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580 - var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); + if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri)) + return; + + using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); connection.Open(); try { - if (string.IsNullOrEmpty(bookmark.Url)) - return; - - // Extract domain from URL - if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri)) - return; - - var domain = uri.Host; - // Query for latest Firefox version favicon structure using var cmd = connection.CreateCommand(); cmd.CommandText = @" - SELECT i.data + SELECT i.id, i.data FROM moz_icons i JOIN moz_icons_to_pages ip ON i.id = ip.icon_id JOIN moz_pages_w_icons p ON ip.page_id = p.id - WHERE p.page_url LIKE @url + WHERE p.page_url GLOB @pattern AND i.data IS NOT NULL - ORDER BY i.width DESC -- Select largest icon available + ORDER BY i.width DESC LIMIT 1"; - cmd.Parameters.AddWithValue("@url", $"%{domain}%"); + cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*"); using var reader = cmd.ExecuteReader(); - if (!reader.Read() || reader.IsDBNull(0)) + if (!reader.Read()) return; + var id = reader.GetInt64(0).ToString(); var imageData = (byte[])reader["data"]; if (imageData is not { Length: > 0 }) return; - string faviconPath; - if (FaviconHelper.IsSvgData(imageData)) + var faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png"); + + if (savedPaths.TryAdd(faviconPath, true)) { - faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.svg"); + if (FaviconHelper.SaveBitmapData(imageData, faviconPath)) + bookmark.FaviconPath = faviconPath; } else { - faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png"); + bookmark.FaviconPath = faviconPath; } - - // Filter out duplicate favicons - if (savedPaths.TryAdd(faviconPath, true)) - { - FaviconHelper.SaveBitmapData(imageData, faviconPath); - } - - bookmark.FaviconPath = faviconPath; } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to extract Firefox favicon: {bookmark.Url}", ex); + Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex); } finally { // Cache connection and clear pool after all operations to avoid issue: // ObjectDisposedException: Safe handle has been closed. + SqliteConnection.ClearPool(connection); connection.Close(); - connection.Dispose(); } }); }); @@ -257,82 +238,109 @@ public static string MsixPlacesPath } } + /* + Current profiles.ini structure example as of Firefox version 69.0.1 + + [Install736426B0AF4A39CB] + Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile + Locked=1 + + [Profile2] + Name=dummyprofile + IsRelative=0 + Path=C:\t6h2yuq8.dummyprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code. + + [Profile1] + Name=default + IsRelative=1 + Path=Profiles/cydum7q4.default + Default=1 + + [Profile0] + Name=default-release + IsRelative=1 + Path=Profiles/7789f565.default-release + + [General] + StartWithLastProfile=1 + Version=2 + */ private static string GetProfileIniPath(string profileFolderPath) { var profileIni = Path.Combine(profileFolderPath, @"profiles.ini"); if (!File.Exists(profileIni)) return string.Empty; - // get firefox default profile directory from profiles.ini - using var sReader = new StreamReader(profileIni); - var ini = sReader.ReadToEnd(); - - var lines = ini.Split("\r\n").ToList(); - - var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty; + try + { + // Parse the ini file into a dictionary of sections for easier and more reliable access. + var profiles = new Dictionary>(StringComparer.OrdinalIgnoreCase); + Dictionary currentSection = null; + foreach (var line in File.ReadLines(profileIni)) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith('[') && trimmedLine.EndsWith(']')) + { + var sectionName = trimmedLine.Substring(1, trimmedLine.Length - 2); + currentSection = new Dictionary(StringComparer.OrdinalIgnoreCase); + profiles[sectionName] = currentSection; + } + else if (currentSection != null && trimmedLine.Contains('=')) + { + var parts = trimmedLine.Split('=', 2); + currentSection[parts[0]] = parts[1]; + } + } - if (string.IsNullOrEmpty(defaultProfileFolderNameRaw)) - return string.Empty; + Dictionary profileSection = null; - var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last(); + // STRATEGY 1 (Primary): Find the default profile using the 'Default' key in the [Install] or [General] sections. + // This is the most reliable method for modern Firefox versions. + string defaultPathRaw = null; + var installSection = profiles.FirstOrDefault(p => p.Key.StartsWith("Install")); + // Fallback to the [General] section if the [Install] section is not found. + (installSection.Value ?? profiles.GetValueOrDefault("General"))?.TryGetValue("Default", out defaultPathRaw); - var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); + if (!string.IsNullOrEmpty(defaultPathRaw)) + { + // The value of 'Default' is the path itself. We now find the profile section that has this path. + profileSection = profiles.Values.FirstOrDefault(v => v.TryGetValue("Path", out var path) && path == defaultPathRaw); + } - /* - Current profiles.ini structure example as of Firefox version 69.0.1 + // STRATEGY 2 (Fallback): If the primary strategy fails, look for a profile with the 'Default=1' flag. + // This is for older versions or non-standard configurations. + if (profileSection == null) + { + profileSection = profiles.Values.FirstOrDefault(section => section.TryGetValue("Default", out var value) && value == "1"); + } - [Install736426B0AF4A39CB] - Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile - Locked=1 + // If no profile section was found by either strategy, we cannot proceed. + if (profileSection == null) + return string.Empty; - [Profile2] - Name=dummyprofile - IsRelative=0 - Path=C:\t6h2yuq8.dummyprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code. + // We have the correct profile section, now resolve its path. + if (!profileSection.TryGetValue("Path", out var pathValue) || string.IsNullOrEmpty(pathValue)) + return string.Empty; - [Profile1] - Name=default - IsRelative=1 - Path=Profiles/cydum7q4.default - Default=1 + // Check if the path is relative or absolute. It defaults to relative if 'IsRelative' is not "0". + profileSection.TryGetValue("IsRelative", out var isRelativeRaw); - [Profile0] - Name=default-release - IsRelative=1 - Path=Profiles/7789f565.default-release + // The path in the ini file often uses forward slashes, so normalize them. + var profilePath = isRelativeRaw != "0" + ? Path.Combine(profileFolderPath, pathValue.Replace('/', Path.DirectorySeparatorChar)) + : pathValue; // If IsRelative is "0", the path is absolute and used as-is. - [General] - StartWithLastProfile=1 - Version=2 - */ - // Seen in the example above, the IsRelative attribute is always above the Path attribute + // Path.GetFullPath will resolve any relative parts (like "..") and give us a clean, absolute path. + var fullProfilePath = Path.GetFullPath(profilePath); - var relativePath = Path.Combine(defaultProfileFolderName, "places.sqlite"); - var absolutePath = Path.Combine(profileFolderPath, relativePath); + var placesPath = Path.Combine(fullProfilePath, "places.sqlite"); - // If the index is out of range, it means that the default profile is in a custom location or the file is malformed - // If the profile is in a custom location, we need to check - if (indexOfDefaultProfileAttributePath - 1 < 0 || - indexOfDefaultProfileAttributePath - 1 >= lines.Count) - { - return Directory.Exists(absolutePath) ? absolutePath : relativePath; + return File.Exists(placesPath) ? placesPath : string.Empty; } - - var relativeAttribute = lines[indexOfDefaultProfileAttributePath - 1]; - - // See above, the profile is located in a custom location, path is not relative, so IsRelative=0 - return (relativeAttribute == "0" || relativeAttribute == "IsRelative=0") - ? relativePath : absolutePath; - } -} - -public static class Extensions -{ - public static IEnumerable Select(this SqliteDataReader reader, Func projection) - { - while (reader.Read()) + catch (Exception ex) { - yield return projection(reader); + Main._context.API.LogException(nameof(FirefoxBookmarkLoader), $"Failed to parse {profileIni}", ex); + return string.Empty; } } } 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 3fb0fa46f64..b80ea385d72 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -96,7 +96,10 @@ - + + + + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index a879dcefd1b..6d507bc878d 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -1,5 +1,12 @@ using System; using System.IO; +using System.Text; +using Microsoft.Data.Sqlite; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; +using SkiaSharp; +using Svg.Skia; namespace Flow.Launcher.Plugin.BrowserBookmark.Helper; @@ -7,70 +14,124 @@ public static class FaviconHelper { private static readonly string ClassName = nameof(FaviconHelper); - public static void LoadFaviconsFromDb(string faviconCacheDir, string dbPath, Action loadAction) + public static void ExecuteWithTempDb(string faviconCacheDir, string dbPath, Action action) { - // Use a copy to avoid lock issues with the original file var tempDbPath = Path.Combine(faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db"); - try { File.Copy(dbPath, tempDbPath, true); - } - catch (Exception ex) - { - try - { - if (File.Exists(tempDbPath)) - { - File.Delete(tempDbPath); - } - } - catch (Exception ex1) + using (var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadWrite")) { - Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1); + connection.Open(); + var command = connection.CreateCommand(); + command.CommandText = "CREATE INDEX IF NOT EXISTS idx_moz_pages_w_icons_page_url ON moz_pages_w_icons(page_url);"; + try { command.ExecuteNonQuery(); } catch (SqliteException) { /* ignore */ } + command.CommandText = "CREATE INDEX IF NOT EXISTS idx_icon_mapping_page_url ON icon_mapping(page_url);"; + try { command.ExecuteNonQuery(); } catch (SqliteException) { /* ignore */ } } - Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex); - return; + action(tempDbPath); } - - try + catch (Exception ex) { - loadAction(tempDbPath); + Main._context.API.LogException(ClassName, $"Failed to process or index SQLite DB: {dbPath}", ex); } - catch (Exception ex) + finally { - Main._context.API.LogException(ClassName, $"Failed to connect to SQLite: {tempDbPath}", ex); + if (File.Exists(tempDbPath)) + { + try { File.Delete(tempDbPath); } catch (Exception ex) { Main._context.API.LogException(ClassName, $"Failed to delete temp favicon DB: {tempDbPath}", ex); } + } } + } + + private static bool IsSvg(byte[] imageData) + { + var text = Encoding.UTF8.GetString(imageData, 0, Math.Min(imageData.Length, 100)).Trim(); + return text.StartsWith(" 0 && pictureRect.Height > 0) + { + // Manually calculate the scaling factors to fill the destination canvas. + float scaleX = info.Width / pictureRect.Width; + float scaleY = info.Height / pictureRect.Height; + + // Apply the scaling transformation directly to the canvas. + canvas.Scale(scaleX, scaleY); + } + + // Draw the picture onto the now-transformed canvas. + canvas.DrawPicture(svg.Picture); + canvas.Restore(); + + using var image = surface.Snapshot(); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + using var fileStream = File.OpenWrite(outputPath); + data.SaveTo(fileStream); + return true; } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex); + Main._context.API.LogException(ClassName, $"Failed to convert SVG to PNG for {Path.GetFileName(outputPath)}.", ex); + return false; } } - public static void SaveBitmapData(byte[] imageData, string outputPath) + public static bool SaveBitmapData(byte[] imageData, string outputPath) { + if (IsSvg(imageData)) + { + return ConvertSvgToPng(imageData, outputPath); + } + try { - File.WriteAllBytes(outputPath, imageData); + // Attempt to load the image data. This will handle all formats ImageSharp + // supports, including common raster formats. + using var image = Image.Load(imageData); + + // Resize the image to a maximum of 64x64. + var options = new ResizeOptions + { + Size = new Size(64, 64), + Mode = ResizeMode.Max + }; + image.Mutate(x => x.Resize(options)); + + // Always save as PNG for maximum compatibility with the UI renderer. + image.SaveAsPng(outputPath, new PngEncoder { CompressionLevel = PngCompressionLevel.DefaultCompression }); + return true; } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex); - } - } - - public static bool IsSvgData(byte[] data) - { - if (data.Length < 5) + // This will now catch errors from loading malformed images, + // preventing them from being saved and crashing the UI. + Main._context.API.LogException(ClassName, $"Failed to load/resize/save image to {outputPath}. It may be a malformed image.", ex); return false; - string start = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(100, data.Length)); - return start.Contains("إشارات المتصفح ابحث في إشارات المتصفح + جاري التهيئة، يرجى الانتظار... + بيانات الإشارات المرجعية فتح الإشارات المرجعية في: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/cs.xaml index 45f8d97da3e..e66cde97f7d 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/cs.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/cs.xaml @@ -5,6 +5,8 @@ Záložky prohlížeče Hledat záložky v prohlížeči + Inicializace, čekejte prosím... + Data záložek Otevřít záložky v: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/da.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/da.xaml index 153b05d7646..fdc5b376774 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/da.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/da.xaml @@ -5,6 +5,8 @@ Browser Bookmarks Search your browser bookmarks + Initialiserer, vent venligst... + Bookmark Data Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/de.xaml index 66e30855f73..9e7f0e406e3 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/de.xaml @@ -5,6 +5,8 @@ Browser-Lesezeichen Ihre Browser-Lesezeichen durchsuchen + Initialisierung, bitte warten... + Lesezeichen-Daten Lesezeichen öffnen in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml index 22830e7c880..4f51d1613cb 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml @@ -6,6 +6,7 @@ Browser Bookmarks Search your browser bookmarks + Initializing, please wait... Bookmark Data diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es-419.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es-419.xaml index 28524229be5..3f1427e9dfe 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es-419.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es-419.xaml @@ -5,6 +5,8 @@ Marcadores del Navegador Busca en los marcadores de tu navegador + Inicializando, por favor espera... + Datos de Marcadores Abrir marcadores en: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml index ba9efd7e0a9..3cdf4c37af8 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml @@ -5,6 +5,8 @@ Marcadores del navegador Busca en los marcadores del navegador + Inicializando, por favor espera... + Datos del marcador Abrir marcadores en: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/fr.xaml index 39546c1024c..40ceec556e1 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/fr.xaml @@ -5,6 +5,8 @@ Favoris du Navigateur Rechercher dans les favoris de votre navigateur + Initialisation, veuillez patienter... + Données des favoris Ouvrir les favoris dans : diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/he.xaml index 79490928dc7..e113e2bce93 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/he.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/he.xaml @@ -5,6 +5,8 @@ סימניות דפדפן חפש בסימניות הדפדפן שלך + מאתחל, אנא המתן... + נתוני סימניות פתח סימניות ב: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/it.xaml index eb13bf852ca..97c3d40e4cb 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/it.xaml @@ -5,6 +5,8 @@ Segnalibri del Browser Cerca nei segnalibri del tuo browser + Inizializzazione, attendere prego... + Dati del segnalibro Apri preferiti in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ja.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ja.xaml index d08d67d9867..ddb9816ce85 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ja.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ja.xaml @@ -5,6 +5,8 @@ ブラウザブックマーク ブラウザのブックマークを検索します + 初期化中です。しばらくお待ちください... + Bookmark Data Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml index bb7c9fc06b1..0a27c3b1627 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml @@ -5,6 +5,8 @@ 브라우저 북마크 브라우저의 북마크 검색 + 초기화 중입니다. 잠시만 기다려주세요... + 북마크 데이터 Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nb.xaml index 1c53a49d27d..c748a82675e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nb.xaml @@ -5,6 +5,8 @@ Nettleserbokmerker Søk i nettleserbokmerker + Initialiserer, vennligst vent... + Bokmerkedata Åpne bokmerker i: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nl.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nl.xaml index 407786cdde2..0061427bf55 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nl.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/nl.xaml @@ -5,6 +5,8 @@ Browser Bookmarks Search your browser bookmarks + Initialiseren, even geduld... + Bookmark Data Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pl.xaml index 9f92d86b153..2762a05ccb5 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pl.xaml @@ -5,6 +5,8 @@ Zakładki przeglądarki Przeszukaj zakładki przeglądarki + Inicjalizacja, proszę czekać... + Dane zakładek Otwórz zakładki w: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-br.xaml index ce264bc5f86..85d86460807 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-br.xaml @@ -5,6 +5,8 @@ Favoritos do Navegador Pesquisar favoritos do seu navegador + Inicializando, por favor aguarde... + Dados de Favoritos Abrir favoritos em: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-pt.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-pt.xaml index 2818a0600f7..ab6a3f292e8 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-pt.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/pt-pt.xaml @@ -5,6 +5,8 @@ Marcadores do navegador Pesquisar nos marcadores do navegador + A inicializar, por favor aguarde... + Dados do marcador Abrir marcadores em: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ru.xaml index bb8639d9704..c5fe7ca5eca 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ru.xaml @@ -5,6 +5,8 @@ Закладки браузера Поиск закладок в браузере + Инициализация, пожалуйста, подождите... + Данные закладок Открыть закладки в: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml index c7556e877d9..a365408523e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml @@ -5,6 +5,8 @@ Záložky prehliadača Vyhľadáva záložky prehliadača + Inicializácia, prosím čakajte... + Nastavenia pluginu Otvoriť záložky v: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sr.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sr.xaml index 84173e616ec..26937f2e4ff 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sr.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sr.xaml @@ -5,6 +5,8 @@ Browser Bookmarks Search your browser bookmarks + Inicijalizacija, molimo sačekajte... + Bookmark Data Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml index 8c4280f63c7..76cc3c1d958 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml @@ -5,6 +5,8 @@ Yer İmleri Tarayıcınızdaki yer işaretlerini arayın + Başlatılıyor, lütfen bekleyin... + Yer İmleri Verisi Yer imlerini şurada aç: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/uk-UA.xaml index b8fd4fb8367..6955196e46c 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/uk-UA.xaml @@ -5,6 +5,8 @@ Закладки браузера Пошук у закладках браузера + Ініціалізація, будь ласка, зачекайте... + Дані закладок Відкрити закладки в: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml index 662c87d49be..9ad30cc4048 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml @@ -5,6 +5,8 @@ Dấu trang trình duyệt Tìm kiếm dấu trang trình duyệt của bạn + Đang khởi tạo, vui lòng chờ... + Dữ liệu đánh dấu Mở dấu trang trong: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-cn.xaml index 93454436724..119106a4503 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-cn.xaml @@ -5,6 +5,8 @@ 浏览器书签 搜索您的浏览器书签 + 正在初始化,请稍候... + 书签数据 在以下位置打开书签: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-tw.xaml index 7fa50a089a3..ed7e4e72ab5 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-tw.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/zh-tw.xaml @@ -5,6 +5,8 @@ 瀏覽器書籤 搜尋你的瀏覽器書籤 + 初始化中,請稍候... + 書籤資料 載入書籤至: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 91ade206b67..5f3e77d25bf 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Channels; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Models; @@ -17,6 +16,8 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex { private static readonly string ClassName = nameof(Main); + private static Main _instance; + internal static string _faviconCacheDir; internal static PluginInitContext _context; @@ -25,169 +26,262 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex private static List _cachedBookmarks = new(); - private static bool _initialized = false; - + private volatile bool _isInitialized = false; + + // A flag to prevent queuing multiple reloads. + private static volatile bool _isReloading = false; + + // Last time a periodic check triggered a Firefox reload. + private static DateTime _firefoxLastReload; + + private static readonly SemaphoreSlim _initializationSemaphore = new(1, 1); + + private static readonly object _periodicReloadLock = new(); + + private const string DefaultIconPath = @"Images\bookmark.png"; + + private static CancellationTokenSource _debounceTokenSource; + public void Init(PluginInitContext context) { + _instance = this; _context = context; - _settings = context.API.LoadSettingJsonStorage(); + _firefoxLastReload = DateTime.UtcNow; _faviconCacheDir = Path.Combine( context.CurrentPluginMetadata.PluginCacheDirectoryPath, "FaviconCache"); - LoadBookmarksIfEnabled(); + // Start loading bookmarks asynchronously without blocking Init. + _ = LoadBookmarksInBackgroundAsync(); } - private static void LoadBookmarksIfEnabled() + private async Task LoadBookmarksInBackgroundAsync() { - if (_context.CurrentPluginMetadata.Disabled) + // Prevent concurrent loading operations. + await _initializationSemaphore.WaitAsync(); + try { - // Don't load or monitor files if disabled - return; - } - - // Validate the cache directory before loading all bookmarks because Flow needs this directory to storage favicons - FilesFolders.ValidateDirectory(_faviconCacheDir); - - _cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings); - _ = MonitorRefreshQueueAsync(); - _initialized = true; - } + // Set initializing state inside the lock. This ensures Query() will show + // the "initializing" message during the entire reload process. + _isInitialized = false; - public List Query(Query query) - { - // For when the plugin being previously disabled and is now re-enabled - if (!_initialized) - { - LoadBookmarksIfEnabled(); - } - - string param = query.Search.TrimStart(); + // Clear data stores inside the lock to ensure a clean slate for the reload. + _cachedBookmarks.Clear(); + try + { + if (Directory.Exists(_faviconCacheDir)) + { + Directory.Delete(_faviconCacheDir, true); + } + } + catch (Exception e) + { + _context.API.LogException(ClassName, $"Failed to clear favicon cache folder: {_faviconCacheDir}", e); + } - // Should top results be returned? (true if no search parameters have been passed) - var topResults = string.IsNullOrEmpty(param); + // The loading operation itself is wrapped in a try/catch to ensure + // that even if it fails, the plugin returns to a stable, initialized state. + try + { + if (!_context.CurrentPluginMetadata.Disabled) + { + // Validate the cache directory before loading all bookmarks because Flow needs this directory to storage favicons + FilesFolders.ValidateDirectory(_faviconCacheDir); + _cachedBookmarks = await Task.Run(() => BookmarkLoader.LoadAllBookmarks(_settings)); - if (!topResults) - { - // Since we mixed chrome and firefox bookmarks, we should order them again - return _cachedBookmarks - .Select( - c => new Result + // Pre-validate all icon paths once to avoid doing it on every query. + foreach (var bookmark in _cachedBookmarks) { - Title = c.Name, - SubTitle = c.Url, - IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath) - ? c.FaviconPath - : @"Images\bookmark.png", - Score = BookmarkLoader.MatchProgram(c, param).Score, - Action = _ => + if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) { - _context.API.OpenUrl(c.Url); - - return true; - }, - ContextData = new BookmarkAttributes { Url = c.Url } + bookmark.FaviconPath = DefaultIconPath; + } } - ) - .Where(r => r.Score > 0) - .ToList(); + } + } + catch (Exception e) + { + _context.API.LogException(ClassName, "An error occurred while trying to load bookmarks.", e); + } + finally + { + // CRITICAL: Always mark the plugin as initialized, even on failure. + // This prevents the plugin from getting stuck in the "initializing" state. + _isInitialized = true; + } } - else + finally { - return _cachedBookmarks - .Select( - c => new Result - { - Title = c.Name, - SubTitle = c.Url, - IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath) - ? c.FaviconPath - : @"Images\bookmark.png", - Score = 5, - Action = _ => - { - _context.API.OpenUrl(c.Url); - return true; - }, - ContextData = new BookmarkAttributes { Url = c.Url } - } - ) - .ToList(); + _initializationSemaphore.Release(); } } - private static readonly Channel _refreshQueue = Channel.CreateBounded(1); - - private static readonly SemaphoreSlim _fileMonitorSemaphore = new(1, 1); - - private static async Task MonitorRefreshQueueAsync() + public List Query(Query query) { - if (_fileMonitorSemaphore.CurrentCount < 1) + // Smart check for Firefox: periodically trigger a background reload on query. + // This avoids watching the "hot" places.sqlite file but keeps data reasonably fresh. + if (!_isReloading && DateTime.UtcNow - _firefoxLastReload > TimeSpan.FromMinutes(2)) { - return; + lock (_periodicReloadLock) + { + if (!_isReloading && DateTime.UtcNow - _firefoxLastReload > TimeSpan.FromMinutes(2)) + { + _isReloading = true; + _context.API.LogInfo(ClassName, "Periodic check triggered a background reload of bookmarks."); + _ = Task.Run(async () => + { + try + { + await ReloadAllBookmarksAsync(false); + _firefoxLastReload = DateTime.UtcNow; + } + catch (Exception e) + { + _context.API.LogException(ClassName, "Periodic reload failed", e); + } + finally + { + _isReloading = false; + } + }); + } + } } - await _fileMonitorSemaphore.WaitAsync(); - var reader = _refreshQueue.Reader; - while (await reader.WaitToReadAsync()) + + // Immediately return if the initial load is not complete, providing feedback to the user. + if (!_isInitialized) { - if (reader.TryRead(out _)) + var initializingTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); + var initializingSubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_initializing"); + + return new List { - ReloadAllBookmarks(false); - } + new() + { + Title = initializingTitle, + SubTitle = initializingSubTitle, + IcoPath = DefaultIconPath + } + }; } - _fileMonitorSemaphore.Release(); + + string param = query.Search.TrimStart(); + bool topResults = string.IsNullOrEmpty(param); + + var results = _cachedBookmarks + .Select(c => + { + var score = topResults ? 5 : BookmarkLoader.MatchProgram(c, param).Score; + if (!topResults && score <= 0) + return null; + + return new Result + { + Title = c.Name, + SubTitle = c.Url, + IcoPath = c.FaviconPath, // Use the pre-validated path directly. + Score = score, + Action = _ => + { + _context.API.OpenUrl(c.Url); + return true; + }, + ContextData = new BookmarkAttributes { Url = c.Url } + }; + }) + .Where(r => r != null); + + return (topResults ? results : results.OrderByDescending(r => r.Score)).ToList(); } private static readonly List Watchers = new(); - internal static void RegisterBookmarkFile(string path) + // The watcher now monitors the directory but intelligently filters events. + internal static void RegisterBrowserDataDirectory(string path) { - var directory = Path.GetDirectoryName(path); - if (!Directory.Exists(directory) || !File.Exists(path)) + if (!Directory.Exists(path)) { return; } - if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase))) + + if (Watchers.Any(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase))) { return; } - var watcher = new FileSystemWatcher(directory!) + var watcher = new FileSystemWatcher(path) { - Filter = Path.GetFileName(path), - NotifyFilter = NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size + // Watch the directory, not a specific file, to catch WAL journal updates. + NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size, + EnableRaisingEvents = true }; - watcher.Changed += static (_, _) => - { - _refreshQueue.Writer.TryWrite(default); - }; + watcher.Changed += OnBookmarkFileChanged; + watcher.Renamed += OnBookmarkFileChanged; + watcher.Deleted += OnBookmarkFileChanged; + watcher.Created += OnBookmarkFileChanged; - watcher.Renamed += static (_, _) => + Watchers.Add(watcher); + } + + private static void OnBookmarkFileChanged(object sender, FileSystemEventArgs e) + { + // Event filter: only react to changes in key database files or their journals. + var file = e.Name.AsSpan(); + if (!(file.StartsWith("Bookmarks") || file.StartsWith("Favicons"))) { - _refreshQueue.Writer.TryWrite(default); - }; + return; // Ignore irrelevant file changes. + } - watcher.EnableRaisingEvents = true; + var oldCts = Interlocked.Exchange(ref _debounceTokenSource, new CancellationTokenSource()); + oldCts?.Cancel(); + oldCts?.Dispose(); - Watchers.Add(watcher); + var newCts = _debounceTokenSource; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(3), newCts.Token); + _context.API.LogInfo(ClassName, $"Bookmark file change detected ({e.Name}). Reloading bookmarks after delay."); + await ReloadAllBookmarksAsync(false); + } + catch (TaskCanceledException) + { + // Debouncing in action + } + catch (Exception ex) + { + _context.API.LogException(ClassName, $"Debounced reload failed for {e.Name}", ex); + } + }, newCts.Token); } public void ReloadData() { - ReloadAllBookmarks(); + _ = ReloadAllBookmarksAsync(); } - public static void ReloadAllBookmarks(bool disposeFileWatchers = true) + public static async Task ReloadAllBookmarksAsync(bool disposeFileWatchers = true) { - _cachedBookmarks.Clear(); - if (disposeFileWatchers) - DisposeFileWatchers(); - LoadBookmarksIfEnabled(); + try + { + if (_instance == null) return; + + // Simply dispose watchers if needed and then call the main loading method. + // All state management is now handled inside LoadBookmarksInBackgroundAsync. + if (disposeFileWatchers) + DisposeFileWatchers(); + + await _instance.LoadBookmarksInBackgroundAsync(); + } + catch (Exception e) + { + _context?.API.LogException(ClassName, "An error occurred while reloading bookmarks", e); + } } public string GetTranslatedPluginTitle() @@ -218,16 +312,13 @@ public List LoadContextMenus(Result selectedResult) try { _context.API.CopyToClipboard(((BookmarkAttributes)selectedResult.ContextData).Url); - return true; } catch (Exception e) { var message = "Failed to set url in clipboard"; _context.API.LogException(ClassName, message, e); - _context.API.ShowMsg(message); - return false; } }, @@ -245,6 +336,9 @@ internal class BookmarkAttributes public void Dispose() { DisposeFileWatchers(); + var cts = Interlocked.Exchange(ref _debounceTokenSource, null); + cts?.Cancel(); + cts?.Dispose(); } private static void DisposeFileWatchers() diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml index 80b004ff993..a7c9b1d6f86 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml @@ -101,13 +101,6 @@ FontSize="14" Text="{DynamicResource flowlauncher_plugin_browserbookmark_guideMessage02}" TextWrapping="WrapWithOverflow" /> - + IsChecked="{Binding EnableFavicons}" /> \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 1ee6b5c4551..c8d8017a61e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -1,6 +1,5 @@ using System.Windows; using System.Windows.Input; -using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Flow.Launcher.Plugin.BrowserBookmark.Models; @@ -24,7 +23,7 @@ public bool LoadChromeBookmark set { Settings.LoadChromeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -34,7 +33,7 @@ public bool LoadFirefoxBookmark set { Settings.LoadFirefoxBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -44,7 +43,18 @@ public bool LoadEdgeBookmark set { Settings.LoadEdgeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); + } + } + + public bool EnableFavicons + { + get => Settings.EnableFavicons; + set + { + Settings.EnableFavicons = value; + _ = Main.ReloadAllBookmarksAsync(); + OnPropertyChanged(); } } @@ -62,15 +72,11 @@ private void NewCustomBrowser(object sender, RoutedEventArgs e) { var newBrowser = new CustomBrowser(); var window = new CustomBrowserSettingWindow(newBrowser); - window.ShowDialog(); - if (newBrowser is not - { - Name: null, - DataDirectoryPath: null - }) + var result = window.ShowDialog() ?? false; + if (result) { Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -79,7 +85,7 @@ private void DeleteCustomBrowser(object sender, RoutedEventArgs e) if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) { Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -111,7 +117,7 @@ private void EditSelectedCustomBrowser() var result = window.ShowDialog() ?? false; if (result) { - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarksAsync(); } } -} +} \ No newline at end of file