From c77de8e8dea8f48bd254c1fc848ff42d6ac8a6e3 Mon Sep 17 00:00:00 2001 From: Vedaant Date: Tue, 18 Nov 2025 18:40:51 -0800 Subject: [PATCH 1/4] fix(tabs): persist pinned tab favicons Improves pinned tab handling so favicon data is stored and reused Adds DB column for icon data and prefers saved icons over recomputing Introduces lazy migration and async capture to backfill missing favicons Adds temporary debug logging to trace favicon load and storage paths --- src/zen/tabs/ZenPinnedTabManager.mjs | 351 +++++++++++++++++++++++++- src/zen/tabs/ZenPinnedTabsStorage.mjs | 85 +++++-- 2 files changed, 410 insertions(+), 26 deletions(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 68104fd182..12a6b1c0a8 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -90,6 +90,8 @@ this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this)); this._zenClickEventListener = this._onTabClick.bind(this); + // DEBUG_FAVICON: Track pins we've attempted to migrate to avoid duplicate attempts + this._migrationAttempted = new Set(); gZenWorkspaces._resolvePinnedInitialized(); } @@ -100,14 +102,24 @@ } } - onTabIconChanged(tab, url = null) { - const iconUrl = url ?? tab.iconImage.src; + async onTabIconChanged(tab, url = null) { + // Get the favicon URL from the browser (already computed by Firefox) + const iconUrl = url ?? gBrowser.getIcon(tab); + if (!iconUrl && tab.hasAttribute('zen-pin-id')) { + // No favicon yet, wait and try Places API try { setTimeout(async () => { const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); if (favicon) { gBrowser.setIcon(tab, favicon); + const pinId = tab.getAttribute('zen-pin-id'); + const pin = this._pinsCache?.find((p) => p.uuid === pinId); + if (pin) { + console.log('[DEBUG_FAVICON] Saving favicon to pin storage from onTabIconChanged', pinId); + pin.iconUrl = favicon; + await this.savePin(pin, false); + } } }); } catch { @@ -117,6 +129,18 @@ if (tab.hasAttribute('zen-essential')) { tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } + // Save favicon if it's a data URI + if (iconUrl && tab.hasAttribute('zen-pin-id')) { + const pinId = tab.getAttribute('zen-pin-id'); + const pin = this._pinsCache?.find((p) => p.uuid === pinId); + if (pin && iconUrl.startsWith('data:image/')) { + console.log('[DEBUG_FAVICON] Saving favicon to pin storage from onTabIconChanged (data URI)', pinId); + pin.iconUrl = iconUrl; + this.savePin(pin, false).catch((ex) => { + console.log('[DEBUG_FAVICON] Failed to save favicon in onTabIconChanged:', ex); + }); + } + } } } @@ -170,20 +194,65 @@ // Get pin data const pins = await ZenPinnedTabsStorage.getPins(); - // Enhance pins with favicons + // DEBUG_FAVICON: Inspect what we got from database + console.log('[DEBUG_FAVICON] Raw pins from database:', pins.map(p => ({ + uuid: p.uuid, + hasIconUrl: !!p.iconUrl, + iconUrlType: typeof p.iconUrl, + iconUrlLength: p.iconUrl?.length || 0, + iconUrlPreview: p.iconUrl ? `${p.iconUrl.substring(0, 50)}...` : null + }))); + + // DEBUG_FAVICON: Track pins with/without stored icons (check for truthy AND non-empty) + const pinsWithIcons = pins.filter(p => p.iconUrl && p.iconUrl.trim().length > 0).length; + console.log('[DEBUG_FAVICON] Initializing pins cache:', { + totalPins: pins.length, + pinsWithStoredIcons: pinsWithIcons, + pinsNeedingMigration: pins.length - pinsWithIcons + }); + + // Enhance pins with favicons - use stored iconData first, fall back to Places, then lazy migration this._pinsCache = await Promise.all( pins.map(async (pin) => { try { if (pin.isGroup) { return pin; // Skip groups for now } + + // DEBUG_FAVICON: Use stored iconData first (check for truthy AND non-empty) + if (pin.iconUrl && pin.iconUrl.trim().length > 0) { + console.log('[DEBUG_FAVICON] Using stored iconData for pin', pin.uuid, 'length:', pin.iconUrl.length); + return { + ...pin, + iconUrl: pin.iconUrl, + }; + } + + // DEBUG_FAVICON: Fall back to Places API + console.log('[DEBUG_FAVICON] No stored iconData, trying Places API for pin', pin.uuid); const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); + if (image) { + console.log('[DEBUG_FAVICON] Places API returned favicon for pin', pin.uuid); + // Save it to storage for future use + pin.iconUrl = image; + this.savePin(pin, false).catch((ex) => { + console.log('[DEBUG_FAVICON] Failed to save favicon from Places API:', ex); + }); + return { + ...pin, + iconUrl: image, + }; + } + + // DEBUG_FAVICON: No icon found, will be migrated lazily + console.log('[DEBUG_FAVICON] No favicon found for pin, will migrate lazily', pin.uuid); return { ...pin, - iconUrl: image || null, + iconUrl: null, }; - } catch { - // If favicon fetch fails, continue without icon + } catch (ex) { + // DEBUG_FAVICON: If favicon fetch fails, continue without icon + console.log('[DEBUG_FAVICON] Error loading favicon for pin', pin.uuid, ex); return { ...pin, iconUrl: null, @@ -191,6 +260,9 @@ } }) ); + + // DEBUG_FAVICON: Schedule lazy migration for pins without icons + this.#scheduleLazyMigration(); } catch (ex) { console.error('Failed to initialize pins cache:', ex); this._pinsCache = []; @@ -209,6 +281,111 @@ this.hasInitializedPins = true; } + // DEBUG_FAVICON: Schedule lazy migration for pins without favicons + #scheduleLazyMigration() { + if (this._migrationScheduled) { + return; + } + this._migrationScheduled = true; + + // Wait a bit after initialization before starting migration + setTimeout(() => { + this.#migrateMissingFavicons(); + }, 2000); + + console.log('[DEBUG_FAVICON] Lazy migration scheduled'); + } + + // DEBUG_FAVICON: Migrate missing favicons with priority for essential tabs + async #migrateMissingFavicons() { + if (!this._pinsCache) { + return; + } + + const pinsNeedingMigration = this._pinsCache.filter( + (pin) => !pin.isGroup && !pin.iconUrl && pin.url + ); + + if (pinsNeedingMigration.length === 0) { + console.log('[DEBUG_FAVICON] No pins need favicon migration'); + return; + } + + // Prioritize essential tabs + const essentialPins = pinsNeedingMigration.filter((pin) => pin.isEssential); + const regularPins = pinsNeedingMigration.filter((pin) => !pin.isEssential); + + console.log('[DEBUG_FAVICON] Starting favicon migration:', { + total: pinsNeedingMigration.length, + essential: essentialPins.length, + regular: regularPins.length, + }); + + // Migrate essential tabs first + for (const pin of essentialPins) { + await this.#migratePinFavicon(pin); + // Small delay between migrations to avoid overwhelming the system + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + // Then migrate regular pins + for (const pin of regularPins) { + await this.#migratePinFavicon(pin); + // Small delay between migrations + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + console.log('[DEBUG_FAVICON] Favicon migration completed'); + } + + // DEBUG_FAVICON: Helper method to migrate a single pin's favicon + async #migratePinFavicon(pin) { + // Skip if already has favicon or already attempted + if (pin.iconUrl && pin.iconUrl.trim().length > 0) { + console.log('[DEBUG_FAVICON] Pin already has favicon, skipping migration', pin.uuid); + return; + } + + if (this._migrationAttempted?.has(pin.uuid)) { + console.log('[DEBUG_FAVICON] Migration already attempted for pin, skipping', pin.uuid); + return; + } + + // Mark as attempted immediately to prevent duplicate attempts + this._migrationAttempted?.add(pin.uuid); + + try { + console.log('[DEBUG_FAVICON] Migrating favicon for pin', pin.uuid, pin.url); + + // Try Places API first + let favicon = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); + + // If Places API fails, try network fetch + if (!favicon) { + console.log('[DEBUG_FAVICON] Places API failed, trying network fetch for pin', pin.uuid); + favicon = await this.fetchFaviconFromNetwork(pin.url); + } + + if (favicon) { + console.log('[DEBUG_FAVICON] Favicon migration successful for pin', pin.uuid); + pin.iconUrl = favicon; + await this.savePin(pin, false); + + // Update the tab if it exists + const tab = gBrowser.tabs.find( + (t) => t.getAttribute('zen-pin-id') === pin.uuid + ); + if (tab) { + gBrowser.setIcon(tab, favicon); + } + } else { + console.log('[DEBUG_FAVICON] Favicon migration failed for pin', pin.uuid); + } + } catch (ex) { + console.log('[DEBUG_FAVICON] Error migrating favicon for pin', pin.uuid, ex); + } + } + async #initializePinnedTabs(init = false) { const pins = this._pinsCache; if (!pins?.length || !init) { @@ -715,6 +892,18 @@ entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); } + // Get favicon from tab (browser already has it loaded) + const tabFavicon = gBrowser.getIcon(tab); + const initialIconUrl = tabFavicon && tabFavicon.startsWith('data:image/') ? tabFavicon : null; + + console.log('[DEBUG_FAVICON] Creating new pin', { + uuid, + url: browser.currentURI.spec, + hasTabFavicon: !!tabFavicon, + isDataURI: tabFavicon?.startsWith('data:image/') || false, + willCaptureAsync: !initialIconUrl + }); + await this.savePin({ uuid, title: entry?.title || tab.label || browser.contentTitle, @@ -724,8 +913,17 @@ isEssential: tab.getAttribute('zen-essential') === 'true', parentUuid: tab.group?.getAttribute('zen-pin-id') || null, position: tab._pPos, + iconUrl: initialIconUrl, // Save immediately if we have it }); + // If we didn't get a data URI, try to capture it async + if (!initialIconUrl) { + console.log('[DEBUG_FAVICON] Starting async favicon capture for new pin', uuid); + this.captureFaviconForTab(tab, uuid); + } else { + console.log('[DEBUG_FAVICON] Saved pin with immediate favicon', uuid, initialIconUrl.length); + } + tab.setAttribute('zen-pin-id', uuid); tab.dispatchEvent( new CustomEvent('ZenPinnedTabCreated', { @@ -972,23 +1170,132 @@ existingEntry.title = pin.title; state.entries = [existingEntry]; } + // DEBUG_FAVICON: Prefer stored iconData over iconUrl state.image = pin.iconUrl || state.image; state.index = 0; + // DEBUG_FAVICON: Log which icon source was used + if (pin.iconUrl) { + console.log('[DEBUG_FAVICON] Using stored iconData for tab reset', pin.uuid); + } else { + console.log('[DEBUG_FAVICON] No stored iconData, using existing tab state image', pin.uuid); + } + SessionStore.setTabState(tab, state); this.resetPinChangedUrl(tab); } + // Capture favicon for a tab asynchronously + captureFaviconForTab(tab, pinId) { + setTimeout(async () => { + try { + // First, check if the tab's favicon loaded (might be a chrome:// URL or other non-data URI) + const tabFavicon = gBrowser.getIcon(tab); + let faviconDataURI = null; + + if (tabFavicon && tabFavicon.startsWith('data:image/')) { + // Tab now has a data URI favicon + faviconDataURI = tabFavicon; + console.log('[DEBUG_FAVICON] Tab favicon loaded as data URI', pinId, tabFavicon.length); + } else { + // Try Places API + console.log('[DEBUG_FAVICON] Trying Places API for', pinId); + faviconDataURI = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); + + // If Places API failed, try network fetch + if (!faviconDataURI) { + console.log('[DEBUG_FAVICON] Places API failed, trying network fetch for', pinId); + faviconDataURI = await this.fetchFaviconFromNetwork(tab.linkedBrowser.currentURI.spec); + } + } + + if (faviconDataURI) { + console.log('[DEBUG_FAVICON] Favicon captured successfully for', pinId, faviconDataURI.length); + const pin = this._pinsCache?.find(p => p.uuid === pinId); + if (pin) { + pin.iconUrl = faviconDataURI; + await this.savePin(pin, false); + console.log('[DEBUG_FAVICON] Favicon saved to database for', pinId); + } + } else { + console.log('[DEBUG_FAVICON] No favicon captured for', pinId); + } + } catch (ex) { + console.log('[DEBUG_FAVICON] Favicon capture error for', pinId, ex); + } + }, 100); // Small delay to let tab finish loading + } + async getFaviconAsBase64(pageUrl) { try { const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl); - if (!faviconData) { - // empty favicon + if (!faviconData || !faviconData.dataURI) { return null; } - return faviconData.dataURI; + + // getFaviconForPage returns an object with dataURI as an nsIURI object + // We need to get the .spec property to extract the data URI string + const dataURI = faviconData.dataURI.spec || null; + + // Validate it's a proper data URI + if (dataURI && typeof dataURI === 'string' && dataURI.startsWith('data:image/')) { + console.log('[DEBUG_FAVICON] getFaviconAsBase64: Success, length:', dataURI.length); + return dataURI; + } + + console.log('[DEBUG_FAVICON] getFaviconAsBase64: Invalid data URI format'); + return null; + } catch (ex) { + console.log('[DEBUG_FAVICON] getFaviconAsBase64: Places API failed:', ex.message); + return null; + } + } + + // DEBUG_FAVICON: Fetch favicon directly from network as fallback + async fetchFaviconFromNetwork(pageUrl) { + try { + // Skip network fetch for special URLs that don't have real favicons + if (pageUrl.startsWith('about:') || pageUrl.startsWith('chrome:') || pageUrl.startsWith('moz-extension:')) { + console.log('[DEBUG_FAVICON] Skipping network fetch for special URL:', pageUrl); + return null; + } + + const uri = Services.io.newURI(pageUrl); + const faviconUrl = uri.prePath + '/favicon.ico'; + + console.log('[DEBUG_FAVICON] Fetching favicon from network:', faviconUrl); + + const response = await fetch(faviconUrl, { + method: 'GET', + credentials: 'omit', + }); + + if (!response.ok) { + console.log('[DEBUG_FAVICON] Network fetch failed:', response.status, faviconUrl); + return null; + } + + const blob = await response.blob(); + if (!blob.type.startsWith('image/')) { + console.log('[DEBUG_FAVICON] Network fetch returned non-image:', blob.type); + return null; + } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const dataURI = reader.result; + console.log('[DEBUG_FAVICON] Network fetch successful, converted to data URI'); + resolve(dataURI); + }; + reader.onerror = () => { + console.log('[DEBUG_FAVICON] Failed to convert blob to data URI'); + reject(new Error('Failed to convert blob to data URI')); + }; + reader.readAsDataURL(blob); + }); } catch (ex) { - console.error('Failed to get favicon:', ex); + console.log('[DEBUG_FAVICON] Network fetch error:', ex); return null; } } @@ -1020,9 +1327,24 @@ if (tab.pinned && tab.hasAttribute('zen-pin-id')) { const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); if (pin) { + const pinId = tab.getAttribute('zen-pin-id'); pin.isEssential = true; pin.workspaceUuid = null; + + // Get favicon from tab if available + const tabFavicon = gBrowser.getIcon(tab); + if (tabFavicon && tabFavicon.startsWith('data:image/')) { + pin.iconUrl = tabFavicon; + console.log('[DEBUG_FAVICON] Essential tab has immediate favicon', pinId, tabFavicon.length); + } + this.savePin(pin); + + // If no favicon yet, capture it async + if (!pin.iconUrl) { + console.log('[DEBUG_FAVICON] Starting async favicon capture for essential tab', pinId); + this.captureFaviconForTab(tab, pinId); + } } gBrowser.zenHandleTabMove(tab, () => { if (tab.ownerGlobal !== window) { @@ -1271,6 +1593,15 @@ if (!pin) { return; } + + // DEBUG_FAVICON: Trigger migration if pin doesn't have favicon and we haven't tried yet + if (!pin.iconUrl && pin.url && !this._migrationAttempted?.has(pin.uuid)) { + console.log('[DEBUG_FAVICON] Tab accessed without favicon, triggering migration', pin.uuid); + this.#migratePinFavicon(pin); + } else if (!pin.iconUrl && pin.url && this._migrationAttempted?.has(pin.uuid)) { + console.log('[DEBUG_FAVICON] Migration already attempted for pin, skipping', pin.uuid); + } + // Remove # and ? from the URL const pinUrl = pin.url.split('#')[0]; const currentUrl = browser.currentURI.spec.split('#')[0]; diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs index bc11213f77..5c3b0a9300 100644 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs @@ -41,6 +41,7 @@ var ZenPinnedTabsStorage = { await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); + await addColumnIfNotExists('icon_data', 'TEXT DEFAULT NULL'); await db.execute(` CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) @@ -116,17 +117,40 @@ var ZenPinnedTabsStorage = { } // Insert or replace the pin + // Map iconUrl to iconData for storage + const iconData = pin.iconUrl !== undefined ? pin.iconUrl : (pin.iconData !== undefined ? pin.iconData : null); + + // Ensure iconData is a valid data URI string, otherwise null + // Note: typeof null === "object" in JavaScript, so we check for null explicitly + let iconDataString = null; + if (iconData !== null && iconData !== undefined) { + if (typeof iconData === 'string' && iconData.trim().length > 0 && iconData.startsWith('data:image/')) { + iconDataString = iconData; + } + } + + // Debug log: Log icon_data before insert + console.log('[ZenPinnedTabsStorage.savePin] INSERT/REPLACE pin:', { + uuid: pin.uuid, + title: pin.title, + url: pin.url, + iconData_received: iconData === null ? 'null' : (iconData === undefined ? 'undefined' : typeof iconData), + iconDataString_to_store: iconDataString === null ? 'null' : 'valid_string', + icon_data_length: iconDataString ? iconDataString.length : 0, + icon_data_preview: iconDataString ? `${iconDataString.substring(0, 50)}...` : null, + }); + await db.executeCached( ` INSERT OR REPLACE INTO zen_pins ( uuid, title, url, container_id, workspace_uuid, position, is_essential, is_group, folder_parent_uuid, edited_title, created_at, - updated_at, is_folder_collapsed, folder_icon + updated_at, is_folder_collapsed, folder_icon, icon_data ) VALUES ( :uuid, :title, :url, :container_id, :workspace_uuid, :position, :is_essential, :is_group, :folder_parent_uuid, :edited_title, COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), - :now, :is_folder_collapsed, :folder_icon + :now, :is_folder_collapsed, :folder_icon, :icon_data ) `, { @@ -143,8 +167,24 @@ var ZenPinnedTabsStorage = { now, folder_icon: pin.folderIcon || null, is_folder_collapsed: pin.isFolderCollapsed || false, + icon_data: iconDataString, } ); + + // Debug log: Verify what was actually stored + const verifyResult = await db.execute( + `SELECT icon_data FROM zen_pins WHERE uuid = :uuid`, + { uuid: pin.uuid } + ); + if (verifyResult.length > 0) { + const storedIconData = verifyResult[0].getResultByName('icon_data'); + console.log('[ZenPinnedTabsStorage.savePin] Verified stored icon_data:', { + uuid: pin.uuid, + stored_value: storedIconData === null ? 'null' : (typeof storedIconData === 'string' ? 'string' : typeof storedIconData), + stored_length: storedIconData ? storedIconData.length : 0, + stored_preview: storedIconData && typeof storedIconData === 'string' ? `${storedIconData.substring(0, 50)}...` : null, + }); + } await db.execute( ` @@ -173,20 +213,33 @@ var ZenPinnedTabsStorage = { SELECT * FROM zen_pins ORDER BY position ASC `); - 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('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - })); + const pins = rows.map((row) => { + const iconData = row.getResultByName('icon_data'); + // DEBUG_FAVICON: Log what we're retrieving from database + console.log('[DEBUG_FAVICON] getPins() retrieved:', { + uuid: row.getResultByName('uuid'), + icon_data_type: typeof iconData, + icon_data_is_null: iconData === null, + icon_data_length: iconData?.length || 0, + icon_data_preview: iconData ? `${iconData.substring(0, 50)}...` : null + }); + return { + 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('folder_parent_uuid'), + editedTitle: Boolean(row.getResultByName('edited_title')), + folderIcon: row.getResultByName('folder_icon'), + isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), + iconUrl: iconData, + }; + }); + return pins; }, /** From 46472452b18817714b59c5db954c9a6104c279f7 Mon Sep 17 00:00:00 2001 From: Vedaant Date: Tue, 18 Nov 2025 20:38:52 -0800 Subject: [PATCH 2/4] refactor(tabs): remove debug favicon logging Removes verbose debug logging around pinned tab favicon handling Uses stored data URIs directly for original icon styling --- src/zen/tabs/ZenPinnedTabManager.mjs | 159 ++------------------------ src/zen/tabs/ZenPinnedTabsStorage.mjs | 70 +++--------- 2 files changed, 26 insertions(+), 203 deletions(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 12a6b1c0a8..28a438fe8c 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -90,7 +90,6 @@ this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this)); this._zenClickEventListener = this._onTabClick.bind(this); - // DEBUG_FAVICON: Track pins we've attempted to migrate to avoid duplicate attempts this._migrationAttempted = new Set(); gZenWorkspaces._resolvePinnedInitialized(); @@ -103,11 +102,9 @@ } async onTabIconChanged(tab, url = null) { - // Get the favicon URL from the browser (already computed by Firefox) const iconUrl = url ?? gBrowser.getIcon(tab); if (!iconUrl && tab.hasAttribute('zen-pin-id')) { - // No favicon yet, wait and try Places API try { setTimeout(async () => { const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); @@ -116,7 +113,6 @@ const pinId = tab.getAttribute('zen-pin-id'); const pin = this._pinsCache?.find((p) => p.uuid === pinId); if (pin) { - console.log('[DEBUG_FAVICON] Saving favicon to pin storage from onTabIconChanged', pinId); pin.iconUrl = favicon; await this.savePin(pin, false); } @@ -129,16 +125,12 @@ if (tab.hasAttribute('zen-essential')) { tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } - // Save favicon if it's a data URI if (iconUrl && tab.hasAttribute('zen-pin-id')) { const pinId = tab.getAttribute('zen-pin-id'); const pin = this._pinsCache?.find((p) => p.uuid === pinId); if (pin && iconUrl.startsWith('data:image/')) { - console.log('[DEBUG_FAVICON] Saving favicon to pin storage from onTabIconChanged (data URI)', pinId); pin.iconUrl = iconUrl; - this.savePin(pin, false).catch((ex) => { - console.log('[DEBUG_FAVICON] Failed to save favicon in onTabIconChanged:', ex); - }); + this.savePin(pin, false).catch(console.error); } } } @@ -191,68 +183,37 @@ async #initializePinsCache() { try { - // Get pin data const pins = await ZenPinnedTabsStorage.getPins(); - // DEBUG_FAVICON: Inspect what we got from database - console.log('[DEBUG_FAVICON] Raw pins from database:', pins.map(p => ({ - uuid: p.uuid, - hasIconUrl: !!p.iconUrl, - iconUrlType: typeof p.iconUrl, - iconUrlLength: p.iconUrl?.length || 0, - iconUrlPreview: p.iconUrl ? `${p.iconUrl.substring(0, 50)}...` : null - }))); - - // DEBUG_FAVICON: Track pins with/without stored icons (check for truthy AND non-empty) - const pinsWithIcons = pins.filter(p => p.iconUrl && p.iconUrl.trim().length > 0).length; - console.log('[DEBUG_FAVICON] Initializing pins cache:', { - totalPins: pins.length, - pinsWithStoredIcons: pinsWithIcons, - pinsNeedingMigration: pins.length - pinsWithIcons - }); - - // Enhance pins with favicons - use stored iconData first, fall back to Places, then lazy migration this._pinsCache = await Promise.all( pins.map(async (pin) => { try { if (pin.isGroup) { - return pin; // Skip groups for now + return pin; } - // DEBUG_FAVICON: Use stored iconData first (check for truthy AND non-empty) if (pin.iconUrl && pin.iconUrl.trim().length > 0) { - console.log('[DEBUG_FAVICON] Using stored iconData for pin', pin.uuid, 'length:', pin.iconUrl.length); return { ...pin, iconUrl: pin.iconUrl, }; } - // DEBUG_FAVICON: Fall back to Places API - console.log('[DEBUG_FAVICON] No stored iconData, trying Places API for pin', pin.uuid); const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); if (image) { - console.log('[DEBUG_FAVICON] Places API returned favicon for pin', pin.uuid); - // Save it to storage for future use pin.iconUrl = image; - this.savePin(pin, false).catch((ex) => { - console.log('[DEBUG_FAVICON] Failed to save favicon from Places API:', ex); - }); + this.savePin(pin, false).catch(console.error); return { ...pin, iconUrl: image, }; } - // DEBUG_FAVICON: No icon found, will be migrated lazily - console.log('[DEBUG_FAVICON] No favicon found for pin, will migrate lazily', pin.uuid); return { ...pin, iconUrl: null, }; } catch (ex) { - // DEBUG_FAVICON: If favicon fetch fails, continue without icon - console.log('[DEBUG_FAVICON] Error loading favicon for pin', pin.uuid, ex); return { ...pin, iconUrl: null, @@ -261,7 +222,6 @@ }) ); - // DEBUG_FAVICON: Schedule lazy migration for pins without icons this.#scheduleLazyMigration(); } catch (ex) { console.error('Failed to initialize pins cache:', ex); @@ -281,22 +241,17 @@ this.hasInitializedPins = true; } - // DEBUG_FAVICON: Schedule lazy migration for pins without favicons #scheduleLazyMigration() { if (this._migrationScheduled) { return; } this._migrationScheduled = true; - // Wait a bit after initialization before starting migration setTimeout(() => { this.#migrateMissingFavicons(); }, 2000); - - console.log('[DEBUG_FAVICON] Lazy migration scheduled'); } - // DEBUG_FAVICON: Migrate missing favicons with priority for essential tabs async #migrateMissingFavicons() { if (!this._pinsCache) { return; @@ -307,82 +262,54 @@ ); if (pinsNeedingMigration.length === 0) { - console.log('[DEBUG_FAVICON] No pins need favicon migration'); return; } - // Prioritize essential tabs const essentialPins = pinsNeedingMigration.filter((pin) => pin.isEssential); const regularPins = pinsNeedingMigration.filter((pin) => !pin.isEssential); - console.log('[DEBUG_FAVICON] Starting favicon migration:', { - total: pinsNeedingMigration.length, - essential: essentialPins.length, - regular: regularPins.length, - }); - - // Migrate essential tabs first for (const pin of essentialPins) { await this.#migratePinFavicon(pin); - // Small delay between migrations to avoid overwhelming the system await new Promise((resolve) => setTimeout(resolve, 100)); } - // Then migrate regular pins for (const pin of regularPins) { await this.#migratePinFavicon(pin); - // Small delay between migrations await new Promise((resolve) => setTimeout(resolve, 100)); } - - console.log('[DEBUG_FAVICON] Favicon migration completed'); } - // DEBUG_FAVICON: Helper method to migrate a single pin's favicon async #migratePinFavicon(pin) { - // Skip if already has favicon or already attempted if (pin.iconUrl && pin.iconUrl.trim().length > 0) { - console.log('[DEBUG_FAVICON] Pin already has favicon, skipping migration', pin.uuid); return; } if (this._migrationAttempted?.has(pin.uuid)) { - console.log('[DEBUG_FAVICON] Migration already attempted for pin, skipping', pin.uuid); return; } - // Mark as attempted immediately to prevent duplicate attempts this._migrationAttempted?.add(pin.uuid); try { - console.log('[DEBUG_FAVICON] Migrating favicon for pin', pin.uuid, pin.url); - - // Try Places API first let favicon = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - // If Places API fails, try network fetch if (!favicon) { - console.log('[DEBUG_FAVICON] Places API failed, trying network fetch for pin', pin.uuid); favicon = await this.fetchFaviconFromNetwork(pin.url); } if (favicon) { - console.log('[DEBUG_FAVICON] Favicon migration successful for pin', pin.uuid); pin.iconUrl = favicon; await this.savePin(pin, false); - // Update the tab if it exists const tab = gBrowser.tabs.find( (t) => t.getAttribute('zen-pin-id') === pin.uuid ); if (tab) { gBrowser.setIcon(tab, favicon); } - } else { - console.log('[DEBUG_FAVICON] Favicon migration failed for pin', pin.uuid); } } catch (ex) { - console.log('[DEBUG_FAVICON] Error migrating favicon for pin', pin.uuid, ex); + console.error('Error migrating favicon for pin', pin.uuid, ex); } } @@ -892,17 +819,8 @@ entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); } - // Get favicon from tab (browser already has it loaded) const tabFavicon = gBrowser.getIcon(tab); const initialIconUrl = tabFavicon && tabFavicon.startsWith('data:image/') ? tabFavicon : null; - - console.log('[DEBUG_FAVICON] Creating new pin', { - uuid, - url: browser.currentURI.spec, - hasTabFavicon: !!tabFavicon, - isDataURI: tabFavicon?.startsWith('data:image/') || false, - willCaptureAsync: !initialIconUrl - }); await this.savePin({ uuid, @@ -913,15 +831,11 @@ isEssential: tab.getAttribute('zen-essential') === 'true', parentUuid: tab.group?.getAttribute('zen-pin-id') || null, position: tab._pPos, - iconUrl: initialIconUrl, // Save immediately if we have it + iconUrl: initialIconUrl, }); - // If we didn't get a data URI, try to capture it async if (!initialIconUrl) { - console.log('[DEBUG_FAVICON] Starting async favicon capture for new pin', uuid); this.captureFaviconForTab(tab, uuid); - } else { - console.log('[DEBUG_FAVICON] Saved pin with immediate favicon', uuid, initialIconUrl.length); } tab.setAttribute('zen-pin-id', uuid); @@ -1170,60 +1084,40 @@ existingEntry.title = pin.title; state.entries = [existingEntry]; } - // DEBUG_FAVICON: Prefer stored iconData over iconUrl state.image = pin.iconUrl || state.image; state.index = 0; - // DEBUG_FAVICON: Log which icon source was used - if (pin.iconUrl) { - console.log('[DEBUG_FAVICON] Using stored iconData for tab reset', pin.uuid); - } else { - console.log('[DEBUG_FAVICON] No stored iconData, using existing tab state image', pin.uuid); - } - SessionStore.setTabState(tab, state); this.resetPinChangedUrl(tab); } - // Capture favicon for a tab asynchronously captureFaviconForTab(tab, pinId) { setTimeout(async () => { try { - // First, check if the tab's favicon loaded (might be a chrome:// URL or other non-data URI) const tabFavicon = gBrowser.getIcon(tab); let faviconDataURI = null; if (tabFavicon && tabFavicon.startsWith('data:image/')) { - // Tab now has a data URI favicon faviconDataURI = tabFavicon; - console.log('[DEBUG_FAVICON] Tab favicon loaded as data URI', pinId, tabFavicon.length); } else { - // Try Places API - console.log('[DEBUG_FAVICON] Trying Places API for', pinId); faviconDataURI = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - // If Places API failed, try network fetch if (!faviconDataURI) { - console.log('[DEBUG_FAVICON] Places API failed, trying network fetch for', pinId); faviconDataURI = await this.fetchFaviconFromNetwork(tab.linkedBrowser.currentURI.spec); } } if (faviconDataURI) { - console.log('[DEBUG_FAVICON] Favicon captured successfully for', pinId, faviconDataURI.length); const pin = this._pinsCache?.find(p => p.uuid === pinId); if (pin) { pin.iconUrl = faviconDataURI; await this.savePin(pin, false); - console.log('[DEBUG_FAVICON] Favicon saved to database for', pinId); } - } else { - console.log('[DEBUG_FAVICON] No favicon captured for', pinId); } } catch (ex) { - console.log('[DEBUG_FAVICON] Favicon capture error for', pinId, ex); + console.error('Favicon capture error for', pinId, ex); } - }, 100); // Small delay to let tab finish loading + }, 100); } async getFaviconAsBase64(pageUrl) { @@ -1233,69 +1127,48 @@ return null; } - // getFaviconForPage returns an object with dataURI as an nsIURI object - // We need to get the .spec property to extract the data URI string const dataURI = faviconData.dataURI.spec || null; - // Validate it's a proper data URI if (dataURI && typeof dataURI === 'string' && dataURI.startsWith('data:image/')) { - console.log('[DEBUG_FAVICON] getFaviconAsBase64: Success, length:', dataURI.length); return dataURI; } - console.log('[DEBUG_FAVICON] getFaviconAsBase64: Invalid data URI format'); return null; } catch (ex) { - console.log('[DEBUG_FAVICON] getFaviconAsBase64: Places API failed:', ex.message); return null; } } - // DEBUG_FAVICON: Fetch favicon directly from network as fallback async fetchFaviconFromNetwork(pageUrl) { try { - // Skip network fetch for special URLs that don't have real favicons if (pageUrl.startsWith('about:') || pageUrl.startsWith('chrome:') || pageUrl.startsWith('moz-extension:')) { - console.log('[DEBUG_FAVICON] Skipping network fetch for special URL:', pageUrl); return null; } const uri = Services.io.newURI(pageUrl); const faviconUrl = uri.prePath + '/favicon.ico'; - console.log('[DEBUG_FAVICON] Fetching favicon from network:', faviconUrl); - const response = await fetch(faviconUrl, { method: 'GET', credentials: 'omit', }); if (!response.ok) { - console.log('[DEBUG_FAVICON] Network fetch failed:', response.status, faviconUrl); return null; } const blob = await response.blob(); if (!blob.type.startsWith('image/')) { - console.log('[DEBUG_FAVICON] Network fetch returned non-image:', blob.type); return null; } return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onloadend = () => { - const dataURI = reader.result; - console.log('[DEBUG_FAVICON] Network fetch successful, converted to data URI'); - resolve(dataURI); - }; - reader.onerror = () => { - console.log('[DEBUG_FAVICON] Failed to convert blob to data URI'); - reject(new Error('Failed to convert blob to data URI')); - }; + reader.onloadend = () => resolve(reader.result); + reader.onerror = () => reject(new Error('Failed to convert blob to data URI')); reader.readAsDataURL(blob); }); } catch (ex) { - console.log('[DEBUG_FAVICON] Network fetch error:', ex); return null; } } @@ -1331,18 +1204,14 @@ pin.isEssential = true; pin.workspaceUuid = null; - // Get favicon from tab if available const tabFavicon = gBrowser.getIcon(tab); if (tabFavicon && tabFavicon.startsWith('data:image/')) { pin.iconUrl = tabFavicon; - console.log('[DEBUG_FAVICON] Essential tab has immediate favicon', pinId, tabFavicon.length); } - this.savePin(pin); + this.savePin(pin).catch(console.error); - // If no favicon yet, capture it async if (!pin.iconUrl) { - console.log('[DEBUG_FAVICON] Starting async favicon capture for essential tab', pinId); this.captureFaviconForTab(tab, pinId); } } @@ -1594,18 +1463,12 @@ return; } - // DEBUG_FAVICON: Trigger migration if pin doesn't have favicon and we haven't tried yet if (!pin.iconUrl && pin.url && !this._migrationAttempted?.has(pin.uuid)) { - console.log('[DEBUG_FAVICON] Tab accessed without favicon, triggering migration', pin.uuid); this.#migratePinFavicon(pin); - } else if (!pin.iconUrl && pin.url && this._migrationAttempted?.has(pin.uuid)) { - console.log('[DEBUG_FAVICON] Migration already attempted for pin, skipping', pin.uuid); } - // Remove # and ? from the URL const pinUrl = pin.url.split('#')[0]; const currentUrl = browser.currentURI.spec.split('#')[0]; - // Add an indicator that the pin has been changed if (pinUrl === currentUrl) { this.resetPinChangedUrl(tab); return; @@ -1631,7 +1494,7 @@ } else { tab.setAttribute('zen-pinned-changed', 'true'); } - tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`); + tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl})`); } removeTabContainersDragoverClass(hideIndicator = true) { diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs index 5c3b0a9300..0771261d78 100644 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs @@ -117,11 +117,8 @@ var ZenPinnedTabsStorage = { } // Insert or replace the pin - // Map iconUrl to iconData for storage const iconData = pin.iconUrl !== undefined ? pin.iconUrl : (pin.iconData !== undefined ? pin.iconData : null); - // Ensure iconData is a valid data URI string, otherwise null - // Note: typeof null === "object" in JavaScript, so we check for null explicitly let iconDataString = null; if (iconData !== null && iconData !== undefined) { if (typeof iconData === 'string' && iconData.trim().length > 0 && iconData.startsWith('data:image/')) { @@ -129,17 +126,6 @@ var ZenPinnedTabsStorage = { } } - // Debug log: Log icon_data before insert - console.log('[ZenPinnedTabsStorage.savePin] INSERT/REPLACE pin:', { - uuid: pin.uuid, - title: pin.title, - url: pin.url, - iconData_received: iconData === null ? 'null' : (iconData === undefined ? 'undefined' : typeof iconData), - iconDataString_to_store: iconDataString === null ? 'null' : 'valid_string', - icon_data_length: iconDataString ? iconDataString.length : 0, - icon_data_preview: iconDataString ? `${iconDataString.substring(0, 50)}...` : null, - }); - await db.executeCached( ` INSERT OR REPLACE INTO zen_pins ( @@ -170,21 +156,6 @@ var ZenPinnedTabsStorage = { icon_data: iconDataString, } ); - - // Debug log: Verify what was actually stored - const verifyResult = await db.execute( - `SELECT icon_data FROM zen_pins WHERE uuid = :uuid`, - { uuid: pin.uuid } - ); - if (verifyResult.length > 0) { - const storedIconData = verifyResult[0].getResultByName('icon_data'); - console.log('[ZenPinnedTabsStorage.savePin] Verified stored icon_data:', { - uuid: pin.uuid, - stored_value: storedIconData === null ? 'null' : (typeof storedIconData === 'string' ? 'string' : typeof storedIconData), - stored_length: storedIconData ? storedIconData.length : 0, - stored_preview: storedIconData && typeof storedIconData === 'string' ? `${storedIconData.substring(0, 50)}...` : null, - }); - } await db.execute( ` @@ -213,32 +184,21 @@ var ZenPinnedTabsStorage = { SELECT * FROM zen_pins ORDER BY position ASC `); - const pins = rows.map((row) => { - const iconData = row.getResultByName('icon_data'); - // DEBUG_FAVICON: Log what we're retrieving from database - console.log('[DEBUG_FAVICON] getPins() retrieved:', { - uuid: row.getResultByName('uuid'), - icon_data_type: typeof iconData, - icon_data_is_null: iconData === null, - icon_data_length: iconData?.length || 0, - icon_data_preview: iconData ? `${iconData.substring(0, 50)}...` : null - }); - return { - 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('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - iconUrl: iconData, - }; - }); + const pins = 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('folder_parent_uuid'), + editedTitle: Boolean(row.getResultByName('edited_title')), + folderIcon: row.getResultByName('folder_icon'), + isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), + iconUrl: row.getResultByName('icon_data'), + })); return pins; }, From 810c54a3b0e01c66b9d8b2ff022eeb0a0ba3f8c2 Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Wed, 19 Nov 2025 12:21:16 -0800 Subject: [PATCH 3/4] refactor(tabs): clean up whitespace and improve code readability --- src/zen/tabs/ZenPinnedTabManager.mjs | 58 ++++++++++++++------------- src/zen/tabs/ZenPinnedTabsStorage.mjs | 17 ++++++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 28a438fe8c..e8f999ea6f 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -103,7 +103,7 @@ async onTabIconChanged(tab, url = null) { const iconUrl = url ?? gBrowser.getIcon(tab); - + if (!iconUrl && tab.hasAttribute('zen-pin-id')) { try { setTimeout(async () => { @@ -191,7 +191,7 @@ if (pin.isGroup) { return pin; } - + if (pin.iconUrl && pin.iconUrl.trim().length > 0) { return { ...pin, @@ -213,7 +213,7 @@ ...pin, iconUrl: null, }; - } catch (ex) { + } catch { return { ...pin, iconUrl: null, @@ -246,7 +246,7 @@ return; } this._migrationScheduled = true; - + setTimeout(() => { this.#migrateMissingFavicons(); }, 2000); @@ -283,7 +283,7 @@ if (pin.iconUrl && pin.iconUrl.trim().length > 0) { return; } - + if (this._migrationAttempted?.has(pin.uuid)) { return; } @@ -292,7 +292,7 @@ try { let favicon = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - + if (!favicon) { favicon = await this.fetchFaviconFromNetwork(pin.url); } @@ -300,10 +300,8 @@ if (favicon) { pin.iconUrl = favicon; await this.savePin(pin, false); - - const tab = gBrowser.tabs.find( - (t) => t.getAttribute('zen-pin-id') === pin.uuid - ); + + const tab = gBrowser.tabs.find((t) => t.getAttribute('zen-pin-id') === pin.uuid); if (tab) { gBrowser.setIcon(tab, favicon); } @@ -1096,19 +1094,21 @@ try { const tabFavicon = gBrowser.getIcon(tab); let faviconDataURI = null; - + if (tabFavicon && tabFavicon.startsWith('data:image/')) { faviconDataURI = tabFavicon; } else { faviconDataURI = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - + if (!faviconDataURI) { - faviconDataURI = await this.fetchFaviconFromNetwork(tab.linkedBrowser.currentURI.spec); + faviconDataURI = await this.fetchFaviconFromNetwork( + tab.linkedBrowser.currentURI.spec + ); } } - + if (faviconDataURI) { - const pin = this._pinsCache?.find(p => p.uuid === pinId); + const pin = this._pinsCache?.find((p) => p.uuid === pinId); if (pin) { pin.iconUrl = faviconDataURI; await this.savePin(pin, false); @@ -1126,28 +1126,32 @@ if (!faviconData || !faviconData.dataURI) { return null; } - + const dataURI = faviconData.dataURI.spec || null; - + if (dataURI && typeof dataURI === 'string' && dataURI.startsWith('data:image/')) { return dataURI; } - + return null; - } catch (ex) { + } catch { return null; } } async fetchFaviconFromNetwork(pageUrl) { try { - if (pageUrl.startsWith('about:') || pageUrl.startsWith('chrome:') || pageUrl.startsWith('moz-extension:')) { + if ( + pageUrl.startsWith('about:') || + pageUrl.startsWith('chrome:') || + pageUrl.startsWith('moz-extension:') + ) { return null; } const uri = Services.io.newURI(pageUrl); const faviconUrl = uri.prePath + '/favicon.ico'; - + const response = await fetch(faviconUrl, { method: 'GET', credentials: 'omit', @@ -1168,7 +1172,7 @@ reader.onerror = () => reject(new Error('Failed to convert blob to data URI')); reader.readAsDataURL(blob); }); - } catch (ex) { + } catch { return null; } } @@ -1203,14 +1207,14 @@ const pinId = tab.getAttribute('zen-pin-id'); pin.isEssential = true; pin.workspaceUuid = null; - + const tabFavicon = gBrowser.getIcon(tab); if (tabFavicon && tabFavicon.startsWith('data:image/')) { pin.iconUrl = tabFavicon; } - + this.savePin(pin).catch(console.error); - + if (!pin.iconUrl) { this.captureFaviconForTab(tab, pinId); } @@ -1462,11 +1466,11 @@ if (!pin) { return; } - + if (!pin.iconUrl && pin.url && !this._migrationAttempted?.has(pin.uuid)) { this.#migratePinFavicon(pin); } - + const pinUrl = pin.url.split('#')[0]; const currentUrl = browser.currentURI.spec.split('#')[0]; if (pinUrl === currentUrl) { diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs index 0771261d78..48aaa1241a 100644 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs @@ -117,15 +117,24 @@ var ZenPinnedTabsStorage = { } // Insert or replace the pin - const iconData = pin.iconUrl !== undefined ? pin.iconUrl : (pin.iconData !== undefined ? pin.iconData : null); - + const iconData = + pin.iconUrl !== undefined + ? pin.iconUrl + : pin.iconData !== undefined + ? pin.iconData + : null; + let iconDataString = null; if (iconData !== null && iconData !== undefined) { - if (typeof iconData === 'string' && iconData.trim().length > 0 && iconData.startsWith('data:image/')) { + if ( + typeof iconData === 'string' && + iconData.trim().length > 0 && + iconData.startsWith('data:image/') + ) { iconDataString = iconData; } } - + await db.executeCached( ` INSERT OR REPLACE INTO zen_pins ( From 099cd8095a983f620a98573eacb9e06840ed90ae Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Fri, 21 Nov 2025 16:46:14 -0800 Subject: [PATCH 4/4] style(zen:pinned-tabs): fix indentation and formatting --- src/zen/tabs/ZenPinnedTabManager.mjs | 612 +++++++++++++-------------- 1 file changed, 305 insertions(+), 307 deletions(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index df37098f94..3a4a52b55c 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -91,8 +91,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { this._insertItemsIntoTabContextMenu(); this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this)); - this._zenClickEventListener = this._onTabClick.bind(this); - this._migrationAttempted = new Set(); + this._zenClickEventListener = this._onTabClick.bind(this); + this._migrationAttempted = new Set(); gZenWorkspaces._resolvePinnedInitialized(); } @@ -103,40 +103,40 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async onTabIconChanged(tab, url = null) { - const iconUrl = url ?? gBrowser.getIcon(tab); - - if (!iconUrl && tab.hasAttribute('zen-pin-id')) { - try { - setTimeout(async () => { - const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - if (favicon) { - gBrowser.setIcon(tab, favicon); - const pinId = tab.getAttribute('zen-pin-id'); - const pin = this._pinsCache?.find((p) => p.uuid === pinId); - if (pin) { - pin.iconUrl = favicon; - await this.savePin(pin, false); - } + async onTabIconChanged(tab, url = null) { + const iconUrl = url ?? gBrowser.getIcon(tab); + + if (!iconUrl && tab.hasAttribute('zen-pin-id')) { + try { + setTimeout(async () => { + const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); + if (favicon) { + gBrowser.setIcon(tab, favicon); + const pinId = tab.getAttribute('zen-pin-id'); + const pin = this._pinsCache?.find((p) => p.uuid === pinId); + if (pin) { + pin.iconUrl = favicon; + await this.savePin(pin, false); } - }); - } catch { - // Handle error - } - } else { - if (tab.hasAttribute('zen-essential')) { - tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); - } - if (iconUrl && tab.hasAttribute('zen-pin-id')) { - const pinId = tab.getAttribute('zen-pin-id'); - const pin = this._pinsCache?.find((p) => p.uuid === pinId); - if (pin && iconUrl.startsWith('data:image/')) { - pin.iconUrl = iconUrl; - this.savePin(pin, false).catch(console.error); } + }); + } catch { + // Handle error + } + } else { + if (tab.hasAttribute('zen-essential')) { + tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); + } + if (iconUrl && tab.hasAttribute('zen-pin-id')) { + const pinId = tab.getAttribute('zen-pin-id'); + const pin = this._pinsCache?.find((p) => p.uuid === pinId); + if (pin && iconUrl.startsWith('data:image/')) { + pin.iconUrl = iconUrl; + this.savePin(pin, false).catch(console.error); } } } + } _onTabResetPinButton(event, tab) { event.stopPropagation(); @@ -188,131 +188,131 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { // Get pin data const pins = await ZenPinnedTabsStorage.getPins(); - this._pinsCache = await Promise.all( - pins.map(async (pin) => { - try { - if (pin.isGroup) { - return pin; - } - - if (pin.iconUrl && pin.iconUrl.trim().length > 0) { - return { - ...pin, - iconUrl: pin.iconUrl, - }; - } - - const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - if (image) { - pin.iconUrl = image; - this.savePin(pin, false).catch(console.error); - return { - ...pin, - iconUrl: image, - }; - } + this._pinsCache = await Promise.all( + pins.map(async (pin) => { + try { + if (pin.isGroup) { + return pin; + } + if (pin.iconUrl && pin.iconUrl.trim().length > 0) { return { ...pin, - iconUrl: null, + iconUrl: pin.iconUrl, }; - } catch { + } + + const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); + if (image) { + pin.iconUrl = image; + this.savePin(pin, false).catch(console.error); return { ...pin, - iconUrl: null, + iconUrl: image, }; } - }) - ); - this.#scheduleLazyMigration(); - } catch (ex) { - console.error('Failed to initialize pins cache:', ex); - this._pinsCache = []; - } + return { + ...pin, + iconUrl: null, + }; + } catch { + return { + ...pin, + iconUrl: null, + }; + } + }) + ); + + this.#scheduleLazyMigration(); + } catch (ex) { + console.error('Failed to initialize pins cache:', ex); + this._pinsCache = []; + } this.log(`Initialized pins cache with ${this._pinsCache.length} pins`); return this._pinsCache; } - #finishedInitializingPins() { - if (this.hasInitializedPins) { - return; - } - this._resolvePinnedInitializedInternal(); - delete this._resolvePinnedInitializedInternal; - this.hasInitializedPins = true; + #finishedInitializingPins() { + if (this.hasInitializedPins) { + return; } + this._resolvePinnedInitializedInternal(); + delete this._resolvePinnedInitializedInternal; + this.hasInitializedPins = true; + } - #scheduleLazyMigration() { - if (this._migrationScheduled) { - return; - } - this._migrationScheduled = true; - - setTimeout(() => { - this.#migrateMissingFavicons(); - }, 2000); + #scheduleLazyMigration() { + if (this._migrationScheduled) { + return; } + this._migrationScheduled = true; - async #migrateMissingFavicons() { - if (!this._pinsCache) { - return; - } + setTimeout(() => { + this.#migrateMissingFavicons(); + }, 2000); + } - const pinsNeedingMigration = this._pinsCache.filter( - (pin) => !pin.isGroup && !pin.iconUrl && pin.url - ); + async #migrateMissingFavicons() { + if (!this._pinsCache) { + return; + } - if (pinsNeedingMigration.length === 0) { - return; - } + const pinsNeedingMigration = this._pinsCache.filter( + (pin) => !pin.isGroup && !pin.iconUrl && pin.url + ); - const essentialPins = pinsNeedingMigration.filter((pin) => pin.isEssential); - const regularPins = pinsNeedingMigration.filter((pin) => !pin.isEssential); + if (pinsNeedingMigration.length === 0) { + return; + } - for (const pin of essentialPins) { - await this.#migratePinFavicon(pin); - await new Promise((resolve) => setTimeout(resolve, 100)); - } + const essentialPins = pinsNeedingMigration.filter((pin) => pin.isEssential); + const regularPins = pinsNeedingMigration.filter((pin) => !pin.isEssential); - for (const pin of regularPins) { - await this.#migratePinFavicon(pin); - await new Promise((resolve) => setTimeout(resolve, 100)); - } + for (const pin of essentialPins) { + await this.#migratePinFavicon(pin); + await new Promise((resolve) => setTimeout(resolve, 100)); } - async #migratePinFavicon(pin) { - if (pin.iconUrl && pin.iconUrl.trim().length > 0) { - return; - } + for (const pin of regularPins) { + await this.#migratePinFavicon(pin); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } - if (this._migrationAttempted?.has(pin.uuid)) { - return; - } + async #migratePinFavicon(pin) { + if (pin.iconUrl && pin.iconUrl.trim().length > 0) { + return; + } - this._migrationAttempted?.add(pin.uuid); + if (this._migrationAttempted?.has(pin.uuid)) { + return; + } - try { - let favicon = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); + this._migrationAttempted?.add(pin.uuid); - if (!favicon) { - favicon = await this.fetchFaviconFromNetwork(pin.url); - } + try { + let favicon = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - if (favicon) { - pin.iconUrl = favicon; - await this.savePin(pin, false); + if (!favicon) { + favicon = await this.fetchFaviconFromNetwork(pin.url); + } - const tab = gBrowser.tabs.find((t) => t.getAttribute('zen-pin-id') === pin.uuid); - if (tab) { - gBrowser.setIcon(tab, favicon); - } + if (favicon) { + pin.iconUrl = favicon; + await this.savePin(pin, false); + + const tab = gBrowser.tabs.find((t) => t.getAttribute('zen-pin-id') === pin.uuid); + if (tab) { + gBrowser.setIcon(tab, favicon); } - } catch (ex) { - console.error('Error migrating favicon for pin', pin.uuid, ex); } + } catch (ex) { + console.error('Error migrating favicon for pin', pin.uuid, ex); } + } async #initializePinnedTabs(init = false) { const pins = this._pinsCache; @@ -813,28 +813,28 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { let entry = null; - if (tab.getAttribute('zen-pinned-entry')) { - entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); - } - - const tabFavicon = gBrowser.getIcon(tab); - const initialIconUrl = tabFavicon && tabFavicon.startsWith('data:image/') ? tabFavicon : null; - - await this.savePin({ - uuid, - title: entry?.title || tab.label || browser.contentTitle, - url: entry?.url || browser.currentURI.spec, - containerTabId: userContextId ? parseInt(userContextId, 10) : 0, - workspaceUuid: tab.getAttribute('zen-workspace-id'), - isEssential: tab.getAttribute('zen-essential') === 'true', - parentUuid: tab.group?.getAttribute('zen-pin-id') || null, - position: tab._pPos, - iconUrl: initialIconUrl, - }); + if (tab.getAttribute('zen-pinned-entry')) { + entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); + } + + const tabFavicon = gBrowser.getIcon(tab); + const initialIconUrl = tabFavicon && tabFavicon.startsWith('data:image/') ? tabFavicon : null; + + await this.savePin({ + uuid, + title: entry?.title || tab.label || browser.contentTitle, + url: entry?.url || browser.currentURI.spec, + containerTabId: userContextId ? parseInt(userContextId, 10) : 0, + workspaceUuid: tab.getAttribute('zen-workspace-id'), + isEssential: tab.getAttribute('zen-essential') === 'true', + parentUuid: tab.group?.getAttribute('zen-pin-id') || null, + position: tab._pPos, + iconUrl: initialIconUrl, + }); - if (!initialIconUrl) { - this.captureFaviconForTab(tab, uuid); - } + if (!initialIconUrl) { + this.captureFaviconForTab(tab, uuid); + } tab.setAttribute('zen-pin-id', uuid); tab.dispatchEvent( @@ -1085,170 +1085,168 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { state.image = pin.iconUrl || state.image; state.index = 0; - SessionStore.setTabState(tab, state); - this.resetPinChangedUrl(tab); - } - - captureFaviconForTab(tab, pinId) { - setTimeout(async () => { - try { - const tabFavicon = gBrowser.getIcon(tab); - let faviconDataURI = null; + SessionStore.setTabState(tab, state); + this.resetPinChangedUrl(tab); + } - if (tabFavicon && tabFavicon.startsWith('data:image/')) { - faviconDataURI = tabFavicon; - } else { - faviconDataURI = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); + captureFaviconForTab(tab, pinId) { + setTimeout(async () => { + try { + const tabFavicon = gBrowser.getIcon(tab); + let faviconDataURI = null; - if (!faviconDataURI) { - faviconDataURI = await this.fetchFaviconFromNetwork( - tab.linkedBrowser.currentURI.spec - ); - } - } + if (tabFavicon && tabFavicon.startsWith('data:image/')) { + faviconDataURI = tabFavicon; + } else { + faviconDataURI = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - if (faviconDataURI) { - const pin = this._pinsCache?.find((p) => p.uuid === pinId); - if (pin) { - pin.iconUrl = faviconDataURI; - await this.savePin(pin, false); - } + if (!faviconDataURI) { + faviconDataURI = await this.fetchFaviconFromNetwork(tab.linkedBrowser.currentURI.spec); } - } catch (ex) { - console.error('Favicon capture error for', pinId, ex); } - }, 100); - } - async getFaviconAsBase64(pageUrl) { - try { - const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl); - if (!faviconData || !faviconData.dataURI) { - return null; + if (faviconDataURI) { + const pin = this._pinsCache?.find((p) => p.uuid === pinId); + if (pin) { + pin.iconUrl = faviconDataURI; + await this.savePin(pin, false); + } } + } catch (ex) { + console.error('Favicon capture error for', pinId, ex); + } + }, 100); + } - const dataURI = faviconData.dataURI.spec || null; + async getFaviconAsBase64(pageUrl) { + try { + const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl); + if (!faviconData || !faviconData.dataURI) { + return null; + } - if (dataURI && typeof dataURI === 'string' && dataURI.startsWith('data:image/')) { - return dataURI; - } + const dataURI = faviconData.dataURI.spec || null; - return null; - } catch { - return null; + if (dataURI && typeof dataURI === 'string' && dataURI.startsWith('data:image/')) { + return dataURI; } - } - async fetchFaviconFromNetwork(pageUrl) { - try { - if ( - pageUrl.startsWith('about:') || - pageUrl.startsWith('chrome:') || - pageUrl.startsWith('moz-extension:') - ) { - return null; - } + return null; + } catch { + return null; + } + } - const uri = Services.io.newURI(pageUrl); - const faviconUrl = uri.prePath + '/favicon.ico'; + async fetchFaviconFromNetwork(pageUrl) { + try { + if ( + pageUrl.startsWith('about:') || + pageUrl.startsWith('chrome:') || + pageUrl.startsWith('moz-extension:') + ) { + return null; + } - const response = await fetch(faviconUrl, { - method: 'GET', - credentials: 'omit', - }); + const uri = Services.io.newURI(pageUrl); + const faviconUrl = uri.prePath + '/favicon.ico'; - if (!response.ok) { - return null; - } + const response = await fetch(faviconUrl, { + method: 'GET', + credentials: 'omit', + }); - const blob = await response.blob(); - if (!blob.type.startsWith('image/')) { - return null; - } + if (!response.ok) { + return null; + } - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.onerror = () => reject(new Error('Failed to convert blob to data URI')); - reader.readAsDataURL(blob); - }); - } catch { + const blob = await response.blob(); + if (!blob.type.startsWith('image/')) { return null; } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = () => reject(new Error('Failed to convert blob to data URI')); + reader.readAsDataURL(blob); + }); + } catch { + return null; } + } - addToEssentials(tab) { - const tabs = tab - ? // if it's already an array, dont make it [tab] - tab?.length - ? tab - : [tab] - : TabContextMenu.contextTab.multiselected - ? gBrowser.selectedTabs - : [TabContextMenu.contextTab]; - let movedAll = true; - for (let i = 0; i < tabs.length; i++) { - let tab = tabs[i]; - const section = gZenWorkspaces.getEssentialsSection(tab); - if (!this.canEssentialBeAdded(tab)) { - movedAll = false; - continue; - } - if (tab.hasAttribute('zen-essential')) { - continue; - } - tab.setAttribute('zen-essential', 'true'); - if (tab.hasAttribute('zen-workspace-id')) { - tab.removeAttribute('zen-workspace-id'); - } - if (tab.pinned && tab.hasAttribute('zen-pin-id')) { - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (pin) { - const pinId = tab.getAttribute('zen-pin-id'); - pin.isEssential = true; - pin.workspaceUuid = null; + addToEssentials(tab) { + const tabs = tab + ? // if it's already an array, dont make it [tab] + tab?.length + ? tab + : [tab] + : TabContextMenu.contextTab.multiselected + ? gBrowser.selectedTabs + : [TabContextMenu.contextTab]; + let movedAll = true; + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + const section = gZenWorkspaces.getEssentialsSection(tab); + if (!this.canEssentialBeAdded(tab)) { + movedAll = false; + continue; + } + if (tab.hasAttribute('zen-essential')) { + continue; + } + tab.setAttribute('zen-essential', 'true'); + if (tab.hasAttribute('zen-workspace-id')) { + tab.removeAttribute('zen-workspace-id'); + } + if (tab.pinned && tab.hasAttribute('zen-pin-id')) { + const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); + if (pin) { + const pinId = tab.getAttribute('zen-pin-id'); + pin.isEssential = true; + pin.workspaceUuid = null; - const tabFavicon = gBrowser.getIcon(tab); - if (tabFavicon && tabFavicon.startsWith('data:image/')) { - pin.iconUrl = tabFavicon; - } + const tabFavicon = gBrowser.getIcon(tab); + if (tabFavicon && tabFavicon.startsWith('data:image/')) { + pin.iconUrl = tabFavicon; + } - this.savePin(pin).catch(console.error); + this.savePin(pin).catch(console.error); - if (!pin.iconUrl) { - this.captureFaviconForTab(tab, pinId); - } + if (!pin.iconUrl) { + this.captureFaviconForTab(tab, pinId); } - gBrowser.zenHandleTabMove(tab, () => { - if (tab.ownerGlobal !== window) { - tab = gBrowser.adoptTab(tab, { - selectTab: tab.selected, - }); - tab.setAttribute('zen-essential', 'true'); - } else { - section.appendChild(tab); - } - }); - } else { - gBrowser.pinTab(tab); - this._ignoreNextTabPinnedEvent = true; - } - tab.setAttribute('zenDefaultUserContextId', true); - if (tab.selected) { - gZenWorkspaces.switchTabIfNeeded(tab); } - this.onTabIconChanged(tab); - // Dispatch the event to update the UI - const event = new CustomEvent('TabAddedToEssentials', { - detail: { tab }, - bubbles: true, - cancelable: false, + gBrowser.zenHandleTabMove(tab, () => { + if (tab.ownerGlobal !== window) { + tab = gBrowser.adoptTab(tab, { + selectTab: tab.selected, + }); + tab.setAttribute('zen-essential', 'true'); + } else { + section.appendChild(tab); + } }); - tab.dispatchEvent(event); + } else { + gBrowser.pinTab(tab); + this._ignoreNextTabPinnedEvent = true; + } + tab.setAttribute('zenDefaultUserContextId', true); + if (tab.selected) { + gZenWorkspaces.switchTabIfNeeded(tab); } - gZenUIManager.updateTabsToolbar(); - return movedAll; + this.onTabIconChanged(tab); + // Dispatch the event to update the UI + const event = new CustomEvent('TabAddedToEssentials', { + detail: { tab }, + bubbles: true, + cancelable: false, + }); + tab.dispatchEvent(event); } + gZenUIManager.updateTabsToolbar(); + return movedAll; + } removeEssentials(tab, unpin = true) { const tabs = tab @@ -1457,28 +1455,28 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async onLocationChange(browser) { - const tab = gBrowser.getTabForBrowser(browser); - if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) { - return; - } - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (!pin) { - return; - } + async onLocationChange(browser) { + const tab = gBrowser.getTabForBrowser(browser); + if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) { + return; + } + const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); + if (!pin) { + return; + } - if (!pin.iconUrl && pin.url && !this._migrationAttempted?.has(pin.uuid)) { - this.#migratePinFavicon(pin); - } + if (!pin.iconUrl && pin.url && !this._migrationAttempted?.has(pin.uuid)) { + this.#migratePinFavicon(pin); + } - const pinUrl = pin.url.split('#')[0]; - const currentUrl = browser.currentURI.spec.split('#')[0]; - if (pinUrl === currentUrl) { - this.resetPinChangedUrl(tab); - return; - } - this.pinHasChangedUrl(tab, pin); + const pinUrl = pin.url.split('#')[0]; + const currentUrl = browser.currentURI.spec.split('#')[0]; + if (pinUrl === currentUrl) { + this.resetPinChangedUrl(tab); + return; } + this.pinHasChangedUrl(tab, pin); + } resetPinChangedUrl(tab) { if (!tab.hasAttribute('zen-pinned-changed')) { @@ -1489,17 +1487,17 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { tab.style.removeProperty('--zen-original-tab-icon'); } - pinHasChangedUrl(tab, pin) { - if (tab.hasAttribute('zen-pinned-changed')) { - return; - } - if (tab.group?.hasAttribute('split-view-group')) { - tab.setAttribute('had-zen-pinned-changed', 'true'); - } else { - tab.setAttribute('zen-pinned-changed', 'true'); - } - tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl})`); + pinHasChangedUrl(tab, pin) { + if (tab.hasAttribute('zen-pinned-changed')) { + return; } + if (tab.group?.hasAttribute('split-view-group')) { + tab.setAttribute('had-zen-pinned-changed', 'true'); + } else { + tab.setAttribute('zen-pinned-changed', 'true'); + } + tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl})`); + } removeTabContainersDragoverClass(hideIndicator = true) { if (this._dragIndicator) {