diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index e859976bdc2..e102e43b67b 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Text.Json; -using System; +using System.Threading.Tasks; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Microsoft.Data.Sqlite; @@ -43,16 +45,23 @@ protected List LoadBookmarks(string browserDataPath, string name) 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); // Load favicons after loading bookmarks - var faviconDbPath = Path.Combine(profile, "Favicons"); - if (File.Exists(faviconDbPath)) + if (Main._settings.EnableFavicons) { - LoadFaviconsFromDb(faviconDbPath, profileBookmarks); + var faviconDbPath = Path.Combine(profile, "Favicons"); + if (File.Exists(faviconDbPath)) + { + Main._context.API.StopwatchLogInfo(ClassName, $"Load {profileBookmarks.Count} favicons cost", () => + { + LoadFaviconsFromDb(faviconDbPath, profileBookmarks); + }); + } } bookmarks.AddRange(profileBookmarks); @@ -148,19 +157,24 @@ private void LoadFaviconsFromDb(string dbPath, List bookmarks) try { - using var connection = new SqliteConnection($"Data Source={tempDbPath}"); - connection.Open(); + // Since some bookmarks may have same favicon id, we need to record them to avoid duplicates + var savedPaths = new ConcurrentDictionary(); - foreach (var bookmark in bookmarks) + // Get favicons based on bookmarks concurrently + Parallel.ForEach(bookmarks, bookmark => { + // Use read-only connection to avoid locking issues + var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly"); + connection.Open(); + try { var url = bookmark.Url; - if (string.IsNullOrEmpty(url)) continue; + if (string.IsNullOrEmpty(url)) return; // Extract domain from URL if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - continue; + return; var domain = uri.Host; @@ -178,16 +192,21 @@ ORDER BY b.width DESC using var reader = cmd.ExecuteReader(); if (!reader.Read() || reader.IsDBNull(1)) - continue; + return; var iconId = reader.GetInt64(0).ToString(); var imageData = (byte[])reader["image_data"]; if (imageData is not { Length: > 0 }) - continue; + return; var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png"); - SaveBitmapData(imageData, faviconPath); + + // Filter out duplicate favicons + if (savedPaths.TryAdd(faviconPath, true)) + { + SaveBitmapData(imageData, faviconPath); + } bookmark.FaviconPath = faviconPath; } @@ -195,11 +214,14 @@ ORDER BY b.width DESC { Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex); } - } - - // https://github.com/dotnet/efcore/issues/26580 - SqliteConnection.ClearPool(connection); - connection.Close(); + finally + { + // https://github.com/dotnet/efcore/issues/26580 + SqliteConnection.ClearPool(connection); + connection.Close(); + connection.Dispose(); + } + }); } catch (Exception ex) { diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index 75f26d32252..492a76c7bc0 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.Models; using Microsoft.Data.Sqlite; @@ -30,8 +32,6 @@ INNER JOIN moz_bookmarks ON ( ORDER BY moz_places.visit_count DESC """; - private const string DbPathFormat = "Data Source={0}"; - protected List GetBookmarksFromPath(string placesPath) { // Variable to store bookmark list @@ -41,30 +41,32 @@ 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; + } + var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite"); try { - // 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); - } - // Use a copy to avoid lock issues with the original file File.Copy(placesPath, tempDbPath, true); - // Connect to database and execute query - string dbPath = string.Format(DbPathFormat, tempDbPath); - using var dbConnection = new SqliteConnection(dbPath); + // Create the connection string and init the connection + using var dbConnection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly"); + + // Open connection to the database file and execute the query dbConnection.Open(); var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader(); - // Create bookmark list + // Get results in List format bookmarks = reader .Select( x => new Bookmark( @@ -75,12 +77,20 @@ protected List GetBookmarksFromPath(string placesPath) ) .ToList(); - // Path to favicon database - var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite"); - if (File.Exists(faviconDbPath)) + // Load favicons after loading bookmarks + if (Main._settings.EnableFavicons) { - LoadFaviconsFromDb(faviconDbPath, bookmarks); + var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite"); + if (File.Exists(faviconDbPath)) + { + Main._context.API.StopwatchLogInfo(ClassName, $"Load {bookmarks.Count} favicons cost", () => + { + LoadFaviconsFromDb(faviconDbPath, bookmarks); + }); + } } + + // Close the connection so that we can delete the temporary file // https://github.com/dotnet/efcore/issues/26580 SqliteConnection.ClearPool(dbConnection); dbConnection.Close(); @@ -93,7 +103,10 @@ protected List GetBookmarksFromPath(string placesPath) // Delete temporary file try { - File.Delete(tempDbPath); + if (File.Exists(tempDbPath)) + { + File.Delete(tempDbPath); + } } catch (Exception ex) { @@ -103,34 +116,52 @@ protected List GetBookmarksFromPath(string placesPath) return bookmarks; } - private void LoadFaviconsFromDb(string faviconDbPath, List bookmarks) + private void LoadFaviconsFromDb(string dbPath, List bookmarks) { + // Use a copy to avoid lock issues with the original file var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.sqlite"); try { - // Use a copy to avoid lock issues with the original file - File.Copy(faviconDbPath, tempDbPath, true); - - var defaultIconPath = Path.Combine( - Path.GetDirectoryName(typeof(FirefoxBookmarkLoaderBase).Assembly.Location), - "bookmark.png"); - - string dbPath = string.Format(DbPathFormat, tempDbPath); - using var connection = new SqliteConnection(dbPath); - connection.Open(); - - // Get favicons based on bookmark URLs - foreach (var bookmark in bookmarks) + File.Copy(dbPath, tempDbPath, true); + } + catch (Exception ex) + { + try + { + if (File.Exists(tempDbPath)) + { + File.Delete(tempDbPath); + } + } + catch (Exception ex1) + { + Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1); + } + Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex); + return; + } + + try + { + // 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 + var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly"); + connection.Open(); + try { if (string.IsNullOrEmpty(bookmark.Url)) - continue; + return; // Extract domain from URL if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri)) - continue; + return; var domain = uri.Host; @@ -150,12 +181,12 @@ ORDER BY i.width DESC -- Select largest icon available using var reader = cmd.ExecuteReader(); if (!reader.Read() || reader.IsDBNull(0)) - continue; + return; var imageData = (byte[])reader["data"]; if (imageData is not { Length: > 0 }) - continue; + return; string faviconPath; if (IsSvgData(imageData)) @@ -166,7 +197,12 @@ ORDER BY i.width DESC -- Select largest icon available { faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png"); } - SaveBitmapData(imageData, faviconPath); + + // Filter out duplicate favicons + if (savedPaths.TryAdd(faviconPath, true)) + { + SaveBitmapData(imageData, faviconPath); + } bookmark.FaviconPath = faviconPath; } @@ -174,15 +210,18 @@ ORDER BY i.width DESC -- Select largest icon available { Main._context.API.LogException(ClassName, $"Failed to extract Firefox favicon: {bookmark.Url}", ex); } - } - - // https://github.com/dotnet/efcore/issues/26580 - SqliteConnection.ClearPool(connection); - connection.Close(); + finally + { + // https://github.com/dotnet/efcore/issues/26580 + SqliteConnection.ClearPool(connection); + connection.Close(); + connection.Dispose(); + } + }); } catch (Exception ex) { - Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {faviconDbPath}", ex); + Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {tempDbPath}", ex); } // Delete temporary file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml index e5f3d541e11..22830e7c880 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml @@ -27,4 +27,6 @@ Browser Engine If you are not using Chrome, Firefox or Edge, or you are using their portable version, you need to add bookmarks data directory and select correct browser engine to make this plugin work. For example: Brave's engine is Chromium; and its default bookmarks data location is: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". For Firefox engine, the bookmarks directory is the userdata folder contains the places.sqlite file. + Load favicons (can be time consuming during startup) + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 155069495bc..91ade206b67 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Channels; +using System.Threading.Tasks; +using System.Threading; using System.Windows.Controls; using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Views; -using System.IO; -using System.Threading.Channels; -using System.Threading.Tasks; -using System.Threading; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Plugin.BrowserBookmark; @@ -21,9 +21,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex internal static PluginInitContext _context; - private static List _cachedBookmarks = new(); + internal static Settings _settings; - private static Settings _settings; + private static List _cachedBookmarks = new(); private static bool _initialized = false; diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs index 86532d27598..a0041e0d6a0 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs @@ -8,6 +8,8 @@ public class Settings : BaseModel public string BrowserPath { get; set; } + public bool EnableFavicons { get; set; } = false; + public bool LoadChromeBookmark { get; set; } = true; public bool LoadFirefoxBookmark { get; set; } = true; public bool LoadEdgeBookmark { get; set; } = true; diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml index 30424f4e892..0767ee98069 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml @@ -12,6 +12,7 @@ + + \ No newline at end of file