From 03a131fdd638d42addcb438087df197fb8b2e222 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 21 Apr 2025 14:06:37 -0500 Subject: [PATCH 1/6] Change manifest.json for Firefox compatibility. --- manifest.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manifest.json b/manifest.json index 3c2af25..c712e23 100755 --- a/manifest.json +++ b/manifest.json @@ -35,7 +35,11 @@ } }, "options_page": "options.html", + "options_ui": { + "page": "options.html" + }, "background": { + "scripts": ["background.js"], "service_worker": "background.js", "type": "module" }, @@ -51,5 +55,10 @@ "mac": "Command+Shift+X" } } + }, + "browser_specific_settings": { + "gecko": { + "id": "archivebox@example.com" + } } } From 4d615ecf001313a5257ba29256a38af4ab0ef9c4 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 21 Apr 2025 14:07:20 -0500 Subject: [PATCH 2/6] Change callback-based message passing in popup.js. --- popup.js | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/popup.js b/popup.js index 772efab..a943854 100755 --- a/popup.js +++ b/popup.js @@ -40,20 +40,14 @@ async function sendToArchiveBox(url, tags) { try { console.log('i Sending to ArchiveBox', { url, tags }); - await new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ - type: 'archivebox_add', - body: JSON.stringify({ - urls: [url], - tags: tags, - }) - }, (response) => { - if (!response.ok) { - reject(`${response.errorMessage}`); - } - resolve(response); - }); - }) + + await chrome.runtime.sendMessage({ + type: 'archivebox_add', + body: JSON.stringify({ + urls: [url], + tags: tags, + }) + }); ok = true; status = 'Saved to ArchiveBox Server' From 460660490d48e4d9d1e64373f3a985271a6325d4 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 23 Apr 2025 22:33:58 -0500 Subject: [PATCH 3/6] Clean up message passing. Fixing the message handling also fixes a bug (that at least existed on Firefox) where attempting to archive with an unconfigured ArchiveBox server would lead to an undefined type error instead of an error message that the server wasn't configured. --- background.js | 53 ++++++++++++++++++++++++++++----------------------- manifest.json | 3 --- popup.js | 12 ++++++++---- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/background.js b/background.js index 9cbc92d..eaf952a 100755 --- a/background.js +++ b/background.js @@ -136,35 +136,40 @@ chrome.storage.onChanged.addListener((changes, area) => { }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'archivebox_add') { - try { - const { urls = [], tags=[] } = JSON.parse(message.body); - - addToArchiveBox(urls, tags) - .then(() => { - console.log(`Successfully archived ${urls}`); - sendResponse({ok: true}); - } - ) - .catch((error) => sendResponse({ok: false, errorMessage: error.message})); - } catch (error) { - console.error(`Failed to parse archivebox_add message, no URLs sent to ArchiveBox server: ${error.message}`); - sendResponse({ok: false, errorMessage: error.message}); - return true; - } + switch (message.type) { + case 'archivebox_add': + (async () => { + try { + const { urls = [], tags=[] } = message.body; + await addToArchiveBox(urls, tags); + console.log(`Successfully archived ${urls}`); + sendResponse({ok: true}); + } catch (error) { + console.error(`Failed send to ArchiveBox server: ${error.message}`); + sendResponse({ok: false, errorMessage: error.message}); + } + })(); + break; + + case 'open_options': + (async () => { + try { + const options_url = chrome.runtime.getURL('options.html') + `?search=${message.id}`; + console.log('i ArchiveBox Collector showing options.html', options_url); + await chrome.tabs.create({ url: options_url }); + } catch (error) { + console.error(`Failed to open options page: ${error.message}`); + } + })(); + break; + + default: + console.error('Invalid message: ', message); } return true; }); -chrome.runtime.onMessage.addListener(async (message) => { - const options_url = chrome.runtime.getURL('options.html') + `?search=${message.id}`; - console.log('i ArchiveBox Collector showing options.html', options_url); - if (message.action === 'openOptionsPage') { - await chrome.tabs.create({ url: options_url }); - } -}); - chrome.runtime.onInstalled.addListener(function () { chrome.contextMenus.removeAll(); chrome.contextMenus.create({ diff --git a/manifest.json b/manifest.json index c712e23..8fc7a2e 100755 --- a/manifest.json +++ b/manifest.json @@ -35,9 +35,6 @@ } }, "options_page": "options.html", - "options_ui": { - "page": "options.html" - }, "background": { "scripts": ["background.js"], "service_worker": "background.js", diff --git a/popup.js b/popup.js index a943854..25d740e 100755 --- a/popup.js +++ b/popup.js @@ -41,14 +41,18 @@ async function sendToArchiveBox(url, tags) { try { console.log('i Sending to ArchiveBox', { url, tags }); - await chrome.runtime.sendMessage({ + const response = await chrome.runtime.sendMessage({ type: 'archivebox_add', - body: JSON.stringify({ + body: { urls: [url], tags: tags, - }) + } }); + if (response && !response.ok) { + throw new Error(`${response.errorMessage}`) + } + ok = true; status = 'Saved to ArchiveBox Server' } catch (error) { @@ -409,7 +413,7 @@ window.createPopup = async function() { // Add message passing for options link popup.querySelector('.options-link').addEventListener('click', (e) => { e.preventDefault(); - chrome.runtime.sendMessage({ action: 'openOptionsPage', id: current_snapshot.id }); + chrome.runtime.sendMessage({ type: 'open_options', id: current_snapshot.id }); }); const input = popup.querySelector('input'); From c41c30703b071dc056fe52a6d7293a4a243bfbde Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 23 Apr 2025 22:53:32 -0500 Subject: [PATCH 4/6] Fix CSP error. --- snapshots-tab.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/snapshots-tab.js b/snapshots-tab.js index cdee044..3dea411 100755 --- a/snapshots-tab.js +++ b/snapshots-tab.js @@ -423,7 +423,6 @@ export function initializeSnapshotsTab() {
${snapshot.url} @@ -443,6 +442,13 @@ export function initializeSnapshotsTab() { updateSelectionCount(); updateActionButtonStates(); + // Show the ArchiveBox favicon if an entry doesn't have one + document.querySelectorAll('.favicon').forEach(img => { + img.addEventListener('error', function() { + this.src = '128.png'; + }); + }); + // Update tags list with filtered snapshots await renderTagsList(filteredSnapshots); } From a4b1c17ecf5d2d310e705bf2b97c3ae4c1653150 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Sun, 27 Apr 2025 17:23:25 -0500 Subject: [PATCH 5/6] Make server test requests from background script. In Firefox, making requests directly from the options page (as we were in config-tab.js) violates CORS, so we have to send messages to the background script which then makes the requests to the server, as we're doing with other requests. --- background.js | 122 ++++++- config-tab.js | 39 +-- manifest.json | 7 +- options.js | 2 +- popup.js | 879 ++++++++++++++++++++++++++------------------------ utils.js | 3 + 6 files changed, 589 insertions(+), 463 deletions(-) diff --git a/background.js b/background.js index eaf952a..543ecf5 100755 --- a/background.js +++ b/background.js @@ -126,7 +126,7 @@ async function configureAutoArchiving() { } // Initialize auto-archiving setup on extension load -configureAutoArchiving(); +chrome.runtime.onStartup.addListener(configureAutoArchiving); // Listen for changes to the auto-archive setting chrome.storage.onChanged.addListener((changes, area) => { @@ -149,7 +149,117 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse({ok: false, errorMessage: error.message}); } })(); - break; + return true; + + case 'test_server_url': + (async () => { + try { + const serverUrl = message.serverUrl; + console.log("Testing server URL:", serverUrl); + + if (!serverUrl || !serverUrl.startsWith('http')) { + sendResponse({ok: false, error: "Invalid server URL"}); + return; + } + + const origin = new URL(serverUrl).origin; + console.log("Server origin:", origin); + + // First try without credentials as Firefox is stricter + try { + console.log("Trying server API endpoint"); + let response = await fetch(`${serverUrl}/api/`, { + method: 'GET', + mode: 'cors' + }); + + if (response.ok) { + console.log("API endpoint test successful"); + sendResponse({ok: true}); + return; + } + + // Try the root URL for older ArchiveBox versions + if (response.status === 404) { + console.log("API endpoint not found, trying root URL"); + response = await fetch(`${serverUrl}`, { + method: 'GET', + mode: 'cors' + }); + + if (response.ok) { + console.log("Root URL test successful"); + sendResponse({ok: true}); + return; + } + } + + console.log("Server returned non-OK response:", response.status, response.statusText); + throw new Error(`${response.status} ${response.statusText}`); + } catch (fetchError) { + console.error("Fetch error:", fetchError); + throw new Error(`NetworkError: ${fetchError.message}`); + } + } catch (error) { + console.error("test_server_url failed:", error); + sendResponse({ok: false, error: error.message}); + } + })(); + return true; + + case 'test_api_key': + (async () => { + try { + const { serverUrl, apiKey } = message; + console.log("Testing API key for server:", serverUrl); + + if (!serverUrl || !serverUrl.startsWith('http')) { + sendResponse({ok: false, error: "Invalid server URL"}); + return; + } + + if (!apiKey) { + sendResponse({ok: false, error: "API key is required"}); + return; + } + + try { + console.log("Attempting to verify API key..."); + const response = await fetch(`${serverUrl}/api/v1/auth/check_api_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + mode: 'cors', + body: JSON.stringify({ + token: apiKey, + }) + }); + + console.log("API key check response status:", response.status); + + if (response.ok) { + const data = await response.json(); + console.log("API key check response data:", data); + + if (data.user_id) { + sendResponse({ok: true, user_id: data.user_id}); + } else { + sendResponse({ok: false, error: 'Invalid API key response'}); + } + } else { + sendResponse({ok: false, error: `${response.status} ${response.statusText}`}); + } + } catch (fetchError) { + console.error("API key check fetch error:", fetchError); + sendResponse({ok: false, error: `NetworkError: ${fetchError.message}`}); + } + } catch (error) { + console.error("test_api_key failed:", error); + sendResponse({ok: false, error: error.message}); + } + })(); + return true; case 'open_options': (async () => { @@ -157,19 +267,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const options_url = chrome.runtime.getURL('options.html') + `?search=${message.id}`; console.log('i ArchiveBox Collector showing options.html', options_url); await chrome.tabs.create({ url: options_url }); + sendResponse({ok: true}); } catch (error) { console.error(`Failed to open options page: ${error.message}`); + sendResponse({ok: false, error: error.message}); } })(); - break; + return true; default: console.error('Invalid message: ', message); + return true; } - - return true; }); +// Create context menus chrome.runtime.onInstalled.addListener(function () { chrome.contextMenus.removeAll(); chrome.contextMenus.create({ diff --git a/config-tab.js b/config-tab.js index dea495e..a9fddae 100755 --- a/config-tab.js +++ b/config-tab.js @@ -48,25 +48,15 @@ export async function initializeConfigTab() { // Test request to server. try { - let response = await fetch(`${serverUrl.value}/api/`, { - method: 'GET', - mode: 'cors', - credentials: 'omit' + const testResult = await chrome.runtime.sendMessage({ + type: 'test_server_url', + serverUrl: serverUrl.value }); - // fall back to pre-v0.8.0 endpoint for backwards compatibility - if (response.status === 404) { - response = await fetch(`${serverUrl.value}`, { - method: 'GET', - mode: 'cors', - credentials: 'omit' - }); - } - - if (response.ok) { + if (testResult.ok) { updateStatusIndicator(statusIndicator, statusText, true, '✓ Server is reachable'); } else { - updateStatusIndicator(statusIndicator, statusText, false, `✗ Server error: ${response.status} ${response.statusText}`); + updateStatusIndicator(statusIndicator, statusText, false, `✗ Server error: ${testResult.error}`); } } catch (err) { updateStatusIndicator(statusIndicator, statusText, false, `✗ Connection failed: ${err.message}`); @@ -79,20 +69,17 @@ export async function initializeConfigTab() { const statusText = document.getElementById('apiKeyStatusText'); try { - const response = await fetch(`${serverUrl.value}/api/v1/auth/check_api_token`, { - method: 'POST', - mode: 'cors', - credentials: 'omit', - body: JSON.stringify({ - token: apiKey.value, - }) + // Use the background script to avoid CORS issues + const testResult = await chrome.runtime.sendMessage({ + type: 'test_api_key', + serverUrl: serverUrl.value, + apiKey: apiKey.value }); - const data = await response.json(); - if (data.user_id) { - updateStatusIndicator(statusIndicator, statusText, true, `✓ API key is valid: user_id = ${data.user_id}`); + if (testResult.ok) { + updateStatusIndicator(statusIndicator, statusText, true, `✓ API key is valid: user_id = ${testResult.user_id}`); } else { - updateStatusIndicator(statusIndicator, statusText, false, `✗ API key error: ${response.status} ${response.statusText} ${JSON.stringify(data)}`); + updateStatusIndicator(statusIndicator, statusText, false, `✗ API key error: ${testResult.error}`); } } catch (err) { updateStatusIndicator(statusIndicator, statusText, false, `✗ API test failed: ${err.message}`); diff --git a/manifest.json b/manifest.json index 8fc7a2e..6b9b377 100755 --- a/manifest.json +++ b/manifest.json @@ -16,9 +16,12 @@ "bookmarks", "tabs" ], - "optional_host_permissions": [ + "host_permissions": [ "" ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'" + }, "icons": { "16": "16.png", "32": "32.png", @@ -42,7 +45,7 @@ }, "web_accessible_resources": [{ "resources": ["popup.css", "popup.js"], - "matches": ["*://*\/*"] + "matches": [""] }], "commands": { "save-to-archivebox-action": { diff --git a/options.js b/options.js index 241642a..abfb2fe 100755 --- a/options.js +++ b/options.js @@ -24,7 +24,7 @@ document.addEventListener('DOMContentLoaded', () => { var tabEls = document.querySelectorAll('a.nav-link[data-bs-toggle="tab"]') for (const tabEl of tabEls) { tabEl.addEventListener('shown.bs.tab', function (event) { - console.log('ArchiveBox tab switched to:', event.target); + console.debug('ArchiveBox tab switched to:', event.target); event.target // newly activated tab event.relatedTarget // previous active tab // window.location.hash = event.target.id; diff --git a/popup.js b/popup.js index 25d740e..58bc97f 100755 --- a/popup.js +++ b/popup.js @@ -71,7 +71,7 @@ async function sendToArchiveBox(url, tags) { window.getCurrentSnapshot = async function() { const { entries: snapshots = [] } = await chrome.storage.local.get('entries'); let current_snapshot = snapshots.find(snapshot => snapshot.url === window.location.href); - + if (!current_snapshot) { current_snapshot = new Snapshot(String(window.location.href), [], document.title); snapshots.push(current_snapshot); @@ -148,7 +148,7 @@ window.createPopup = async function() { document.querySelector('.archive-box-iframe')?.remove(); const iframe = document.createElement('iframe'); iframe.className = 'archive-box-iframe'; - + // Set iframe styles for positioning Object.assign(iframe.style, { position: 'fixed', @@ -182,471 +182,492 @@ window.createPopup = async function() { } } - // Create popup content inside iframe - const doc = iframe.contentDocument || iframe.contentWindow.document; - - // Add styles to iframe - const style = doc.createElement('style'); - style.textContent = ` - html, body { - margin: 0; - padding: 0; - font-family: system-ui, -apple-system, sans-serif; - font-size: 16px; - width: 100%; - height: auto; - overflow: visible; - } - - .archive-box-popup { - border-radius: 13px; - min-height: 90px; - background: #bf7070; - margin: 0px; - padding: 6px; - padding-top: 8px; - color: white; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - font-family: system-ui, -apple-system, sans-serif; - transition: all 0.2s ease-out; - } - - .archive-box-popup:hover { - animation: slideDown -0.3s ease-in-out forwards; - opacity: 1; - } - - .archive-box-popup small { - display: block; - width: 100%; - text-align: center; - margin-top: 5px; - color: #fefefe; - overflow: hidden; - font-size: 11px; - opacity: 1.0; - } + // Function to create iframe content that works for both browsers + async function initializeIframeContent() { + console.log('Initializing iframe content'); + const doc = iframe.contentDocument || iframe.contentWindow.document; - .archive-box-popup small.fade-out { - animation: fadeOut 10s ease-in-out forwards; - } - - .archive-box-popup img { - width: 15%; - max-width: 40px; - display: inline-block; - vertical-align: top; - } - - .archive-box-popup .options-link { - border: 1px solid #00000026; - border-right: 0px; - margin-right: -9px; - margin-top: -1px; - border-radius: 6px 0px 0px 6px; - padding-right: 7px; - padding-left: 3px; - text-decoration: none; - text-align: center; - font-size: 24px; - line-height: 1.4; - display: inline-block; - width: 34px; - transition: text-shadow 0.1s ease-in-out; - } - .archive-box-popup a.options-link:hover { - text-shadow: 0 0 10px #a1a1a1; - } - - .archive-box-popup .metadata { - display: inline-block; - max-width: 80%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .archive-box-popup input { - width: calc(100% - 42px); - border: 0px; - margin: 0px; - padding: 5px; - padding-left: 13px; - border-radius: 6px; - min-width: 100px; - background-color: #fefefe; - color: #1a1a1a; - vertical-align: top; - display: inline-block; - line-height: 1.75 !important; - margin-bottom: 8px; - } - - @keyframes fadeOut { - 0% { opacity: 1; } - 80% { opacity: 0.8;} - 100% { opacity: 0; display: none; } - } - - @keyframes slideDown { - 0% { top: -500px; } - 100% { top: 20px } - } - - .ARCHIVEBOX__tag-suggestions { - margin-top: 20px; - display: inline; - min-height: 0; - background-color: rgba(0, 0, 0, 0); - border: 0; - box-shadow: 0 0 0 0; - } - .ARCHIVEBOX__current-tags { - display: inline; - } - - .current-tags { - margin-top: 20px; - display: inline; - } - - .ARCHIVEBOX__tag-badge { - display: inline-block; - background: #e9ecef; - padding: 3px 8px; - border-radius: 3px; - padding-left: 18px; - margin: 2px; - font-size: 15px; - cursor: pointer; - user-select: none; - } - - .ARCHIVEBOX__tag-badge.suggestion { - background: #007bff; - color: white; - opacity: 0.2; - } - .ARCHIVEBOX__tag-badge.suggestion:hover { - opacity: 0.8; - } - .ARCHIVEBOX__tag-badge.suggestion:active { - opacity: 1; - } - - .ARCHIVEBOX__tag-badge.suggestion:after { - content: ' +'; - } - - .ARCHIVEBOX__tag-badge.current { - background: #007bff; - color: #ddd; - position: relative; - padding-right: 20px; - } - - .ARCHIVEBOX__tag-badge.current:hover::after { - content: '×'; - position: absolute; - right: 5px; - top: 50%; - transform: translateY(-50%); - font-weight: bold; - cursor: pointer; - } - - .status-indicator { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - margin-right: 5px; - } - - .status-indicator.success { - background: #28a745; - } - - .status-indicator.error { - background: #dc3545; - } + // Write minimal HTML structure to ensure we have access to document + if (!doc.body) { + doc.write(''); + } + + // Add styles to iframe + const style = doc.createElement('style'); + style.textContent = ` + html, body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, sans-serif; + font-size: 16px; + width: 100%; + height: auto; + overflow: visible; + } - .ARCHIVEBOX__autocomplete-dropdown { - background: white; - border: 1px solid #ddd; - border-radius: 0 0 6px 6px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - max-height: 200px; - overflow-y: auto; - transition: all 0.2s ease-out; - } - - .ARCHIVEBOX__autocomplete-item { - padding: 8px 12px; - cursor: pointer; - color: #333; - } - - .ARCHIVEBOX__autocomplete-item:hover, - .ARCHIVEBOX__autocomplete-item.selected { - background: #f0f0f0; - } - `; - doc.head.appendChild(style); - - // Create popup content - const popup = doc.createElement('div'); - popup.className = 'archive-box-popup'; - popup.innerHTML = ` - 🏛️ -
-

- - - Saved locally... - - `; + .archive-box-popup { + border-radius: 13px; + min-height: 90px; + background: #bf7070; + margin: 0px; + padding: 6px; + padding-top: 8px; + color: white; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + font-family: system-ui, -apple-system, sans-serif; + transition: all 0.2s ease-out; + } - doc.body.appendChild(popup); - window.popup_element = popup; + .archive-box-popup:hover { + animation: slideDown -0.3s ease-in-out forwards; + opacity: 1; + } - // Add message passing for options link - popup.querySelector('.options-link').addEventListener('click', (e) => { - e.preventDefault(); - chrome.runtime.sendMessage({ type: 'open_options', id: current_snapshot.id }); - }); + .archive-box-popup small { + display: block; + width: 100%; + text-align: center; + margin-top: 5px; + color: #fefefe; + overflow: hidden; + font-size: 11px; + opacity: 1.0; + } - const input = popup.querySelector('input'); - const suggestions_div = popup.querySelector('.ARCHIVEBOX__tag-suggestions'); - const current_tags_div = popup.querySelector('.ARCHIVEBOX__current-tags'); - - // console.log('Getting current tags and suggestions'); - - // Initial display of current tags and suggestions - await window.updateCurrentTags(); - await window.updateSuggestions(); - - // Add click handlers for suggestion badges - suggestions_div.addEventListener('click', async (e) => { - if (e.target.classList.contains('suggestion')) { - const { current_snapshot, snapshots } = await getCurrentSnapshot(); - const tag = e.target.textContent.replace(' +', ''); - if (!current_snapshot.tags.includes(tag)) { - current_snapshot.tags.push(tag); - await chrome.storage.local.set({ entries: snapshots }); - await updateCurrentTags(); - await updateSuggestions(); + .archive-box-popup small.fade-out { + animation: fadeOut 10s ease-in-out forwards; } - } - }); - current_tags_div.addEventListener('click', async (e) => { - if (e.target.classList.contains('current')) { - const tag = e.target.dataset.tag; - console.log('Removing tag', tag); - const { current_snapshot, snapshots } = await getCurrentSnapshot(); - current_snapshot.tags = current_snapshot.tags.filter(t => t !== tag); - await chrome.storage.local.set({ entries: snapshots }); - await updateCurrentTags(); - await updateSuggestions(); - } - }); - // Add dropdown container - const dropdownContainer = document.createElement('div'); - dropdownContainer.className = 'ARCHIVEBOX__autocomplete-dropdown'; - dropdownContainer.style.display = 'none'; - input.parentNode.insertBefore(dropdownContainer, input.nextSibling); - - let selectedIndex = -1; - let filteredTags = []; - - async function updateDropdown() { - const inputValue = input.value.toLowerCase(); - const allTags = await getAllTags(); - - // Filter tags that match input and aren't already used - const { current_snapshot } = await getCurrentSnapshot(); - filteredTags = allTags - .filter(tag => - tag.toLowerCase().includes(inputValue) && - !current_snapshot.tags.includes(tag) && - inputValue - ) - .slice(0, 5); // Limit to 5 suggestions - - if (filteredTags.length === 0) { - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - } else { - dropdownContainer.innerHTML = filteredTags - .map((tag, index) => ` -
- ${tag} -
- `) - .join(''); - - dropdownContainer.style.display = 'block'; - } + .archive-box-popup img { + width: 15%; + max-width: 40px; + display: inline-block; + vertical-align: top; + } - // Trigger resize after dropdown visibility changes - setTimeout(resizeIframe, 0); - } + .archive-box-popup .options-link { + border: 1px solid #00000026; + border-right: 0px; + margin-right: -9px; + margin-top: -1px; + border-radius: 6px 0px 0px 6px; + padding-right: 7px; + padding-left: 3px; + text-decoration: none; + text-align: center; + font-size: 24px; + line-height: 1.4; + display: inline-block; + width: 34px; + transition: text-shadow 0.1s ease-in-out; + } + .archive-box-popup a.options-link:hover { + text-shadow: 0 0 10px #a1a1a1; + } - // Handle input changes - input.addEventListener('input', updateDropdown); + .archive-box-popup .metadata { + display: inline-block; + max-width: 80%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - // Handle keyboard navigation + .archive-box-popup input { + width: calc(100% - 42px); + border: 0px; + margin: 0px; + padding: 5px; + padding-left: 13px; + border-radius: 6px; + min-width: 100px; + background-color: #fefefe; + color: #1a1a1a; + vertical-align: top; + display: inline-block; + line-height: 1.75 !important; + margin-bottom: 8px; + } - // handle escape key when popup has focus - input.addEventListener("keydown", async (e) => { - if (e.key === "Escape") { - e.stopPropagation(); - dropdownContainer.style.display = "none"; - selectedIndex = -1; - closePopup(); - return; - } + @keyframes fadeOut { + 0% { opacity: 1; } + 80% { opacity: 0.8;} + 100% { opacity: 0; display: none; } + } + + @keyframes slideDown { + 0% { top: -500px; } + 100% { top: 20px } + } + + .ARCHIVEBOX__tag-suggestions { + margin-top: 20px; + display: inline; + min-height: 0; + background-color: rgba(0, 0, 0, 0); + border: 0; + box-shadow: 0 0 0 0; + } + .ARCHIVEBOX__current-tags { + display: inline; + } + + .current-tags { + margin-top: 20px; + display: inline; + } + + .ARCHIVEBOX__tag-badge { + display: inline-block; + background: #e9ecef; + padding: 3px 8px; + border-radius: 3px; + padding-left: 18px; + margin: 2px; + font-size: 15px; + cursor: pointer; + user-select: none; + } + + .ARCHIVEBOX__tag-badge.suggestion { + background: #007bff; + color: white; + opacity: 0.2; + } + .ARCHIVEBOX__tag-badge.suggestion:hover { + opacity: 0.8; + } + .ARCHIVEBOX__tag-badge.suggestion:active { + opacity: 1; + } + + .ARCHIVEBOX__tag-badge.suggestion:after { + content: ' +'; + } + + .ARCHIVEBOX__tag-badge.current { + background: #007bff; + color: #ddd; + position: relative; + padding-right: 20px; + } + + .ARCHIVEBOX__tag-badge.current:hover::after { + content: '×'; + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + font-weight: bold; + cursor: pointer; + } + + .status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 5px; + } + + .status-indicator.success { + background: #28a745; + } + + .status-indicator.error { + background: #dc3545; + } + + .ARCHIVEBOX__autocomplete-dropdown { + background: white; + border: 1px solid #ddd; + border-radius: 0 0 6px 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + transition: all 0.2s ease-out; + } + + .ARCHIVEBOX__autocomplete-item { + padding: 8px 12px; + cursor: pointer; + color: #333; + } + + .ARCHIVEBOX__autocomplete-item:hover, + .ARCHIVEBOX__autocomplete-item.selected { + background: #f0f0f0; + } + `; + doc.head.appendChild(style); + + // Create popup content + const popup = doc.createElement('div'); + popup.className = 'archive-box-popup'; + popup.innerHTML = ` + 🏛️ +
+

+ + + Saved locally... + + `; + + doc.body.appendChild(popup); + window.popup_element = popup; + + // Add message passing for options link + popup.querySelector('.options-link').addEventListener('click', (e) => { + e.preventDefault(); + chrome.runtime.sendMessage({ type: 'open_options', id: current_snapshot.id }); + }); + + const input = popup.querySelector('input'); + const suggestions_div = popup.querySelector('.ARCHIVEBOX__tag-suggestions'); + const current_tags_div = popup.querySelector('.ARCHIVEBOX__current-tags'); + + // Initial display of current tags and suggestions + await window.updateCurrentTags(); + await window.updateSuggestions(); - if (!filteredTags.length) { - if (e.key === 'Enter' && input.value.trim()) { - e.preventDefault(); + // Add click handlers for suggestion badges + suggestions_div.addEventListener('click', async (e) => { + if (e.target.classList.contains('suggestion')) { const { current_snapshot, snapshots } = await getCurrentSnapshot(); - const newTag = input.value.trim(); - if (!current_snapshot.tags.includes(newTag)) { - current_snapshot.tags.push(newTag); + const tag = e.target.textContent.replace(' +', ''); + if (!current_snapshot.tags.includes(tag)) { + current_snapshot.tags.push(tag); await chrome.storage.local.set({ entries: snapshots }); - input.value = ''; await updateCurrentTags(); await updateSuggestions(); } } - return; + }); + current_tags_div.addEventListener('click', async (e) => { + if (e.target.classList.contains('current')) { + const tag = e.target.dataset.tag; + console.log('Removing tag', tag); + const { current_snapshot, snapshots } = await getCurrentSnapshot(); + current_snapshot.tags = current_snapshot.tags.filter(t => t !== tag); + await chrome.storage.local.set({ entries: snapshots }); + await updateCurrentTags(); + await updateSuggestions(); + } + }); + + // Add dropdown container + const dropdownContainer = document.createElement('div'); + dropdownContainer.className = 'ARCHIVEBOX__autocomplete-dropdown'; + dropdownContainer.style.display = 'none'; + input.parentNode.insertBefore(dropdownContainer, input.nextSibling); + + let selectedIndex = -1; + let filteredTags = []; + + async function updateDropdown() { + const inputValue = input.value.toLowerCase(); + const allTags = await getAllTags(); + + // Filter tags that match input and aren't already used + const { current_snapshot } = await getCurrentSnapshot(); + filteredTags = allTags + .filter(tag => + tag.toLowerCase().includes(inputValue) && + !current_snapshot.tags.includes(tag) && + inputValue + ) + .slice(0, 5); // Limit to 5 suggestions + + if (filteredTags.length === 0) { + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + } else { + dropdownContainer.innerHTML = filteredTags + .map((tag, index) => ` +
+ ${tag} +
+ `) + .join(''); + + dropdownContainer.style.display = 'block'; + } + + // Trigger resize after dropdown visibility changes + setTimeout(resizeIframe, 0); } - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - selectedIndex = Math.min(selectedIndex + 1, filteredTags.length - 1); - updateDropdown(); - break; - - case 'ArrowUp': - e.preventDefault(); - selectedIndex = Math.max(selectedIndex - 1, -1); - updateDropdown(); - break; - - case 'Enter': - e.preventDefault(); - if (selectedIndex >= 0) { - const selectedTag = filteredTags[selectedIndex]; + // Handle input changes + input.addEventListener('input', updateDropdown); + + // Handle keyboard navigation + + // handle escape key when popup has focus + input.addEventListener("keydown", async (e) => { + if (e.key === "Escape") { + e.stopPropagation(); + dropdownContainer.style.display = "none"; + selectedIndex = -1; + closePopup(); + return; + } + + if (!filteredTags.length) { + if (e.key === 'Enter' && input.value.trim()) { + e.preventDefault(); const { current_snapshot, snapshots } = await getCurrentSnapshot(); - if (!current_snapshot.tags.includes(selectedTag)) { - current_snapshot.tags.push(selectedTag); - await chrome.storage.local.set({ entries: snapshots}); + const newTag = input.value.trim(); + if (!current_snapshot.tags.includes(newTag)) { + current_snapshot.tags.push(newTag); + await chrome.storage.local.set({ entries: snapshots }); + input.value = ''; + await updateCurrentTags(); + await updateSuggestions(); } - input.value = ''; - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - await updateCurrentTags(); - await updateSuggestions(); } - break; - - case 'Tab': - if (selectedIndex >= 0) { + return; + } + + switch (e.key) { + case 'ArrowDown': e.preventDefault(); - input.value = filteredTags[selectedIndex]; - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - } - break; - } - }); + selectedIndex = Math.min(selectedIndex + 1, filteredTags.length - 1); + updateDropdown(); + break; + case 'ArrowUp': + e.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, -1); + updateDropdown(); + break; - // Handle click selection - dropdownContainer.addEventListener('click', async (e) => { - const item = e.target.closest('.ARCHIVEBOX__autocomplete-item'); - if (item) { - const selectedTag = item.dataset.tag; - const { current_snapshot, snapshots } = await getCurrentSnapshot(); - if (!current_snapshot.tags.includes(selectedTag)) { - current_snapshot.tags.push(selectedTag); - await chrome.storage.local.set({ entries: snapshots }); + case 'Enter': + e.preventDefault(); + if (selectedIndex >= 0) { + const selectedTag = filteredTags[selectedIndex]; + const { current_snapshot, snapshots } = await getCurrentSnapshot(); + if (!current_snapshot.tags.includes(selectedTag)) { + current_snapshot.tags.push(selectedTag); + await chrome.storage.local.set({ entries: snapshots}); + } + input.value = ''; + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + await updateCurrentTags(); + await updateSuggestions(); + } + break; + + case 'Tab': + if (selectedIndex >= 0) { + e.preventDefault(); + input.value = filteredTags[selectedIndex]; + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + } + break; } - input.value = ''; - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - await updateCurrentTags(); - await updateSuggestions(); - } - }); + }); - // Hide dropdown when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.ARCHIVEBOX__autocomplete-dropdown') && - !e.target.closest('input')) { - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - } - }); - input.focus(); - console.log('+ Showed ArchiveBox popup in iframe'); + // Handle click selection + dropdownContainer.addEventListener('click', async (e) => { + const item = e.target.closest('.ARCHIVEBOX__autocomplete-item'); + if (item) { + const selectedTag = item.dataset.tag; + const { current_snapshot, snapshots } = await getCurrentSnapshot(); + if (!current_snapshot.tags.includes(selectedTag)) { + current_snapshot.tags.push(selectedTag); + await chrome.storage.local.set({ entries: snapshots }); + } + input.value = ''; + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + await updateCurrentTags(); + await updateSuggestions(); + } + }); - // Add resize triggers - const resizeObserver = new ResizeObserver(() => { - resizeIframe(); - }); + // Hide dropdown when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.ARCHIVEBOX__autocomplete-dropdown') && + !e.target.closest('input')) { + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + } + }); - // Observe the popup content for size changes - resizeObserver.observe(popup); + input.focus(); + console.log('+ Showed ArchiveBox popup in iframe'); - const originalUpdateCurrentTags = window.updateCurrentTags; - window.updateCurrentTags = async function() { - await originalUpdateCurrentTags(); - resizeIframe(); - } + // Add resize triggers + const resizeObserver = new ResizeObserver(() => { + resizeIframe(); + }); - async function updateDropdown() { - const inputValue = input.value.toLowerCase(); - const allTags = await getAllTags(); - - // Filter tags that match input and aren't already used - const { current_snapshot } = await getCurrentSnapshot(); - filteredTags = allTags - .filter(tag => - tag.toLowerCase().includes(inputValue) && - !current_snapshot.tags.includes(tag) && - inputValue - ) - .slice(0, 5); // Limit to 5 suggestions - - if (filteredTags.length === 0) { - dropdownContainer.style.display = 'none'; - selectedIndex = -1; - } else { - dropdownContainer.innerHTML = filteredTags - .map((tag, index) => ` -
- ${tag} -
- `) - .join(''); - - dropdownContainer.style.display = 'block'; + // Observe the popup content for size changes + resizeObserver.observe(popup); + + const originalUpdateCurrentTags = window.updateCurrentTags; + window.updateCurrentTags = async function() { + await originalUpdateCurrentTags(); + resizeIframe(); + } + + async function updateDropdown() { + const inputValue = input.value.toLowerCase(); + const allTags = await getAllTags(); + + // Filter tags that match input and aren't already used + const { current_snapshot } = await getCurrentSnapshot(); + filteredTags = allTags + .filter(tag => + tag.toLowerCase().includes(inputValue) && + !current_snapshot.tags.includes(tag) && + inputValue + ) + .slice(0, 5); // Limit to 5 suggestions + + if (filteredTags.length === 0) { + dropdownContainer.style.display = 'none'; + selectedIndex = -1; + } else { + dropdownContainer.innerHTML = filteredTags + .map((tag, index) => ` +
+ ${tag} +
+ `) + .join(''); + + dropdownContainer.style.display = 'block'; + } + + // Trigger resize after dropdown visibility changes + setTimeout(resizeIframe, 0); } - // Trigger resize after dropdown visibility changes + // Initial resize setTimeout(resizeIframe, 0); + + console.log("Initialized successfully"); } - // Initial resize - setTimeout(resizeIframe, 0); + // Handle both Firefox and Chrome differences with initialization + // Chrome doesn't always fire onload for newly created iframes, while + // Firefox requires onload event to initialize it + + iframe.onload = initializeIframeContent; + + // Try to initialize directly (for Chrome) + try { + await initializeIframeContent(); + } catch (error) { + console.error('Direct iframe initialization failed: ', error); + } } window.createPopup(); diff --git a/utils.js b/utils.js index 987486b..5e86bc0 100755 --- a/utils.js +++ b/utils.js @@ -69,10 +69,12 @@ export async function addToArchiveBox(urls, tags = [], depth = 0, update = false console.log('i Using v0.8.5 REST API'); const response = await fetch(`${archivebox_server_url}/api/v1/cli/add`, { headers: { + 'Content-Type': 'application/json', 'x-archivebox-api-key': `${archivebox_api_key}` }, method: 'post', credentials: 'include', + mode: 'cors', body: JSON.stringify({ urls, formattedTags, depth, update, update_all }) }); @@ -96,6 +98,7 @@ export async function addToArchiveBox(urls, tags = [], depth = 0, update = false const response = await fetch(`${archivebox_server_url}/add/`, { method: "post", credentials: "include", + mode: "cors", body: body }); From ea33845725247591fded3ff55277fab0ad743b12 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Thu, 1 May 2025 12:29:54 -0500 Subject: [PATCH 6/6] Make popup initialization more robust. --- popup.js | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/popup.js b/popup.js index 58bc97f..e311899 100755 --- a/popup.js +++ b/popup.js @@ -9,11 +9,7 @@ class Snapshot { } } -const IS_IN_POPUP = window.location.href.startsWith('chrome-extension://') && window.location.href.endsWith('/popup.html'); -const IS_ON_WEBSITE = !window.location.href.startsWith('chrome-extension://'); - window.popup_element = null; // Global reference to popup element -window.hide_timer = null; window.closePopup = function () { document.querySelector(".archive-box-iframe")?.remove(); @@ -184,13 +180,7 @@ window.createPopup = async function() { // Function to create iframe content that works for both browsers async function initializeIframeContent() { - console.log('Initializing iframe content'); - const doc = iframe.contentDocument || iframe.contentWindow.document; - - // Write minimal HTML structure to ensure we have access to document - if (!doc.body) { - doc.write(''); - } + const doc = iframe.contentDocument; // Add styles to iframe const style = doc.createElement('style'); @@ -504,7 +494,7 @@ window.createPopup = async function() { // Handle keyboard navigation - // handle escape key when popup has focus + // Handle escape key when popup has focus input.addEventListener("keydown", async (e) => { if (e.key === "Escape") { e.stopPropagation(); @@ -652,21 +642,35 @@ window.createPopup = async function() { // Initial resize setTimeout(resizeIframe, 0); - - console.log("Initialized successfully"); } // Handle both Firefox and Chrome differences with initialization // Chrome doesn't always fire onload for newly created iframes, while - // Firefox requires onload event to initialize it - - iframe.onload = initializeIframeContent; + // Firefox requires an onload event to correctly initialize the popup. + + // Prevent double initialization + let initialized = false; + iframe.onload = () => { + if (!initialized) { + initialized = true; + initializeIframeContent(); + } + }; - // Try to initialize directly (for Chrome) - try { - await initializeIframeContent(); - } catch (error) { - console.error('Direct iframe initialization failed: ', error); + // Manually check that iframe is loaded and initialize directly (for Chrome) + if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { + if (!initialized) { + initialized = true; + initializeIframeContent(); + } + } else { + // We can do something smarter here, but empirically this seems to work + setTimeout(() => { + if (!initialized) { + initialized = true; + initializeIframeContent(); + } + }, 100); } }