Skip to content

Commit a865a3b

Browse files
authored
Merge pull request #3641 from Flow-Launcher/favorite_icons_enabled_disabled
Support Enable / Disable Favicons & Use Concurrent Way to Load Favicons
2 parents 524fb6a + 94b8a66 commit a865a3b

File tree

6 files changed

+143
-70
lines changed

6 files changed

+143
-70
lines changed

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

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
24
using System.IO;
35
using System.Text.Json;
4-
using System;
6+
using System.Threading.Tasks;
57
using Flow.Launcher.Plugin.BrowserBookmark.Models;
68
using Microsoft.Data.Sqlite;
79

@@ -43,16 +45,23 @@ protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
4345
catch (Exception ex)
4446
{
4547
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
48+
continue;
4649
}
4750

4851
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
4952
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
5053

5154
// Load favicons after loading bookmarks
52-
var faviconDbPath = Path.Combine(profile, "Favicons");
53-
if (File.Exists(faviconDbPath))
55+
if (Main._settings.EnableFavicons)
5456
{
55-
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
57+
var faviconDbPath = Path.Combine(profile, "Favicons");
58+
if (File.Exists(faviconDbPath))
59+
{
60+
Main._context.API.StopwatchLogInfo(ClassName, $"Load {profileBookmarks.Count} favicons cost", () =>
61+
{
62+
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
63+
});
64+
}
5665
}
5766

5867
bookmarks.AddRange(profileBookmarks);
@@ -148,19 +157,24 @@ private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
148157

149158
try
150159
{
151-
using var connection = new SqliteConnection($"Data Source={tempDbPath}");
152-
connection.Open();
160+
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
161+
var savedPaths = new ConcurrentDictionary<string, bool>();
153162

154-
foreach (var bookmark in bookmarks)
163+
// Get favicons based on bookmarks concurrently
164+
Parallel.ForEach(bookmarks, bookmark =>
155165
{
166+
// Use read-only connection to avoid locking issues
167+
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
168+
connection.Open();
169+
156170
try
157171
{
158172
var url = bookmark.Url;
159-
if (string.IsNullOrEmpty(url)) continue;
173+
if (string.IsNullOrEmpty(url)) return;
160174

161175
// Extract domain from URL
162176
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
163-
continue;
177+
return;
164178

165179
var domain = uri.Host;
166180

@@ -178,28 +192,36 @@ ORDER BY b.width DESC
178192

179193
using var reader = cmd.ExecuteReader();
180194
if (!reader.Read() || reader.IsDBNull(1))
181-
continue;
195+
return;
182196

183197
var iconId = reader.GetInt64(0).ToString();
184198
var imageData = (byte[])reader["image_data"];
185199

186200
if (imageData is not { Length: > 0 })
187-
continue;
201+
return;
188202

189203
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
190-
SaveBitmapData(imageData, faviconPath);
204+
205+
// Filter out duplicate favicons
206+
if (savedPaths.TryAdd(faviconPath, true))
207+
{
208+
SaveBitmapData(imageData, faviconPath);
209+
}
191210

192211
bookmark.FaviconPath = faviconPath;
193212
}
194213
catch (Exception ex)
195214
{
196215
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
197216
}
198-
}
199-
200-
// https://github.com/dotnet/efcore/issues/26580
201-
SqliteConnection.ClearPool(connection);
202-
connection.Close();
217+
finally
218+
{
219+
// https://github.com/dotnet/efcore/issues/26580
220+
SqliteConnection.ClearPool(connection);
221+
connection.Close();
222+
connection.Dispose();
223+
}
224+
});
203225
}
204226
catch (Exception ex)
205227
{

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

Lines changed: 85 additions & 46 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.Models;
68
using Microsoft.Data.Sqlite;
79

@@ -30,8 +32,6 @@ INNER JOIN moz_bookmarks ON (
3032
ORDER BY moz_places.visit_count DESC
3133
""";
3234

33-
private const string DbPathFormat = "Data Source={0}";
34-
3535
protected List<Bookmark> GetBookmarksFromPath(string placesPath)
3636
{
3737
// Variable to store bookmark list
@@ -41,30 +41,32 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
4141
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
4242
return bookmarks;
4343

44+
// Try to register file monitoring
45+
try
46+
{
47+
Main.RegisterBookmarkFile(placesPath);
48+
}
49+
catch (Exception ex)
50+
{
51+
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
52+
return bookmarks;
53+
}
54+
4455
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite");
4556

4657
try
4758
{
48-
// Try to register file monitoring
49-
try
50-
{
51-
Main.RegisterBookmarkFile(placesPath);
52-
}
53-
catch (Exception ex)
54-
{
55-
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
56-
}
57-
5859
// Use a copy to avoid lock issues with the original file
5960
File.Copy(placesPath, tempDbPath, true);
6061

61-
// Connect to database and execute query
62-
string dbPath = string.Format(DbPathFormat, tempDbPath);
63-
using var dbConnection = new SqliteConnection(dbPath);
62+
// Create the connection string and init the connection
63+
using var dbConnection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
64+
65+
// Open connection to the database file and execute the query
6466
dbConnection.Open();
6567
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
6668

67-
// Create bookmark list
69+
// Get results in List<Bookmark> format
6870
bookmarks = reader
6971
.Select(
7072
x => new Bookmark(
@@ -75,12 +77,20 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
7577
)
7678
.ToList();
7779

78-
// Path to favicon database
79-
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
80-
if (File.Exists(faviconDbPath))
80+
// Load favicons after loading bookmarks
81+
if (Main._settings.EnableFavicons)
8182
{
82-
LoadFaviconsFromDb(faviconDbPath, bookmarks);
83+
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
84+
if (File.Exists(faviconDbPath))
85+
{
86+
Main._context.API.StopwatchLogInfo(ClassName, $"Load {bookmarks.Count} favicons cost", () =>
87+
{
88+
LoadFaviconsFromDb(faviconDbPath, bookmarks);
89+
});
90+
}
8391
}
92+
93+
// Close the connection so that we can delete the temporary file
8494
// https://github.com/dotnet/efcore/issues/26580
8595
SqliteConnection.ClearPool(dbConnection);
8696
dbConnection.Close();
@@ -93,7 +103,10 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
93103
// Delete temporary file
94104
try
95105
{
96-
File.Delete(tempDbPath);
106+
if (File.Exists(tempDbPath))
107+
{
108+
File.Delete(tempDbPath);
109+
}
97110
}
98111
catch (Exception ex)
99112
{
@@ -103,34 +116,52 @@ protected List<Bookmark> GetBookmarksFromPath(string placesPath)
103116
return bookmarks;
104117
}
105118

106-
private void LoadFaviconsFromDb(string faviconDbPath, List<Bookmark> bookmarks)
119+
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
107120
{
121+
// Use a copy to avoid lock issues with the original file
108122
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.sqlite");
109123

110124
try
111125
{
112-
// Use a copy to avoid lock issues with the original file
113-
File.Copy(faviconDbPath, tempDbPath, true);
114-
115-
var defaultIconPath = Path.Combine(
116-
Path.GetDirectoryName(typeof(FirefoxBookmarkLoaderBase).Assembly.Location),
117-
"bookmark.png");
118-
119-
string dbPath = string.Format(DbPathFormat, tempDbPath);
120-
using var connection = new SqliteConnection(dbPath);
121-
connection.Open();
122-
123-
// Get favicons based on bookmark URLs
124-
foreach (var bookmark in bookmarks)
126+
File.Copy(dbPath, tempDbPath, true);
127+
}
128+
catch (Exception ex)
129+
{
130+
try
131+
{
132+
if (File.Exists(tempDbPath))
133+
{
134+
File.Delete(tempDbPath);
135+
}
136+
}
137+
catch (Exception ex1)
138+
{
139+
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1);
140+
}
141+
Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex);
142+
return;
143+
}
144+
145+
try
146+
{
147+
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
148+
var savedPaths = new ConcurrentDictionary<string, bool>();
149+
150+
// Get favicons based on bookmarks concurrently
151+
Parallel.ForEach(bookmarks, bookmark =>
125152
{
153+
// Use read-only connection to avoid locking issues
154+
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
155+
connection.Open();
156+
126157
try
127158
{
128159
if (string.IsNullOrEmpty(bookmark.Url))
129-
continue;
160+
return;
130161

131162
// Extract domain from URL
132163
if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri))
133-
continue;
164+
return;
134165

135166
var domain = uri.Host;
136167

@@ -150,12 +181,12 @@ ORDER BY i.width DESC -- Select largest icon available
150181

151182
using var reader = cmd.ExecuteReader();
152183
if (!reader.Read() || reader.IsDBNull(0))
153-
continue;
184+
return;
154185

155186
var imageData = (byte[])reader["data"];
156187

157188
if (imageData is not { Length: > 0 })
158-
continue;
189+
return;
159190

160191
string faviconPath;
161192
if (IsSvgData(imageData))
@@ -166,23 +197,31 @@ ORDER BY i.width DESC -- Select largest icon available
166197
{
167198
faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png");
168199
}
169-
SaveBitmapData(imageData, faviconPath);
200+
201+
// Filter out duplicate favicons
202+
if (savedPaths.TryAdd(faviconPath, true))
203+
{
204+
SaveBitmapData(imageData, faviconPath);
205+
}
170206

171207
bookmark.FaviconPath = faviconPath;
172208
}
173209
catch (Exception ex)
174210
{
175211
Main._context.API.LogException(ClassName, $"Failed to extract Firefox favicon: {bookmark.Url}", ex);
176212
}
177-
}
178-
179-
// https://github.com/dotnet/efcore/issues/26580
180-
SqliteConnection.ClearPool(connection);
181-
connection.Close();
213+
finally
214+
{
215+
// https://github.com/dotnet/efcore/issues/26580
216+
SqliteConnection.ClearPool(connection);
217+
connection.Close();
218+
connection.Dispose();
219+
}
220+
});
182221
}
183222
catch (Exception ex)
184223
{
185-
Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {faviconDbPath}", ex);
224+
Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {tempDbPath}", ex);
186225
}
187226

188227
// Delete temporary file

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@
2727
<system:String x:Key="flowlauncher_plugin_browserbookmark_browserEngine">Browser Engine</system:String>
2828
<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>
2929
<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>
30+
<system:String x:Key="flowlauncher_plugin_browserbookmark_enable_favicons">Load favicons (can be time consuming during startup)</system:String>
31+
3032
</ResourceDictionary>

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Linq;
5+
using System.Threading.Channels;
6+
using System.Threading.Tasks;
7+
using System.Threading;
48
using System.Windows.Controls;
59
using Flow.Launcher.Plugin.BrowserBookmark.Commands;
610
using Flow.Launcher.Plugin.BrowserBookmark.Models;
711
using Flow.Launcher.Plugin.BrowserBookmark.Views;
8-
using System.IO;
9-
using System.Threading.Channels;
10-
using System.Threading.Tasks;
11-
using System.Threading;
1212
using Flow.Launcher.Plugin.SharedCommands;
1313

1414
namespace Flow.Launcher.Plugin.BrowserBookmark;
@@ -21,9 +21,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
2121

2222
internal static PluginInitContext _context;
2323

24-
private static List<Bookmark> _cachedBookmarks = new();
24+
internal static Settings _settings;
2525

26-
private static Settings _settings;
26+
private static List<Bookmark> _cachedBookmarks = new();
2727

2828
private static bool _initialized = false;
2929

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public class Settings : BaseModel
88

99
public string BrowserPath { get; set; }
1010

11+
public bool EnableFavicons { get; set; } = false;
12+
1113
public bool LoadChromeBookmark { get; set; } = true;
1214
public bool LoadFirefoxBookmark { get; set; } = true;
1315
public bool LoadEdgeBookmark { get; set; } = true;

0 commit comments

Comments
 (0)