From 78d10b4c1bf0698251daa377cb4dca6338f021da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Tue, 26 Nov 2024 22:38:52 +0100 Subject: [PATCH 1/8] Added workspace selector on bookmarks create/edit modal, added table for storing relationships between workspaces and bookmarks. --- .../base/zen-components/ZenWorkspaces.mjs | 71 ++++++-- .../zen-components/ZenWorkspacesStorage.mjs | 152 +++++++++++++++- .../places/PlacesUIUtils-sys-mjs.patch | 159 ++++++++++++++++ .../content/bookmarkProperties-xhtml.patch | 10 +- .../content/browserPlacesViews-js.patch | 110 ++++++++++- .../places/content/editBookmark-js.patch | 172 ++++++++++++++++++ .../content/editBookmarkPanel-inc-xhtml.patch | 18 +- .../shared/places/editBookmark-css.patch | 74 ++++++++ 8 files changed, 745 insertions(+), 21 deletions(-) create mode 100644 src/browser/components/places/PlacesUIUtils-sys-mjs.patch create mode 100644 src/browser/components/places/content/editBookmark-js.patch create mode 100644 src/browser/themes/shared/places/editBookmark-css.patch diff --git a/src/browser/base/zen-components/ZenWorkspaces.mjs b/src/browser/base/zen-components/ZenWorkspaces.mjs index 15c5272da..9123cf5ce 100644 --- a/src/browser/base/zen-components/ZenWorkspaces.mjs +++ b/src/browser/base/zen-components/ZenWorkspaces.mjs @@ -13,6 +13,13 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { }; _hoveringSidebar = false; _lastScrollTime = 0; + bookmarkMenus = [ + "PlacesToolbar", + "bookmarks-menu-button", + "BMB_bookmarksToolbar", + "BMB_unsortedBookmarks", + "BMB_mobileBookmarks" + ]; async init() { if (!this.shouldHaveWorkspaces) { @@ -61,6 +68,11 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } Services.obs.addObserver(this, 'weave:engine:sync:finish'); + Services.obs.addObserver(async function observe(subject) { + this._workspaceBookmarksCache = null; + await this.workspaceBookmarks(); + this._invalidateBookmarkContainers(); + }.bind(this), "workspace-bookmarks-updated"); } initializeWorkspaceNavigation() { @@ -128,7 +140,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } // Change workspace based on scroll direction - const direction = event.deltaX > 0 ? -1 : 1; + const direction = event.deltaX > 0 ? 1 : -1; await this.changeWorkspaceShortcut(direction); this._lastScrollTime = currentTime; }, { passive: true }); @@ -320,6 +332,21 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { return this._workspaceCache; } + async workspaceBookmarks() { + if (this._workspaceBookmarksCache) { + return this._workspaceBookmarksCache; + } + + const [bookmarks, lastChangeTimestamp] = await Promise.all([ + ZenWorkspaceBookmarksStorage.getBookmarkGuidsByWorkspace(), + ZenWorkspaceBookmarksStorage.getLastChangeTimestamp(), + ]); + + this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp }; + + return this._workspaceCache; + } + async onWorkspacesEnabledChanged() { if (this.workspaceEnabled) { throw Error("Shoud've had reloaded the window"); @@ -339,6 +366,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { if (this.workspaceEnabled) { this._initializeWorkspaceCreationIcons(); this._initializeWorkspaceTabContextMenus(); + await this.workspaceBookmarks(); window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this)); await SessionStore.promiseInitialized; let workspaces = await this._workspaces(); @@ -754,8 +782,10 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { if(clearCache) { browser.ZenWorkspaces._workspaceCache = null; + browser.ZenWorkspaces._bookmarkWorkspaceCache = null; } let workspaces = await browser.ZenWorkspaces._workspaces(); + await browser.ZenWorkspaces.workspaceBookmarks(); workspaceList.innerHTML = ''; workspaceList.parentNode.style.display = 'flex'; if (workspaces.workspaces.length <= 0) { @@ -1252,16 +1282,23 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } } - // Reset bookmarks toolbar - const placesToolbar = document.getElementById("PlacesToolbar"); - if (placesToolbar?._placesView) { - placesToolbar._placesView.invalidateContainer(placesToolbar._placesView._resultNode); - } + // Reset bookmarks + this._invalidateBookmarkContainers(); // Update workspace indicator await this.updateWorkspaceIndicator(); } + _invalidateBookmarkContainers() { + for (let i = 0, len = this.bookmarkMenus.length; i < len; i++) { + const element = document.getElementById(this.bookmarkMenus[i]); + if (element && element._placesView) { + const placesView = element._placesView; + placesView.invalidateContainer(placesView._resultNode); + } + } + } + async updateWorkspaceIndicator() { // Update current workspace indicator const currentWorkspace = await this.getActiveWorkspace(); @@ -1534,12 +1571,24 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } isBookmarkInAnotherWorkspace(bookmark) { - let tags = bookmark.tags; - // if any tag starts with "_workspace_id" and the workspace id doesnt match the active workspace id, return null - if (tags) { - for (let tag of tags.split(",")) { - return !!(tag.startsWith("zen_workspace_") && this.getActiveWorkspaceFromCache()?.uuid !== tag.split("_")[2]); + if (!this._workspaceBookmarksCache?.bookmarks) return false; + const bookmarkGuid = bookmark.bookmarkGuid; + const activeWorkspaceUuid = this.activeWorkspace; + let isInActiveWorkspace = false; + let isInOtherWorkspace = false; + + for (const [workspaceUuid, bookmarkGuids] of Object.entries(this._workspaceBookmarksCache.bookmarks)) { + if (bookmarkGuids.includes(bookmarkGuid)) { + if (workspaceUuid === activeWorkspaceUuid) { + isInActiveWorkspace = true; + } else { + isInOtherWorkspace = true; + } } } + + // Return true only if the bookmark is in another workspace and not in the active one + return isInOtherWorkspace && !isInActiveWorkspace; } + })(); diff --git a/src/browser/base/zen-components/ZenWorkspacesStorage.mjs b/src/browser/base/zen-components/ZenWorkspacesStorage.mjs index 8fda26d3f..0041e4fdf 100644 --- a/src/browser/base/zen-components/ZenWorkspacesStorage.mjs +++ b/src/browser/base/zen-components/ZenWorkspacesStorage.mjs @@ -2,6 +2,8 @@ var ZenWorkspacesStorage = { async init() { console.log('ZenWorkspacesStorage: Initializing...'); await this._ensureTable(); + await ZenWorkspaceBookmarksStorage.init(); + ZenWorkspaces._delayedStartup(); }, async _ensureTable() { @@ -64,7 +66,6 @@ var ZenWorkspacesStorage = { await ZenWorkspacesStorage.migrateWorkspacesFromJSON(); } - ZenWorkspaces._delayedStartup(); }); }, @@ -405,3 +406,152 @@ var ZenWorkspacesStorage = { this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs)); }, }; + +// Integration of workspace-specific bookmarks into Places +var ZenWorkspaceBookmarksStorage = { + async init() { + await this._ensureTable(); + }, + + async _ensureTable() { + await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.init', async (db) => { + // Create table using GUIDs instead of IDs + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces ( + id INTEGER PRIMARY KEY, + bookmark_guid TEXT NOT NULL, + workspace_uuid TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(bookmark_guid, workspace_uuid), + FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE, + FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE + ) + `); + + // Create index for fast lookups + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_lookup + ON zen_bookmarks_workspaces(workspace_uuid, bookmark_guid) + `); + + // Add changes tracking table + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces_changes ( + id INTEGER PRIMARY KEY, + bookmark_guid TEXT NOT NULL, + workspace_uuid TEXT NOT NULL, + change_type TEXT NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE(bookmark_guid, workspace_uuid), + FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE, + FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE + ) + `); + + // Create index for changes tracking + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_changes + ON zen_bookmarks_workspaces_changes(bookmark_guid, workspace_uuid) + `); + + }); + }, + + /** + * Updates the last change timestamp in the metadata table. + * @param {Object} db - The database connection. + */ + async updateLastChangeTimestamp(db) { + const now = Date.now(); + await db.execute(` + INSERT OR REPLACE INTO moz_meta (key, value) + VALUES ('zen_bookmarks_workspaces_last_change', :now) + `, { now }); + }, + + /** + * Gets the timestamp of the last change. + * @returns {Promise} The timestamp of the last change. + */ + async getLastChangeTimestamp() { + const db = await PlacesUtils.promiseDBConnection(); + const result = await db.executeCached(` + SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change' + `); + return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; + }, + + async getBookmarkWorkspaces(bookmarkGuid) { + const db = await PlacesUtils.promiseDBConnection(); + + const rows = await db.execute(` + SELECT workspace_uuid + FROM zen_bookmarks_workspaces + WHERE bookmark_guid = :bookmark_guid + `, { bookmark_guid: bookmarkGuid }); + + return rows.map(row => row.getResultByName("workspace_uuid")); + }, + + /** + * Get all bookmark GUIDs organized by workspace UUID. + * @returns {Promise} A dictionary with workspace UUIDs as keys and arrays of bookmark GUIDs as values. + * @example + * // Returns: + * { + * "workspace-uuid-1": ["bookmark-guid-1", "bookmark-guid-2"], + * "workspace-uuid-2": ["bookmark-guid-3"] + * } + */ + async getBookmarkGuidsByWorkspace() { + const db = await PlacesUtils.promiseDBConnection(); + + const rows = await db.execute(` + SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids + FROM zen_bookmarks_workspaces + GROUP BY workspace_uuid + `); + + const result = {}; + for (const row of rows) { + const workspaceUuid = row.getResultByName("workspace_uuid"); + const bookmarkGuids = row.getResultByName("bookmark_guids"); + result[workspaceUuid] = bookmarkGuids ? bookmarkGuids.split(',') : []; + } + + return result; + }, + + /** + * Get all changed bookmarks with their change types. + * @returns {Promise} An object mapping bookmark+workspace pairs to their change data. + */ + async getChangedIDs() { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute(` + SELECT bookmark_guid, workspace_uuid, change_type, timestamp + FROM zen_bookmarks_workspaces_changes + `); + + const changes = {}; + for (const row of rows) { + const key = `${row.getResultByName('bookmark_guid')}:${row.getResultByName('workspace_uuid')}`; + changes[key] = { + type: row.getResultByName('change_type'), + timestamp: row.getResultByName('timestamp') + }; + } + return changes; + }, + + /** + * Clear all recorded changes. + */ + async clearChangedIDs() { + await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.clearChangedIDs', async (db) => { + await db.execute(`DELETE FROM zen_bookmarks_workspaces_changes`); + }); + }, + +}; \ No newline at end of file diff --git a/src/browser/components/places/PlacesUIUtils-sys-mjs.patch b/src/browser/components/places/PlacesUIUtils-sys-mjs.patch new file mode 100644 index 000000000..4d83a7ee9 --- /dev/null +++ b/src/browser/components/places/PlacesUIUtils-sys-mjs.patch @@ -0,0 +1,159 @@ +diff --git a/browser/components/places/PlacesUIUtils.sys.mjs b/browser/components/places/PlacesUIUtils.sys.mjs +index 0f79ba5dd42116d626445b86f6b24731d2fa8aad..76d692db1731e84b28d9035b03e34c176c12bd23 100644 +--- a/browser/components/places/PlacesUIUtils.sys.mjs ++++ b/browser/components/places/PlacesUIUtils.sys.mjs +@@ -58,6 +58,7 @@ class BookmarkState { + info, + tags = "", + keyword = "", ++ workspaces = [], + isFolder = false, + children = [], + autosave = false, +@@ -82,12 +83,18 @@ class BookmarkState { + keyword, + parentGuid: info.parentGuid, + index, ++ workspaces, + }; + + // Edited bookmark + this._newState = {}; + } + ++ async _workspacesChanged(workspaces) { ++ this._newState.workspaces = workspaces; ++ await this._maybeSave(); ++ } ++ + /** + * Save edited title for the bookmark + * +@@ -181,6 +188,14 @@ class BookmarkState { + "BookmarkState::createBookmark" + ); + this._guid = results?.[0]; ++ ++ if ('workspaces' in this._newState) { ++ try { ++ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces); ++ } catch (ex) { ++ console.error("Failed to update workspace assignments:", ex); ++ } ++ } + return this._guid; + } + +@@ -214,6 +229,14 @@ class BookmarkState { + "BookmarkState::save::createFolder" + ); + this._guid = results[0]; ++ ++ if ('workspaces' in this._newState) { ++ try { ++ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces); ++ } catch (ex) { ++ console.error("Failed to update workspace assignments:", ex); ++ } ++ } + return this._guid; + } + +@@ -300,11 +323,97 @@ class BookmarkState { + await lazy.PlacesTransactions.batch(transactions, "BookmarkState::save"); + } + ++ if ('workspaces' in this._newState) { ++ try { ++ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces); ++ } catch (ex) { ++ console.error("Failed to update workspace assignments:", ex); ++ } ++ } + this._originalState = { ...this._originalState, ...this._newState }; + this._newState = {}; + return this._guid; + } + ++ async updateBookmarkWorkspaces(bookmarkGuid, workspaces) { ++ await lazy.PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.updateBookmarkWorkspaces', async (db) => { ++ const now = Date.now(); ++ ++ await db.executeTransaction(async () => { ++ const rows = await db.execute(` ++ SELECT workspace_uuid ++ FROM zen_bookmarks_workspaces ++ WHERE bookmark_guid = :bookmark_guid ++ `, { bookmark_guid: bookmarkGuid }); ++ ++ const currentWorkspaces = rows.map(row => row.getResultByName("workspace_uuid")); ++ const workspacesToRemove = currentWorkspaces.filter(w => !workspaces.includes(w)); ++ const workspacesToAdd = workspaces.filter(w => !currentWorkspaces.includes(w)); ++ ++ // If there are workspaces to remove, delete only those specific associations ++ if (workspacesToRemove.length > 0) { ++ const placeholders = workspacesToRemove.map(() => '?').join(','); ++ await db.execute(` ++ DELETE FROM zen_bookmarks_workspaces ++ WHERE bookmark_guid = :bookmark_guid ++ AND workspace_uuid IN (${placeholders}) ++ `, [bookmarkGuid, ...workspacesToRemove]); ++ ++ // Record removals ++ for (const workspace of workspacesToRemove) { ++ await this._recordChange(db, bookmarkGuid, workspace, 'removed'); ++ } ++ } ++ ++ // Add only new associations ++ for (const workspaceUuid of workspacesToAdd) { ++ await db.execute(` ++ INSERT INTO zen_bookmarks_workspaces ( ++ bookmark_guid, workspace_uuid, created_at, updated_at ++ ) VALUES ( ++ :bookmark_guid, :workspace_uuid, :now, :now ++ ) ++ `, { ++ bookmark_guid: bookmarkGuid, ++ workspace_uuid: workspaceUuid, ++ now ++ }); ++ ++ await this._recordChange(db, bookmarkGuid, workspaceUuid, 'added'); ++ } ++ }); ++ }); ++ ++ const changes = { bookmarkGuid, workspaces }; ++ Services.obs.notifyObservers(null, "workspace-bookmarks-updated", JSON.stringify(changes)); ++ } ++ ++ async _recordChange(db, bookmarkGuid, workspaceUuid, changeType) { ++ const now = Date.now(); ++ await db.execute(` ++ INSERT OR REPLACE INTO zen_bookmarks_workspaces_changes ( ++ bookmark_guid, workspace_uuid, change_type, timestamp ++ ) VALUES ( ++ :bookmark_guid, :workspace_uuid, :change_type, :timestamp ++ ) ++ `, { ++ bookmark_guid: bookmarkGuid, ++ workspace_uuid: workspaceUuid, ++ change_type: changeType, ++ timestamp: Math.floor(now / 1000) ++ }); ++ ++ await this._updateLastChangeTimestamp(db); ++ } ++ ++ async _updateLastChangeTimestamp(db) { ++ const now = Date.now(); ++ await db.execute(` ++ INSERT OR REPLACE INTO moz_meta (key, value) ++ VALUES ('zen_bookmarks_workspaces_last_change', :now) ++ `, { now }); ++ } ++ + /** + * Append transactions to update tags by given information. + * diff --git a/src/browser/components/places/content/bookmarkProperties-xhtml.patch b/src/browser/components/places/content/bookmarkProperties-xhtml.patch index b2995290d..0360d40e5 100644 --- a/src/browser/components/places/content/bookmarkProperties-xhtml.patch +++ b/src/browser/components/places/content/bookmarkProperties-xhtml.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/places/content/bookmarkProperties.xhtml b/browser/components/places/content/bookmarkProperties.xhtml -index 047652a52e705d49f870399992873fce536c07b9..2932eb94e8c16eb05f172322a6ce3ea201ecd0b1 100644 +index 047652a52e705d49f870399992873fce536c07b9..8bc7d1c5e44c33d90f82fdc6f66d9e2e80c60bae 100644 --- a/browser/components/places/content/bookmarkProperties.xhtml +++ b/browser/components/places/content/bookmarkProperties.xhtml @@ -37,6 +37,7 @@ @@ -10,3 +10,11 @@ index 047652a52e705d49f870399992873fce536c07b9..2932eb94e8c16eb05f172322a6ce3ea2 +@@ -44,6 +45,7 @@ + src="chrome://browser/locale/places/bookmarkProperties.properties"/> + + ++