diff --git a/background.js b/background.js index 5444c75..53000d2 100644 --- a/background.js +++ b/background.js @@ -128,17 +128,6 @@ function maybeClearTabState(tabId) { } } -function toHistoryRow(entry) { - return { - sampleId: entry.sampleId || "", - previewUrl: entry.previewUrl || "", - fullUrl: entry.fullUrl || "", - url: entry.fullUrl || entry.previewUrl || "", - source: entry.source || "", - capturedAt: entry.capturedAt || Date.now() - }; -} - function mergeEntry(tabState, inputEntry, source) { const sampleId = inputEntry.sampleId || extractSampleIdFromUrl(inputEntry.previewUrl || inputEntry.fullUrl || ""); if (!sampleId) { @@ -241,6 +230,36 @@ async function appendHistoryRows(rows) { await browser.storage.local.set({ [HISTORY_KEY]: combined.slice(-MAX_HISTORY_ITEMS) }); } +async function appendDownloadHistory(entry, result) { + const resolvedEntry = result && result.resolvedEntry ? result.resolvedEntry : entry; + const historyRow = { + sampleId: resolvedEntry.sampleId || "", + previewUrl: resolvedEntry.previewUrl || "", + fullUrl: resolvedEntry.fullUrl || "", + url: result.historyUrl || resolvedEntry.fullUrl || "", + source: result.mode === "full" ? "full_download" : "decoded_preview", + capturedAt: Date.now(), + mode: result.mode + }; + await appendHistoryRows([historyRow]); +} + +function makeDecodedPlaceholderRow(entry, source) { + const sampleId = entry.sampleId || ""; + if (!sampleId) { + return null; + } + return { + sampleId, + previewUrl: entry.previewUrl || "", + fullUrl: entry.fullUrl || "", + url: `decoded://${sampleId}`, + source: source || entry.source || "captured_preview", + capturedAt: entry.capturedAt || Date.now(), + mode: "pending_decoded" + }; +} + function decodePass(startIdx, arr, encodeBlk, endIdx) { let blockIdx = 0; for (let i = startIdx; i < endIdx; i += 1) { @@ -292,7 +311,7 @@ async function downloadDirectUrl(url, entry) { filename, conflictAction: "uniquify" }); - return { ok: true, mode: "full", filename }; + return { ok: true, mode: "full", filename, historyUrl: url }; } async function downloadDecodedPreview(previewUrl, entry) { @@ -315,7 +334,12 @@ async function downloadDecodedPreview(previewUrl, entry) { } finally { window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30000); } - return { ok: true, mode: "decoded", filename }; + return { + ok: true, + mode: "decoded", + filename, + historyUrl: `decoded://${entry.sampleId || Date.now()}` + }; } function extractAudioUrlsFromObject(input, output) { @@ -387,27 +411,76 @@ async function queryBackendForFullUrl(entry) { } } +function findEntryInTabStates(sampleId) { + for (const state of TAB_CAPTURED.values()) { + if (!state || !state.entriesBySample) { + continue; + } + const entry = state.entriesBySample.get(sampleId); + if (entry && (entry.previewUrl || entry.fullUrl)) { + return { + sampleId: entry.sampleId || sampleId, + previewUrl: normalizeUrl(entry.previewUrl || ""), + fullUrl: normalizeUrl(entry.fullUrl || "") + }; + } + } + return null; +} + +async function hydrateEntryFromHistory(sampleId) { + const data = await browser.storage.local.get(HISTORY_KEY); + const rows = Array.isArray(data[HISTORY_KEY]) ? data[HISTORY_KEY] : []; + for (let i = rows.length - 1; i >= 0; i -= 1) { + const row = rows[i]; + if (!row || row.sampleId !== sampleId) { + continue; + } + const previewUrl = normalizeUrl(row.previewUrl || ""); + const fullUrl = normalizeUrl(row.fullUrl || ""); + if (previewUrl || fullUrl) { + return { sampleId, previewUrl, fullUrl }; + } + } + return null; +} + async function resolveAndDownloadEntry(entryInput) { const entry = entryInput || {}; const sampleId = entry.sampleId || extractSampleIdFromUrl(entry.previewUrl || entry.fullUrl || ""); - const normalizedEntry = { + let normalizedEntry = { sampleId, previewUrl: normalizeUrl(entry.previewUrl || ""), fullUrl: normalizeUrl(entry.fullUrl || "") }; + if (normalizedEntry.sampleId && !normalizedEntry.previewUrl && !normalizedEntry.fullUrl) { + const fromTabState = findEntryInTabStates(normalizedEntry.sampleId); + if (fromTabState) { + normalizedEntry = fromTabState; + } else { + const fromHistory = await hydrateEntryFromHistory(normalizedEntry.sampleId); + if (fromHistory) { + normalizedEntry = fromHistory; + } + } + } + if (normalizedEntry.fullUrl && isFullUrl(normalizedEntry.fullUrl)) { - return downloadDirectUrl(normalizedEntry.fullUrl, normalizedEntry); + const result = await downloadDirectUrl(normalizedEntry.fullUrl, normalizedEntry); + return { ...result, resolvedEntry: normalizedEntry }; } const discoveredFull = await queryBackendForFullUrl(normalizedEntry); if (discoveredFull) { normalizedEntry.fullUrl = discoveredFull; - return downloadDirectUrl(discoveredFull, normalizedEntry); + const result = await downloadDirectUrl(discoveredFull, normalizedEntry); + return { ...result, resolvedEntry: normalizedEntry }; } if (normalizedEntry.previewUrl) { - return downloadDecodedPreview(normalizedEntry.previewUrl, normalizedEntry); + const result = await downloadDecodedPreview(normalizedEntry.previewUrl, normalizedEntry); + return { ...result, resolvedEntry: normalizedEntry }; } throw new Error("No preview or full URL available for this sample."); @@ -457,10 +530,13 @@ browser.runtime.onMessage.addListener((message, sender) => { for (const context of contexts) { const merged = upsertPreviewContext(tabId, context, "graphql_preview"); if (merged) { - rows.push(toHistoryRow(merged)); + const row = makeDecodedPlaceholderRow(merged, "graphql_preview"); + if (row) { + rows.push(row); + } } } - return appendHistoryRows(rows).then(() => ({ ok: true, added: rows.length })); + return appendHistoryRows(rows).then(() => ({ ok: true, added: contexts.length })); } if (message.type === "CAPTURE_URLS") { @@ -469,11 +545,14 @@ browser.runtime.onMessage.addListener((message, sender) => { const rows = []; for (const raw of urls) { const merged = upsertUrl(tabId, raw, message.source || "network_preview"); - if (merged) { - rows.push(toHistoryRow(merged)); + if (merged && merged.previewUrl) { + const row = makeDecodedPlaceholderRow(merged, message.source || "network_preview"); + if (row) { + rows.push(row); + } } } - return appendHistoryRows(rows).then(() => ({ ok: true, added: rows.length })); + return appendHistoryRows(rows).then(() => ({ ok: true, added: urls.length })); } if (message.type === "GET_TAB_URLS") { @@ -491,14 +570,30 @@ browser.runtime.onMessage.addListener((message, sender) => { if (message.type === "GET_HISTORY") { return browser.storage.local.get(HISTORY_KEY).then((data) => { const rows = Array.isArray(data[HISTORY_KEY]) ? data[HISTORY_KEY] : []; - rows.sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0)); - return { ok: true, history: rows }; + const filtered = rows.filter((row) => { + const url = typeof row.url === "string" ? row.url : ""; + if (url.startsWith("decoded://")) { + return true; + } + if (typeof row.fullUrl === "string" && isFullUrl(row.fullUrl)) { + return true; + } + return isFullUrl(url); + }); + filtered.sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0)); + return { ok: true, history: filtered }; }); } if (message.type === "DOWNLOAD_ENTRY") { return resolveAndDownloadEntry(message.entry) - .then((result) => result) + .then(async (result) => { + if (result && result.ok) { + const entry = message.entry || {}; + await appendDownloadHistory(entry, result); + } + return result; + }) .catch((error) => ({ ok: false, error: error.message || "Download failed." })); } @@ -509,7 +604,12 @@ browser.runtime.onMessage.addListener((message, sender) => { return Promise.resolve({ ok: false, error: "No captured sample is available yet." }); } return resolveAndDownloadEntry(latest) - .then((result) => result) + .then(async (result) => { + if (result && result.ok) { + await appendDownloadHistory(latest, result); + } + return result; + }) .catch((error) => ({ ok: false, error: error.message || "Download failed." })); } @@ -529,11 +629,7 @@ function handleWebRequestCapture(details) { const tabId = typeof details.tabId === "number" ? details.tabId : -1; const merged = upsertUrl(tabId, url, "webrequest"); - if (merged) { - appendHistoryRows([toHistoryRow(merged)]).catch(() => { - // Ignore history write failures. - }); - } + void merged; } browser.webRequest.onBeforeRequest.addListener( diff --git a/history.js b/history.js index a5dc90c..4cce27a 100644 --- a/history.js +++ b/history.js @@ -17,7 +17,7 @@ function buildItem(entry) { const url = document.createElement("p"); url.className = "history-url"; - url.textContent = entry.fullUrl || entry.previewUrl || entry.url || ""; + url.textContent = entry.fullUrl || entry.url || ""; const meta = document.createElement("p"); meta.className = "history-meta";