From b734593451b4051c380463de7fb055157fabb844 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:57:48 +0100 Subject: [PATCH 01/20] Add JS API to reorder bookmarks Introduces a new JavaScript callback 'reorderBookmarks' to allow reordering of bookmarks from the UI. The callback parses a JSON array of bookmark IDs and updates the bookmark store accordingly. --- src/Tab.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/Tab.h | 1 + 2 files changed, 48 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index 78b3a4c..b534606 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -686,6 +686,7 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const global["removeBookmark"] = BindJSCallback(&Tab::JS_RemoveBookmark); global["isBookmarked"] = BindJSCallbackWithRetval(&Tab::JS_IsBookmarked); global["toggleBookmark"] = BindJSCallback(&Tab::JS_ToggleBookmark); + global["reorderBookmarks"] = BindJSCallback(&Tab::JS_ReorderBookmarks); const char *attachScript = R"JS((function(){ try{ @@ -1682,6 +1683,52 @@ void Tab::JS_ToggleBookmark(const JSObject &obj, const JSArgs &args) } } +void Tab::JS_ReorderBookmarks(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->bookmark_store() || args.empty()) + return; + + // Parse the JSON array of IDs + ultralight::String json_ul = args[0].ToString(); + auto json_str = json_ul.utf8(); + std::string json = json_str.data() ? json_str.data() : ""; + + std::vector ordered_ids; + + // Simple JSON array parsing for [id1, id2, id3, ...] + size_t pos = json.find('['); + if (pos == std::string::npos) return; + pos++; + + while (pos < json.length()) + { + // Skip whitespace + while (pos < json.length() && std::isspace(json[pos])) pos++; + + if (json[pos] == ']') break; + + // Parse number + std::string num; + while (pos < json.length() && std::isdigit(json[pos])) + { + num += json[pos++]; + } + + if (!num.empty()) + { + ordered_ids.push_back(std::stoull(num)); + } + + // Skip comma and whitespace + while (pos < json.length() && (json[pos] == ',' || std::isspace(json[pos]))) pos++; + } + + if (!ordered_ids.empty()) + { + ui_->bookmark_store()->ReorderBookmarks(ordered_ids); + } +} + JSValue Tab::OnDownloadsGetData(const JSObject &obj, const JSArgs &args) { if (!ui_) diff --git a/src/Tab.h b/src/Tab.h index 989e3ee..329af0b 100644 --- a/src/Tab.h +++ b/src/Tab.h @@ -109,6 +109,7 @@ class Tab : public ViewListener, 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); + void JS_ReorderBookmarks(const JSObject &obj, const JSArgs &args); // Downloads page callbacks JSValue OnDownloadsGetData(const JSObject &obj, const JSArgs &args); From 061943abe7078a2b75ac1491b21310e54565060f Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:57:55 +0100 Subject: [PATCH 02/20] Add bookmark bar ordering and position support Introduces a 'position' field to bookmarks for ordering items on the bookmark bar. Bookmarks added to the bar are assigned the next available position, and bar items are now sorted by position. Adds ReorderBookmarks to update positions based on a given order, and updates JSON serialization/deserialization to include the position field. --- src/BookmarkStore.cpp | 42 ++++++++++++++++++++++++++++++++++++++---- src/BookmarkStore.h | 6 +++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/BookmarkStore.cpp b/src/BookmarkStore.cpp index 58b3fca..29ab79b 100644 --- a/src/BookmarkStore.cpp +++ b/src/BookmarkStore.cpp @@ -34,6 +34,14 @@ uint64_t BookmarkStore::AddBookmark(const std::string& url, const std::string& t return bm.id; // Already exists, return existing ID } + // Find the highest position among bar items + int max_position = -1; + for (const auto& bm : bookmarks_) + { + if (bm.show_on_bar && bm.position > max_position) + max_position = bm.position; + } + Bookmark bm; bm.id = next_id_++; bm.url = url; @@ -41,6 +49,7 @@ uint64_t BookmarkStore::AddBookmark(const std::string& url, const std::string& t bm.favicon = favicon; bm.created_at = GetCurrentTimestamp(); bm.show_on_bar = show_on_bar; + bm.position = show_on_bar ? (max_position + 1) : 0; bookmarks_.push_back(bm); SaveToDisk(); @@ -121,9 +130,29 @@ std::vector BookmarkStore::GetBookmarkBarItems() const if (bm.show_on_bar) bar_items.push_back(bm); } + // Sort by position + std::sort(bar_items.begin(), bar_items.end(), + [](const Bookmark& a, const Bookmark& b) { return a.position < b.position; }); return bar_items; } +bool BookmarkStore::ReorderBookmarks(const std::vector& ordered_ids) +{ + // Update positions based on the order in ordered_ids + for (size_t i = 0; i < ordered_ids.size(); ++i) + { + for (auto& bm : bookmarks_) + { + if (bm.id == ordered_ids[i]) + { + bm.position = static_cast(i); + break; + } + } + } + return SaveToDisk(); +} + // Helper to escape JSON strings static std::string EscapeJSON(const std::string& s) { @@ -168,7 +197,8 @@ std::string BookmarkStore::ToJSON() const 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 << "\"show_on_bar\":" << (bm.show_on_bar ? "true" : "false") << ","; + ss << "\"position\":" << bm.position; ss << "}"; } ss << "]"; @@ -177,12 +207,14 @@ std::string BookmarkStore::ToJSON() const std::string BookmarkStore::BookmarkBarToJSON() const { + // Get sorted bar items + auto bar_items = GetBookmarkBarItems(); + std::ostringstream ss; ss << "["; bool first = true; - for (const auto& bm : bookmarks_) + for (const auto& bm : bar_items) { - if (!bm.show_on_bar) continue; if (!first) ss << ","; first = false; ss << "{"; @@ -217,7 +249,8 @@ bool BookmarkStore::SaveToDisk() 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 << " \"show_on_bar\": " << (bm.show_on_bar ? "true" : "false") << ",\n"; + ss << " \"position\": " << bm.position << "\n"; ss << " }"; } @@ -364,6 +397,7 @@ bool BookmarkStore::LoadFromDisk() 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); + bm.position = static_cast(ExtractUint64(obj_json, "position")); if (!bm.url.empty()) { diff --git a/src/BookmarkStore.h b/src/BookmarkStore.h index a8cbce5..b469f2f 100644 --- a/src/BookmarkStore.h +++ b/src/BookmarkStore.h @@ -19,6 +19,7 @@ class BookmarkStore std::string favicon; // Data URL or empty uint64_t created_at; // Timestamp in milliseconds bool show_on_bar; // Whether to show on bookmark bar + int position; // Position in bookmark bar (for ordering) }; BookmarkStore(); @@ -50,9 +51,12 @@ class BookmarkStore // Get all bookmarks const std::vector& GetAllBookmarks() const { return bookmarks_; } - // Get bookmarks for the bookmark bar only + // Get bookmarks for the bookmark bar only (sorted by position) std::vector GetBookmarkBarItems() const; + // Reorder bookmarks on the bar - takes a vector of bookmark IDs in new order + bool ReorderBookmarks(const std::vector& ordered_ids); + // Save bookmarks to disk bool SaveToDisk(); From a5595c9d2dc976bd1639d63180da6f2d89c31aeb Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 22:58:01 +0100 Subject: [PATCH 03/20] Add drag-and-drop reordering to bookmark bar Implements drag-and-drop functionality for bookmark items, allowing users to reorder bookmarks in the bar. Adds related CSS styles and event handlers for drag state, touch/mouse long press, and order persistence via saveBookmarkOrder. --- assets/static-sties/google-static.html | 123 ++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index da5536b..9a70775 100644 --- a/assets/static-sties/google-static.html +++ b/assets/static-sties/google-static.html @@ -301,6 +301,21 @@ background: rgba(255, 60, 60, 1); } + /* Drag and drop styles */ + .bookmark-item.dragging { + opacity: 0.5; + background: rgba(100, 100, 255, 0.3); + } + + .bookmark-item.drag-over { + border-left: 2px solid #6C63FF; + margin-left: -2px; + } + + .bookmark-bar.drag-active { + background: rgba(29, 29, 33, 0.98); + } + /* Adjust main container for bookmark bar */ .main-container { padding-top: 50px; @@ -368,17 +383,26 @@ } } - // Create a bookmark element + // Drag and drop state + let draggedElement = null; + let draggedBookmarkId = null; + let longPressTimer = null; + let isDragging = false; + + // Create a bookmark element with drag support function createBookmarkElement(bookmark) { const item = document.createElement('a'); item.className = 'bookmark-item'; item.href = bookmark.url; item.title = bookmark.title || bookmark.url; + item.dataset.bookmarkId = bookmark.id; + item.draggable = true; // Icon (favicon or default) if (bookmark.favicon && bookmark.favicon.startsWith('data:')) { const img = document.createElement('img'); img.src = bookmark.favicon; + img.draggable = false; img.onerror = function() { const defaultIcon = document.createElement('div'); defaultIcon.className = 'default-icon'; @@ -412,9 +436,106 @@ }; item.appendChild(removeBtn); + // Drag events + item.addEventListener('dragstart', handleDragStart); + item.addEventListener('dragend', handleDragEnd); + item.addEventListener('dragover', handleDragOver); + item.addEventListener('dragenter', handleDragEnter); + item.addEventListener('dragleave', handleDragLeave); + item.addEventListener('drop', handleDrop); + + // Long press for touch/mouse + item.addEventListener('mousedown', function(e) { + if (e.button !== 0) return; + longPressTimer = setTimeout(() => { + isDragging = true; + item.classList.add('dragging'); + }, 300); + }); + + item.addEventListener('mouseup', function() { + clearTimeout(longPressTimer); + }); + + item.addEventListener('click', function(e) { + if (isDragging) { + e.preventDefault(); + isDragging = false; + } + }); + return item; } + function handleDragStart(e) { + draggedElement = this; + draggedBookmarkId = this.dataset.bookmarkId; + this.classList.add('dragging'); + document.getElementById('bookmarkBar').classList.add('drag-active'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', draggedBookmarkId); + } + + function handleDragEnd(e) { + this.classList.remove('dragging'); + document.getElementById('bookmarkBar').classList.remove('drag-active'); + document.querySelectorAll('.bookmark-item').forEach(item => { + item.classList.remove('drag-over'); + }); + draggedElement = null; + draggedBookmarkId = null; + isDragging = false; + } + + function handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + } + + function handleDragEnter(e) { + e.preventDefault(); + if (this !== draggedElement) { + this.classList.add('drag-over'); + } + } + + function handleDragLeave(e) { + this.classList.remove('drag-over'); + } + + function handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + + if (this === draggedElement) return; + + const bar = document.getElementById('bookmarkBar'); + const items = Array.from(bar.querySelectorAll('.bookmark-item')); + const draggedIndex = items.indexOf(draggedElement); + const dropIndex = items.indexOf(this); + + if (draggedIndex < dropIndex) { + this.parentNode.insertBefore(draggedElement, this.nextSibling); + } else { + this.parentNode.insertBefore(draggedElement, this); + } + + this.classList.remove('drag-over'); + + // Save new order + saveBookmarkOrder(); + } + + function saveBookmarkOrder() { + const bar = document.getElementById('bookmarkBar'); + const items = bar.querySelectorAll('.bookmark-item'); + const orderedIds = Array.from(items).map(item => parseInt(item.dataset.bookmarkId)); + + if (window.reorderBookmarks && orderedIds.length > 0) { + window.reorderBookmarks(JSON.stringify(orderedIds)); + } + } + // Load and display bookmarks function loadBookmarks() { const bar = document.getElementById('bookmarkBar'); From 756e9d80d2c8925c476faef764dcaad0f477b58e Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 23:01:28 +0100 Subject: [PATCH 04/20] Refactor bookmark drag-and-drop to use mouse events Replaces the previous HTML5 drag-and-drop API with a custom mouse-based drag-and-drop implementation for bookmark items. Improves visual feedback, adds left/right drop indicators, and ensures better compatibility and control over drag behavior. Also prevents accidental navigation during drag and refines the UI for drag states. --- assets/static-sties/google-static.html | 207 ++++++++++++++++--------- 1 file changed, 130 insertions(+), 77 deletions(-) diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index 9a70775..6930c53 100644 --- a/assets/static-sties/google-static.html +++ b/assets/static-sties/google-static.html @@ -302,14 +302,24 @@ } /* Drag and drop styles */ + .bookmark-item { + cursor: grab; + } + .bookmark-item.dragging { opacity: 0.5; background: rgba(100, 100, 255, 0.3); + cursor: grabbing; } .bookmark-item.drag-over { - border-left: 2px solid #6C63FF; - margin-left: -2px; + border-left: 3px solid #6C63FF; + padding-left: 7px; + } + + .bookmark-item.drag-over-right { + border-right: 3px solid #6C63FF; + padding-right: 7px; } .bookmark-bar.drag-active { @@ -383,20 +393,24 @@ } } - // Drag and drop state - let draggedElement = null; - let draggedBookmarkId = null; - let longPressTimer = null; - let isDragging = false; + // Mouse-based drag and drop state + let dragState = { + isDragging: false, + element: null, + bookmarkId: null, + startX: 0, + startY: 0, + offsetX: 0, + clone: null, + moved: false + }; - // Create a bookmark element with drag support + // Create a bookmark element with mouse drag support function createBookmarkElement(bookmark) { - const item = document.createElement('a'); + const item = document.createElement('div'); item.className = 'bookmark-item'; - item.href = bookmark.url; - item.title = bookmark.title || bookmark.url; item.dataset.bookmarkId = bookmark.id; - item.draggable = true; + item.dataset.url = bookmark.url; // Icon (favicon or default) if (bookmark.favicon && bookmark.favicon.startsWith('data:')) { @@ -426,6 +440,9 @@ const removeBtn = document.createElement('div'); removeBtn.className = 'remove-btn'; removeBtn.textContent = '×'; + removeBtn.onmousedown = function(e) { + e.stopPropagation(); + }; removeBtn.onclick = function(e) { e.preventDefault(); e.stopPropagation(); @@ -436,94 +453,130 @@ }; item.appendChild(removeBtn); - // Drag events - item.addEventListener('dragstart', handleDragStart); - item.addEventListener('dragend', handleDragEnd); - item.addEventListener('dragover', handleDragOver); - item.addEventListener('dragenter', handleDragEnter); - item.addEventListener('dragleave', handleDragLeave); - item.addEventListener('drop', handleDrop); - - // Long press for touch/mouse + // Mouse down to start drag item.addEventListener('mousedown', function(e) { if (e.button !== 0) return; - longPressTimer = setTimeout(() => { - isDragging = true; - item.classList.add('dragging'); - }, 300); - }); - - item.addEventListener('mouseup', function() { - clearTimeout(longPressTimer); + if (e.target.classList.contains('remove-btn')) return; + + e.preventDefault(); + + dragState.isDragging = true; + dragState.element = item; + dragState.bookmarkId = bookmark.id; + dragState.startX = e.clientX; + dragState.startY = e.clientY; + dragState.moved = false; + + const rect = item.getBoundingClientRect(); + dragState.offsetX = e.clientX - rect.left; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); }); + // Click to navigate (only if not dragged) item.addEventListener('click', function(e) { - if (isDragging) { + if (dragState.moved) { e.preventDefault(); - isDragging = false; + return; } + // Navigate to bookmark + window.location.href = bookmark.url; }); return item; } - function handleDragStart(e) { - draggedElement = this; - draggedBookmarkId = this.dataset.bookmarkId; - this.classList.add('dragging'); - document.getElementById('bookmarkBar').classList.add('drag-active'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', draggedBookmarkId); - } - - function handleDragEnd(e) { - this.classList.remove('dragging'); - document.getElementById('bookmarkBar').classList.remove('drag-active'); - document.querySelectorAll('.bookmark-item').forEach(item => { - item.classList.remove('drag-over'); - }); - draggedElement = null; - draggedBookmarkId = null; - isDragging = false; - } - - function handleDragOver(e) { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - } - - function handleDragEnter(e) { - e.preventDefault(); - if (this !== draggedElement) { - this.classList.add('drag-over'); + function handleMouseMove(e) { + if (!dragState.isDragging || !dragState.element) return; + + const dx = e.clientX - dragState.startX; + const dy = e.clientY - dragState.startY; + + // Only start visual drag after moving a bit + if (!dragState.moved && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { + dragState.moved = true; + dragState.element.classList.add('dragging'); + document.getElementById('bookmarkBar').classList.add('drag-active'); + } + + if (!dragState.moved) return; + + // Find the bookmark item we're hovering over + const bar = document.getElementById('bookmarkBar'); + const items = Array.from(bar.querySelectorAll('.bookmark-item')); + + // Clear all drag-over states + items.forEach(item => item.classList.remove('drag-over', 'drag-over-right')); + + // Find which item we're over + for (const item of items) { + if (item === dragState.element) continue; + + const rect = item.getBoundingClientRect(); + const midX = rect.left + rect.width / 2; + + if (e.clientX >= rect.left && e.clientX <= rect.right) { + if (e.clientX < midX) { + item.classList.add('drag-over'); + } else { + item.classList.add('drag-over-right'); + } + break; + } } } - function handleDragLeave(e) { - this.classList.remove('drag-over'); - } - - function handleDrop(e) { - e.preventDefault(); - e.stopPropagation(); + function handleMouseUp(e) { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); - if (this === draggedElement) return; + if (!dragState.isDragging) return; const bar = document.getElementById('bookmarkBar'); const items = Array.from(bar.querySelectorAll('.bookmark-item')); - const draggedIndex = items.indexOf(draggedElement); - const dropIndex = items.indexOf(this); - if (draggedIndex < dropIndex) { - this.parentNode.insertBefore(draggedElement, this.nextSibling); - } else { - this.parentNode.insertBefore(draggedElement, this); + // Find drop target + let dropTarget = null; + let insertAfter = false; + + for (const item of items) { + if (item.classList.contains('drag-over')) { + dropTarget = item; + insertAfter = false; + break; + } + if (item.classList.contains('drag-over-right')) { + dropTarget = item; + insertAfter = true; + break; + } } - this.classList.remove('drag-over'); + // Move the element + if (dropTarget && dropTarget !== dragState.element && dragState.moved) { + if (insertAfter) { + dropTarget.parentNode.insertBefore(dragState.element, dropTarget.nextSibling); + } else { + dropTarget.parentNode.insertBefore(dragState.element, dropTarget); + } + // Save new order + saveBookmarkOrder(); + } + + // Clean up + items.forEach(item => { + item.classList.remove('dragging', 'drag-over', 'drag-over-right'); + }); + bar.classList.remove('drag-active'); - // Save new order - saveBookmarkOrder(); + // Reset state after a short delay to prevent click from firing + setTimeout(() => { + dragState.isDragging = false; + dragState.element = null; + dragState.bookmarkId = null; + dragState.moved = false; + }, 10); } function saveBookmarkOrder() { From 4fadf4f941071beaa91fedec372cf91b01393c25 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 23:10:14 +0100 Subject: [PATCH 05/20] Enhance middle and ctrl+click to open bookmarks in new tab Enhanced bookmark interaction: middle mouse button and Ctrl+click now open bookmarks in a new tab using a native bridge if available, otherwise falling back to navigation in the current tab. --- assets/static-sties/google-static.html | 29 +++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/assets/static-sties/google-static.html b/assets/static-sties/google-static.html index 6930c53..74f6c3d 100644 --- a/assets/static-sties/google-static.html +++ b/assets/static-sties/google-static.html @@ -474,19 +474,46 @@ document.addEventListener('mouseup', handleMouseUp); }); + // Middle click opens in new tab + item.addEventListener('auxclick', function(e) { + if (e.button === 1) { // Middle mouse button + e.preventDefault(); + openInNewTab(bookmark.url); + } + }); + // Click to navigate (only if not dragged) item.addEventListener('click', function(e) { if (dragState.moved) { e.preventDefault(); return; } - // Navigate to bookmark + // Ctrl+Click opens in new tab + if (e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + openInNewTab(bookmark.url); + return; + } + // Regular click navigates in current tab window.location.href = bookmark.url; }); return item; } + // Open URL in new tab using native bridge + function openInNewTab(url) { + if (window.__ul_newTab) { + window.__ul_newTab(url); + } else if (window.__ul && window.__ul.newTab) { + window.__ul.newTab(url); + } else { + // Fallback: just navigate + window.location.href = url; + } + } + function handleMouseMove(e) { if (!dragState.isDragging || !dragState.element) return; From d84dc6dcac9c53b4e4a75cf8b9831dfb0c33c58f Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 23:11:31 +0100 Subject: [PATCH 06/20] Add bookmarks management page Introduces a new bookmarks.html file with UI and JavaScript for displaying, managing, and clearing bookmarks. The page includes styling, favicon support, and integration with window methods for bookmark operations. --- assets/bookmarks.html | 453 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 assets/bookmarks.html diff --git a/assets/bookmarks.html b/assets/bookmarks.html new file mode 100644 index 0000000..1210437 --- /dev/null +++ b/assets/bookmarks.html @@ -0,0 +1,453 @@ + + + + + Bookmarks + + + + + + + +
+
+

+ + + + Bookmarks +

+
+ + +
+
+ + + +
    +
    + + + + + From 5813428d16e055be84c4b0be19c741363b7bde2a Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 23:12:39 +0100 Subject: [PATCH 07/20] Add Bookmarks Management menu item with shortcut Introduces a Bookmarks entry to the menu with Ctrl+B shortcut and corresponding action handler for opening bookmarks in a new tab. --- assets/menu.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/menu.html b/assets/menu.html index 563e642..2589a6d 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -88,6 +88,10 @@ History Ctrl+H +