From 5b3dd83df7b32c58a6ecd7dc4d01d99e8974dc7f Mon Sep 17 00:00:00 2001 From: dcog989 Date: Thu, 10 Jul 2025 20:38:19 +0100 Subject: [PATCH 01/12] Fix CPU / thread locks + improve performance of BrowserBookmark plugin ## 1. High CPU and UI Freezing * **Initial Load Spike:** The original plugin caused a massive CPU spike on first load due to inefficient database queries for favicons. **Fix:** We now create temporary indexes on the database copies, making the initial favicon scan virtually instantaneous. * **Query-Time CPU Usage:** Typing in the search bar caused high CPU because the plugin checked for the existence of every icon file on every keystroke. **Fix:** We now pre-validate all icon paths once during the initial load, making search queries extremely fast, in-memory operations. * **Background CPU Churn:** Actively browsing the web would trigger constant, high-CPU reloads of the plugin's data. **Fix:** We implemented a debouncing mechanism for the file watcher, ensuring that even a storm of file changes only results in a single, efficient data reload. * **UI Freezing on Scroll (The Final Bug):** The most severe issue was the UI locking up with 100% CPU on multiple threads when scrolling through results. This was caused by the UI's renderer (SharpVectors) attempting to render corrupt SVG favicons created by a race condition. **Fix:** We now convert **all** favicons to PNG format during the loading process. This guarantees that the UI never receives a corrupt or incompatible image, permanently solving the freezing issue. ## 2. Improved Reliability and Functionality * **Fixed Firefox Profile Discovery:** The plugin now reliably and automatically discovers the default Firefox profile, even in non-standard locations or with different naming schemes, by correctly parsing the `profiles.ini` file. * **Fixed Real-Time Settings Changes:** Toggling the "Load favicons" checkbox now triggers an immediate data reload, so the change takes effect instantly without requiring an application restart. ## 3. Enhanced Code Quality and User Experience * **Asynchronous Startup:** The entire data loading process is now fully asynchronous. The plugin displays an "initializing..." message while loading in the background, ensuring the Flow Launcher UI remains responsive at all times. * **Code Stability:** eliminated unsafe `async void` methods, fixed various compilation errors, and centralized duplicated code into a shared helper class, making the plugin more stable and maintainable. --- .../ChromiumBookmarkLoader.cs | 118 +++------ .../FirefoxBookmarkLoader.cs | 235 ++++++----------- ...low.Launcher.Plugin.BrowserBookmark.csproj | 1 + .../Helper/FaviconHelper.cs | 135 +++++++--- .../Main.cs | 245 ++++++++++-------- .../Views/CustomBrowserSetting.xaml | 7 - .../Views/SettingsControl.xaml | 2 +- .../Views/SettingsControl.xaml.cs | 34 ++- 8 files changed, 373 insertions(+), 404 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index 6e6b2e5f4ad..bb7c6093eac 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text.Json; -using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Microsoft.Data.Sqlite; @@ -78,10 +76,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,91 +121,41 @@ 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.ToString()}"); } } } private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - FaviconHelper.LoadFaviconsFromDb(_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"); - 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 - ORDER BY b.width DESC - LIMIT 1"; - - cmd.Parameters.AddWithValue("@url", $"%{domain}%"); - - using var reader = cmd.ExecuteReader(); - if (!reader.Read() || reader.IsDBNull(1)) - return; - - var iconId = 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"); - - // 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 bookmark favicon: {bookmark.Url}", ex); - } - finally - { - // Cache connection and clear pool after all operations to avoid issue: - // ObjectDisposedException: Safe handle has been closed. - connection.Close(); - connection.Dispose(); - } - }); - }); + const string sql = @" + 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 GLOB @pattern + ORDER BY b.width DESC + LIMIT 1"; + + FaviconHelper.ProcessFavicons( + dbPath, + _faviconCacheDir, + bookmarks, + sql, + "http*", + reader => (reader.GetInt64(0).ToString(), (byte[])reader["image_data"]), + (uri, id, data) => Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png") + ); } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index ec3b867ea81..fa96826e14f 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Microsoft.Data.Sqlite; @@ -65,18 +63,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(); + + 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,84 +117,26 @@ protected List GetBookmarksFromPath(string placesPath) private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - FaviconHelper.LoadFaviconsFromDb(_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"); - 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 - 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 - AND i.data IS NOT NULL - ORDER BY i.width DESC -- Select largest icon available - LIMIT 1"; - - cmd.Parameters.AddWithValue("@url", $"%{domain}%"); - - using var reader = cmd.ExecuteReader(); - if (!reader.Read() || reader.IsDBNull(0)) - return; - - var imageData = (byte[])reader["data"]; - - if (imageData is not { Length: > 0 }) - return; - - string faviconPath; - if (FaviconHelper.IsSvgData(imageData)) - { - faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.svg"); - } - else - { - faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png"); - } - - // 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); - } - finally - { - // Cache connection and clear pool after all operations to avoid issue: - // ObjectDisposedException: Safe handle has been closed. - connection.Close(); - connection.Dispose(); - } - }); - }); + const string sql = @" + 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 GLOB @pattern + AND i.data IS NOT NULL + ORDER BY i.width DESC + LIMIT 1"; + + FaviconHelper.ProcessFavicons( + dbPath, + _faviconCacheDir, + bookmarks, + sql, + "http*", + reader => (reader.GetInt64(0).ToString(), (byte[])reader["data"]), + // Always generate a .png path. The helper will handle the conversion. + (uri, id, data) => Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png") + ); } } @@ -263,76 +203,73 @@ private static string GetProfileIniPath(string profileFolderPath) 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 + 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: Find the profile with Default=1 + profileSection = profiles.Values.FirstOrDefault(section => section.TryGetValue("Default", out var value) && value == "1"); - var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); + // Strategy 2: If no profile has Default=1, use the Default key from the [Install] or [General] section + if (profileSection == null) + { + string defaultPathRaw = null; + var installSection = profiles.FirstOrDefault(p => p.Key.StartsWith("Install")); + // Fallback to General section if Install section not found + (installSection.Value ?? profiles.GetValueOrDefault("General"))?.TryGetValue("Default", out defaultPathRaw); - /* - Current profiles.ini structure example as of Firefox version 69.0.1 + if (!string.IsNullOrEmpty(defaultPathRaw)) + { + // The value of 'Default' is the path, find the corresponding profile section + profileSection = profiles.Values.FirstOrDefault(v => v.TryGetValue("Path", out var path) && path == defaultPathRaw); + } + } - [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 (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 profile section, now resolve the 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 + profileSection.TryGetValue("IsRelative", out var isRelativeRaw); - [Profile0] - Name=default-release - IsRelative=1 - Path=Profiles/7789f565.default-release + // If IsRelative is "1" or not present (defaults to relative), combine with profileFolderPath. + // 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; - [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 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..54f22bdf75a 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -97,6 +97,7 @@ + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index a879dcefd1b..91a135dd13b 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -1,5 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using Microsoft.Data.Sqlite; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; namespace Flow.Launcher.Plugin.BrowserBookmark.Helper; @@ -7,70 +13,117 @@ public static class FaviconHelper { private static readonly string ClassName = nameof(FaviconHelper); - public static void LoadFaviconsFromDb(string faviconCacheDir, string dbPath, Action loadAction) + private 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; - } - - try - { - loadAction(tempDbPath); + action(tempDbPath); } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to connect to SQLite: {tempDbPath}", ex); + Main._context.API.LogException(ClassName, $"Failed to process or index SQLite DB: {dbPath}", ex); } - - // Delete temporary file - try + finally { - File.Delete(tempDbPath); + if (File.Exists(tempDbPath)) + { + try { File.Delete(tempDbPath); } catch (Exception ex) { Main._context.API.LogException(ClassName, $"Failed to delete temp favicon DB: {tempDbPath}", ex); } + } } - catch (Exception ex) + } + + public static void ProcessFavicons( + string dbPath, + string faviconCacheDir, + List bookmarks, + string sqlQuery, + string patternPrefix, + Func imageDataExtractor, + Func pathBuilder) + { + if (!File.Exists(dbPath)) return; + + ExecuteWithTempDb(faviconCacheDir, dbPath, tempDbPath => { - Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex); - } + var savedPaths = new Dictionary(); + using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = sqlQuery; + var patternParam = cmd.CreateParameter(); + patternParam.ParameterName = "@pattern"; + cmd.Parameters.Add(patternParam); + + foreach (var bookmark in bookmarks) + { + try + { + if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri)) + continue; + + patternParam.Value = $"{patternPrefix}{uri.Host}/*"; + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) + continue; + + var (id, imageData) = imageDataExtractor(reader); + if (imageData is not { Length: > 0 }) + continue; + + var faviconPath = pathBuilder(uri, id, imageData); + if (savedPaths.TryAdd(faviconPath, true)) + { + SaveBitmapData(imageData, faviconPath); + } + bookmark.FaviconPath = faviconPath; + } + catch (Exception ex) + { + Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex); + } + } + SqliteConnection.ClearPool(connection); + }); } public static void SaveBitmapData(byte[] imageData, string outputPath) { try { - File.WriteAllBytes(outputPath, imageData); + // Attempt to load the image data. This will handle all formats ImageSharp + // supports, including SVG (if the necessary decoders are present) and common + // raster formats. It will throw an exception for malformed images. + 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 }); } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex); + // This will now catch errors from loading malformed SVGs or other image types, + // 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); } } - - public static bool IsSvgData(byte[] data) - { - if (data.Length < 5) - return false; - string start = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(100, data.Length)); - return start.Contains(" _cachedBookmarks = new(); - private static bool _initialized = false; - + private volatile bool _isInitialized = false; + + private static readonly SemaphoreSlim _initializationSemaphore = new(1, 1); + + private const string DefaultIconPath = @"Images\bookmark.png"; + + private static CancellationTokenSource _debounceTokenSource; + public void Init(PluginInitContext context) { + _instance = this; _context = context; - _settings = context.API.LoadSettingJsonStorage(); _faviconCacheDir = Path.Combine( context.CurrentPluginMetadata.PluginCacheDirectoryPath, "FaviconCache"); - LoadBookmarksIfEnabled(); - } - - private static void LoadBookmarksIfEnabled() - { - if (_context.CurrentPluginMetadata.Disabled) - { - // 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; + // Start loading bookmarks asynchronously without blocking Init. + _ = LoadBookmarksInBackgroundAsync(); } - public List Query(Query query) + private async Task LoadBookmarksInBackgroundAsync() { - // For when the plugin being previously disabled and is now re-enabled - if (!_initialized) + // Prevent concurrent loading operations. + await _initializationSemaphore.WaitAsync(); + try { - LoadBookmarksIfEnabled(); - } + if (_isInitialized) return; - string param = query.Search.TrimStart(); - - // Should top results be returned? (true if no search parameters have been passed) - var topResults = string.IsNullOrEmpty(param); + 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) + { + if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) { - 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 = _ => - { - _context.API.OpenUrl(c.Url); - - return true; - }, - ContextData = new BookmarkAttributes { Url = c.Url } + bookmark.FaviconPath = DefaultIconPath; } - ) - .Where(r => r.Score > 0) - .ToList(); + } + } + + _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) + // Immediately return if the initial load is not complete, providing feedback to the user. + if (!_isInitialized) { - return; - } - await _fileMonitorSemaphore.WaitAsync(); - var reader = _refreshQueue.Reader; - while (await reader.WaitToReadAsync()) - { - if (reader.TryRead(out _)) + var initializingTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); + var initializingSubTitle = "Plugin is initializing, please try again in a few seconds"; + try + { + initializingSubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_initializing"); + } + catch (KeyNotFoundException) { - ReloadAllBookmarks(false); + // Ignoring since not all language files will have this key. } + + return new List + { + 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(); @@ -149,7 +145,8 @@ internal static void RegisterBookmarkFile(string path) { return; } - if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase))) + + if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase) && x.Filter == Path.GetFileName(path))) { return; } @@ -157,37 +154,63 @@ internal static void RegisterBookmarkFile(string path) var watcher = new FileSystemWatcher(directory!) { Filter = Path.GetFileName(path), - NotifyFilter = NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size + 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 (_, _) => - { - _refreshQueue.Writer.TryWrite(default); - }; + Watchers.Add(watcher); + } - watcher.EnableRaisingEvents = true; + private static void OnBookmarkFileChanged(object sender, FileSystemEventArgs e) + { + 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. Reloading bookmarks after delay."); + await ReloadAllBookmarks(false); + } + catch (TaskCanceledException) + { + // Debouncing in action + } + }, newCts.Token); } public void ReloadData() { - ReloadAllBookmarks(); + _ = ReloadAllBookmarks(); } - public static void ReloadAllBookmarks(bool disposeFileWatchers = true) + public static async Task ReloadAllBookmarks(bool disposeFileWatchers = true) { - _cachedBookmarks.Clear(); - if (disposeFileWatchers) - DisposeFileWatchers(); - LoadBookmarksIfEnabled(); + try + { + if (_instance == null) return; + + _instance._isInitialized = false; + _cachedBookmarks.Clear(); + 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 +241,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 +265,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..8d3afff3393 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -1,13 +1,13 @@ using System.Windows; +using System.Windows.Controls; using System.Windows.Input; -using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Flow.Launcher.Plugin.BrowserBookmark.Models; namespace Flow.Launcher.Plugin.BrowserBookmark.Views; [INotifyPropertyChanged] -public partial class SettingsControl +public partial class SettingsControl : UserControl { public Settings Settings { get; } public CustomBrowser SelectedCustomBrowser { get; set; } @@ -24,7 +24,7 @@ public bool LoadChromeBookmark set { Settings.LoadChromeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarks(); } } @@ -34,7 +34,7 @@ public bool LoadFirefoxBookmark set { Settings.LoadFirefoxBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarks(); } } @@ -44,7 +44,18 @@ public bool LoadEdgeBookmark set { Settings.LoadEdgeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarks(); + } + } + + public bool EnableFavicons + { + get => Settings.EnableFavicons; + set + { + Settings.EnableFavicons = value; + _ = Main.ReloadAllBookmarks(); + OnPropertyChanged(); } } @@ -62,15 +73,10 @@ 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 - }) + if (window.ShowDialog() == true) { Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarks(); } } @@ -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.ReloadAllBookmarks(); } } @@ -111,7 +117,7 @@ private void EditSelectedCustomBrowser() var result = window.ShowDialog() ?? false; if (result) { - _ = Task.Run(() => Main.ReloadAllBookmarks()); + _ = Main.ReloadAllBookmarks(); } } } From 68f3f213fd5e8293bc27dce7cd4d1d5ed95bc42c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:07:30 +0800 Subject: [PATCH 02/12] Improve code quality --- .../Flow.Launcher.Plugin.BrowserBookmark/Main.cs | 12 ++++++------ .../Views/SettingsControl.xaml.cs | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 3c2d090eb58..89352213304 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -16,8 +16,6 @@ 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; @@ -26,6 +24,8 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex private static List _cachedBookmarks = new(); + private static Main _instance; + private volatile bool _isInitialized = false; private static readonly SemaphoreSlim _initializationSemaphore = new(1, 1); @@ -174,13 +174,13 @@ private static void OnBookmarkFileChanged(object sender, FileSystemEventArgs e) var newCts = _debounceTokenSource; - Task.Run(async () => + _ = Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(3), newCts.Token); _context.API.LogInfo(ClassName, "Bookmark file change detected. Reloading bookmarks after delay."); - await ReloadAllBookmarks(false); + await ReloadAllBookmarksAsync(false); } catch (TaskCanceledException) { @@ -191,10 +191,10 @@ private static void OnBookmarkFileChanged(object sender, FileSystemEventArgs e) public void ReloadData() { - _ = ReloadAllBookmarks(); + _ = ReloadAllBookmarksAsync(); } - public static async Task ReloadAllBookmarks(bool disposeFileWatchers = true) + public static async Task ReloadAllBookmarksAsync(bool disposeFileWatchers = true) { try { diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 8d3afff3393..89478bbd5f6 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -24,7 +24,7 @@ public bool LoadChromeBookmark set { Settings.LoadChromeBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -34,7 +34,7 @@ public bool LoadFirefoxBookmark set { Settings.LoadFirefoxBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -44,7 +44,7 @@ public bool LoadEdgeBookmark set { Settings.LoadEdgeBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -54,7 +54,7 @@ public bool EnableFavicons set { Settings.EnableFavicons = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); OnPropertyChanged(); } } @@ -76,7 +76,7 @@ private void NewCustomBrowser(object sender, RoutedEventArgs e) if (window.ShowDialog() == true) { Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -85,7 +85,7 @@ private void DeleteCustomBrowser(object sender, RoutedEventArgs e) if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) { Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -117,7 +117,7 @@ private void EditSelectedCustomBrowser() var result = window.ShowDialog() ?? false; if (result) { - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } } From 17aab46b78d051bab5ae2b9c962c292354a63481 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:11:50 +0800 Subject: [PATCH 03/12] Check disabled flag first --- .../Main.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 89352213304..9355c2bcacc 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -44,31 +44,34 @@ public void Init(PluginInitContext context) context.CurrentPluginMetadata.PluginCacheDirectoryPath, "FaviconCache"); - // Start loading bookmarks asynchronously without blocking Init. + // Start loading bookmarks asynchronously without blocking Init _ = LoadBookmarksInBackgroundAsync(); } private async Task LoadBookmarksInBackgroundAsync() { + if (_context.CurrentPluginMetadata.Disabled) + { + // Don't load or monitor files if disabled + return; + } + // Prevent concurrent loading operations. await _initializationSemaphore.WaitAsync(); try { if (_isInitialized) return; - 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)); + // 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)); - // Pre-validate all icon paths once to avoid doing it on every query. - foreach (var bookmark in _cachedBookmarks) + // Pre-validate all icon paths once to avoid doing it on every query + foreach (var bookmark in _cachedBookmarks) + { + if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) { - if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) - { - bookmark.FaviconPath = DefaultIconPath; - } + bookmark.FaviconPath = DefaultIconPath; } } From e59902cda31afc4d187f1b4e7fa672661a4fa0db Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:17:56 +0800 Subject: [PATCH 04/12] If the list is not initialized, we need to wait for the list to be refreshed before querying --- .../Main.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 9355c2bcacc..c90311182d1 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -12,7 +12,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark; -public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable +public class Main : ISettingProvider, IAsyncPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable { private static readonly string ClassName = nameof(Main); @@ -34,7 +34,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex private static CancellationTokenSource _debounceTokenSource; - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { _instance = this; _context = context; @@ -46,6 +46,7 @@ public void Init(PluginInitContext context) // Start loading bookmarks asynchronously without blocking Init _ = LoadBookmarksInBackgroundAsync(); + await Task.CompletedTask; } private async Task LoadBookmarksInBackgroundAsync() @@ -83,33 +84,29 @@ private async Task LoadBookmarksInBackgroundAsync() } } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { - // Immediately return if the initial load is not complete, providing feedback to the user. if (!_isInitialized) { - var initializingTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); - var initializingSubTitle = "Plugin is initializing, please try again in a few seconds"; + // If the list is not initialized, we need to wait for the list to be refreshed before querying + await _initializationSemaphore.WaitAsync(token); try { - initializingSubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_initializing"); + return QueryResults(query); } - catch (KeyNotFoundException) + finally { - // Ignoring since not all language files will have this key. + _initializationSemaphore.Release(); } - - return new List - { - new() - { - Title = initializingTitle, - SubTitle = initializingSubTitle, - IcoPath = DefaultIconPath - } - }; } + else + { + return QueryResults(query); + } + } + private static List QueryResults(Query query) + { string param = query.Search.TrimStart(); bool topResults = string.IsNullOrEmpty(param); From 10c954978903bca5364d6463b5287b6b7ed327a8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:20:51 +0800 Subject: [PATCH 05/12] Start initialization when bookmarks are not initialized --- .../Flow.Launcher.Plugin.BrowserBookmark/Main.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index c90311182d1..1e2adc1b5f8 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -88,16 +88,9 @@ public async Task> QueryAsync(Query query, CancellationToken token) { if (!_isInitialized) { - // If the list is not initialized, we need to wait for the list to be refreshed before querying - await _initializationSemaphore.WaitAsync(token); - try - { - return QueryResults(query); - } - finally - { - _initializationSemaphore.Release(); - } + // If the list is not initialized, we need to wait for the list to be initialized before querying + await LoadBookmarksInBackgroundAsync(); + return QueryResults(query); } else { From 4fdea6c7c168080ff820f6146f76185022dd81a7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:22:18 +0800 Subject: [PATCH 06/12] Improve code quality --- Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 1e2adc1b5f8..ba27642b83c 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -86,20 +86,12 @@ private async Task LoadBookmarksInBackgroundAsync() public async Task> QueryAsync(Query query, CancellationToken token) { + // If the list is not initialized, we need to wait for the list to be initialized before querying if (!_isInitialized) { - // If the list is not initialized, we need to wait for the list to be initialized before querying await LoadBookmarksInBackgroundAsync(); - return QueryResults(query); } - else - { - return QueryResults(query); - } - } - private static List QueryResults(Query query) - { string param = query.Search.TrimStart(); bool topResults = string.IsNullOrEmpty(param); From 850794ea2bce9d279a6caf1b7c9265591ae88d38 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:24:39 +0800 Subject: [PATCH 07/12] Improve code comments --- .../Flow.Launcher.Plugin.BrowserBookmark/Main.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index ba27642b83c..4d9b2a09342 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -26,7 +26,7 @@ public class Main : ISettingProvider, IAsyncPlugin, IReloadable, IPluginI18n, IC private static Main _instance; - private volatile bool _isInitialized = false; + private volatile bool _initialized = false; private static readonly SemaphoreSlim _initializationSemaphore = new(1, 1); @@ -61,7 +61,7 @@ private async Task LoadBookmarksInBackgroundAsync() await _initializationSemaphore.WaitAsync(); try { - if (_isInitialized) return; + if (_initialized) return; // Validate the cache directory before loading all bookmarks because Flow needs this directory to storage favicons FilesFolders.ValidateDirectory(_faviconCacheDir); @@ -76,7 +76,7 @@ private async Task LoadBookmarksInBackgroundAsync() } } - _isInitialized = true; + _initialized = true; } finally { @@ -86,8 +86,9 @@ private async Task LoadBookmarksInBackgroundAsync() public async Task> QueryAsync(Query query, CancellationToken token) { - // If the list is not initialized, we need to wait for the list to be initialized before querying - if (!_isInitialized) + // For when the plugin being previously disabled and is now re-enabled + // Or when the plugin is still initializing + if (!_initialized) { await LoadBookmarksInBackgroundAsync(); } @@ -185,7 +186,7 @@ public static async Task ReloadAllBookmarksAsync(bool disposeFileWatchers = true { if (_instance == null) return; - _instance._isInitialized = false; + _instance._initialized = false; _cachedBookmarks.Clear(); if (disposeFileWatchers) DisposeFileWatchers(); From f4b0c093a289689a59b119f17fb6c273e83285d8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:34:29 +0800 Subject: [PATCH 08/12] Improve code quality --- Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs | 8 +++++--- .../Views/SettingsControl.xaml.cs | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 4d9b2a09342..f906a0da84e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -93,8 +93,10 @@ public async Task> QueryAsync(Query query, CancellationToken token) await LoadBookmarksInBackgroundAsync(); } - string param = query.Search.TrimStart(); - bool topResults = string.IsNullOrEmpty(param); + var param = query.Search.TrimStart(); + + // Should top results be returned? (true if no search parameters have been passed) + var topResults = string.IsNullOrEmpty(param); var results = _cachedBookmarks .Select(c => @@ -107,7 +109,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) { Title = c.Name, SubTitle = c.Url, - IcoPath = c.FaviconPath, // Use the pre-validated path directly. + IcoPath = c.FaviconPath, // Use the pre-validated path directly Score = score, Action = _ => { diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 89478bbd5f6..3772cbd605e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -1,5 +1,4 @@ using System.Windows; -using System.Windows.Controls; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; using Flow.Launcher.Plugin.BrowserBookmark.Models; @@ -7,7 +6,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Views; [INotifyPropertyChanged] -public partial class SettingsControl : UserControl +public partial class SettingsControl { public Settings Settings { get; } public CustomBrowser SelectedCustomBrowser { get; set; } @@ -73,7 +72,8 @@ private void NewCustomBrowser(object sender, RoutedEventArgs e) { var newBrowser = new CustomBrowser(); var window = new CustomBrowserSettingWindow(newBrowser); - if (window.ShowDialog() == true) + var result = window.ShowDialog() ?? false; + if (result) { Settings.CustomChromiumBrowsers.Add(newBrowser); _ = Main.ReloadAllBookmarksAsync(); From 4e0e35e9927708737cdecc5d87e351fda87187d8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:36:04 +0800 Subject: [PATCH 09/12] No need to clear pool since we are openning database in false pooling mode --- .../Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index 91a135dd13b..a7e4af1be35 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -95,7 +95,6 @@ public static void ProcessFavicons( Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex); } } - SqliteConnection.ClearPool(connection); }); } From 1df706bbe18986eea92e787c2a87a80c7fb2653d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:44:55 +0800 Subject: [PATCH 10/12] Do not use pooling --- .../Helper/FaviconHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index a7e4af1be35..8057332cff6 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -19,7 +19,8 @@ private static void ExecuteWithTempDb(string faviconCacheDir, string dbPath, Act try { File.Copy(dbPath, tempDbPath, true); - using (var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadWrite")) + // Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580 + using (var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadWrite;Pooling=false")) { connection.Open(); var command = connection.CreateCommand(); From dec30d0924facad5db3c4b913e80abc86a042bc9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Jul 2025 13:49:15 +0800 Subject: [PATCH 11/12] Slight changes for code quality --- .../ChromiumBookmarkLoader.cs | 3 +- .../FirefoxBookmarkLoader.cs | 29 ++++++++++++++++++- .../Helper/FaviconHelper.cs | 2 ++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index bb7c6093eac..b6adeffd7fb 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -4,7 +4,6 @@ using System.Text.Json; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; -using Microsoft.Data.Sqlite; namespace Flow.Launcher.Plugin.BrowserBookmark; @@ -132,7 +131,7 @@ private static void EnumerateFolderBookmark(JsonElement folderElement, ICollecti } else { - Main._context.API.LogError(ClassName, $"type property not found for {subElement.ToString()}"); + Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString() ?? string.Empty}"); } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index fa96826e14f..38117631a57 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -66,6 +66,7 @@ protected List GetBookmarksFromPath(string placesPath) using var command = new SqliteCommand(QueryAllBookmarks, dbConnection); using var reader = command.ExecuteReader(); + // Put results in list while (reader.Read()) { bookmarks.Add(new Bookmark( @@ -75,7 +76,6 @@ protected List GetBookmarksFromPath(string placesPath) )); } - // Load favicons after loading bookmarks if (Main._settings.EnableFavicons) { @@ -197,6 +197,33 @@ 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"); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index 8057332cff6..d9157f7190a 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -57,6 +57,8 @@ public static void ProcessFavicons( ExecuteWithTempDb(faviconCacheDir, dbPath, tempDbPath => { + // 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 savedPaths = new Dictionary(); using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); connection.Open(); From e13159604470b26a388492ca279078c70cff9ba4 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sat, 12 Jul 2025 21:09:05 +0100 Subject: [PATCH 12/12] fixes and revisions from PR review --- .../ChromiumBookmarkLoader.cs | 108 +++++++++----- .../CustomChromiumBookmarkLoader.cs | 1 + .../FirefoxBookmarkLoader.cs | 140 ++++++++++++------ ...low.Launcher.Plugin.BrowserBookmark.csproj | 4 +- .../Helper/FaviconHelper.cs | 116 ++++++++------- .../Languages/ar.xaml | 2 + .../Languages/cs.xaml | 2 + .../Languages/da.xaml | 2 + .../Languages/de.xaml | 2 + .../Languages/en.xaml | 1 + .../Languages/es-419.xaml | 2 + .../Languages/es.xaml | 2 + .../Languages/fr.xaml | 2 + .../Languages/he.xaml | 2 + .../Languages/it.xaml | 2 + .../Languages/ja.xaml | 2 + .../Languages/ko.xaml | 2 + .../Languages/nb.xaml | 2 + .../Languages/nl.xaml | 2 + .../Languages/pl.xaml | 2 + .../Languages/pt-br.xaml | 2 + .../Languages/pt-pt.xaml | 2 + .../Languages/ru.xaml | 2 + .../Languages/sk.xaml | 2 + .../Languages/sr.xaml | 2 + .../Languages/tr.xaml | 2 + .../Languages/uk-UA.xaml | 2 + .../Languages/vi.xaml | 2 + .../Languages/zh-cn.xaml | 2 + .../Languages/zh-tw.xaml | 2 + .../Main.cs | 137 ++++++++++++----- .../Views/SettingsControl.xaml.cs | 16 +- 32 files changed, 392 insertions(+), 179 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index bb7c6093eac..4ed0b3df794 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Microsoft.Data.Sqlite; @@ -25,28 +27,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); @@ -139,23 +128,72 @@ private static void EnumerateFolderBookmark(JsonElement folderElement, ICollecti private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - const string sql = @" - 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 GLOB @pattern - ORDER BY b.width DESC - LIMIT 1"; - - FaviconHelper.ProcessFavicons( - dbPath, - _faviconCacheDir, - bookmarks, - sql, - "http*", - reader => (reader.GetInt64(0).ToString(), (byte[])reader["image_data"]), - (uri, id, data) => Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png") - ); + 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 + 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 + { + 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 GLOB @pattern + ORDER BY b.width DESC + LIMIT 1"; + + cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*"); + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) + return; + + 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_{uri.Host}_{id}.png"); + + if (savedPaths.TryAdd(faviconPath, true)) + { + if (FaviconHelper.SaveBitmapData(imageData, faviconPath)) + bookmark.FaviconPath = faviconPath; + } + else + { + bookmark.FaviconPath = faviconPath; + } + } + catch (Exception 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(); + } + }); + }); } } 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 fa96826e14f..0356ad322f7 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Helper; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Microsoft.Data.Sqlite; @@ -40,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"); @@ -117,26 +112,72 @@ protected List GetBookmarksFromPath(string placesPath) private void LoadFaviconsFromDb(string dbPath, List bookmarks) { - const string sql = @" - 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 GLOB @pattern - AND i.data IS NOT NULL - ORDER BY i.width DESC - LIMIT 1"; - - FaviconHelper.ProcessFavicons( - dbPath, - _faviconCacheDir, - bookmarks, - sql, - "http*", - reader => (reader.GetInt64(0).ToString(), (byte[])reader["data"]), - // Always generate a .png path. The helper will handle the conversion. - (uri, id, data) => Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png") - ); + if (!File.Exists(dbPath)) return; + + FaviconHelper.ExecuteWithTempDb(_faviconCacheDir, dbPath, tempDbPath => + { + var savedPaths = new ConcurrentDictionary(); + + // Get favicons based on bookmarks concurrently + Parallel.ForEach(bookmarks, bookmark => + { + 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 + { + // Query for latest Firefox version favicon structure + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" + 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 GLOB @pattern + AND i.data IS NOT NULL + ORDER BY i.width DESC + LIMIT 1"; + + cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*"); + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) + return; + + var id = reader.GetInt64(0).ToString(); + var imageData = (byte[])reader["data"]; + + if (imageData is not { Length: > 0 }) + return; + + var faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png"); + + if (savedPaths.TryAdd(faviconPath, true)) + { + if (FaviconHelper.SaveBitmapData(imageData, faviconPath)) + bookmark.FaviconPath = faviconPath; + } + else + { + bookmark.FaviconPath = faviconPath; + } + } + catch (Exception 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(); + } + }); + }); } } @@ -205,7 +246,7 @@ private static string GetProfileIniPath(string profileFolderPath) try { - // Parse the ini file into a dictionary of sections + // 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)) @@ -226,40 +267,43 @@ private static string GetProfileIniPath(string profileFolderPath) Dictionary profileSection = null; - // Strategy 1: Find the profile with Default=1 - profileSection = profiles.Values.FirstOrDefault(section => section.TryGetValue("Default", out var value) && value == "1"); + // 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); - // Strategy 2: If no profile has Default=1, use the Default key from the [Install] or [General] section - if (profileSection == null) + if (!string.IsNullOrEmpty(defaultPathRaw)) { - string defaultPathRaw = null; - var installSection = profiles.FirstOrDefault(p => p.Key.StartsWith("Install")); - // Fallback to General section if Install section not found - (installSection.Value ?? profiles.GetValueOrDefault("General"))?.TryGetValue("Default", out 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); + } - if (!string.IsNullOrEmpty(defaultPathRaw)) - { - // The value of 'Default' is the path, find the corresponding profile section - profileSection = profiles.Values.FirstOrDefault(v => v.TryGetValue("Path", out var path) && path == defaultPathRaw); - } + // 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"); } + // If no profile section was found by either strategy, we cannot proceed. if (profileSection == null) return string.Empty; - // We have the profile section, now resolve the path + // We have the correct profile section, now resolve its path. if (!profileSection.TryGetValue("Path", out var pathValue) || string.IsNullOrEmpty(pathValue)) return string.Empty; + // Check if the path is relative or absolute. It defaults to relative if 'IsRelative' is not "0". profileSection.TryGetValue("IsRelative", out var isRelativeRaw); - // If IsRelative is "1" or not present (defaults to relative), combine with profileFolderPath. // 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; + : pathValue; // If IsRelative is "0", the path is absolute and used as-is. - // Path.GetFullPath will resolve any relative parts and give us a clean absolute path. + // Path.GetFullPath will resolve any relative parts (like "..") and give us a clean, absolute path. var fullProfilePath = Path.GetFullPath(profilePath); var placesPath = Path.Combine(fullProfilePath, "places.sqlite"); 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 54f22bdf75a..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,8 +96,10 @@ - + + + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs index 91a135dd13b..6d507bc878d 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs @@ -1,11 +1,12 @@ using System; -using System.Collections.Generic; using System.IO; -using Flow.Launcher.Plugin.BrowserBookmark.Models; +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; @@ -13,7 +14,7 @@ public static class FaviconHelper { private static readonly string ClassName = nameof(FaviconHelper); - private static void ExecuteWithTempDb(string faviconCacheDir, string dbPath, Action action) + public static void ExecuteWithTempDb(string faviconCacheDir, string dbPath, Action action) { var tempDbPath = Path.Combine(faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db"); try @@ -43,69 +44,74 @@ private static void ExecuteWithTempDb(string faviconCacheDir, string dbPath, Act } } - public static void ProcessFavicons( - string dbPath, - string faviconCacheDir, - List bookmarks, - string sqlQuery, - string patternPrefix, - Func imageDataExtractor, - Func pathBuilder) + private static bool IsSvg(byte[] imageData) { - if (!File.Exists(dbPath)) return; + var text = Encoding.UTF8.GetString(imageData, 0, Math.Min(imageData.Length, 100)).Trim(); + return text.StartsWith(" + private static bool ConvertSvgToPng(byte[] svgData, string outputPath) + { + try { - var savedPaths = new Dictionary(); - using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false"); - connection.Open(); + using var stream = new MemoryStream(svgData); + using var svg = new SKSvg(); - using var cmd = connection.CreateCommand(); - cmd.CommandText = sqlQuery; - var patternParam = cmd.CreateParameter(); - patternParam.ParameterName = "@pattern"; - cmd.Parameters.Add(patternParam); + svg.Load(stream); - foreach (var bookmark in bookmarks) + if (svg.Picture == null) { - try - { - if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri)) - continue; - - patternParam.Value = $"{patternPrefix}{uri.Host}/*"; - - using var reader = cmd.ExecuteReader(); - if (!reader.Read()) - continue; - - var (id, imageData) = imageDataExtractor(reader); - if (imageData is not { Length: > 0 }) - continue; - - var faviconPath = pathBuilder(uri, id, imageData); - if (savedPaths.TryAdd(faviconPath, true)) - { - SaveBitmapData(imageData, faviconPath); - } - bookmark.FaviconPath = faviconPath; - } - catch (Exception ex) - { - Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex); - } + Main._context.API.LogWarn(ClassName, $"Failed to load SVG picture from stream for {Path.GetFileName(outputPath)}."); + return false; + } + + var info = new SKImageInfo(64, 64); + using var surface = SKSurface.Create(info); + var canvas = surface.Canvas; + canvas.Clear(SKColors.Transparent); + + var pictureRect = svg.Picture.CullRect; + + canvas.Save(); + if (pictureRect.Width > 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); } - SqliteConnection.ClearPool(connection); - }); + + // 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 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 { // Attempt to load the image data. This will handle all formats ImageSharp - // supports, including SVG (if the necessary decoders are present) and common - // raster formats. It will throw an exception for malformed images. + // supports, including common raster formats. using var image = Image.Load(imageData); // Resize the image to a maximum of 64x64. @@ -118,12 +124,14 @@ public static void SaveBitmapData(byte[] imageData, string outputPath) // Always save as PNG for maximum compatibility with the UI renderer. image.SaveAsPng(outputPath, new PngEncoder { CompressionLevel = PngCompressionLevel.DefaultCompression }); + return true; } catch (Exception ex) { - // This will now catch errors from loading malformed SVGs or other image types, + // 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; } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml index 29b0d4ed1ec..85efd357d63 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml @@ -5,6 +5,8 @@ إشارات المتصفح ابحث في إشارات المتصفح + جاري التهيئة، يرجى الانتظار... + بيانات الإشارات المرجعية فتح الإشارات المرجعية في: 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 3c2d090eb58..5f3e77d25bf 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -28,8 +28,16 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex 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; @@ -39,6 +47,7 @@ public void Init(PluginInitContext context) _instance = this; _context = context; _settings = context.API.LoadSettingJsonStorage(); + _firefoxLastReload = DateTime.UtcNow; _faviconCacheDir = Path.Combine( context.CurrentPluginMetadata.PluginCacheDirectoryPath, @@ -54,25 +63,54 @@ private async Task LoadBookmarksInBackgroundAsync() await _initializationSemaphore.WaitAsync(); try { - if (_isInitialized) return; + // Set initializing state inside the lock. This ensures Query() will show + // the "initializing" message during the entire reload process. + _isInitialized = false; - if (!_context.CurrentPluginMetadata.Disabled) + // Clear data stores inside the lock to ensure a clean slate for the reload. + _cachedBookmarks.Clear(); + try { - // 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 (Directory.Exists(_faviconCacheDir)) + { + Directory.Delete(_faviconCacheDir, true); + } + } + catch (Exception e) + { + _context.API.LogException(ClassName, $"Failed to clear favicon cache folder: {_faviconCacheDir}", e); + } - // Pre-validate all icon paths once to avoid doing it on every query. - foreach (var bookmark in _cachedBookmarks) + // 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) { - if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) + // 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)); + + // Pre-validate all icon paths once to avoid doing it on every query. + foreach (var bookmark in _cachedBookmarks) { - bookmark.FaviconPath = DefaultIconPath; + if (string.IsNullOrEmpty(bookmark.FaviconPath) || !File.Exists(bookmark.FaviconPath)) + { + bookmark.FaviconPath = DefaultIconPath; + } } } } - - _isInitialized = true; + 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; + } } finally { @@ -82,19 +120,41 @@ private async Task LoadBookmarksInBackgroundAsync() public List Query(Query query) { + // 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)) + { + 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; + } + }); + } + } + } + // Immediately return if the initial load is not complete, providing feedback to the user. if (!_isInitialized) { var initializingTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); - var initializingSubTitle = "Plugin is initializing, please try again in a few seconds"; - try - { - initializingSubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_initializing"); - } - catch (KeyNotFoundException) - { - // Ignoring since not all language files will have this key. - } + var initializingSubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_initializing"); return new List { @@ -138,22 +198,22 @@ public List Query(Query query) 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) && x.Filter == Path.GetFileName(path))) + 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), + // Watch the directory, not a specific file, to catch WAL journal updates. NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size, EnableRaisingEvents = true }; @@ -168,40 +228,51 @@ internal static void RegisterBookmarkFile(string path) 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"))) + { + return; // Ignore irrelevant file changes. + } + var oldCts = Interlocked.Exchange(ref _debounceTokenSource, new CancellationTokenSource()); oldCts?.Cancel(); oldCts?.Dispose(); var newCts = _debounceTokenSource; - Task.Run(async () => + _ = Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(3), newCts.Token); - _context.API.LogInfo(ClassName, "Bookmark file change detected. Reloading bookmarks after delay."); - await ReloadAllBookmarks(false); + _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 async Task ReloadAllBookmarks(bool disposeFileWatchers = true) + public static async Task ReloadAllBookmarksAsync(bool disposeFileWatchers = true) { try { if (_instance == null) return; - _instance._isInitialized = false; - _cachedBookmarks.Clear(); + // Simply dispose watchers if needed and then call the main loading method. + // All state management is now handled inside LoadBookmarksInBackgroundAsync. if (disposeFileWatchers) DisposeFileWatchers(); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 8d3afff3393..5aba9ec6a9f 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -24,7 +24,7 @@ public bool LoadChromeBookmark set { Settings.LoadChromeBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -34,7 +34,7 @@ public bool LoadFirefoxBookmark set { Settings.LoadFirefoxBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -44,7 +44,7 @@ public bool LoadEdgeBookmark set { Settings.LoadEdgeBookmark = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -54,7 +54,7 @@ public bool EnableFavicons set { Settings.EnableFavicons = value; - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); OnPropertyChanged(); } } @@ -76,7 +76,7 @@ private void NewCustomBrowser(object sender, RoutedEventArgs e) if (window.ShowDialog() == true) { Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -85,7 +85,7 @@ private void DeleteCustomBrowser(object sender, RoutedEventArgs e) if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) { Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } @@ -117,7 +117,7 @@ private void EditSelectedCustomBrowser() var result = window.ShowDialog() ?? false; if (result) { - _ = Main.ReloadAllBookmarks(); + _ = Main.ReloadAllBookmarksAsync(); } } -} +} \ No newline at end of file