Skip to content

Support enable/disable Favicons & use concurrency to speed up loading #3641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -43,16 +45,23 @@
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

Check warning on line 54 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`favicons` is not a recognized word. (unrecognized-spelling)
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
if (Main._settings.EnableFavicons)
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
var faviconDbPath = Path.Combine(profile, "Favicons");

Check warning on line 57 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Favicons` is not a recognized word. (unrecognized-spelling)
if (File.Exists(faviconDbPath))
{
Main._context.API.StopwatchLogInfo(ClassName, $"Load {profileBookmarks.Count} favicons cost", () =>

Check warning on line 60 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`favicons` is not a recognized word. (unrecognized-spelling)
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);

Check warning on line 62 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Favicons` is not a recognized word. (unrecognized-spelling)
});
}
}

bookmarks.AddRange(profileBookmarks);
Expand Down Expand Up @@ -120,7 +129,7 @@
}
}

private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)

Check warning on line 132 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Favicons` is not a recognized word. (unrecognized-spelling)
{
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db");
Expand Down Expand Up @@ -148,26 +157,31 @@

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<string, bool>();

foreach (var bookmark in bookmarks)
// Get favicons based on bookmarks concurrently

Check warning on line 163 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`favicons` is not a recognized word. (unrecognized-spelling)
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;

using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT f.id, b.image_data
FROM favicons f

Check warning on line 184 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`favicons` is not a recognized word. (unrecognized-spelling)
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
Expand All @@ -178,28 +192,36 @@

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;
}
catch (Exception ex)
{
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)
{
Expand Down
131 changes: 85 additions & 46 deletions Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -22,16 +24,14 @@

// Updated query - removed favicon_id column
private const string QueryAllBookmarks = """
SELECT moz_places.url, moz_bookmarks.title

Check warning on line 27 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`moz` is not a recognized word. (unrecognized-spelling)
FROM moz_places

Check warning on line 28 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`moz` is not a recognized word. (unrecognized-spelling)
INNER JOIN moz_bookmarks ON (

Check warning on line 29 in Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`moz` is not a recognized word. (unrecognized-spelling)
moz_bookmarks.fk NOT NULL AND moz_bookmarks.title NOT NULL AND moz_bookmarks.fk = moz_places.id
)
ORDER BY moz_places.visit_count DESC
""";

private const string DbPathFormat = "Data Source={0}";

protected List<Bookmark> GetBookmarksFromPath(string placesPath)
{
// Variable to store bookmark list
Expand All @@ -41,30 +41,32 @@
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<Bookmark> format
bookmarks = reader
.Select(
x => new Bookmark(
Expand All @@ -75,12 +77,20 @@
)
.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();
Expand All @@ -93,7 +103,10 @@
// Delete temporary file
try
{
File.Delete(tempDbPath);
if (File.Exists(tempDbPath))
{
File.Delete(tempDbPath);
}
}
catch (Exception ex)
{
Expand All @@ -103,34 +116,52 @@
return bookmarks;
}

private void LoadFaviconsFromDb(string faviconDbPath, List<Bookmark> bookmarks)
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> 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<string, bool>();

// 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;

Expand All @@ -150,12 +181,12 @@

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))
Expand All @@ -166,23 +197,31 @@
{
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;
}
catch (Exception ex)
{
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@
<system:String x:Key="flowlauncher_plugin_browserbookmark_browserEngine">Browser Engine</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_guideMessage01">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.</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_guideMessage02">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.</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_enable_favicons">Load favicons (can be time consuming during startup)</system:String>

</ResourceDictionary>
12 changes: 6 additions & 6 deletions Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,9 +21,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex

internal static PluginInitContext _context;

private static List<Bookmark> _cachedBookmarks = new();
internal static Settings _settings;

private static Settings _settings;
private static List<Bookmark> _cachedBookmarks = new();

private static bool _initialized = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading