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" diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index 514eb1d..da5536b 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 @@ - + 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(); }; 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); +}; 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); 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;