11using System ;
2+ using System . Collections . Concurrent ;
23using System . Collections . Generic ;
34using System . IO ;
45using System . Linq ;
6+ using System . Threading . Tasks ;
57using Flow . Launcher . Plugin . BrowserBookmark . Helper ;
68using Flow . Launcher . Plugin . BrowserBookmark . Models ;
79using 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" ) ;
0 commit comments