Skip to content

Instantly share code, notes, and snippets.

@JuenTingShie
Created March 3, 2026 08:23
Show Gist options
  • Select an option

  • Save JuenTingShie/24e4e3939f40b1d7c458418137078dc7 to your computer and use it in GitHub Desktop.

Select an option

Save JuenTingShie/24e4e3939f40b1d7c458418137078dc7 to your computer and use it in GitHub Desktop.
Soundiiz.com batch export
(async () => {
// ========= 可調參數(如你的頁面類別不同請改這裡) =========
const MODAL_SELECTOR = '.modal-inner.modal-entrance'; // 可滾動的 Modal 內容容器
const GRID_IN_MODAL = '.ReactVirtualized__Grid'; // Modal 裡的虛擬清單 Grid(如果有)
const INNER_SELECTOR = '.ReactVirtualized__Grid__innerScrollContainer';
const ROW_SELECTOR = '.list-selector.track-selector'; // 單列容器
const TITLE_SELECTOR = '.selector-content .item-title'; // 標題(顯示用)
const CHECKBOX_SELECTOR = 'input[type="checkbox"]'; // 勾選的 input
const CLICKABLE_FALLBACK = '.checkbox-inner, .selector-check, .checkbox'; // 非原生 input 的候選
// ========= 互動提示:輸入區間 =========
const rangeInput = prompt('請輸入要勾選的區間(格式:起-迄,例如 201-400)', '201-400');
if (!rangeInput) { console.warn('已取消。'); return; }
const rangeMatch = rangeInput.trim().match(/^(\d+)\s*[-~–—]\s*(\d+)$/);
if (!rangeMatch) { console.error('格式錯誤,請使用 201-400 這種格式'); return; }
const START_INDEX = parseInt(rangeMatch[1], 10);
const END_INDEX = parseInt(rangeMatch[2], 10);
if (!(START_INDEX > 0 && END_INDEX >= START_INDEX)) {
console.error('區間不合法:請確認為正整數且起點 ≤ 終點'); return;
}
// ========= 互動提示:行高 =========
let rowHeightInput = prompt('行高(像素)。輸入數字或留空使用自動偵測', '');
rowHeightInput = rowHeightInput && rowHeightInput.trim();
let USER_ROW_HEIGHT = rowHeightInput ? parseInt(rowHeightInput, 10) : null;
if (rowHeightInput && (!Number.isFinite(USER_ROW_HEIGHT) || USER_ROW_HEIGHT <= 0)) {
alert('行高輸入無效,改用自動偵測');
USER_ROW_HEIGHT = null;
}
// ========= 工具函式 =========
const wait = (ms) => new Promise(r => setTimeout(r, ms));
// 鎖住背景滾動(避免 body/html 跟著捲動)
const lockBackgroundScroll = () => {
const original = {
bodyOverflow: document.body.style.overflow,
htmlOverflow: document.documentElement.style.overflow,
bodyPosition: document.body.style.position,
bodyWidth: document.body.style.width,
};
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
// 有些頁面需要加 fixed 來避免滾輪穿透
document.body.style.position = 'fixed';
document.body.style.width = '100%';
const unlock = () => {
document.body.style.overflow = original.bodyOverflow || '';
document.documentElement.style.overflow = original.htmlOverflow || '';
document.body.style.position = original.bodyPosition || '';
document.body.style.width = original.bodyWidth || '';
};
return unlock;
};
// 尋找可滾動容器(modal 內的 grid,或 modal 自身)
const modal = document.querySelector(MODAL_SELECTOR);
if (!modal) { console.error('找不到 Modal:', MODAL_SELECTOR); return; }
const grid = modal.querySelector(GRID_IN_MODAL) || modal;
const inner = grid.querySelector(INNER_SELECTOR) || grid;
// 焦點放到 grid(避免鍵盤/滾輪作用到 window)
if (!grid.hasAttribute('tabindex')) grid.setAttribute('tabindex', '0');
grid.focus({ preventScroll: true });
// 事件攔截:避免滾動事件傳到背景頁面
const swallow = (evt) => {
if (!grid.contains(evt.target)) {
evt.stopPropagation();
evt.preventDefault();
}
};
// 幾何/索引相關
const rowTop = (row) => {
if (row.style && row.style.top) {
const m = row.style.top.match(/(-?\d+(?:\.\d+)?)px/);
if (m) return parseFloat(m[1]);
}
const rectTop = row.getBoundingClientRect().top;
const gridTop = grid.getBoundingClientRect().top;
return Math.round(grid.scrollTop + (rectTop - gridTop));
};
const computeIndexFromTop = (topPx, rh) => Math.floor(topPx / rh) + 1;
// 偵測行高(若使用者沒指定)
const detectRowHeight = () => {
const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR));
if (rows.length < 2) return 67;
const tops = rows
.map(r => {
if (r.style && r.style.top) {
const m = r.style.top.match(/(-?\d+(?:\.\d+)?)px/);
if (m) return parseFloat(m[1]);
}
const rectTop = r.getBoundingClientRect().top;
const gridTop = grid.getBoundingClientRect().top;
return Math.round(grid.scrollTop + (rectTop - gridTop));
})
.sort((a, b) => a - b);
const deltas = {};
for (let i = 1; i < tops.length; i++) {
const d = Math.max(1, Math.round(tops[i] - tops[i - 1]));
deltas[d] = (deltas[d] || 0) + 1;
}
const best = Object.entries(deltas).sort((a, b) => b[1] - a[1])[0];
return best ? parseInt(best[0], 10) : 67;
};
// 捲動到指定 top(像素)
const RENDER_DELAY_MS = 90;
const FIND_DELAY_MS = 50;
const scrollToTopPx = async (topPx) => {
grid.scrollTop = topPx;
if (typeof grid.scrollTo === 'function') {
try { grid.scrollTo({ top: topPx, left: 0, behavior: 'instant' }); } catch {}
}
await wait(RENDER_DELAY_MS);
};
// 解析單列顯示用資訊
const parseRowInfo = (row) => {
const titleEl = row.querySelector(TITLE_SELECTOR);
const title = titleEl ? titleEl.textContent.trim() : '';
const creators = Array.from(row.querySelectorAll('.selector-infos .creator-name'))
.map(n => n.textContent.trim())
.filter(Boolean);
const checked = !!row.querySelector(`${CHECKBOX_SELECTOR}:checked`);
return { title, creators, checked };
};
// 收集可見列並依推估 index 存到 map
const collectVisibleRows = (rowHeight, map) => {
const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR));
for (const row of rows) {
const top = rowTop(row);
if (!Number.isFinite(top)) continue;
const idx = computeIndexFromTop(top, rowHeight);
if (!map.has(idx)) map.set(idx, { index: idx, top, ...parseRowInfo(row), _el: row });
}
return rows.length;
};
// 勾選指定 index 對應列(可見區最接近者)
const clickRowCheckboxByIndex = (targetIndex, rowHeight) => {
const approxTop = (targetIndex - 1) * rowHeight;
const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR));
if (!rows.length) return false;
let best = null, bestDelta = Infinity;
for (const row of rows) {
const top = rowTop(row);
const d = Math.abs(top - approxTop);
if (d < bestDelta) { bestDelta = d; best = row; }
}
if (!best) return false;
let target = best.querySelector(CHECKBOX_SELECTOR)
|| best.querySelector(CLICKABLE_FALLBACK);
if (!target) return false;
if ('checked' in target) {
if (!target.checked) target.click();
} else {
target.click();
}
return true;
};
// ========= 主流程 =========
const unlock = lockBackgroundScroll();
window.addEventListener('wheel', swallow, { passive: false, capture: true });
window.addEventListener('keydown', swallow, { capture: true });
try {
// 1) 回到頂端,準備偵測行高
await scrollToTopPx(0);
await wait(FIND_DELAY_MS);
// 2) 行高:使用者輸入優先,否則自動偵測
let rowHeight = USER_ROW_HEIGHT || detectRowHeight();
if (!Number.isFinite(rowHeight) || rowHeight <= 0) rowHeight = 67;
console.log('[Info] 行高(像素):', rowHeight);
// 3) 收集與驗證起點
const rowsByIndex = new Map();
// 先直接跳到起點附近
await scrollToTopPx((START_INDEX - 1) * rowHeight);
await wait(FIND_DELAY_MS);
collectVisibleRows(rowHeight, rowsByIndex);
// 如果還看不到起點,就往回/往前微調幾次
let adjustSteps = 0;
while (!rowsByIndex.has(START_INDEX) && adjustSteps < 8) {
// 先微調往上,再往下
const delta = (adjustSteps % 2 === 0 ? -1 : 1) * Math.ceil((adjustSteps + 1) / 2);
await scrollToTopPx((START_INDEX - 1 + delta) * rowHeight);
await wait(FIND_DELAY_MS);
collectVisibleRows(rowHeight, rowsByIndex);
adjustSteps++;
}
// 預覽區間:起點前後各 6 列
const previewStart = Math.max(1, START_INDEX - 6);
const previewEnd = START_INDEX + 6;
const sorted = Array.from(rowsByIndex.values()).sort((a, b) => a.index - b.index);
const preview = sorted.filter(r => r.index >= previewStart && r.index <= previewEnd);
if (preview.length) {
console.table(preview.map(r => ({
index: r.index,
title: r.title,
creators: r.creators?.join(' / ') || '',
checked: r.checked
})));
} else {
console.warn('在起點附近沒有成功收集到可見列,稍後仍會嘗試勾選。');
}
// 4) 二次確認是否繼續
const go = confirm(`已顯示第 ${START_INDEX} 附近的預覽。\n是否繼續自動勾選 ${START_INDEX}–${END_INDEX} ?`);
if (!go) { console.warn('已取消執行。'); return; }
// 5) 執行勾選
let ok = 0, fail = 0;
for (let i = START_INDEX; i <= END_INDEX; i++) {
await scrollToTopPx((i - 1) * rowHeight);
await wait(FIND_DELAY_MS);
const done = clickRowCheckboxByIndex(i, rowHeight);
if (done) ok++; else fail++;
// 順便豐富索引資料(非必要)
collectVisibleRows(rowHeight, rowsByIndex);
}
alert(`完成。成功勾選:${ok},未能定位/勾選:${fail}`);
} finally {
// 清理:解除事件攔截與背景鎖定
window.removeEventListener('wheel', swallow, { capture: true });
window.removeEventListener('keydown', swallow, { capture: true });
unlock();
}
})();
  1. 開啟該頁面 → 按 F12 或右鍵「檢查」 → 切到 Console。
  2. 貼上腳本 → Enter。
  3. 在提示框輸入範圍(例如 201-400),若知道列高也可輸入(或留空用自動偵測)。
  4. Console 會印出起點附近的預覽表格(index / 標題 / 作者 / 是否已勾選)。
  5. 確認後按「確定」,就會開始勾選。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment