From 0a52e907d78d5c9c49168a80e78a0980d8e53384 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:38:02 +0100 Subject: [PATCH 1/7] Add bookmark manager integration to UI Introduces BookmarkStore to the UI, initializes it, and exposes bookmark management callbacks to JavaScript. Updates the bookmark button state based on the active tab and tab URL changes, enabling add, remove, toggle, and update operations for bookmarks. --- src/UI.cpp | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/UI.h | 13 ++++ 2 files changed, 216 insertions(+) diff --git a/src/UI.cpp b/src/UI.cpp index 5866ba9..6cafbbd 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -501,6 +501,10 @@ UI::UI(RefPtr window) password_manager_ = std::make_unique(); password_manager_->Initialize(SettingsDirectory()); + // Initialize bookmark store + bookmark_store_ = std::make_unique(); + bookmark_store_->Initialize(SettingsDirectory()); + // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); @@ -557,6 +561,10 @@ UI::UI(RefPtr window, AdBlocker *adblock, AdBlocker *tracker) password_manager_ = std::make_unique(); password_manager_->Initialize(SettingsDirectory()); + // Initialize bookmark store + bookmark_store_ = std::make_unique(); + bookmark_store_->Initialize(SettingsDirectory()); + // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); @@ -1424,6 +1432,7 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["GetDarkModeEnabled"] = BindJSCallbackWithRetval(&UI::OnGetDarkModeEnabled); global["OnToggleAdblock"] = BindJSCallback(&UI::OnToggleAdblock); global["GetAdblockEnabled"] = BindJSCallbackWithRetval(&UI::OnGetAdblockEnabled); + global["OnToggleBookmark"] = BindJSCallback(&UI::OnToggleBookmark); global["OnOpenSettingsPanel"] = BindJSCallback(&UI::OnOpenSettingsPanel); global["OnCloseSettingsPanel"] = BindJSCallback(&UI::OnCloseSettingsPanel); // Password save bar callback @@ -1797,6 +1806,9 @@ void UI::OnActiveTabChange(const JSObject &obj, const JSArgs &args) SetCanGoForward(tab_view->CanGoBack()); SetURL(tab_view->url()); } + + // Update bookmark button state for the newly active tab + UpdateBookmarkButtonState(); } } @@ -2341,7 +2353,10 @@ void UI::UpdateTabURL(uint64_t id, const ultralight::String &url) } if (id == active_tab_id_ && !tabs_.empty()) + { SetURL(url); + UpdateBookmarkButtonState(); + } } void UI::UpdateTabNavigation(uint64_t id, bool is_loading, bool can_go_back, bool can_go_forward) @@ -2424,6 +2439,25 @@ void UI::SetCursor(ultralight::Cursor cursor) window_->SetCursor(cursor); } +void UI::UpdateBookmarkButtonState() +{ + if (!bookmark_store_ || !active_tab() || !active_tab()->view()) + return; + + auto url = active_tab()->view()->url(); + auto url_str = url.utf8(); + std::string url_string = url_str.data() ? url_str.data() : ""; + + bool is_bookmarked = bookmark_store_->IsBookmarked(url_string); + + RefPtr lock(view()->LockJSContext()); + JSContextRef ctx = lock->ctx(); + ultralight::String js = ultralight::String("if(typeof updateBookmarkButton === 'function') updateBookmarkButton(") + + ultralight::String(is_bookmarked ? "true" : "false") + + ultralight::String(");"); + view()->EvaluateScript(js, nullptr); +} + String UI::GetFaviconURL(const String &page_url) { // Best-effort: use origin + "/favicon.ico" for http/https URLs. @@ -6293,3 +6327,172 @@ ultralight::JSValue UI::OnIsDarkModeEnabled(const JSObject &obj, const JSArgs &a { return JSValue(dark_mode_enabled_); } + +// ============================================================================ +// Bookmark Manager Implementation +// ============================================================================ + +ultralight::JSValue UI::OnGetBookmarks(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_) + return JSValue(String("[]")); + return JSValue(String(bookmark_store_->ToJSON().c_str())); +} + +ultralight::JSValue UI::OnGetBookmarkBar(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_) + return JSValue(String("[]")); + return JSValue(String(bookmark_store_->BookmarkBarToJSON().c_str())); +} + +ultralight::JSValue UI::OnAddBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_ || args.empty()) + return JSValue(0); + + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + + std::string title; + if (args.size() > 1) { + ultralight::String title_ul = args[1].ToString(); + auto title_str = title_ul.utf8(); + title = title_str.data() ? title_str.data() : ""; + } + + std::string favicon; + if (args.size() > 2) { + ultralight::String favicon_ul = args[2].ToString(); + auto favicon_str = favicon_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + + bool show_on_bar = args.size() > 3 ? (bool)args[3] : true; + + uint64_t id = bookmark_store_->AddBookmark(url, title, favicon, show_on_bar); + return JSValue((double)id); +} + +void UI::OnRemoveBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_ || args.empty()) + return; + + uint64_t id = static_cast((double)args[0]); + bookmark_store_->RemoveBookmark(id); +} + +ultralight::JSValue UI::OnIsBookmarked(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_ || args.empty()) + return JSValue(false); + + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + return JSValue(bookmark_store_->IsBookmarked(url)); +} + +void UI::OnToggleBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_) + return; + + std::string url; + std::string title; + std::string favicon; + + // If no arguments provided, use the active tab's data + if (args.empty()) + { + if (!active_tab()) + return; + + auto tab_url = active_tab()->view()->url(); + auto tab_url_str = tab_url.utf8(); + url = tab_url_str.data() ? tab_url_str.data() : ""; + + auto tab_title = active_tab()->view()->title(); + auto tab_title_str = tab_title.utf8(); + title = tab_title_str.data() ? tab_title_str.data() : ""; + + // Get favicon URL via UI's helper + auto favicon_str_ul = GetFaviconURL(tab_url); + auto favicon_str = favicon_str_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + else + { + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + url = url_str.data() ? url_str.data() : ""; + + if (args.size() > 1) { + ultralight::String title_ul = args[1].ToString(); + auto title_str = title_ul.utf8(); + title = title_str.data() ? title_str.data() : ""; + } + + if (args.size() > 2) { + ultralight::String favicon_ul = args[2].ToString(); + auto favicon_str = favicon_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + } + + if (url.empty()) + return; + + bool was_bookmarked = bookmark_store_->IsBookmarked(url); + + if (was_bookmarked) + { + auto* bm = bookmark_store_->GetBookmarkByUrl(url); + if (bm) + bookmark_store_->RemoveBookmark(bm->id); + } + else + { + bookmark_store_->AddBookmark(url, title, favicon, true); + } + + // Update the bookmark button icon in the UI + RefPtr lock(view()->LockJSContext()); + JSContextRef ctx = lock->ctx(); + ultralight::String js = ultralight::String("if(typeof updateBookmarkButton === 'function') updateBookmarkButton(") + + ultralight::String(was_bookmarked ? "false" : "true") + + ultralight::String(");"); + view()->EvaluateScript(js, nullptr); +} + +void UI::OnUpdateBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!bookmark_store_ || args.size() < 2) + return; + + uint64_t id = static_cast((double)args[0]); + + ultralight::String url_ul = args[1].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + + std::string title; + if (args.size() > 2) { + ultralight::String title_ul = args[2].ToString(); + auto title_str = title_ul.utf8(); + title = title_str.data() ? title_str.data() : ""; + } + + std::string favicon; + if (args.size() > 3) { + ultralight::String favicon_ul = args[3].ToString(); + auto favicon_str = favicon_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + + bool show_on_bar = args.size() > 4 ? (bool)args[4] : true; + + bookmark_store_->UpdateBookmark(id, url, title, favicon, show_on_bar); +} diff --git a/src/UI.h b/src/UI.h index 11c53b0..ae78486 100644 --- a/src/UI.h +++ b/src/UI.h @@ -3,6 +3,7 @@ #include "Tab.h" #include "drm/DRMSettings.h" #include "ExtensionManager.h" +#include "BookmarkStore.h" #include #include #include @@ -194,6 +195,15 @@ class UI : public WindowListener, void OnCreateExtension(const JSObject &obj, const JSArgs &args); void OnOpenExtensionsFolder(const JSObject &obj, const JSArgs &args); + // Bookmark Manager callbacks + ultralight::JSValue OnGetBookmarks(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnGetBookmarkBar(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnAddBookmark(const JSObject &obj, const JSArgs &args); + void OnRemoveBookmark(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnIsBookmarked(const JSObject &obj, const JSArgs &args); + void OnToggleBookmark(const JSObject &obj, const JSArgs &args); + void OnUpdateBookmark(const JSObject &obj, const JSArgs &args); + // Password Manager callbacks ultralight::JSValue OnGetPasswords(const JSObject &obj, const JSArgs &args); ultralight::JSValue OnGetPasswordStats(const JSObject &obj, const JSArgs &args); @@ -223,6 +233,7 @@ class UI : public WindowListener, RefPtr window() { return window_; } DownloadManager *download_manager() { return download_manager_.get(); } password::PasswordManager *password_manager() { return password_manager_.get(); } + BookmarkStore *bookmark_store() { return bookmark_store_.get(); } AdBlocker *network_blocker() { return adblock_; } // Privacy settings accessors for Tab's JavaScript injection @@ -251,6 +262,7 @@ class UI : public WindowListener, void SetCanGoForward(bool can_go_forward); void SetURL(const String &url); void SetCursor(Cursor cursor); + void UpdateBookmarkButtonState(); std::string BuildDrmStatusPayload(); void AdjustUIHeight(uint32_t new_height); void ShowMenuOverlay(); @@ -362,6 +374,7 @@ class UI : public WindowListener, AdBlocker *trackerblock_ = nullptr; std::unique_ptr download_manager_; std::unique_ptr password_manager_; + std::unique_ptr bookmark_store_; bool downloads_overlay_had_active_ = false; bool downloads_overlay_user_dismissed_ = false; uint64_t downloads_last_sequence_seen_ = 0; From 63293103913f289a4e2da81d98b4708299789a22 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:38:12 +0100 Subject: [PATCH 2/7] Add bookmark management JS bridge to Tab Introduces JavaScript bridge functions in Tab for bookmark management, including getting bookmarks, adding, removing, checking, and toggling bookmarks. Implements corresponding C++ methods and exposes them to the JS context for UI integration. --- src/Tab.cpp | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Tab.h | 8 ++++ 2 files changed, 122 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index 62b1830..78b3a4c 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -5,6 +5,7 @@ #include "ExtensionManager.h" #include "AdBlocker.h" #include "PasswordManager.h" +#include "BookmarkStore.h" #include #include #include @@ -677,6 +678,14 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const global["__ul_toggleDarkMode"] = BindJSCallback(&Tab::JS_ToggleDarkMode); global["__ul_isDarkModeEnabled"] = BindJSCallbackWithRetval(&Tab::JS_IsDarkModeEnabled); global["__ul_getAppInfo"] = BindJSCallbackWithRetval(&Tab::JS_GetAppInfo); + + // Bookmark bridge functions + global["getBookmarks"] = BindJSCallbackWithRetval(&Tab::JS_GetBookmarks); + global["getBookmarkBar"] = BindJSCallbackWithRetval(&Tab::JS_GetBookmarkBar); + global["addBookmark"] = BindJSCallbackWithRetval(&Tab::JS_AddBookmark); + global["removeBookmark"] = BindJSCallback(&Tab::JS_RemoveBookmark); + global["isBookmarked"] = BindJSCallbackWithRetval(&Tab::JS_IsBookmarked); + global["toggleBookmark"] = BindJSCallback(&Tab::JS_ToggleBookmark); const char *attachScript = R"JS((function(){ try{ @@ -694,6 +703,10 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const n.toggleDarkMode = window.__ul_toggleDarkMode; n.isDarkModeEnabled = window.__ul_isDarkModeEnabled; n.getAppInfo = window.__ul_getAppInfo; + // Trigger bookmark loading if function exists (with delay to ensure page JS is loaded) + setTimeout(function() { + if(typeof loadBookmarks === 'function') loadBookmarks(); + }, 50); }catch(e){} })())JS"; caller->EvaluateScript(attachScript, nullptr); @@ -1568,6 +1581,107 @@ JSValue Tab::JS_GetAppInfo(const JSObject &obj, const JSArgs &args) return JSValue(String(json.c_str())); } +// Bookmark bridge implementations +JSValue Tab::JS_GetBookmarks(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store()) + return JSValue(String("[]")); + return JSValue(String(ui_->bookmark_store()->ToJSON().c_str())); +} + +JSValue Tab::JS_GetBookmarkBar(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store()) + return JSValue(String("[]")); + return JSValue(String(ui_->bookmark_store()->BookmarkBarToJSON().c_str())); +} + +JSValue Tab::JS_AddBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store() || args.empty()) + return JSValue(0); + + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + + std::string title; + if (args.size() > 1) { + ultralight::String title_ul = args[1].ToString(); + auto title_str = title_ul.utf8(); + title = title_str.data() ? title_str.data() : ""; + } + + std::string favicon; + if (args.size() > 2) { + ultralight::String favicon_ul = args[2].ToString(); + auto favicon_str = favicon_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + + bool show_on_bar = args.size() > 3 ? (bool)args[3] : true; + + uint64_t id = ui_->bookmark_store()->AddBookmark(url, title, favicon, show_on_bar); + return JSValue((double)id); +} + +void Tab::JS_RemoveBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store() || args.empty()) + return; + + uint64_t id = static_cast((double)args[0]); + ui_->bookmark_store()->RemoveBookmark(id); +} + +JSValue Tab::JS_IsBookmarked(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store() || args.empty()) + return JSValue(false); + + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + return JSValue(ui_->bookmark_store()->IsBookmarked(url)); +} + +void Tab::JS_ToggleBookmark(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store() || args.empty()) + return; + + ultralight::String url_ul = args[0].ToString(); + auto url_str = url_ul.utf8(); + std::string url = url_str.data() ? url_str.data() : ""; + + std::string title; + if (args.size() > 1) { + ultralight::String title_ul = args[1].ToString(); + auto title_str = title_ul.utf8(); + title = title_str.data() ? title_str.data() : ""; + } + + std::string favicon; + if (args.size() > 2) { + ultralight::String favicon_ul = args[2].ToString(); + auto favicon_str = favicon_ul.utf8(); + favicon = favicon_str.data() ? favicon_str.data() : ""; + } + + if (ui_->bookmark_store()->IsBookmarked(url)) + { + // Remove the bookmark + auto* bm = ui_->bookmark_store()->GetBookmarkByUrl(url); + if (bm) + ui_->bookmark_store()->RemoveBookmark(bm->id); + } + else + { + // Add the bookmark + ui_->bookmark_store()->AddBookmark(url, title, favicon, true); + } +} + JSValue Tab::OnDownloadsGetData(const JSObject &obj, const JSArgs &args) { if (!ui_) diff --git a/src/Tab.h b/src/Tab.h index 1eb5cee..989e3ee 100644 --- a/src/Tab.h +++ b/src/Tab.h @@ -102,6 +102,14 @@ class Tab : public ViewListener, JSValue JS_IsDarkModeEnabled(const JSObject &obj, const JSArgs &args); JSValue JS_GetAppInfo(const JSObject &obj, const JSArgs &args); + // Bookmark bridge callbacks + JSValue JS_GetBookmarks(const JSObject &obj, const JSArgs &args); + JSValue JS_GetBookmarkBar(const JSObject &obj, const JSArgs &args); + JSValue JS_AddBookmark(const JSObject &obj, const JSArgs &args); + void JS_RemoveBookmark(const JSObject &obj, const JSArgs &args); + JSValue JS_IsBookmarked(const JSObject &obj, const JSArgs &args); + void JS_ToggleBookmark(const JSObject &obj, const JSArgs &args); + // Downloads page callbacks JSValue OnDownloadsGetData(const JSObject &obj, const JSArgs &args); void OnDownloadsClear(const JSObject &obj, const JSArgs &args); From bcfd28f850734a61902d38ad557bd48a9ebedab8 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:38:19 +0100 Subject: [PATCH 3/7] Add BookmarkStore for managing bookmarks Introduces BookmarkStore class with methods to add, remove, update, and query bookmarks, storing them as JSON in a specified directory. Includes basic JSON serialization/deserialization, normalization of URLs, and support for bookmark bar items. --- src/BookmarkStore.cpp | 423 ++++++++++++++++++++++++++++++++++++++++++ src/BookmarkStore.h | 78 ++++++++ 2 files changed, 501 insertions(+) create mode 100644 src/BookmarkStore.cpp create mode 100644 src/BookmarkStore.h diff --git a/src/BookmarkStore.cpp b/src/BookmarkStore.cpp new file mode 100644 index 0000000..58b3fca --- /dev/null +++ b/src/BookmarkStore.cpp @@ -0,0 +1,423 @@ +#include "BookmarkStore.h" +#include +#include +#include +#include +#include + +BookmarkStore::BookmarkStore() = default; +BookmarkStore::~BookmarkStore() = default; + +void BookmarkStore::Initialize(const std::filesystem::path& storage_dir) +{ + namespace fs = std::filesystem; + + // Ensure directory exists + if (!fs::exists(storage_dir)) + { + std::error_code ec; + fs::create_directories(storage_dir, ec); + } + + storage_path_ = storage_dir / "bookmarks.json"; + LoadFromDisk(); +} + +uint64_t BookmarkStore::AddBookmark(const std::string& url, const std::string& title, + const std::string& favicon, bool show_on_bar) +{ + // Check if already bookmarked + std::string normalized = NormalizeUrl(url); + for (const auto& bm : bookmarks_) + { + if (NormalizeUrl(bm.url) == normalized) + return bm.id; // Already exists, return existing ID + } + + Bookmark bm; + bm.id = next_id_++; + bm.url = url; + bm.title = title.empty() ? url : title; + bm.favicon = favicon; + bm.created_at = GetCurrentTimestamp(); + bm.show_on_bar = show_on_bar; + + bookmarks_.push_back(bm); + SaveToDisk(); + + return bm.id; +} + +bool BookmarkStore::RemoveBookmark(uint64_t id) +{ + auto it = std::find_if(bookmarks_.begin(), bookmarks_.end(), + [id](const Bookmark& bm) { return bm.id == id; }); + + if (it != bookmarks_.end()) + { + bookmarks_.erase(it); + SaveToDisk(); + return true; + } + return false; +} + +bool BookmarkStore::UpdateBookmark(uint64_t id, const std::string& url, const std::string& title, + const std::string& favicon, bool show_on_bar) +{ + auto it = std::find_if(bookmarks_.begin(), bookmarks_.end(), + [id](const Bookmark& bm) { return bm.id == id; }); + + if (it != bookmarks_.end()) + { + it->url = url; + it->title = title.empty() ? url : title; + if (!favicon.empty()) + it->favicon = favicon; + it->show_on_bar = show_on_bar; + SaveToDisk(); + return true; + } + return false; +} + +bool BookmarkStore::IsBookmarked(const std::string& url) const +{ + std::string normalized = NormalizeUrl(url); + for (const auto& bm : bookmarks_) + { + if (NormalizeUrl(bm.url) == normalized) + return true; + } + return false; +} + +const BookmarkStore::Bookmark* BookmarkStore::GetBookmarkByUrl(const std::string& url) const +{ + std::string normalized = NormalizeUrl(url); + for (const auto& bm : bookmarks_) + { + if (NormalizeUrl(bm.url) == normalized) + return &bm; + } + return nullptr; +} + +const BookmarkStore::Bookmark* BookmarkStore::GetBookmarkById(uint64_t id) const +{ + for (const auto& bm : bookmarks_) + { + if (bm.id == id) + return &bm; + } + return nullptr; +} + +std::vector BookmarkStore::GetBookmarkBarItems() const +{ + std::vector bar_items; + for (const auto& bm : bookmarks_) + { + if (bm.show_on_bar) + bar_items.push_back(bm); + } + return bar_items; +} + +// Helper to escape JSON strings +static std::string EscapeJSON(const std::string& s) +{ + std::ostringstream o; + for (char c : s) + { + switch (c) + { + case '"': o << "\\\""; break; + case '\\': o << "\\\\"; break; + case '\b': o << "\\b"; break; + case '\f': o << "\\f"; break; + case '\n': o << "\\n"; break; + case '\r': o << "\\r"; break; + case '\t': o << "\\t"; break; + default: + if ('\x00' <= c && c <= '\x1f') + { + o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c; + } + else + { + o << c; + } + } + } + return o.str(); +} + +std::string BookmarkStore::ToJSON() const +{ + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto& bm : bookmarks_) + { + if (!first) ss << ","; + first = false; + ss << "{"; + ss << "\"id\":" << bm.id << ","; + ss << "\"url\":\"" << EscapeJSON(bm.url) << "\","; + ss << "\"title\":\"" << EscapeJSON(bm.title) << "\","; + ss << "\"favicon\":\"" << EscapeJSON(bm.favicon) << "\","; + ss << "\"created_at\":" << bm.created_at << ","; + ss << "\"show_on_bar\":" << (bm.show_on_bar ? "true" : "false"); + ss << "}"; + } + ss << "]"; + return ss.str(); +} + +std::string BookmarkStore::BookmarkBarToJSON() const +{ + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto& bm : bookmarks_) + { + if (!bm.show_on_bar) continue; + if (!first) ss << ","; + first = false; + ss << "{"; + ss << "\"id\":" << bm.id << ","; + ss << "\"url\":\"" << EscapeJSON(bm.url) << "\","; + ss << "\"title\":\"" << EscapeJSON(bm.title) << "\","; + ss << "\"favicon\":\"" << EscapeJSON(bm.favicon) << "\""; + ss << "}"; + } + ss << "]"; + return ss.str(); +} + +bool BookmarkStore::SaveToDisk() +{ + if (storage_path_.empty()) + return false; + + std::ostringstream ss; + ss << "{\n"; + ss << " \"next_id\": " << next_id_ << ",\n"; + ss << " \"bookmarks\": [\n"; + + bool first = true; + for (const auto& bm : bookmarks_) + { + if (!first) ss << ",\n"; + first = false; + ss << " {\n"; + ss << " \"id\": " << bm.id << ",\n"; + ss << " \"url\": \"" << EscapeJSON(bm.url) << "\",\n"; + ss << " \"title\": \"" << EscapeJSON(bm.title) << "\",\n"; + ss << " \"favicon\": \"" << EscapeJSON(bm.favicon) << "\",\n"; + ss << " \"created_at\": " << bm.created_at << ",\n"; + ss << " \"show_on_bar\": " << (bm.show_on_bar ? "true" : "false") << "\n"; + ss << " }"; + } + + ss << "\n ]\n"; + ss << "}\n"; + + std::ofstream file(storage_path_); + if (!file.is_open()) + return false; + + file << ss.str(); + return true; +} + +// Simple JSON value extraction helpers +static std::string ExtractString(const std::string& json, const std::string& key) +{ + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return ""; + + pos += search.length(); + // Skip whitespace + while (pos < json.length() && std::isspace(json[pos])) pos++; + + if (pos >= json.length() || json[pos] != '"') return ""; + pos++; // Skip opening quote + + std::string result; + while (pos < json.length() && json[pos] != '"') + { + if (json[pos] == '\\' && pos + 1 < json.length()) + { + pos++; + switch (json[pos]) + { + case '"': result += '"'; break; + case '\\': result += '\\'; break; + case 'n': result += '\n'; break; + case 'r': result += '\r'; break; + case 't': result += '\t'; break; + default: result += json[pos]; break; + } + } + else + { + result += json[pos]; + } + pos++; + } + return result; +} + +static uint64_t ExtractUint64(const std::string& json, const std::string& key) +{ + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return 0; + + pos += search.length(); + // Skip whitespace + while (pos < json.length() && std::isspace(json[pos])) pos++; + + std::string num; + while (pos < json.length() && std::isdigit(json[pos])) + { + num += json[pos++]; + } + + if (num.empty()) return 0; + return std::stoull(num); +} + +static bool ExtractBool(const std::string& json, const std::string& key, bool default_val = false) +{ + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return default_val; + + pos += search.length(); + // Skip whitespace + while (pos < json.length() && std::isspace(json[pos])) pos++; + + if (pos + 4 <= json.length() && json.substr(pos, 4) == "true") + return true; + if (pos + 5 <= json.length() && json.substr(pos, 5) == "false") + return false; + + return default_val; +} + +bool BookmarkStore::LoadFromDisk() +{ + if (storage_path_.empty() || !std::filesystem::exists(storage_path_)) + return false; + + std::ifstream file(storage_path_); + if (!file.is_open()) + return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string json = buffer.str(); + + bookmarks_.clear(); + + // Extract next_id + next_id_ = ExtractUint64(json, "next_id"); + if (next_id_ == 0) next_id_ = 1; + + // Find bookmarks array + size_t arr_start = json.find("\"bookmarks\":"); + if (arr_start == std::string::npos) return true; // No bookmarks yet + + arr_start = json.find('[', arr_start); + if (arr_start == std::string::npos) return true; + + // Parse each bookmark object + size_t pos = arr_start + 1; + while (pos < json.length()) + { + // Find next object start + size_t obj_start = json.find('{', pos); + if (obj_start == std::string::npos) break; + + // Find object end + int brace_count = 1; + size_t obj_end = obj_start + 1; + while (obj_end < json.length() && brace_count > 0) + { + if (json[obj_end] == '{') brace_count++; + else if (json[obj_end] == '}') brace_count--; + obj_end++; + } + + if (brace_count != 0) break; + + std::string obj_json = json.substr(obj_start, obj_end - obj_start); + + Bookmark bm; + bm.id = ExtractUint64(obj_json, "id"); + bm.url = ExtractString(obj_json, "url"); + bm.title = ExtractString(obj_json, "title"); + bm.favicon = ExtractString(obj_json, "favicon"); + bm.created_at = ExtractUint64(obj_json, "created_at"); + bm.show_on_bar = ExtractBool(obj_json, "show_on_bar", true); + + if (!bm.url.empty()) + { + bookmarks_.push_back(bm); + if (bm.id >= next_id_) + next_id_ = bm.id + 1; + } + + pos = obj_end; + + // Check for end of array + size_t next_comma = json.find(',', pos); + size_t arr_end = json.find(']', pos); + if (arr_end != std::string::npos && (next_comma == std::string::npos || arr_end < next_comma)) + break; + } + + return true; +} + +uint64_t BookmarkStore::GetCurrentTimestamp() +{ + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + +std::string BookmarkStore::NormalizeUrl(const std::string& url) +{ + if (url.empty()) return url; + + std::string result = url; + + // Remove trailing slash + while (!result.empty() && result.back() == '/') + result.pop_back(); + + // Lowercase the scheme and host part + size_t scheme_end = result.find("://"); + if (scheme_end != std::string::npos) + { + // Lowercase scheme + for (size_t i = 0; i < scheme_end; i++) + result[i] = std::tolower(result[i]); + + // Find end of host (start of path, query, or fragment) + size_t host_start = scheme_end + 3; + size_t host_end = result.find_first_of("/?#", host_start); + if (host_end == std::string::npos) + host_end = result.length(); + + // Lowercase host + for (size_t i = host_start; i < host_end; i++) + result[i] = std::tolower(result[i]); + } + + return result; +} diff --git a/src/BookmarkStore.h b/src/BookmarkStore.h new file mode 100644 index 0000000..a8cbce5 --- /dev/null +++ b/src/BookmarkStore.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include + +/** + * Simple bookmark storage system. + * Stores bookmarks as JSON in the settings directory. + */ +class BookmarkStore +{ +public: + struct Bookmark + { + uint64_t id; + std::string url; + std::string title; + std::string favicon; // Data URL or empty + uint64_t created_at; // Timestamp in milliseconds + bool show_on_bar; // Whether to show on bookmark bar + }; + + BookmarkStore(); + ~BookmarkStore(); + + // Initialize with storage directory path + void Initialize(const std::filesystem::path& storage_dir); + + // Add a new bookmark, returns the assigned ID + uint64_t AddBookmark(const std::string& url, const std::string& title, + const std::string& favicon = "", bool show_on_bar = true); + + // Remove a bookmark by ID, returns true if found and removed + bool RemoveBookmark(uint64_t id); + + // Update an existing bookmark + bool UpdateBookmark(uint64_t id, const std::string& url, const std::string& title, + const std::string& favicon = "", bool show_on_bar = true); + + // Check if a URL is bookmarked + bool IsBookmarked(const std::string& url) const; + + // Get bookmark by URL (returns nullptr if not found) + const Bookmark* GetBookmarkByUrl(const std::string& url) const; + + // Get bookmark by ID (returns nullptr if not found) + const Bookmark* GetBookmarkById(uint64_t id) const; + + // Get all bookmarks + const std::vector& GetAllBookmarks() const { return bookmarks_; } + + // Get bookmarks for the bookmark bar only + std::vector GetBookmarkBarItems() const; + + // Save bookmarks to disk + bool SaveToDisk(); + + // Load bookmarks from disk + bool LoadFromDisk(); + + // Get bookmarks as JSON string + std::string ToJSON() const; + + // Get bookmark bar items as JSON string + std::string BookmarkBarToJSON() const; + +private: + std::vector bookmarks_; + std::filesystem::path storage_path_; + uint64_t next_id_ = 1; + + // Get current timestamp in milliseconds + static uint64_t GetCurrentTimestamp(); + + // Normalize URL for comparison (remove trailing slash, lowercase host) + static std::string NormalizeUrl(const std::string& url); +}; From 96d7eaf66c9a574ae5046596b233fe6737ced262 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:38:28 +0100 Subject: [PATCH 4/7] Add BookmarkStore to build sources Included BookmarkStore.h and BookmarkStore.cpp in the SOURCES list to ensure they are compiled as part of the project. --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3d4223..621b45b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,8 @@ set(SOURCES "src/Browser.cpp" "src/AdBlocker.h" "src/AdBlocker.cpp" + "src/BookmarkStore.h" + "src/BookmarkStore.cpp" "src/DownloadManager.h" "src/DownloadManager.cpp" "src/ExtensionManager.h" From a36eb279bed777c7bd1c3f377cf8fc71e0062c12 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:40:17 +0100 Subject: [PATCH 5/7] Add bookmark toggle button and styles to UI Introduces a bookmark button next to the address bar, with visual states for bookmarked and unbookmarked pages. Includes SVG icons, CSS styling for state transitions, and JavaScript handlers for toggling and updating the bookmark state. --- assets/ui.css | 22 +++++++++++++++++++++ assets/ui.html | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/assets/ui.css b/assets/ui.css index 46f528f..a55a069 100644 --- a/assets/ui.css +++ b/assets/ui.css @@ -148,6 +148,28 @@ svg#icon_defs path { fill: #8a83ff; } +/* Bookmark toggle coloring: highlight when page is bookmarked */ +#toggle-bookmark { + color: #b6b3c9; + fill: #b6b3c9; + transition: color 0.075s ease, fill 0.075s ease; +} + +#toggle-bookmark:hover { + color: #cbc9d8; + fill: #cbc9d8; +} + +#toggle-bookmark.bookmarked { + color: #FFD700; + fill: #FFD700; +} + +#toggle-bookmark.bookmarked:hover { + color: #ffe44d; + fill: #ffe44d; +} + /* Dropdown menu styles */ .menu-dropdown { position: absolute; diff --git a/assets/ui.html b/assets/ui.html index 7f0e2a4..4aa115d 100644 --- a/assets/ui.html +++ b/assets/ui.html @@ -36,6 +36,12 @@ + + + + + + @@ -63,6 +69,12 @@ + + + @@ -157,6 +169,26 @@ chromeTabs.removeTab(tab); } + // Update bookmark button state (called from C++) + function updateBookmarkButton(isBookmarked) { + const btn = document.getElementById('toggle-bookmark'); + const icon = document.getElementById('bookmark-icon'); + if (!btn || !icon) return; + + const useEl = icon.querySelector('use'); + if (useEl) { + if (isBookmarked) { + useEl.setAttribute('xlink:href', '#svg_bookmark_filled'); + btn.classList.add('bookmarked'); + btn.setAttribute('data-tooltip', 'Remove bookmark'); + } else { + useEl.setAttribute('xlink:href', '#svg_bookmark_outline'); + btn.classList.remove('bookmarked'); + btn.setAttribute('data-tooltip', 'Bookmark this page'); + } + } + } + // Session Restore Bar Functions function showSessionRestoreBar(tabCount, wasCrash) { const bar = document.getElementById('session-restore-bar'); @@ -215,6 +247,27 @@ document.querySelector('#forward').addEventListener('click', event => OnForward()); document.querySelector('#refresh').addEventListener('click', event => OnRefresh()); document.querySelector('#stop').addEventListener('click', event => OnStop()); + + // Bookmark button handler + const bookmarkBtn = document.querySelector('#toggle-bookmark'); + if (bookmarkBtn) { + const toggleBookmark = () => { + if (window.OnToggleBookmark) { + OnToggleBookmark(); + } + }; + bookmarkBtn.addEventListener('click', event => { + event.stopPropagation(); + toggleBookmark(); + }); + bookmarkBtn.addEventListener('keydown', event => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + toggleBookmark(); + } + }); + } + const inspectorBtn = document.querySelector('#toggle-tools'); if (inspectorBtn) { const toggleInspector = () => { if (window.OnToggleTools) OnToggleTools(); }; From c06836f46556326130a53c0bf7a2389a43a4f51b Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:40:44 +0100 Subject: [PATCH 6/7] Add bookmark bar UI to Google static page Introduces a bookmark bar at the top of the page with styles and JavaScript for displaying, removing, and managing bookmarks. The bar interacts with external bookmark functions and updates dynamically, enhancing user navigation and customization. --- assets/static-sties/google-static.html | 224 ++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index 514eb1d..3908e16 100644 --- a/assets/static-sties/google-static.html +++ b/assets/static-sties/google-static.html @@ -183,10 +183,138 @@ @keyframes ul_fade_in { from { opacity: 0; } to { opacity: 1; } } @-webkit-keyframes ul_fade_in { from { opacity: 0; } to { opacity: 1; } } + + + +
+ Click the ★ icon to add bookmarks +
+
@@ -229,7 +357,101 @@ - + From 4e1e5bc37095a087add1f4e5ff005fb661d4f055 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:49:05 +0100 Subject: [PATCH 7/7] Update header styles in google-static.html Adjusted the background color and border of the header for improved visual appearance and consistency. --- assets/static-sties/google-static.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index 3908e16..da5536b 100644 --- a/assets/static-sties/google-static.html +++ b/assets/static-sties/google-static.html @@ -191,7 +191,7 @@ top: 0; left: 0; right: 0; - background: rgba(32, 33, 36, 0.95); + background: rgba(29, 29, 33, 0.92); padding: 6px 16px; display: flex; align-items: center; @@ -200,7 +200,7 @@ min-height: 36px; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(95, 99, 104, 0.3); + border-bottom: 1px solid rgba(60, 60, 70, 0.4); z-index: 1000; overflow-x: auto; overflow-y: hidden;