From 2dfa752df3de06874cf07b3579223ef8f75aa695 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 7 Jul 2026 23:04:22 +1000 Subject: [PATCH] =?UTF-8?q?fix(harvest):=20Download=20is=20a=20submenu=20p?= =?UTF-8?q?arent=20=E2=80=94=20hover=20to=20unfurl=201K=20flyout,=20don't?= =?UTF-8?q?=20click;=20looser=20menu=20finder;=20reset=20seen-list=20(v2?= =?UTF-8?q?=20key)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- content.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/content.js b/content.js index d078b16..a4b2c17 100644 --- a/content.js +++ b/content.js @@ -541,8 +541,14 @@ function findTileMenuButton(tile) { } function findMenuItem(re) { - return [...document.querySelectorAll('[role="menu"] [role="menuitem"], [role="menuitem"], [role="menu"] button, [role="dialog"] button')] + const roled = [...document.querySelectorAll('[role="menu"] [role="menuitem"], [role="menuitem"], [role="menu"] button, [role="dialog"] button')] .filter(visible).find(e => re.test((e.textContent || '').trim())); + if (roled) return roled; + // fallback for menus without ARIA roles: small visible leaf nodes with matching text + const leaf = [...document.querySelectorAll('div, span, li, button')].filter(visible) + .filter(e => re.test((e.textContent || '').trim()) && (e.textContent || '').trim().length < 30 && + !e.querySelector('div, span, li, button')); + return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null; } async function executeHarvest(task) { @@ -550,7 +556,7 @@ async function executeHarvest(task) { let got = 0, missed = 0, skipped = 0; try { await waitForAgentReady(30000); - const stored = await new Promise(r => chrome.storage.local.get({ harvested_urls: [] }, d => r(d.harvested_urls))); + const stored = await new Promise(r => chrome.storage.local.get({ harvested_urls_v2: [] }, d => r(d.harvested_urls_v2))); const seen = new Set(stored); await bg({ type: 'harvestRouting', on: true }); window.scrollTo(0, 0); await sleep(800); @@ -568,14 +574,20 @@ async function executeHarvest(task) { const dots = findTileMenuButton(tile); if (!dots) { missed++; seen.add(href); log('info', `harvest: no ⋮ on a tile — ${gridCensus()}`); continue; } await bg({ type: 'clickAt', ...center(dots) }); await sleep(800); - const dl = findMenuItem(/^\s*download\s*$/i); - if (!dl) { missed++; seen.add(href); await bg({ type: 'pressEscape' }); await sleep(400); continue; } - await bg({ type: 'clickAt', ...center(dl) }); await sleep(800); - const oneK = findMenuItem(/1k|original/i); - if (!oneK) { missed++; seen.add(href); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + const dl = findMenuItem(/^\s*download\b/i); + if (!dl) { missed++; seen.add(href); log('info', `harvest: menu open but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + // "Download" is a submenu parent: HOVER unfurls the nested 1K/2K/4K flyout + // (clicking it closes the menu — the bug the first pass hit). + await bg({ type: 'hoverAt', ...center(dl) }); await sleep(800); + let oneK = findMenuItem(/\b1k\b|original/i); + if (!oneK) { // some builds expand on click instead — try once + await bg({ type: 'clickAt', ...center(dl) }); await sleep(800); + oneK = findMenuItem(/\b1k\b|original/i); + } + if (!oneK) { missed++; seen.add(href); log('info', 'harvest: Download hovered but no 1K flyout appeared.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; } await bg({ type: 'clickAt', ...center(oneK) }); got++; seen.add(href); - chrome.storage.local.set({ harvested_urls: [...seen].slice(-3000) }); + chrome.storage.local.set({ harvested_urls_v2: [...seen].slice(-3000) }); if (got % 10 === 0) log('info', `harvest: ${got} downloaded so far…`); await sleep(1800 + Math.random() * 1500); // pace — don't machine-gun the account }