2025-08-31 07:53:21 +00:00
|
|
|
using System;
|
2025-06-04 17:12:46 +00:00
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
using System.Collections.Generic;
|
2021-09-21 18:50:51 +00:00
|
|
|
using System.IO;
|
|
|
|
|
using System.Text.Json;
|
2025-06-04 17:12:46 +00:00
|
|
|
using System.Threading.Tasks;
|
2025-06-08 04:35:39 +00:00
|
|
|
using Flow.Launcher.Plugin.BrowserBookmark.Helper;
|
2025-04-02 14:31:31 +00:00
|
|
|
using Flow.Launcher.Plugin.BrowserBookmark.Models;
|
2025-03-20 08:03:08 +00:00
|
|
|
using Microsoft.Data.Sqlite;
|
2021-09-21 18:50:51 +00:00
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
namespace Flow.Launcher.Plugin.BrowserBookmark;
|
|
|
|
|
|
|
|
|
|
public abstract class ChromiumBookmarkLoader : IBookmarkLoader
|
2021-09-21 18:50:51 +00:00
|
|
|
{
|
2025-04-02 14:39:30 +00:00
|
|
|
private static readonly string ClassName = nameof(ChromiumBookmarkLoader);
|
2025-04-02 14:31:31 +00:00
|
|
|
|
2025-03-19 20:04:19 +00:00
|
|
|
private readonly string _faviconCacheDir;
|
|
|
|
|
|
|
|
|
|
protected ChromiumBookmarkLoader()
|
|
|
|
|
{
|
2025-04-02 14:39:30 +00:00
|
|
|
_faviconCacheDir = Main._faviconCacheDir;
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
public abstract List<Bookmark> GetBookmarks();
|
|
|
|
|
|
|
|
|
|
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
|
2021-09-21 18:50:51 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
var bookmarks = new List<Bookmark>();
|
|
|
|
|
if (!Directory.Exists(browserDataPath)) return bookmarks;
|
|
|
|
|
var paths = Directory.GetDirectories(browserDataPath);
|
2023-12-18 06:30:18 +00:00
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
foreach (var profile in paths)
|
2021-09-21 18:50:51 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
var bookmarkPath = Path.Combine(profile, "Bookmarks");
|
|
|
|
|
if (!File.Exists(bookmarkPath))
|
|
|
|
|
continue;
|
2021-09-21 18:50:51 +00:00
|
|
|
|
2025-03-19 20:04:19 +00:00
|
|
|
// Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile)
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(bookmarkPath))
|
|
|
|
|
{
|
2025-04-09 05:12:05 +00:00
|
|
|
Main.RegisterBookmarkFile(bookmarkPath);
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2025-06-23 04:38:27 +00:00
|
|
|
Main.Context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
|
2025-06-04 15:21:57 +00:00
|
|
|
continue;
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
2023-12-18 06:30:18 +00:00
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
|
2025-03-19 20:04:19 +00:00
|
|
|
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
|
|
|
|
|
|
|
|
|
|
// Load favicons after loading bookmarks
|
2025-06-05 02:21:24 +00:00
|
|
|
if (Main._settings.EnableFavicons)
|
2025-03-19 20:04:19 +00:00
|
|
|
{
|
2025-06-04 15:21:57 +00:00
|
|
|
var faviconDbPath = Path.Combine(profile, "Favicons");
|
|
|
|
|
if (File.Exists(faviconDbPath))
|
|
|
|
|
{
|
2025-06-23 04:38:27 +00:00
|
|
|
Main.Context.API.StopwatchLogInfo(ClassName, $"Load {profileBookmarks.Count} favicons cost", () =>
|
2025-06-04 16:27:03 +00:00
|
|
|
{
|
|
|
|
|
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
|
|
|
|
|
});
|
2025-06-04 15:21:57 +00:00
|
|
|
}
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookmarks.AddRange(profileBookmarks);
|
2021-09-21 18:50:51 +00:00
|
|
|
}
|
2021-10-29 19:09:58 +00:00
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
return bookmarks;
|
|
|
|
|
}
|
2024-03-01 18:28:25 +00:00
|
|
|
|
2025-03-20 08:11:22 +00:00
|
|
|
protected static List<Bookmark> LoadBookmarksFromFile(string path, string source)
|
2024-04-16 07:00:19 +00:00
|
|
|
{
|
|
|
|
|
var bookmarks = new List<Bookmark>();
|
2022-01-08 22:58:12 +00:00
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
if (!File.Exists(path))
|
2024-03-01 18:28:25 +00:00
|
|
|
return bookmarks;
|
|
|
|
|
|
2024-04-16 07:00:19 +00:00
|
|
|
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
|
|
|
|
|
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
|
|
|
|
|
return bookmarks;
|
|
|
|
|
EnumerateRoot(rootElement, bookmarks, source);
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 08:11:22 +00:00
|
|
|
private static void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source)
|
2024-04-16 07:00:19 +00:00
|
|
|
{
|
|
|
|
|
foreach (var folder in rootElement.EnumerateObject())
|
2024-03-01 18:28:25 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
if (folder.Value.ValueKind != JsonValueKind.Object)
|
|
|
|
|
continue;
|
2024-03-01 18:28:25 +00:00
|
|
|
|
2025-03-19 20:04:19 +00:00
|
|
|
// Fix for Opera. It stores bookmarks slightly different than chrome.
|
2024-04-16 07:00:19 +00:00
|
|
|
if (folder.Name == "custom_root")
|
|
|
|
|
EnumerateRoot(folder.Value, bookmarks, source);
|
|
|
|
|
else
|
|
|
|
|
EnumerateFolderBookmark(folder.Value, bookmarks, source);
|
2021-09-21 18:50:51 +00:00
|
|
|
}
|
2024-04-16 07:00:19 +00:00
|
|
|
}
|
2021-09-21 18:50:51 +00:00
|
|
|
|
2025-03-20 08:11:22 +00:00
|
|
|
private static void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks,
|
2024-04-16 07:00:19 +00:00
|
|
|
string source)
|
|
|
|
|
{
|
|
|
|
|
if (!folderElement.TryGetProperty("children", out var childrenElement))
|
|
|
|
|
return;
|
|
|
|
|
foreach (var subElement in childrenElement.EnumerateArray())
|
2021-09-21 18:50:51 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
if (subElement.TryGetProperty("type", out var type))
|
2021-09-21 18:50:51 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
switch (type.GetString())
|
2023-12-18 06:30:18 +00:00
|
|
|
{
|
2024-04-16 07:00:19 +00:00
|
|
|
case "folder":
|
|
|
|
|
case "workspace": // Edge Workspace
|
|
|
|
|
EnumerateFolderBookmark(subElement, bookmarks, source);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
bookmarks.Add(new Bookmark(
|
|
|
|
|
subElement.GetProperty("name").GetString(),
|
|
|
|
|
subElement.GetProperty("url").GetString(),
|
|
|
|
|
source));
|
|
|
|
|
break;
|
2021-09-21 18:50:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-16 07:00:19 +00:00
|
|
|
else
|
|
|
|
|
{
|
2025-06-23 04:38:27 +00:00
|
|
|
Main.Context.API.LogError(ClassName, $"type property not found for {subElement.GetString()}");
|
2024-04-16 07:00:19 +00:00
|
|
|
}
|
2021-09-21 18:50:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-19 20:04:19 +00:00
|
|
|
|
|
|
|
|
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
|
|
|
|
|
{
|
2025-06-08 04:35:39 +00:00
|
|
|
FaviconHelper.LoadFaviconsFromDb(_faviconCacheDir, dbPath, (tempDbPath) =>
|
2025-04-09 09:29:28 +00:00
|
|
|
{
|
2025-06-05 02:21:24 +00:00
|
|
|
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
|
2025-06-04 17:12:46 +00:00
|
|
|
var savedPaths = new ConcurrentDictionary<string, bool>();
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-06-04 17:12:46 +00:00
|
|
|
// Get favicons based on bookmarks concurrently
|
2025-06-05 02:16:02 +00:00
|
|
|
Parallel.ForEach(bookmarks, bookmark =>
|
2025-03-19 20:04:19 +00:00
|
|
|
{
|
2025-06-04 17:12:46 +00:00
|
|
|
// Use read-only connection to avoid locking issues
|
2025-06-08 04:17:47 +00:00
|
|
|
// Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580
|
|
|
|
|
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false");
|
2025-06-04 17:12:46 +00:00
|
|
|
connection.Open();
|
|
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
try
|
2025-03-19 20:04:19 +00:00
|
|
|
{
|
2025-04-09 09:29:28 +00:00
|
|
|
var url = bookmark.Url;
|
2025-06-04 17:12:46 +00:00
|
|
|
if (string.IsNullOrEmpty(url)) return;
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
// Extract domain from URL
|
|
|
|
|
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
|
2025-06-04 17:12:46 +00:00
|
|
|
return;
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
var domain = uri.Host;
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
using var cmd = connection.CreateCommand();
|
|
|
|
|
cmd.CommandText = @"
|
2025-06-08 04:35:39 +00:00
|
|
|
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";
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
|
2025-03-19 20:04:19 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
using var reader = cmd.ExecuteReader();
|
|
|
|
|
if (!reader.Read() || reader.IsDBNull(1))
|
2025-06-04 17:12:46 +00:00
|
|
|
return;
|
2025-03-20 08:43:38 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
var iconId = reader.GetInt64(0).ToString();
|
|
|
|
|
var imageData = (byte[])reader["image_data"];
|
2025-03-20 08:43:38 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
if (imageData is not { Length: > 0 })
|
2025-06-04 17:12:46 +00:00
|
|
|
return;
|
2025-03-20 08:43:38 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
|
2025-06-04 17:12:46 +00:00
|
|
|
|
2025-06-05 02:21:24 +00:00
|
|
|
// Filter out duplicate favicons
|
2025-06-04 17:12:46 +00:00
|
|
|
if (savedPaths.TryAdd(faviconPath, true))
|
|
|
|
|
{
|
2025-06-08 04:35:39 +00:00
|
|
|
FaviconHelper.SaveBitmapData(imageData, faviconPath);
|
2025-06-04 17:12:46 +00:00
|
|
|
}
|
2025-04-09 09:05:29 +00:00
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
bookmark.FaviconPath = faviconPath;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2025-06-23 04:38:27 +00:00
|
|
|
Main.Context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
2025-06-04 17:12:46 +00:00
|
|
|
finally
|
|
|
|
|
{
|
2025-06-07 16:14:31 +00:00
|
|
|
// Cache connection and clear pool after all operations to avoid issue:
|
|
|
|
|
// ObjectDisposedException: Safe handle has been closed.
|
2025-06-04 17:12:46 +00:00
|
|
|
connection.Close();
|
|
|
|
|
connection.Dispose();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-08 04:35:39 +00:00
|
|
|
});
|
2025-03-19 20:04:19 +00:00
|
|
|
}
|
avoid exception in ChromiumBookmarkLoader.cs
Every time I start Flow Launcher since the plugins refactor, the Bookmarks plugin has been throwing an exception. The issue stems from it parsing the Bookmarks file, and assuming that every JsonElement in the "roots" element is a JsonValueKind.Object. My Bookmarks file though has a JsonValueKind.String value off the roots key of `"sync_transaction_version": "20297",` along-side the bookmark_bar, other, and synced keys, which ARE objects. When it hits that sync_transaction_version string, and calls EnumerateFolderBookmark with it, it gets into the method because it IS a JsonElement, but then throws an exception on the folderElement.TryGetProperty("children", ...) call because you can't call TryGetProperty on a String, only Objects.
2022-03-09 01:54:54 +00:00
|
|
|
}
|