diff --git a/content.js b/content.js index 96b6f81..08124eb 100644 --- a/content.js +++ b/content.js @@ -573,6 +573,19 @@ function findMenuItem(re) { return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null; } +// The "Download" menu ROW. NOTE: its size flyout may live inside/next to it so a naive +// textContent reads "Download1K Original size2K Upscaled…" — then /^download\b/ fails +// (no word boundary after 'download'), which silently skipped the whole step. Match +// "download" anywhere and take the SHORTEST visible candidate (the row, not a wrapper). +function findDownloadRow() { + const cands = [...document.querySelectorAll('[role="menuitem"], [role="menu"] button, button, [role="button"], div, span, li')] + .filter(visible) + .filter(e => /download/i.test(e.textContent || '') && (e.textContent || '').trim().length < 60); + if (!cands.length) return null; + cands.sort((a, b) => (a.textContent || '').length - (b.textContent || '').length); + return cands[0].closest('[role="menuitem"], button, [role="button"], li') || cands[0]; +} + // The FREE "original size" download option. Images: "1K Original size". Videos: // "720p Original Size". Both contain "original size" — that's the safe key. // SAFETY: never return an "Upscaled" / "…credits" row. The 4K video upscale costs @@ -636,25 +649,28 @@ async function executeHarvest(task) { // Stage 2/4: click ⋮ ("More"), wait for the menu with Download in it await bg({ type: 'clickAt', ...center(dots) }); - const dl = await waitFor(() => findMenuItem(/^\s*download\b/i), 3000, 300); - if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + const dl = await waitFor(findDownloadRow, 3000, 300); + if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download row found — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + const dc = center(dl); + log('info', `harvest: Download row "${(dl.textContent || '').trim().slice(0, 18)}" @ ${Math.round(dc.x)},${Math.round(dc.y)} — hovering for the size flyout…`); - // Stage 3/4: HOVER Download — the size flyout unfurls beside it (side depends on - // where the tile sits; irrelevant, coords are re-queried fresh). Jiggle the - // pointer a few px and retry — one mouseMoved sometimes isn't enough for the - // hover intent to register. We pick "original size" (1K image / 720p video) and - // NEVER the paid upscales — findSizeOption enforces that. + // Stage 3/4: HOVER Download so the size flyout unfurls beside it. A single CDP jump + // can miss the hover-intent, so move in TWO steps (approach from the left, then + // settle on centre) to fire a real mouseenter, and re-query. We pick "original + // size" (1K image / 720p video) and NEVER the paid upscales (findSizeOption). let oneK = null; - for (let j = 0; j < 3 && !oneK; j++) { + for (let j = 0; j < 4 && !oneK; j++) { const c = center(dl); - await bg({ type: 'hoverAt', x: c.x - 4 + j * 4, y: c.y }); - oneK = await waitFor(findSizeOption, 1600, 250); + await bg({ type: 'hoverAt', x: c.x - 28, y: c.y }); await sleep(130); + await bg({ type: 'hoverAt', x: c.x, y: c.y }); + oneK = await waitFor(findSizeOption, 1400, 200); } - if (!oneK) { // last resort: some builds expand on click + if (!oneK) { // last resort: some builds expand the submenu on click await bg({ type: 'clickAt', ...center(dl) }); oneK = await waitFor(findSizeOption, 1600, 250); } - if (!oneK) { missed++; missedThisPass.add(href); log('info', 'harvest: no "original size" option after hover + click-expand.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + if (!oneK) { missed++; missedThisPass.add(href); log('info', `harvest: Download hovered but size flyout never appeared (no "original size"). ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + log('info', `harvest: size option "${(oneK.textContent || '').trim().slice(0, 22)}" — downloading.`); // Stage 4/4: walk the pointer INTO the flyout (keeps it open), then click 1K, // then VERIFY a download event actually fired before counting it. @@ -737,7 +753,7 @@ async function handleRateLimit(task) { function log(level, text) { console.log(`[Flow][${level}] ${text}`); chrome.storage.local.get({ logs: [] }, (r) => { - chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-100) }); + chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-300) }); }); } diff --git a/popup.html b/popup.html index 50dd613..12c66a8 100644 --- a/popup.html +++ b/popup.html @@ -46,7 +46,7 @@