// Browser API compatibility shim (Chrome uses chrome.*, Firefox exposes browser.*) const api = (typeof browser !== 'undefined' && browser.tabs) ? browser : chrome; // Cached window/tab/group data built at load time let windowSelectorData = []; // [{windowId, focused, tabs, groups}] // Tab group color lookup (Chrome's string enum → CSS hex) const GROUP_COLORS = { grey: '#5f6368', blue: '#1a73e8', red: '#d93025', yellow: '#f9ab00', green: '#1e8e3e', pink: '#d01884', purple: '#a142f4', cyan: '#007b83', orange: '#fa903e' }; // Initialize extension document.addEventListener('DOMContentLoaded', async () => { await buildSelectorData(); renderSelector(); updateStats(); setupEventListeners(); }); // Fetch and cache all window/tab/group data async function buildSelectorData() { try { const [allTabs, allWindows] = await Promise.all([ api.tabs.query({}), api.windows.getAll() ]); windowSelectorData = await Promise.all(allWindows.map(async (win) => { const tabs = allTabs.filter(t => t.windowId === win.id); let groups = []; if (api.tabGroups && typeof api.tabGroups.query === 'function') { try { groups = await api.tabGroups.query({ windowId: win.id }); } catch (_) { // tabGroups API present but query failed — treat as no groups } } return { windowId: win.id, focused: win.focused, tabs, groups }; })); } catch (error) { console.error('Error building selector data:', error); } } // Update tab and window count statistics from cached data function updateStats() { const totalTabs = windowSelectorData.reduce((sum, w) => sum + w.tabs.length, 0); document.getElementById('tab-count').textContent = totalTabs; document.getElementById('window-count').textContent = windowSelectorData.length; } // Render window/group checkboxes into #selector-list function renderSelector() { const list = document.getElementById('selector-list'); if (windowSelectorData.length === 0) { list.innerHTML = '
No windows found.
'; return; } let html = ''; windowSelectorData.forEach((win, index) => { const label = `Window ${index + 1}${win.focused ? ' (active)' : ''}`; const tabCount = win.tabs.length; html += `
`; if (win.groups.length > 0) { win.groups.forEach(group => { const color = GROUP_COLORS[group.color] || '#888888'; const groupTitle = group.title || 'Unnamed group'; const groupTabCount = win.tabs.filter(t => t.groupId === group.id).length; html += `
`; }); } html += '
'; }); list.innerHTML = html; // Stagger animation delays on rendered items list.querySelectorAll('.selector-item:not(.selector-item--group)').forEach((el, i) => { el.style.animationDelay = `${i * 50}ms`; }); } // Setup event listeners function setupEventListeners() { document.getElementById('export-btn').addEventListener('click', handleExport); document.getElementById('select-all-btn').addEventListener('click', () => { document.querySelectorAll('.window-checkbox, .group-checkbox') .forEach(cb => { cb.checked = true; }); }); document.getElementById('deselect-all-btn').addEventListener('click', () => { document.querySelectorAll('.window-checkbox, .group-checkbox') .forEach(cb => { cb.checked = false; }); }); // Keyboard accessibility for option checkboxes document.querySelectorAll('.options input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); checkbox.checked = !checkbox.checked; } }); }); } // Return only the tabs from selected windows/groups function getSelectedTabs() { const checkedWindowIds = new Set( [...document.querySelectorAll('.window-checkbox:checked')] .map(cb => parseInt(cb.dataset.windowId)) ); const checkedGroupIds = new Set( [...document.querySelectorAll('.group-checkbox:checked')] .map(cb => parseInt(cb.dataset.groupId)) ); const result = []; for (const win of windowSelectorData) { if (!checkedWindowIds.has(win.windowId)) continue; for (const tab of win.tabs) { if (win.groups.length > 0 && tab.groupId && tab.groupId > 0) { // Grouped tab: only include if its group is checked if (checkedGroupIds.has(tab.groupId)) result.push(tab); } else { // Ungrouped tab (Chrome sentinel -1 or Firefox undefined): always include result.push(tab); } } } return result; } // Main export handler async function handleExport() { const exportBtn = document.getElementById('export-btn'); const statusEl = document.getElementById('export-status'); // Get user options const includeDescriptions = document.getElementById('include-descriptions').checked; const groupByWindow = document.getElementById('group-by-window').checked; const includeTimestamp = document.getElementById('include-timestamp').checked; // Get filtered tabs const tabs = getSelectedTabs(); if (tabs.length === 0) { statusEl.textContent = '! Select at least one window to export.'; statusEl.className = 'status-message error'; return; } // Disable button and show loading state exportBtn.disabled = true; const originalContent = exportBtn.innerHTML; exportBtn.innerHTML = 'Exporting...'; statusEl.textContent = 'Collecting tab information...'; statusEl.className = 'status-message loading'; try { if (includeDescriptions) { statusEl.textContent = `Fetching descriptions (0/${tabs.length})...`; await fetchDescriptions(tabs, statusEl); } const markdown = await generateMarkdown(tabs, { includeDescriptions, groupByWindow, includeTimestamp }); downloadMarkdown(markdown); statusEl.textContent = '✓ Exported ' + tabs.length + ' tabs successfully.'; statusEl.className = 'status-message success'; } catch (error) { console.error('Export error:', error); statusEl.textContent = '✗ Error: ' + error.message; statusEl.className = 'status-message error'; } finally { exportBtn.disabled = false; exportBtn.innerHTML = originalContent; } } // Fetch descriptions from tabs async function fetchDescriptions(tabs, statusEl) { for (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; statusEl.textContent = `Fetching descriptions (${i + 1}/${tabs.length})...`; try { const results = await api.scripting.executeScript({ target: { tabId: tab.id }, func: extractDescription }); if (results && results[0] && results[0].result) { tab.description = results[0].result; } } catch (error) { // Tab doesn't support script injection (e.g. about:, chrome://, moz:// pages) tab.description = null; } } } // Function to extract description — runs in tab context function extractDescription() { const metaDesc = document.querySelector('meta[name="description"]'); const ogDesc = document.querySelector('meta[property="og:description"]'); return (metaDesc && metaDesc.content) || (ogDesc && ogDesc.content) || null; } // Generate markdown content async function generateMarkdown(tabs, options) { let markdown = '# Browser Tabs Export\n\n'; if (options.includeTimestamp) { const now = new Date(); const timestamp = now.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'short' }); markdown += `**Exported:** ${timestamp}\n\n`; } markdown += `**Total Tabs:** ${tabs.length}\n\n`; markdown += '---\n\n'; if (options.groupByWindow) { // Group selected tabs by their window, using cached window data order const tabsByWindow = {}; tabs.forEach(tab => { if (!tabsByWindow[tab.windowId]) tabsByWindow[tab.windowId] = []; tabsByWindow[tab.windowId].push(tab); }); let windowIndex = 1; for (const windowId in tabsByWindow) { markdown += `## Window ${windowIndex}\n\n`; markdown += formatTabs(tabsByWindow[windowId], options.includeDescriptions); markdown += '---\n\n'; windowIndex++; } } else { markdown += formatTabs(tabs, options.includeDescriptions); } return markdown; } // Format tabs as markdown list function formatTabs(tabs, includeDescriptions) { let content = ''; tabs.forEach(tab => { content += `- **${escapeMarkdown(tab.title)}**\n`; content += ` - URL: ${tab.url}\n`; if (includeDescriptions && tab.description) { content += ` - Description: ${escapeMarkdown(tab.description)}\n`; } content += '\n'; }); return content; } // Escape markdown special characters function escapeMarkdown(text) { if (!text) return ''; return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); } // Escape HTML for safe DOM injection function escapeHtml(text) { if (!text) return ''; return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // Download markdown file function downloadMarkdown(content) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `tabs-export-${timestamp}.md`; api.downloads.download({ url, filename, saveAs: true }); // Always clean up — Firefox's browser.downloads returns a Promise and ignores callbacks setTimeout(() => URL.revokeObjectURL(url), 100); }