Skip to content

Commit e131596

Browse files
committed
fixes and revisions from PR review
1 parent 5b3dd83 commit e131596

32 files changed

+392
-179
lines changed

Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.IO;
45
using System.Text.Json;
6+
using System.Threading.Tasks;
57
using Flow.Launcher.Plugin.BrowserBookmark.Helper;
68
using Flow.Launcher.Plugin.BrowserBookmark.Models;
79
using Microsoft.Data.Sqlite;
@@ -25,28 +27,15 @@ protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
2527
{
2628
var bookmarks = new List<Bookmark>();
2729
if (!Directory.Exists(browserDataPath)) return bookmarks;
30+
31+
// Watch the entire user data directory for changes to catch journal file writes.
32+
Main.RegisterBrowserDataDirectory(browserDataPath);
33+
2834
var paths = Directory.GetDirectories(browserDataPath);
2935

3036
foreach (var profile in paths)
3137
{
3238
var bookmarkPath = Path.Combine(profile, "Bookmarks");
33-
if (!File.Exists(bookmarkPath))
34-
continue;
35-
36-
// Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile)
37-
try
38-
{
39-
if (File.Exists(bookmarkPath))
40-
{
41-
Main.RegisterBookmarkFile(bookmarkPath);
42-
}
43-
}
44-
catch (Exception ex)
45-
{
46-
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
47-
continue;
48-
}
49-
5039
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
5140
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
5241

@@ -139,23 +128,72 @@ private static void EnumerateFolderBookmark(JsonElement folderElement, ICollecti
139128

140129
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
141130
{
142-
const string sql = @"
143-
SELECT f.id, b.image_data
144-
FROM favicons f
145-
JOIN favicon_bitmaps b ON f.id = b.icon_id
146-
JOIN icon_mapping m ON f.id = m.icon_id
147-
WHERE m.page_url GLOB @pattern
148-
ORDER BY b.width DESC
149-
LIMIT 1";
150-
151-
FaviconHelper.ProcessFavicons(
152-
dbPath,
153-
_faviconCacheDir,
154-
bookmarks,
155-
sql,
156-
"http*",
157-
reader => (reader.GetInt64(0).ToString(), (byte[])reader["image_data"]),
158-
(uri, id, data) => Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png")
159-
);
131+
if (!File.Exists(dbPath)) return;
132+
133+
FaviconHelper.ExecuteWithTempDb(_faviconCacheDir, dbPath, tempDbPath =>
134+
{
135+
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
136+
var savedPaths = new ConcurrentDictionary<string, bool>();
137+
138+
// Get favicons based on bookmarks concurrently
139+
Parallel.ForEach(bookmarks, bookmark =>
140+
{
141+
// Use read-only connection to avoid locking issues
142+
// Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580
143+
if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri))
144+
return;
145+
146+
using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false");
147+
connection.Open();
148+
149+
try
150+
{
151+
using var cmd = connection.CreateCommand();
152+
cmd.CommandText = @"
153+
SELECT f.id, b.image_data
154+
FROM favicons f
155+
JOIN favicon_bitmaps b ON f.id = b.icon_id
156+
JOIN icon_mapping m ON f.id = m.icon_id
157+
WHERE m.page_url GLOB @pattern
158+
ORDER BY b.width DESC
159+
LIMIT 1";
160+
161+
cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*");
162+
163+
using var reader = cmd.ExecuteReader();
164+
if (!reader.Read())
165+
return;
166+
167+
var id = reader.GetInt64(0).ToString();
168+
var imageData = (byte[])reader["image_data"];
169+
170+
if (imageData is not { Length: > 0 })
171+
return;
172+
173+
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png");
174+
175+
if (savedPaths.TryAdd(faviconPath, true))
176+
{
177+
if (FaviconHelper.SaveBitmapData(imageData, faviconPath))
178+
bookmark.FaviconPath = faviconPath;
179+
}
180+
else
181+
{
182+
bookmark.FaviconPath = faviconPath;
183+
}
184+
}
185+
catch (Exception ex)
186+
{
187+
Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex);
188+
}
189+
finally
190+
{
191+
// Cache connection and clear pool after all operations to avoid issue:
192+
// ObjectDisposedException: Safe handle has been closed.
193+
SqliteConnection.ClearPool(connection);
194+
connection.Close();
195+
}
196+
});
197+
});
160198
}
161199
}

Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Flow.Launcher.Plugin.BrowserBookmark.Models;
22
using System.Collections.Generic;
3+
using System.IO;
34

45
namespace Flow.Launcher.Plugin.BrowserBookmark;
56

Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.IO;
45
using System.Linq;
6+
using System.Threading.Tasks;
57
using Flow.Launcher.Plugin.BrowserBookmark.Helper;
68
using Flow.Launcher.Plugin.BrowserBookmark.Models;
79
using Microsoft.Data.Sqlite;
@@ -40,16 +42,9 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
4042
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
4143
return bookmarks;
4244

43-
// Try to register file monitoring
44-
try
45-
{
46-
Main.RegisterBookmarkFile(placesPath);
47-
}
48-
catch (Exception ex)
49-
{
50-
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
51-
return bookmarks;
52-
}
45+
// DO NOT watch Firefox files, as places.sqlite is updated on every navigation,
46+
// which would cause constant, performance-killing reloads.
47+
// A periodic check on query is used instead.
5348

5449
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite");
5550

@@ -117,26 +112,72 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
117112

118113
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
119114
{
120-
const string sql = @"
121-
SELECT i.id, i.data
122-
FROM moz_icons i
123-
JOIN moz_icons_to_pages ip ON i.id = ip.icon_id
124-
JOIN moz_pages_w_icons p ON ip.page_id = p.id
125-
WHERE p.page_url GLOB @pattern
126-
AND i.data IS NOT NULL
127-
ORDER BY i.width DESC
128-
LIMIT 1";
129-
130-
FaviconHelper.ProcessFavicons(
131-
dbPath,
132-
_faviconCacheDir,
133-
bookmarks,
134-
sql,
135-
"http*",
136-
reader => (reader.GetInt64(0).ToString(), (byte[])reader["data"]),
137-
// Always generate a .png path. The helper will handle the conversion.
138-
(uri, id, data) => Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png")
139-
);
115+
if (!File.Exists(dbPath)) return;
116+
117+
FaviconHelper.ExecuteWithTempDb(_faviconCacheDir, dbPath, tempDbPath =>
118+
{
119+
var savedPaths = new ConcurrentDictionary<string, bool>();
120+
121+
// Get favicons based on bookmarks concurrently
122+
Parallel.ForEach(bookmarks, bookmark =>
123+
{
124+
if (string.IsNullOrEmpty(bookmark.Url) || !Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uri))
125+
return;
126+
127+
using var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false");
128+
connection.Open();
129+
130+
try
131+
{
132+
// Query for latest Firefox version favicon structure
133+
using var cmd = connection.CreateCommand();
134+
cmd.CommandText = @"
135+
SELECT i.id, i.data
136+
FROM moz_icons i
137+
JOIN moz_icons_to_pages ip ON i.id = ip.icon_id
138+
JOIN moz_pages_w_icons p ON ip.page_id = p.id
139+
WHERE p.page_url GLOB @pattern
140+
AND i.data IS NOT NULL
141+
ORDER BY i.width DESC
142+
LIMIT 1";
143+
144+
cmd.Parameters.AddWithValue("@pattern", $"http*{uri.Host}/*");
145+
146+
using var reader = cmd.ExecuteReader();
147+
if (!reader.Read())
148+
return;
149+
150+
var id = reader.GetInt64(0).ToString();
151+
var imageData = (byte[])reader["data"];
152+
153+
if (imageData is not { Length: > 0 })
154+
return;
155+
156+
var faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{uri.Host}_{id}.png");
157+
158+
if (savedPaths.TryAdd(faviconPath, true))
159+
{
160+
if (FaviconHelper.SaveBitmapData(imageData, faviconPath))
161+
bookmark.FaviconPath = faviconPath;
162+
}
163+
else
164+
{
165+
bookmark.FaviconPath = faviconPath;
166+
}
167+
}
168+
catch (Exception ex)
169+
{
170+
Main._context.API.LogException(ClassName, $"Failed to extract favicon for: {bookmark.Url}", ex);
171+
}
172+
finally
173+
{
174+
// Cache connection and clear pool after all operations to avoid issue:
175+
// ObjectDisposedException: Safe handle has been closed.
176+
SqliteConnection.ClearPool(connection);
177+
connection.Close();
178+
}
179+
});
180+
});
140181
}
141182
}
142183

@@ -205,7 +246,7 @@ private static string GetProfileIniPath(string profileFolderPath)
205246

206247
try
207248
{
208-
// Parse the ini file into a dictionary of sections
249+
// Parse the ini file into a dictionary of sections for easier and more reliable access.
209250
var profiles = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
210251
Dictionary<string, string> currentSection = null;
211252
foreach (var line in File.ReadLines(profileIni))
@@ -226,40 +267,43 @@ private static string GetProfileIniPath(string profileFolderPath)
226267

227268
Dictionary<string, string> profileSection = null;
228269

229-
// Strategy 1: Find the profile with Default=1
230-
profileSection = profiles.Values.FirstOrDefault(section => section.TryGetValue("Default", out var value) && value == "1");
270+
// STRATEGY 1 (Primary): Find the default profile using the 'Default' key in the [Install] or [General] sections.
271+
// This is the most reliable method for modern Firefox versions.
272+
string defaultPathRaw = null;
273+
var installSection = profiles.FirstOrDefault(p => p.Key.StartsWith("Install"));
274+
// Fallback to the [General] section if the [Install] section is not found.
275+
(installSection.Value ?? profiles.GetValueOrDefault("General"))?.TryGetValue("Default", out defaultPathRaw);
231276

232-
// Strategy 2: If no profile has Default=1, use the Default key from the [Install] or [General] section
233-
if (profileSection == null)
277+
if (!string.IsNullOrEmpty(defaultPathRaw))
234278
{
235-
string defaultPathRaw = null;
236-
var installSection = profiles.FirstOrDefault(p => p.Key.StartsWith("Install"));
237-
// Fallback to General section if Install section not found
238-
(installSection.Value ?? profiles.GetValueOrDefault("General"))?.TryGetValue("Default", out defaultPathRaw);
279+
// The value of 'Default' is the path itself. We now find the profile section that has this path.
280+
profileSection = profiles.Values.FirstOrDefault(v => v.TryGetValue("Path", out var path) && path == defaultPathRaw);
281+
}
239282

240-
if (!string.IsNullOrEmpty(defaultPathRaw))
241-
{
242-
// The value of 'Default' is the path, find the corresponding profile section
243-
profileSection = profiles.Values.FirstOrDefault(v => v.TryGetValue("Path", out var path) && path == defaultPathRaw);
244-
}
283+
// STRATEGY 2 (Fallback): If the primary strategy fails, look for a profile with the 'Default=1' flag.
284+
// This is for older versions or non-standard configurations.
285+
if (profileSection == null)
286+
{
287+
profileSection = profiles.Values.FirstOrDefault(section => section.TryGetValue("Default", out var value) && value == "1");
245288
}
246289

290+
// If no profile section was found by either strategy, we cannot proceed.
247291
if (profileSection == null)
248292
return string.Empty;
249293

250-
// We have the profile section, now resolve the path
294+
// We have the correct profile section, now resolve its path.
251295
if (!profileSection.TryGetValue("Path", out var pathValue) || string.IsNullOrEmpty(pathValue))
252296
return string.Empty;
253297

298+
// Check if the path is relative or absolute. It defaults to relative if 'IsRelative' is not "0".
254299
profileSection.TryGetValue("IsRelative", out var isRelativeRaw);
255300

256-
// If IsRelative is "1" or not present (defaults to relative), combine with profileFolderPath.
257301
// The path in the ini file often uses forward slashes, so normalize them.
258302
var profilePath = isRelativeRaw != "0"
259303
? Path.Combine(profileFolderPath, pathValue.Replace('/', Path.DirectorySeparatorChar))
260-
: pathValue;
304+
: pathValue; // If IsRelative is "0", the path is absolute and used as-is.
261305

262-
// Path.GetFullPath will resolve any relative parts and give us a clean absolute path.
306+
// Path.GetFullPath will resolve any relative parts (like "..") and give us a clean, absolute path.
263307
var fullProfilePath = Path.GetFullPath(profilePath);
264308

265309
var placesPath = Path.Combine(fullProfilePath, "places.sqlite");

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@
9696

9797
<ItemGroup>
9898
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
99-
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
99+
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.7" />
100100
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
101+
<PackageReference Include="SkiaSharp" Version="3.119.0" />
102+
<PackageReference Include="Svg.Skia" Version="3.0.3" />
101103
</ItemGroup>
102104

103105
</Project>

0 commit comments

Comments
 (0)