diff --git a/background.js b/background.js index 9cbc92d..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) => { @@ -136,35 +136,152 @@ 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}); + 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}); + } + })(); + 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) => 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}); + } catch (error) { + console.error("test_server_url failed:", error); + sendResponse({ok: false, error: error.message}); + } + })(); return true; - } - } - 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; + } -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 }); + 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 () => { + 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 }); + sendResponse({ok: true}); + } catch (error) { + console.error(`Failed to open options page: ${error.message}`); + sendResponse({ok: false, error: error.message}); + } + })(); + return true; + + default: + console.error('Invalid message: ', message); + 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 3c2af25..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", @@ -36,12 +39,13 @@ }, "options_page": "options.html", "background": { + "scripts": ["background.js"], "service_worker": "background.js", "type": "module" }, "web_accessible_resources": [{ "resources": ["popup.css", "popup.js"], - "matches": ["*://*\/*"] + "matches": [""] }], "commands": { "save-to-archivebox-action": { @@ -51,5 +55,10 @@ "mac": "Command+Shift+X" } } + }, + "browser_specific_settings": { + "gecko": { + "id": "archivebox@example.com" + } } } 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 772efab..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(); @@ -40,20 +36,18 @@ 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); - }); - }) + + const response = await chrome.runtime.sendMessage({ + type: 'archivebox_add', + body: { + urls: [url], + tags: tags, + } + }); + + if (response && !response.ok) { + throw new Error(`${response.errorMessage}`) + } ok = true; status = 'Saved to ArchiveBox Server' @@ -73,7 +67,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); @@ -150,7 +144,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', @@ -184,471 +178,500 @@ 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() { + const doc = iframe.contentDocument; + + // 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 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; - } + .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; + } - .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:hover { + animation: slideDown -0.3s ease-in-out forwards; + opacity: 1; + } - doc.body.appendChild(popup); - window.popup_element = popup; + .archive-box-popup small { + display: block; + width: 100%; + text-align: center; + margin-top: 5px; + color: #fefefe; + overflow: hidden; + font-size: 11px; + opacity: 1.0; + } - // Add message passing for options link - popup.querySelector('.options-link').addEventListener('click', (e) => { - e.preventDefault(); - chrome.runtime.sendMessage({ action: 'openOptionsPage', id: current_snapshot.id }); - }); + .archive-box-popup small.fade-out { + animation: fadeOut 10s ease-in-out forwards; + } - 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 img { + width: 15%; + max-width: 40px; + display: inline-block; + vertical-align: top; } - } - }); - 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 .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; + } - // Trigger resize after dropdown visibility changes - setTimeout(resizeIframe, 0); - } + .archive-box-popup .metadata { + display: inline-block; + max-width: 80%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - // Handle input changes - input.addEventListener('input', updateDropdown); + .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 keyboard navigation + @keyframes fadeOut { + 0% { opacity: 1; } + 80% { opacity: 0.8;} + 100% { opacity: 0; display: none; } + } - // 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 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; + } - if (!filteredTags.length) { - if (e.key === 'Enter' && input.value.trim()) { - e.preventDefault(); + .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(); + + // 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); } - // 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 an onload event to correctly initialize the popup. + + // Prevent double initialization + let initialized = false; + iframe.onload = () => { + if (!initialized) { + initialized = true; + initializeIframeContent(); + } + }; + + // 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); + } } window.createPopup(); 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); } 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 });