您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Press Shift+Alt+Q to batch open links in the main list in a page. Handy when you want to quick explore all google search results or visit all details pages in a list page.
// ==UserScript== // @name Page Flood // @namespace [email protected] // @match http://*/* // @match https://*/* // @grant none // @version 2.1.1 // @author [email protected] // @description Press Shift+Alt+Q to batch open links in the main list in a page. Handy when you want to quick explore all google search results or visit all details pages in a list page. // ==/UserScript== // - get all links // - get features for each link // - group similar links // - get score for groups // - sort groups by score // - pick the best group // - open links in the best group main(); document.addEventListener("DOMContentLoaded", () => { console.debug(getMainListLinks()); }); function main() { globalThis.PageFloodController?.abort(); const ac = (globalThis.PageFloodController = new AbortController()); window.addEventListener( "keydown", async (e) => { if (e.shiftKey && e.altKey && e.code == "KeyQ") await openLinksInList(); }, { signal: ac.signal } ); } async function openLinksInList() { return await openLinks(getMainListLinks()); } async function openLinks(links) { // max 8 page on 1 origin once batch // max 16 page on all origin once batch const urlss = Object.values( Object.groupBy(links, (url, i) => String(Math.floor(i / 8))) ); for await (const urls of urlss) { const urlList = urls.join("\n"); const confirmMsg = `confirm to open ${urls.length} pages?\n\n${urlList}`; if (!confirm(confirmMsg)) throw alert("cancelled by user"); urls.toReversed().map(openDeduplicatedUrl); await new Promise((r) => setTimeout(r, 1e3)); // 1s cd await new Promise((r) => document.addEventListener("visibilitychange", r, { once: true }) ); // wait for page visible } } function openDeduplicatedUrl(url) { const opened = (globalThis.openDeduplicatedUrl_opened ??= new Set()); return opened.has(url) || (window.open(url, "_blank") && opened.add(url)); } function BagOfWordsModel() { const wordSet = new Set(); return { wordSet, fit: (texts) => { texts.forEach((text) => text .toLowerCase() .split(/\W+/) .forEach((word) => wordSet.add(word)) ); }, transform: (text) => { const words = text.toLowerCase().split(/\W+/); const vec = Array.from(wordSet).map((word) => words.includes(word) ? 1 : 0 ); return vec; }, }; } function getMainListLinks() { // groupBy words and then return map return ( [{ sel: "a" }] .map((e) => ({ ...e, list: [...document.querySelectorAll(e.sel)] })) .map((e) => ({ ...e, bow: BagOfWordsModel(), })) .map((e) => ({ ...e, _: e.bow.fit( e.list.map((el) => el.className + " " + getElementAttributeNames(el)) ), })) .map((e) => ({ ...e, vec: e.list.map((el, i) => [ elementDepth(el), area(el.parentElement?.getBoundingClientRect()), el.parentElement?.getBoundingClientRect().width, el.parentElement?.getBoundingClientRect().height, ...e.bow.transform(el.className + " " + getElementAttributeNames(el)), ]), })) .map((e) => ({ ...e, nor: normalize(e.vec) })) .map((e) => ({ ...e, vecGrp: groupByCosineSimilarity(e.nor, 0.99) })) .map((e) => ({ ...e, grp: e.vecGrp.map((g) => g.map((i) => e.list[i])) })) .map((e) => ({ ...e, rank: e.grp .map((g) => ({ links: g, area: area(maxRect(g.map((el) => el.getBoundingClientRect()))), areaSum: g .map((el) => area(el.getBoundingClientRect())) .reduce((a, b) => a + b, 0), })) .map((g) => ({ ...g, score: Math.log(g.area * g.areaSum) })) .toSorted(compareBy((g) => -g.score)), })) .map((e) => ({ ...e, _: e.rank .slice(0, 1) .map((grp, i, a) => grp.links.map((el) => flashBorder( el, getOklch(i / a.length), 500 + (a.length - i) * 500 ) ) ), })) // debug .map((e) => (console.log(e), { ...e })) .map((e) => e.rank.at(0).links.map((a) => a.href)) .at(0) ); } function getOklch(t) { const l = 0.9 - 0.5 * t; const c = 0.2 + 0.3 * t; const h = 360 * t; return `oklch(${l} ${c} ${h})`; } function flashBorder(el, color, duration = 1000) { const orig = el.style.outline; el.style.outline = `3px solid ${color}`; return setTimeout(() => (el.style.outline = orig), duration); } function compareBy(fn) { return (a, b) => fn(a) - fn(b); } function maxRect(rects) { return { left: Math.min(...rects.map((e) => e.left)), top: Math.min(...rects.map((e) => e.top)), right: Math.max(...rects.map((e) => e.right)), bottom: Math.max(...rects.map((e) => e.bottom)), }; } function area({ left, right, top, bottom }) { return (right - left) * (bottom - top); } function elementDepth(e) { return !e ? 0 : 1 + elementDepth(e.parentElement); } function normalize(arr) { const maxs = arr.reduce( (a, b) => a.map((e, i) => Math.max(e, b[i])), Array(arr[0].length).fill(-Infinity) ); return arr.map((e) => e.map((v, i) => v / maxs[i])); } function dot(a, b) { return a.reduce((s, v, i) => s + v * b[i], 0); } function magnitude(a) { return Math.sqrt(dot(a, a)); } function cosineSimilarity(a, b) { return dot(a, b) / (magnitude(a) * magnitude(b)); } function groupByCosineSimilarity(arr, threshold = 0.99) { const groups = []; const visited = new Set(); arr.forEach((vec, i) => { if (visited.has(i)) return; const group = [i]; visited.add(i); for (let j = i + 1; j < arr.length; j++) { if (cosineSimilarity(vec, arr[j]) > threshold) { group.push(j); visited.add(j); } } groups.push(group); }); return groups; } function getElementAttributeNames(el) { if (!el) return ""; const attrs = Array.from(el.attributes || []) .map((attr) => attr.name) .join(" "); return attrs; }