Add local favicon load

This commit is contained in:
DB p 2025-03-20 05:04:19 +09:00
parent e28a69ca91
commit 5481a6aa43
21 changed files with 338 additions and 53 deletions

View file

@ -57,6 +57,7 @@
<PackageReference Include="FSharp.Core" Version="9.0.201" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.21.10" />
</ItemGroup>

View file

@ -67,6 +67,7 @@
</PackageReference>
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->

View file

@ -77,6 +77,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -55,6 +55,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -104,6 +104,7 @@
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="TaskScheduler" Version="2.12.1" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
</ItemGroup>

View file

@ -3,11 +3,24 @@ using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Flow.Launcher.Infrastructure.Logger;
using System;
using System.Data.SQLite;
using SkiaSharp;
namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class ChromiumBookmarkLoader : IBookmarkLoader
{
private readonly string _faviconCacheDir;
protected ChromiumBookmarkLoader()
{
_faviconCacheDir = Path.Combine(
Path.GetDirectoryName(typeof(ChromiumBookmarkLoader).Assembly.Location),
"FaviconCache");
Directory.CreateDirectory(_faviconCacheDir);
}
public abstract List<Bookmark> GetBookmarks();
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
@ -22,10 +35,30 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
if (!File.Exists(bookmarkPath))
continue;
Main.RegisterBookmarkFile(bookmarkPath);
// Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile)
try
{
if (File.Exists(bookmarkPath))
{
//Main.RegisterBookmarkFile(bookmarkPath);
}
}
catch (Exception ex)
{
Log.Exception($"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
}
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source));
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
// Load favicons after loading bookmarks
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
}
bookmarks.AddRange(profileBookmarks);
}
return bookmarks;
@ -52,8 +85,7 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
if (folder.Value.ValueKind != JsonValueKind.Object)
continue;
// Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details.
// If various exceptions start to build up here consider splitting this Loader into multiple separate ones.
// Fix for Opera. It stores bookmarks slightly different than chrome.
if (folder.Name == "custom_root")
EnumerateRoot(folder.Value, bookmarks, source);
else
@ -91,4 +123,108 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
}
}
}
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
{
try
{
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db");
try
{
File.Copy(dbPath, tempDbPath, true);
}
catch (Exception ex)
{
Log.Exception($"Failed to copy favicon DB: {dbPath}", ex);
return;
}
try
{
using var connection = new SQLiteConnection($"Data Source={tempDbPath};Version=3;Read Only=True;");
connection.Open();
foreach (var bookmark in bookmarks)
{
try
{
var url = bookmark.Url;
if (string.IsNullOrEmpty(url)) continue;
// Extract domain from URL
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
continue;
var domain = uri.Host;
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT f.id, b.image_data
FROM favicons f
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
ORDER BY b.width DESC
LIMIT 1";
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
using var reader = cmd.ExecuteReader();
if (reader.Read() && !reader.IsDBNull(1))
{
var iconId = reader.GetInt64(0).ToString();
var imageData = (byte[])reader["image_data"];
if (imageData != null && imageData.Length > 0)
{
var faviconPath = Path.Combine(_faviconCacheDir, $"{domain}_{iconId}.png");
if (!File.Exists(faviconPath))
{
SaveBitmapData(imageData, faviconPath);
}
bookmark.FaviconPath = faviconPath;
}
}
}
catch (Exception ex)
{
Log.Exception($"Failed to extract bookmark favicon: {bookmark.Url}", ex);
}
}
}
catch (Exception ex)
{
Log.Exception($"Failed to connect to SQLite: {tempDbPath}", ex);
}
// Delete temporary file
try { File.Delete(tempDbPath); } catch { /* Ignore */ }
}
catch (Exception ex)
{
Log.Exception($"Failed to load favicon DB: {dbPath}", ex);
}
}
private void SaveBitmapData(byte[] imageData, string outputPath)
{
try
{
using var ms = new MemoryStream(imageData);
using var bitmap = SKBitmap.Decode(ms);
if (bitmap != null)
{
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var fs = File.OpenWrite(outputPath);
data.SaveTo(fs);
}
}
catch (Exception ex)
{
Log.Exception($"Failed to save image: {outputPath}", ex);
}
}
}

View file

@ -4,13 +4,26 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Flow.Launcher.Infrastructure.Logger;
using SkiaSharp;
namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
{
private readonly string _faviconCacheDir;
protected FirefoxBookmarkLoaderBase()
{
_faviconCacheDir = Path.Combine(
Path.GetDirectoryName(typeof(FirefoxBookmarkLoaderBase).Assembly.Location),
"FaviconCache");
Directory.CreateDirectory(_faviconCacheDir);
}
public abstract List<Bookmark> GetBookmarks();
// Updated query - removed favicon_id column
private const string QueryAllBookmarks = """
SELECT moz_places.url, moz_bookmarks.title
FROM moz_places
@ -20,36 +33,161 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
ORDER BY moz_places.visit_count DESC
""";
private const string DbPathFormat = "Data Source ={0}";
private const string DbPathFormat = "Data Source={0}";
protected static List<Bookmark> GetBookmarksFromPath(string placesPath)
protected List<Bookmark> GetBookmarksFromPath(string placesPath)
{
// Return empty list if the places.sqlite file cannot be found
// Variable to store bookmark list
var bookmarks = new List<Bookmark>();
// Return empty list if places.sqlite file doesn't exist
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
return new List<Bookmark>();
return bookmarks;
Main.RegisterBookmarkFile(placesPath);
try
{
// Try to register file monitoring
try
{
Main.RegisterBookmarkFile(placesPath);
}
catch (Exception ex)
{
Log.Exception($"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
}
// create the connection string and init the connection
string dbPath = string.Format(DbPathFormat, placesPath);
using var dbConnection = new SqliteConnection(dbPath);
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite");
File.Copy(placesPath, tempDbPath, true);
// return results in List<Bookmark> format
return reader
.Select(
x => new Bookmark(
x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString()
// Connect to database and execute query
string dbPath = string.Format(DbPathFormat, tempDbPath);
using var dbConnection = new SqliteConnection(dbPath);
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
// Create bookmark list
bookmarks = reader
.Select(
x => new Bookmark(
x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString(),
"Firefox"
)
)
)
.ToList();
.ToList();
// Path to favicon database
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
if (File.Exists(faviconDbPath))
{
LoadFaviconsFromDb(faviconDbPath, bookmarks);
}
// Delete temporary file
try { File.Delete(tempDbPath); } catch { /* Ignore */ }
}
catch (Exception ex)
{
Log.Exception($"Failed to load Firefox bookmarks: {placesPath}", ex);
}
return bookmarks;
}
private void LoadFaviconsFromDb(string faviconDbPath, List<Bookmark> bookmarks)
{
try
{
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.sqlite");
File.Copy(faviconDbPath, tempDbPath, true);
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)
{
try
{
if (string.IsNullOrEmpty(bookmark.Url))
continue;
// Extract domain from URL
if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri))
continue;
var domain = uri.Host;
// Query for latest Firefox version favicon structure
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT i.data
FROM moz_icons i
JOIN moz_icons_to_pages ip ON i.id = ip.icon_id
JOIN moz_pages_w_icons p ON ip.page_id = p.id
WHERE p.page_url LIKE @url
AND i.data IS NOT NULL
ORDER BY i.width DESC -- Select largest icon available
LIMIT 1";
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
using var reader = cmd.ExecuteReader();
if (reader.Read() && !reader.IsDBNull(0))
{
var imageData = (byte[])reader["data"];
if (imageData != null && imageData.Length > 0)
{
var faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png");
if (!File.Exists(faviconPath))
{
SaveBitmapData(imageData, faviconPath);
}
bookmark.FaviconPath = faviconPath;
}
}
}
catch (Exception ex)
{
Log.Exception($"Failed to extract Firefox favicon: {bookmark.Url}", ex);
}
}
// Delete temporary file
try { File.Delete(tempDbPath); } catch { /* Ignore */ }
}
catch (Exception ex)
{
Log.Exception($"Failed to load Firefox favicon DB: {faviconDbPath}", ex);
}
}
private void SaveBitmapData(byte[] imageData, string outputPath)
{
try
{
using var ms = new MemoryStream(imageData);
using var bitmap = SKBitmap.Decode(ms);
if (bitmap != null)
{
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var fs = File.OpenWrite(outputPath);
data.SaveTo(fs);
}
}
catch (Exception ex)
{
Log.Exception($"Failed to save image: {outputPath}", ex);
}
}
}
public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
{
/// <summary>
@ -77,33 +215,6 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
using var sReader = new StreamReader(profileIni);
var ini = sReader.ReadToEnd();
/*
Current profiles.ini structure example as of Firefox version 69.0.1
[Install736426B0AF4A39CB]
Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
Locked=1
[Profile2]
Name=newblahprofile
IsRelative=0
Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
[Profile1]
Name=default
IsRelative=1
Path=Profiles/cydum7q4.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/7789f565.default-release
[General]
StartWithLastProfile=1
Version=2
*/
var lines = ini.Split("\r\n").ToList();
var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty;

View file

@ -96,6 +96,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
</ItemGroup>
</Project>

View file

@ -68,7 +68,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath)
? c.FaviconPath
: @"Images\bookmark.png",
Score = BookmarkLoader.MatchProgram(c, param).Score,
Action = _ =>
{
@ -90,7 +92,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath)
? c.FaviconPath
: @"Images\bookmark.png",
Score = 5,
Action = _ =>
{

View file

@ -18,4 +18,5 @@ public record Bookmark(string Name, string Url, string Source = "")
}
public List<CustomBrowser> CustomBrowsers { get; set; } = new();
public string FaviconPath { get; set; } = string.Empty;
}

View file

@ -63,6 +63,7 @@
<ItemGroup>
<PackageReference Include="Mages" Version="3.0.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -46,6 +46,7 @@
<ItemGroup>
<!-- Do not upgrade System.Data.OleDb since we are .Net7.0 -->
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="System.Data.OleDb" Version="8.0.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />

View file

@ -56,5 +56,9 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -37,4 +37,8 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -55,6 +55,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
<ItemGroup>

View file

@ -69,6 +69,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -60,6 +60,7 @@
<ItemGroup>
<PackageReference Include="InputSimulator" Version="1.0.4" NoWarn="NU1701" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -64,5 +64,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -56,4 +56,8 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -56,4 +56,8 @@
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>

View file

@ -68,5 +68,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
</Project>