2025-04-02 14:31:31 +00:00
|
|
|
|
using System.Collections.Generic;
|
2021-09-21 18:50:51 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Text.Json;
|
2025-03-19 20:04:19 +00:00
|
|
|
|
using System;
|
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-04-02 14:31:31 +00:00
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
|
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
|
|
|
|
|
|
var faviconDbPath = Path.Combine(profile, "Favicons");
|
|
|
|
|
|
if (File.Exists(faviconDbPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-04-09 04:14:07 +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-04-09 09:29:28 +00:00
|
|
|
|
// Use a copy to avoid lock issues with the original file
|
|
|
|
|
|
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db");
|
|
|
|
|
|
|
2025-03-19 20:04:19 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-04-09 09:29:28 +00:00
|
|
|
|
File.Copy(dbPath, tempDbPath, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-05-12 07:12:23 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (File.Exists(tempDbPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(tempDbPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex1)
|
|
|
|
|
|
{
|
|
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1);
|
|
|
|
|
|
}
|
2025-04-09 09:29:28 +00:00
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-03-19 20:04:19 +00:00
|
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var connection = new SqliteConnection($"Data Source={tempDbPath}");
|
|
|
|
|
|
connection.Open();
|
2025-03-19 20:04:19 +00:00
|
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
|
foreach (var bookmark in bookmarks)
|
2025-03-19 20:04:19 +00:00
|
|
|
|
{
|
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;
|
|
|
|
|
|
if (string.IsNullOrEmpty(url)) continue;
|
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))
|
|
|
|
|
|
continue;
|
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-03-20 07:16:46 +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))
|
|
|
|
|
|
continue;
|
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 })
|
|
|
|
|
|
continue;
|
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");
|
|
|
|
|
|
SaveBitmapData(imageData, faviconPath);
|
2025-04-09 09:05:29 +00:00
|
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
|
bookmark.FaviconPath = faviconPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
|
2025-03-19 20:04:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-09 09:29:28 +00:00
|
|
|
|
// https://github.com/dotnet/efcore/issues/26580
|
|
|
|
|
|
SqliteConnection.ClearPool(connection);
|
|
|
|
|
|
connection.Close();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to connect to SQLite: {tempDbPath}", ex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete temporary file
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(tempDbPath);
|
2025-03-19 20:04:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-04-09 09:29:28 +00:00
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex);
|
2025-03-19 20:04:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-20 08:11:22 +00:00
|
|
|
|
private static void SaveBitmapData(byte[] imageData, string outputPath)
|
2025-03-19 20:04:19 +00:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-03-20 08:03:08 +00:00
|
|
|
|
File.WriteAllBytes(outputPath, imageData);
|
2025-03-19 20:04:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-04-02 14:31:31 +00:00
|
|
|
|
Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex);
|
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
|
|
|
|
}
|