From ad4df28a66dbf033ce038787a9866f5fc17a9e99 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 22 May 2026 21:27:12 +0200 Subject: [PATCH] Step 7a: Bulk ID Check moves to a detached popup window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New files: bulk-check.html, bulk-check.js, bulk-check.css Popup gains a ๐Ÿ“‹ launcher button next to the โš™ Options gear. Clicking it sends `open-bulk-check` to background.js and closes the popup; background.js owns window lifecycle: - chrome.storage.session.bulkCheckWindowId stashes the open window id - existing id โ†’ chrome.windows.update({ focused, drawAttention }) - missing or stale id โ†’ chrome.windows.create({ type:'popup', width:640, height:540 }) and stash the new id - chrome.windows.onRemoved clears the stale id on close Last-paste persisted to chrome.storage.local.bulkCheckLastPaste, debounced 500ms on input, restored on window open. quickMode is read from settings at run time, matching previous behavior. Ctrl/Cmd+Enter inside the textarea triggers the check. Options page no longer carries the Bulk ID Check fieldset: removed from options.html (Library Review pdesc updated to note the relocation) and the matching handlers from options.js (1903 โ†’ 1852 lines). No manifest permission changes โ€” own-page chrome.windows.create needs no extra permission. Co-Authored-By: Claude Opus 4.7 --- AGENTS.md | 4 +- background.js | 46 +++++++++++++++++ bulk-check.css | 102 +++++++++++++++++++++++++++++++++++++ bulk-check.html | 32 ++++++++++++ bulk-check.js | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ options.html | 12 +---- options.js | 51 ------------------- popup.html | 1 + popup.js | 4 ++ 9 files changed, 321 insertions(+), 64 deletions(-) create mode 100644 bulk-check.css create mode 100644 bulk-check.html create mode 100644 bulk-check.js diff --git a/AGENTS.md b/AGENTS.md index 457b4ee..4a3f9db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,13 +137,13 @@ Done in rc-jav catalog loading. Catalog CSV/XML paths are normalized from Window 5. **Recent Activity + Search Troubleshooting moved to new Debug Tools pane.** Verified Recent Activity is search-trigger-only by reading `background.js` โ€” `recordActivity()` is NOT called from `delete-file` handler. No audit-value split needed. New sidebar entry "Debug Tools" under System group; new `pane-debug` houses both fieldsets. 6. **options.js split โ€” Cache & Scans + Duplicate Review paired extraction.** `options.js` 3133 โ†’ 2356 lines. New files: `options-cache.js` (161 lines, Cache & Scans block), `options-dupe-review.js` (616 lines, Dup Review + Keep Ranking incl. bottom `loadKeepRanking()` call). Script-tag order in `options.html`: cache โ†’ dupe-review โ†’ options.js (body bottom). Cross-script binding visibility (vanilla classic scripts share global declarative env): Library Issues code still in options.js reads `_configuredScanRoots` / `_cacheSkippedByRemote` / calls `rememberConfiguredScanRoots` from cache file by bare reference. Calls to `escapeHtml` / `openModal` / `closeModal` / `keepActionViewport` / `clearNativeRepairCard` / `renderNativeMessagingFailure` from extracted files all occur inside event handlers (resolved at call time, after options.js parses). Repo `git init`'d before this step; baseline commit `f8e781f` is the rollback point. Verified by `node --check` on each file and on concatenated script. 6b. **options.js split โ€” Library Issues extraction.** `options.js` 2356 โ†’ 1903 lines. New file: `options-library-issues.js` (453 lines) โ€” covers `lastLibraryIssues`, `_libraryIssuesDirty`, `renderLibraryIssues`, `_closeLibraryIssues`, and the bottom IIFE that wraps `_optScanTimer` / `_setOptScanningState` / `_pollOptProgress` for optimization-scan progress polling. Block was fully self-contained (no external callers of its identifiers). Reads `_configuredScanRoots` / `_cacheSkippedByRemote` / calls `rememberConfiguredScanRoots` from `options-cache.js` โ€” same cross-file binding pattern proven in step 6. Script-tag order in `options.html`: cache โ†’ dupe-review โ†’ library-issues โ†’ options.js. `node --check` passes on each file and on concatenation; line count of concat (3133) matches pre-split total exactly. +7a. **Bulk Check standalone window.** New `bulk-check.{html,js,css}` opened as detached `chrome.windows.create({ type: 'popup', width: 640, height: 540 })`. Launcher = ๐Ÿ“‹ icon button in popup header next to โš™ Options; click sends `open-bulk-check` message to background and closes the popup. Background owns the lifecycle: `openBulkCheckWindow()` reads `chrome.storage.session.bulkCheckWindowId`; existing id โ†’ `chrome.windows.update({ focused, drawAttention })`; failure or no id โ†’ create new window + stash id. `chrome.windows.onRemoved` clears the stale id on close. Last-paste persisted to `chrome.storage.local.bulkCheckLastPaste` (debounced 500ms), restored on window open. `quickMode` read from settings on each run (parity with old options behavior). Removed the Bulk ID Check fieldset from `options.html` (Library Review pane description updated to note the relocation) and its handlers from `options.js` (1903 โ†’ 1852 lines). No manifest permission changes needed. (Step 4 in the plan is a paired-extraction sub-task of step 6; folded into step 6 ship.) **Pending (in execution order):** -- **Step 6c โ€” finish options.js split (optional).** Remaining options.js (1903 lines) still holds: settings load/save, backup/restore, recent activity, search test bench, bulk ID check, adapters, ID normalizers, part detectors, element picker, overlay previews, diagnostics, profiles, paths, and the bottom-entry IIFE. Candidates for extraction: Diagnostics (~250 lines, 1354โ€“1603 in current options.js), Profiles (~265 lines), Adapters + ID normalizers + Part detectors as a "rules editors" file (~330 lines combined). Diminishing returns past this point โ€” bottom IIFE + load/save core should stay in `options.js` as the entry point. -- **Step 7a โ€” Bulk Check standalone window.** New `bulk-check.html` opened as detached `chrome.windows.create({ type: 'popup', width: 640, height: 540 })` from a "Bulk Check" launcher button in the popup. Single canonical entry path โ€” NOT a Console sidebar tab. Window dedup via `chrome.storage.session`, last-paste persisted via `chrome.storage.local`. +- **Step 6c โ€” finish options.js split (optional).** Remaining options.js (1852 lines) still holds: settings load/save, backup/restore, recent activity, search test bench, adapters, ID normalizers, part detectors, element picker, overlay previews, diagnostics, profiles, paths, and the bottom-entry IIFE. Candidates for extraction: Diagnostics (~250 lines), Profiles (~265 lines), Adapters + ID normalizers + Part detectors as a "rules editors" file (~330 lines combined). Diminishing returns past this point โ€” bottom IIFE + load/save core should stay in `options.js` as the entry point. - **Step 8 โ€” Shared fixture corpus.** Top-level `D:\DEV\Project\rclone-jav\fixtures\` (neutral location, NOT inside Python or extension repo). JSON cases for query-ID extraction (extension), filename ID extraction (Python), shared normalization. - **Step 9 โ€” Cache contract design.** CACHE_VERSION already exists (currently 3). Add ID_RULES_VERSION concept: schema bump = force rebuild, rules bump = warn-and-mark-stale. - **Step 10 โ€” `rc-jav.py` module split** into `rcjav/` package (ids, cache, dupes, catalog, rclone_io, output, cli). Keep `rc-jav.py` as thin entrypoint that imports from `rcjav.cli.main`. diff --git a/background.js b/background.js index d086f83..741288c 100644 --- a/background.js +++ b/background.js @@ -692,6 +692,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { })(); return true; } + if (msg.type === "open-bulk-check") { + openBulkCheckWindow(); + sendResponse({ ok: true }); + return false; + } if (msg.type === "dupe-review") { (async () => { try { @@ -1075,3 +1080,44 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // On install / startup, build context menu chrome.runtime.onInstalled.addListener(() => ensureContextMenu()); chrome.runtime.onStartup.addListener(() => ensureContextMenu()); + +// ---------- Bulk Check standalone window ---------- +const BULK_CHECK_WIN_KEY = "bulkCheckWindowId"; + +async function openBulkCheckWindow() { + const session = chrome.storage.session; + try { + const stored = session ? await session.get(BULK_CHECK_WIN_KEY) : {}; + const existingId = stored?.[BULK_CHECK_WIN_KEY]; + if (existingId) { + try { + await chrome.windows.update(existingId, { focused: true, drawAttention: true }); + return; + } catch { + if (session) await session.remove(BULK_CHECK_WIN_KEY); + } + } + const win = await chrome.windows.create({ + url: chrome.runtime.getURL("bulk-check.html"), + type: "popup", + width: 640, + height: 540, + }); + if (session && win?.id != null) { + await session.set({ [BULK_CHECK_WIN_KEY]: win.id }); + } + } catch (e) { + console.warn("openBulkCheckWindow failed:", e); + } +} + +chrome.windows.onRemoved.addListener(async (windowId) => { + const session = chrome.storage.session; + if (!session) return; + try { + const stored = await session.get(BULK_CHECK_WIN_KEY); + if (stored?.[BULK_CHECK_WIN_KEY] === windowId) { + await session.remove(BULK_CHECK_WIN_KEY); + } + } catch { /* ignore */ } +}); diff --git a/bulk-check.css b/bulk-check.css new file mode 100644 index 0000000..3543034 --- /dev/null +++ b/bulk-check.css @@ -0,0 +1,102 @@ +body { + font-family: -apple-system, Segoe UI, sans-serif; + font-size: 13px; + margin: 0; + padding: 12px; + background: #1a1a1a; + color: #ddd; +} + +#header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +#header strong { font-size: 15px; } + +#mode-pill { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.4px; + background: #1a2a3a; + color: #6ec1ff; + padding: 2px 8px; + border-radius: 3px; + border: 1px solid #2a4a6a; +} +#mode-pill[data-mode="CACHE"] { + background: #1a3a1a; + color: #afa; + border-color: #2a5a2a; +} + +.help { + font-size: 12px; + color: #aaa; + line-height: 1.4; + margin-bottom: 8px; +} +.help code { + background: #0d0d0d; + padding: 1px 4px; + border-radius: 2px; + font-family: Consolas, monospace; + color: #ddd; +} +.dim { color: #777; } + +textarea#bulk-id-input { + width: 100%; + height: 140px; + resize: vertical; + background: #0d0d0d; + color: #ddd; + border: 1px solid #444; + border-radius: 3px; + padding: 6px 8px; + font-size: 12px; + font-family: Consolas, monospace; + box-sizing: border-box; +} +textarea#bulk-id-input:focus { outline: none; border-color: #6ec1ff; } + +.button-row { + display: flex; + align-items: center; + gap: 6px; + margin: 8px 0; +} + +button { + background: #333; + color: #ddd; + border: 1px solid #555; + border-radius: 3px; + padding: 4px 10px; + cursor: pointer; + font-size: 12px; +} +button:hover { background: #444; } +button:disabled { opacity: 0.5; cursor: default; } +button.running { background: #3a1a1a; border-color: #722; color: #faa; } + +#count-pill { + margin-left: auto; + font-size: 11px; +} + +.mono-output { + background: #0d0d0d; + border: 1px solid #333; + border-radius: 3px; + padding: 8px 10px; + font-family: Consolas, monospace; + font-size: 12px; + line-height: 1.5; + max-height: 260px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; +} +.mono-output:empty { display: none; } diff --git a/bulk-check.html b/bulk-check.html new file mode 100644 index 0000000..3ea26cc --- /dev/null +++ b/bulk-check.html @@ -0,0 +1,32 @@ + + + + + Bulk ID Check โ€” rclone-jav + + + + + +
+ Paste IDs separated by lines, commas, or spaces. Uses the current LIVE/CACHE mode and active library profile. +
Examples: BLK-474, FC2-4865786, PRTD-[027-030] +
+ + + +
+ + + +
+ +
+ + + + diff --git a/bulk-check.js b/bulk-check.js new file mode 100644 index 0000000..c1e0b1c --- /dev/null +++ b/bulk-check.js @@ -0,0 +1,133 @@ +// Standalone Bulk ID Check window. +// Launched via chrome.windows.create from popup; background.js owns +// the windowId lifecycle (chrome.storage.session.bulkCheckWindowId). + +const LAST_PASTE_KEY = "bulkCheckLastPaste"; +const SAVE_DEBOUNCE_MS = 500; + +function escapeHtml(s) { + return String(s ?? "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); +} + +function readBulkIds() { + return [...new Set(document.getElementById("bulk-id-input").value + .split(/[\s,]+/) + .map((x) => x.trim()) + .filter(Boolean))]; +} + +function updateCountPill() { + const ids = readBulkIds(); + const pill = document.getElementById("count-pill"); + pill.textContent = ids.length ? `${ids.length} unique ID${ids.length === 1 ? "" : "s"}` : ""; +} + +function renderBulkResults(r) { + const out = document.getElementById("bulk-id-results"); + if (!r || !r.ok) { + out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`; + return; + } + const rows = [ + `
Mode: ${escapeHtml(r.search_mode || "?")} ยท Queries: ${escapeHtml(r.query_count || 0)} ยท Hits: ${escapeHtml(r.hits || 0)} ยท Host: ${escapeHtml(r.timings?.host_rcjav_ms ?? "?")}ms
`, + ]; + for (const q of r.queries || []) { + const hit = q.hits > 0; + const sample = (q.structured || []).slice(0, 3).map((h) => h.full_path || h.path || h.jav_id).join(" | "); + rows.push(`
+ ${hit ? "HIT" : "MISS"} + ${escapeHtml(q.query || "?")} ยท ${escapeHtml(q.hits || 0)} hit(s) + ${sample ? `
${escapeHtml(sample)}
` : `
${escapeHtml(q.no_match_title || "No library hit")}
`} +
`); + } + out.innerHTML = rows.join(""); +} + +async function refreshModePill() { + const pill = document.getElementById("mode-pill"); + try { + const settings = await chrome.runtime.sendMessage({ type: "get-settings" }); + const mode = settings?.quickMode !== false ? "LIVE" : "CACHE"; + pill.textContent = mode; + pill.dataset.mode = mode; + } catch { + pill.textContent = "?"; + } +} + +let _saveTimer = null; +function schedulePersist() { + if (_saveTimer) clearTimeout(_saveTimer); + _saveTimer = setTimeout(async () => { + _saveTimer = null; + const value = document.getElementById("bulk-id-input").value; + try { + await chrome.storage.local.set({ [LAST_PASTE_KEY]: value }); + } catch { /* ignore */ } + }, SAVE_DEBOUNCE_MS); +} + +async function restoreLastPaste() { + try { + const stored = await chrome.storage.local.get(LAST_PASTE_KEY); + const value = stored?.[LAST_PASTE_KEY]; + if (typeof value === "string" && value) { + document.getElementById("bulk-id-input").value = value; + updateCountPill(); + } + } catch { /* ignore */ } +} + +document.getElementById("bulk-id-input").addEventListener("input", () => { + updateCountPill(); + schedulePersist(); +}); + +document.getElementById("bulk-id-clear").addEventListener("click", () => { + document.getElementById("bulk-id-input").value = ""; + document.getElementById("bulk-id-results").innerHTML = ""; + updateCountPill(); + schedulePersist(); +}); + +document.getElementById("bulk-id-run").addEventListener("click", async () => { + const runBtn = document.getElementById("bulk-id-run"); + const out = document.getElementById("bulk-id-results"); + const queries = readBulkIds(); + if (!queries.length) { + out.innerHTML = `paste at least one ID`; + return; + } + runBtn.disabled = true; + runBtn.classList.add("running"); + out.textContent = `checking ${queries.length} ID(s)...`; + try { + const settings = await chrome.runtime.sendMessage({ type: "get-settings" }); + const quick = settings?.quickMode !== false; + const r = await chrome.runtime.sendMessage({ + type: "bulk-query", + queries, + quick, + }); + renderBulkResults(r); + } catch (err) { + out.innerHTML = `error: ${escapeHtml(err?.message || String(err))}`; + } finally { + runBtn.disabled = false; + runBtn.classList.remove("running"); + } +}); + +// Ctrl/Cmd+Enter inside textarea = run +document.getElementById("bulk-id-input").addEventListener("keydown", (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { + e.preventDefault(); + document.getElementById("bulk-id-run").click(); + } +}); + +(async () => { + await restoreLastPaste(); + await refreshModePill(); +})(); diff --git a/options.html b/options.html index 045b971..74f3f86 100644 --- a/options.html +++ b/options.html @@ -410,7 +410,7 @@

Library Review

-
Review duplicate groups, check IDs in bulk, and fix non-canonical filenames in your library.
+
Review duplicate groups and fix non-canonical filenames in your library. (Bulk ID Check now opens in its own window from the popup.)
@@ -478,16 +478,6 @@
-
-
Bulk ID check
-
Paste IDs separated by lines, commas, or spaces. Uses the current LIVE/CACHE mode and active library profile.
- -
- - -
-
-
diff --git a/options.js b/options.js index 6fb6d96..71d44c5 100644 --- a/options.js +++ b/options.js @@ -674,57 +674,6 @@ document.getElementById("search-bench-clear").addEventListener("click", () => { document.getElementById("search-bench-results").innerHTML = ""; }); -// ---------- bulk ID check ---------- - -function readBulkIds() { - return [...new Set(document.getElementById("bulk-id-input").value - .split(/[\s,]+/) - .map((x) => x.trim()) - .filter(Boolean))]; -} - -function renderBulkResults(r) { - const out = document.getElementById("bulk-id-results"); - if (!r || !r.ok) { - out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`; - return; - } - const rows = [ - `
Mode: ${escapeHtml(r.search_mode || "?")} ยท Queries: ${escapeHtml(r.query_count || 0)} ยท Hits: ${escapeHtml(r.hits || 0)} ยท Host: ${escapeHtml(r.timings?.host_rcjav_ms ?? "?")}ms
`, - ]; - for (const q of r.queries || []) { - const hit = q.hits > 0; - const sample = (q.structured || []).slice(0, 3).map((h) => h.full_path || h.path || h.jav_id).join(" | "); - rows.push(`
- ${hit ? "HIT" : "MISS"} - ${escapeHtml(q.query || "?")} ยท ${escapeHtml(q.hits || 0)} hit(s) - ${sample ? `
${escapeHtml(sample)}
` : `
${escapeHtml(q.no_match_title || "No library hit")}
`} -
`); - } - out.innerHTML = rows.join(""); -} - -document.getElementById("bulk-id-run").addEventListener("click", async () => { - const out = document.getElementById("bulk-id-results"); - const queries = readBulkIds(); - if (!queries.length) { - out.innerHTML = `paste at least one ID`; - return; - } - out.textContent = `checking ${queries.length} ID(s)...`; - const r = await chrome.runtime.sendMessage({ - type: "bulk-query", - queries, - quick: document.getElementById("quickMode").checked, - }); - renderBulkResults(r); -}); -document.getElementById("bulk-id-clear").addEventListener("click", () => { - document.getElementById("bulk-id-input").value = ""; - document.getElementById("bulk-id-results").innerHTML = ""; -}); - - // ---------- adapters ---------- function renderAdapters(list) { diff --git a/popup.html b/popup.html index ff6ac97..1a46f1c 100644 --- a/popup.html +++ b/popup.html @@ -14,6 +14,7 @@ + diff --git a/popup.js b/popup.js index aeff5cb..0e6c713 100644 --- a/popup.js +++ b/popup.js @@ -560,6 +560,10 @@ document.getElementById("recheck").addEventListener("click", async () => { else runCheck(true); }); document.getElementById("open-options").addEventListener("click", () => chrome.runtime.openOptionsPage()); +document.getElementById("open-bulk").addEventListener("click", () => { + chrome.runtime.sendMessage({ type: "open-bulk-check" }); + window.close(); +}); $modeLive.addEventListener("click", () => setSearchMode("live")); $modeCache.addEventListener("click", () => setSearchMode("cache")); $pauseScan.addEventListener("click", () => setScanPaused(!(settings && settings.scanPaused)));