From af5f4f97d7f38dbb201e0f09d1b000de6f0c92cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Sun, 3 Nov 2024 20:22:35 +0100 Subject: [PATCH 1/3] feat: Separate pinned tab initialization and handling This commit refactors the `ZenPinnedTabManager` class to separate the initialization of pinned tabs from the handling of pinned tab events. - **Initialization:** The `initTabs()` method is now responsible for initializing the pinned tab storage, waiting for the session store to be initialized, and then refreshing the pinned tabs. - **Event handling:** The `init()` method focuses on setting up the event listeners and observers for pinned tabs. This separation improves the code structure and allows for better handling of pinned tab initialization, which was previously problematic due to the asynchronous nature of the process. Additionally, this commit includes minor fixes and improvements: - Removed the `TabClose` event handler, as it is currently causing issues with the tab deletion process. - Added a check to prevent duplicate pinned tab attributes. - Improved the way favicons are set for pinned tabs. --- src/ZenPinnedTabManager.mjs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/ZenPinnedTabManager.mjs b/src/ZenPinnedTabManager.mjs index 7a424d4..7f22e77 100644 --- a/src/ZenPinnedTabManager.mjs +++ b/src/ZenPinnedTabManager.mjs @@ -36,13 +36,10 @@ } } - class ZenPinnedTabManager extends ZenMultiWindowFeature { + class ZenPinnedTabManager extends ZenDOMOperatedFeature { - async init() { + init() { this.observer = new ZenPinnedTabsObserver(); - await ZenPinnedTabsStorage.init(); - await SessionStore.promiseInitialized; - await this._refreshPinnedTabs(); this._initClosePinnedTabShortcut(); this._insertItemsIntoTabContextMenu(); this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this)); @@ -50,6 +47,12 @@ this._zenClickEventListener = this._onTabClick.bind(this); } + async initTabs() { + await ZenPinnedTabsStorage.init(); + await SessionStore.promiseInitialized; + await this._refreshPinnedTabs(); + } + async _refreshPinnedTabs() { await this._initializePinsCache(); this._initializePinnedTabs(); @@ -139,9 +142,10 @@ }); // Set the favicon from cache - if (pin.iconUrl) { - gBrowser.setIcon(newTab, pin.iconUrl, null, - Services.scriptSecurityManager.getSystemPrincipal()); + if (!!pin.iconUrl) { + // TODO: Figure out if there is a better way - + // calling gBrowser.setIcon messes shit up and should be avoided. I think this works for now. + newTab.setAttribute("image", pin.iconUrl); } newTab.setAttribute("zen-pin-id", pin.uuid); @@ -175,9 +179,10 @@ delete tab._zenClickEventListener; } break; - case "TabClose": - this._removePinnedAttributes(tab); - break; + // TODO: Do this in a better way. Closing a second window could trigger remove tab and delete it from db + // case "TabClose": + // this._removePinnedAttributes(tab); + // break; default: console.warn('ZenPinnedTabManager: Unhandled tab event', action); break; @@ -228,6 +233,11 @@ } async _setPinnedAttributes(tab) { + + if (tab.hasAttribute("zen-pin-id")) { + return; + } + const browser = tab.linkedBrowser; const uuid = gZenUIManager.generateUuidv4(); From a04b5c30b6f81dd00344226f2b6bb7004b6dd63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Sun, 3 Nov 2024 20:29:55 +0100 Subject: [PATCH 2/3] feat(zen-components): Remove unnecessary SessionStore dependency This commit removes the unnecessary dependency on the `SessionStore` in the `ZenPinnedTabManager`. The `SessionStore.promiseInitialized` was being awaited before refreshing pinned tabs, but this was not necessary. The pinned tab storage is initialized independently and does not rely on the session store. This change improves the efficiency of the code and reduces the number of dependencies. --- src/ZenPinnedTabManager.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ZenPinnedTabManager.mjs b/src/ZenPinnedTabManager.mjs index 7f22e77..c626bfd 100644 --- a/src/ZenPinnedTabManager.mjs +++ b/src/ZenPinnedTabManager.mjs @@ -49,7 +49,6 @@ async initTabs() { await ZenPinnedTabsStorage.init(); - await SessionStore.promiseInitialized; await this._refreshPinnedTabs(); } From a82e2c2765ce2ef901ffba2015f5675e7917865f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Sun, 3 Nov 2024 20:50:59 +0100 Subject: [PATCH 3/3] feat: Add group support for pinned tabs This commit introduces the ability to create groups of pinned tabs, allowing for more organized and hierarchical management of pinned tabs. Key changes: - Added `parentUuid` field to `zen_pins` table to represent parent group relationships. - Implemented `getGroupChildren` function to fetch children of a given group. - Updated `removePin` function to also remove all children of a group when deleting. - Modified `getPins` function to sort pins by parent group and then by position. - Added `isGroup` and `isEssential` fields to `zen_pins` table to represent the type of pinned item. - Improved database schema with additional indices for performance optimization. - Removed unused `recordChange` function. --- src/ZenPinnedTabsStorage.mjs | 136 +++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 39 deletions(-) diff --git a/src/ZenPinnedTabsStorage.mjs b/src/ZenPinnedTabsStorage.mjs index 8c84312..d529ea8 100644 --- a/src/ZenPinnedTabsStorage.mjs +++ b/src/ZenPinnedTabsStorage.mjs @@ -1,11 +1,7 @@ var ZenPinnedTabsStorage = { async init() { console.log('ZenPinnedTabsStorage: Initializing...'); - try { - await this._ensureTable(); - } catch (e) { - console.warn('ZenPinnedTabsStorage: Failed to initialize', e); - } + await this._ensureTable(); }, async _ensureTable() { @@ -13,23 +9,32 @@ var ZenPinnedTabsStorage = { // Create the pins table if it doesn't exist await db.execute(` CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT NOT NULL, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) + id INTEGER PRIMARY KEY, + uuid TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + url TEXT, + container_id INTEGER, + workspace_uuid TEXT, + position INTEGER NOT NULL DEFAULT 0, + is_essential BOOLEAN NOT NULL DEFAULT 0, + is_group BOOLEAN NOT NULL DEFAULT 0, + parent_uuid TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (parent_uuid) REFERENCES zen_pins(uuid) ON DELETE SET NULL + ) `); - // Create an index on the uuid column + + // Create indices await db.execute(` CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) `); + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_parent_uuid ON zen_pins(parent_uuid) + `); + // Create the changes tracking table if it doesn't exist await db.execute(` CREATE TABLE IF NOT EXISTS zen_pins_changes ( @@ -70,33 +75,40 @@ var ZenPinnedTabsStorage = { if ('position' in pin && Number.isFinite(pin.position)) { newPosition = pin.position; } else { - // Get the maximum position - const maxPositionResult = await db.execute(`SELECT MAX("position") as max_position FROM zen_pins`); + // Get the maximum position within the same parent group (or null for root level) + const maxPositionResult = await db.execute(` + SELECT MAX("position") as max_position + FROM zen_pins + WHERE COALESCE(parent_uuid, '') = COALESCE(:parent_uuid, '') + `, { parent_uuid: pin.parentUuid || null }); const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; // Add a large increment to avoid frequent reordering + newPosition = maxPosition + 1000; } // Insert or replace the pin await db.executeCached(` INSERT OR REPLACE INTO zen_pins ( uuid, title, url, container_id, workspace_uuid, position, - created_at, updated_at + is_essential, is_group, parent_uuid, created_at, updated_at ) VALUES ( :uuid, :title, :url, :container_id, :workspace_uuid, :position, + :is_essential, :is_group, :parent_uuid, COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), :now ) `, { uuid: pin.uuid, title: pin.title, - url: pin.url, + url: pin.isGroup ? null : pin.url, container_id: pin.containerTabId || null, workspace_uuid: pin.workspaceUuid || null, position: newPosition, + is_essential: pin.isEssential || false, + is_group: pin.isGroup || false, + parent_uuid: pin.parentUuid || null, now }); - // Record the change await db.execute(` INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) VALUES (:uuid, :timestamp) @@ -106,7 +118,6 @@ var ZenPinnedTabsStorage = { }); changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); }); }); @@ -119,7 +130,8 @@ var ZenPinnedTabsStorage = { async getPins() { const db = await PlacesUtils.promiseDBConnection(); const rows = await db.executeCached(` - SELECT * FROM zen_pins ORDER BY position ASC + SELECT * FROM zen_pins + ORDER BY parent_uuid NULLS FIRST, position ASC `); return rows.map((row) => ({ uuid: row.getResultByName('uuid'), @@ -127,7 +139,31 @@ var ZenPinnedTabsStorage = { url: row.getResultByName('url'), containerTabId: row.getResultByName('container_id'), workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position') + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('parent_uuid') + })); + }, + + async getGroupChildren(groupUuid) { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached(` + SELECT * FROM zen_pins + WHERE parent_uuid = :groupUuid + ORDER BY position ASC + `, { groupUuid }); + + return rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + title: row.getResultByName('title'), + url: row.getResultByName('url'), + containerTabId: row.getResultByName('container_id'), + workspaceUuid: row.getResultByName('workspace_uuid'), + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('parent_uuid') })); }, @@ -135,22 +171,44 @@ var ZenPinnedTabsStorage = { const changedUUIDs = [uuid]; await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.execute( - `DELETE FROM zen_pins WHERE uuid = :uuid`, - { uuid } - ); + await db.executeTransaction(async () => { + // Get all child UUIDs first for change tracking + const children = await db.execute( + `SELECT uuid FROM zen_pins WHERE parent_uuid = :uuid`, + { uuid } + ); - // Record the removal as a change - const now = Date.now(); - await db.execute(` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, { - uuid, - timestamp: Math.floor(now / 1000) + // Add child UUIDs to changedUUIDs array + for (const child of children) { + changedUUIDs.push(child.getResultByName('uuid')); + } + + // Delete all children in a single statement + await db.execute( + `DELETE FROM zen_pins WHERE parent_uuid = :uuid`, + { uuid } + ); + + // Delete the pin/group itself + await db.execute( + `DELETE FROM zen_pins WHERE uuid = :uuid`, + { uuid } + ); + + // Record the changes + const now = Math.floor(Date.now() / 1000); + for (const changedUuid of changedUUIDs) { + await db.execute(` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, { + uuid: changedUuid, + timestamp: now + }); + } + + await this.updateLastChangeTimestamp(db); }); - - await this.updateLastChangeTimestamp(db); }); if (notifyObservers) {