您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
键页通支持为不同网页(URL)独立配置按键策略与翻页按钮定位规则。自动识别当前访问网址,精准匹配预设的自定义按键及翻页按钮CSS选择器,实现一键翻页的个性化操控体验。
// ==UserScript== // @name PagePilot // @name:zh-CN 键页通 // @namespace https://greasyfork.org/scripts/541642 // @version 0.2.1 // @author oajsdfk // @description PagePilot allows custom keybindings and CSS selectors for page-turning buttons to be uniquely configured per URL. Automatically detects webpage addresses and applies your preset rules, enabling precise one-click navigation across all sites. // @description:zh-CN 键页通支持为不同网页(URL)独立配置按键策略与翻页按钮定位规则。自动识别当前访问网址,精准匹配预设的自定义按键及翻页按钮CSS选择器,实现一键翻页的个性化操控体验。 // @license MIT // @match *://*/* // @grant GM_addElement // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; function pilot(url, event, options2) { const find = (keys) => { if (!keys) return false; if (event.type === "keyup") { if (keys.key?.find((i) => i === event.key)) return true; if (keys.code?.find((i) => i === event.code)) return true; } else if (event.type === "mouseup") { if (keys.button === event.button) return true; } return false; }; const g = []; for (const [k, v] of Object.entries(options2.globalKeys)) { if (find(v)) g.push(k); } function matchGlobal(k, p) { if (find(p.keys)) return true; if (typeof p.globalKeys === "boolean") { if (!p.globalKeys) return false; return g.indexOf(k) != -1; } else if (typeof p.globalKeys === "string") { return g.indexOf(p.globalKeys) != -1; } return g.indexOf(k) != -1; } function matchKey(p) { if (p.cur) { if (!p.next && !p.prev && !p.other) { for (const i of g) { if (i === "next") return { child: p.cur, sibling: 1 }; if (i === "prev") return { child: p.cur, sibling: -1 }; } return; } for (const k of ["prev", "next"]) { if (!p[k]) continue; let r = typeof p[k] === "string" ? { child: p.cur, sibling: k === "next" ? 1 : -1, deep: { child: p[k] } } : p[k]; if (matchGlobal(k, r)) return r; } for (const [k, v] of Object.entries(p.other ?? [])) { let r = typeof v === "string" ? { child: p.cur, sibling: k === "next" ? 1 : -1, deep: { child: v } } : v; if (matchGlobal(k, r)) return r; } } else { for (const k of ["prev", "next"]) { if (!p[k]) continue; let r = typeof p[k] === "string" ? { child: p[k] } : p[k]; if (matchGlobal(k, r)) return r; } for (const [k, v] of Object.entries(p.other ?? [])) { let r = typeof v === "string" ? { child: v } : v; if (matchGlobal(k, r)) return r; } } return; } for (const [r, p] of Object.entries(options2.pilots)) { if (!url.match(new RegExp(r))) continue; const q = matchKey(p); if (!q) continue; const { child, sibling, deep } = q; if (!child) continue; const cur = document.querySelector(child); const ele = queryDeep({ sibling, deep }, cur); if (!ele) continue; ele.click(); return ele; } return null; } function queryDeep({ sibling, child, deep }, e) { if (child) e = e?.querySelector(child); if (sibling) { let c = Math.abs(sibling); while (c > 0 && e) { c--; e = sibling > 0 ? e.nextElementSibling : e.previousElementSibling; } if (c !== 0) return null; } return deep ? queryDeep(deep, e) : e; } function renderPilotConfig(root, options2) { root.innerHTML = ` <style> #pilot-config-form button { transition: background 0.2s, color 0.2s, box-shadow 0.2s; } #pilot-config-form button:not([type="submit"]):hover, #pilot-config-form button:not([type="submit"]):focus { background: #ffd369; color: #232931; box-shadow: 0 2px 8px rgba(255, 211, 105, 0.15); } #pilot-config-form button:not([type="submit"]):active { background: #e1c15a; color: #232931; } #pilot-config-form button[type="submit"]:hover, #pilot-config-form button[type="submit"]:focus { background: #ff6b7c; color: #fff; box-shadow: 0 2px 8px rgba(255, 75, 92, 0.15); } #pilot-config-form button[type="submit"]:active { background: #d93a4a; color: #fff; } #pilot-config-form button[type="reset"]:hover, #pilot-config-form button[type="reset"]:focus { background: #ffd369; color: #232931; box-shadow: 0 2px 8px rgba(255, 211, 105, 0.15); } #pilot-config-form button[type="reset"]:active { background: #e1c15a; color: #232931; } #pilot-config-form button { border: none; border-radius: 6px; padding: 8px 18px; font-weight: 500; cursor: pointer; background: #393e46; color: #ffd369; } </style> <form style=" display: flex; flex-direction: column; height: 100%; width: 100%; padding: 24px 32px; box-sizing: border-box; z-index: 50; position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: linear-gradient(135deg, #232526 0%, #414345 100%); color: #f5f6fa; font-family: 'Segoe UI', 'Roboto', sans-serif; font-size: 16px; overflow-y: auto; " id="pilot-config-form"> <h1 style=" font-weight: bold; text-align: center; height: 48px; margin-bottom: 20px; letter-spacing: 1px; color: #ffd369; text-shadow: 0 2px 8px rgba(0,0,0,0.2); ">Page Pilot Setting</h1> <textarea id="pagepilot_detail" style=" flex-grow: 1; resize: none; border-radius: 8px; border: 1px solid #393e46; background: #232931; color: #f5f6fa; padding: 12px; margin-bottom: 18px; font-size: 15px; outline: none; box-shadow: 0 2px 8px rgba(0,0,0,0.08); "> </textarea> <div style=" display: flex; flex-direction: row; height: 48px; gap: 14px; justify-content: center; align-items: center; padding: 0; margin-bottom: 8px; "> <button type="button" id="pilot-sample-btn">Sample</button> <button type="button" id="pilot-clear-btn">Clear</button> <button type="button" id="pilot-reset-btn">Reset</button> <button type="button" id="pilot-import-btn">Import</button> <button type="submit" style=" background: #ff4b5c; color: #fff; ">Save</button> <button type="reset" style=" background: #393e46; color: #fff; ">Close</button> </div> </form> `; const form = root.querySelector("#pilot-config-form"); const textareaEl = root.querySelector("#pagepilot_detail"); form.onsubmit = (e) => { e.preventDefault(); options2.save?.(); }; form.onreset = (e) => { e.preventDefault(); options2.close?.(); }; root.querySelector("#pilot-sample-btn").onclick = () => { if (!options2.sample) return; textareaEl.value = options2.sample(); }; root.querySelector("#pilot-clear-btn").onclick = () => { if (!options2.clear) return; options2.clear(); textareaEl.value = ""; }; root.querySelector("#pilot-reset-btn").onclick = () => { if (!options2.export) return; textareaEl.value = options2.export(); }; root.querySelector("#pilot-import-btn").onclick = () => { if (!options2.import) return; textareaEl.value = options2.import(textareaEl.value); }; } function stringifyWithDepth(obj, maxDepth, space) { function helper(value, depth) { if (depth > maxDepth || value === null || typeof value !== "object") { return JSON.stringify(value); } if (Array.isArray(value)) { const items = value.map((item) => helper(item, depth + 1)); if (depth < maxDepth) { return "[\n" + items.map((i) => " ".repeat((depth + 1) * space) + i).join(",\n") + "\n" + " ".repeat(depth * space) + "]"; } else { return "[" + items.join(", ") + "]"; } } else { const entries = Object.entries(value).map( ([k, v]) => depth < maxDepth ? " ".repeat((depth + 1) * space) + JSON.stringify(k) + ": " + helper(v, depth + 1) : JSON.stringify(k) + ": " + helper(v, depth + 1) ); if (depth < maxDepth) { return "{\n" + entries.join(",\n") + "\n" + " ".repeat(depth * space) + "}"; } else { return "{" + entries.join(", ") + "}"; } } } return helper(obj, 0); } const lastOptionsVer = GM_getValue("pagepilot_options_ver"); let options = GM_getValue("pagepilot_options"); const optionsVer = 1; if (!options || lastOptionsVer !== optionsVer) { options = { globalKeys: { // 绑定名称的通用按键,所有网站的元素根据名称自动绑定到这些按键上 // Common keys bound by name, elements on all sites will be automatically bound to these keys prev: { code: ["ArrowUp", "PageUp", "KeyP", "KeyD"], button: 5 }, next: { code: ["ArrowDown", "PageDown", "KeyN", "KeyF"], button: 4 }, top: { code: ["Home", "KeyT"] }, bottom: { code: ["End", "KeyB"] } }, pilots: { // 站点正则 => 元素选择器或按键 // site regex => element selector or key "http://example1.com(/.*)?": { // 当点击元素对应的按键时候,触发元素的点击事件: PageUp => querySelector('.pg .prev_btn').click() // When the corresponding key is pressed, trigger the element's click event: PageUp => querySelector('.pg .prev_btn').click() prev: ".pg .prev_btn", next: ".pg .next_btn" }, // 通过当前页元素的兄弟来定位翻页按钮 // locate the page-turning buttons by the siblings of the current page element "https://(.*.)?example2.org(/.*)?": { cur: "#cur_page" }, // 通过当前页元素的兄弟的子孙来定位翻页按钮 // locate the page-turning buttons by the descendants of the siblings of the current page element "https?://(.*.)?example3.org(/.*)?": { cur: "#cur_page", prev: "a", // querySelector('#cur_page').prevElment.querySelector(a).click() next: "a" // querySelector('#cur_page').nextElment.querySelector(a).click() }, "https?://(.*.)?example4.org(/.*)?": { // 多层级子孙、兄弟的翻页按钮的定位规则: querySelector('.nav .pg').prevE.prevE.prevE.querySelector(a).click() // multi-level descendants and siblings page button location rules (): querySelector('.nav .pg').prevE.prevE.prevE.querySelector(a).click() prev: { child: ".nav", deep: { child: ".pg", sibling: -3, deep: { child: "a" } } }, next: { child: ".nav", deep: { child: ".pg", sibling: 5, // querySelector('.nav .pg') .nextE x5 .querySelector(a).click() deep: { child: "a" } } }, // 定义翻页以外的任意按钮 // define any buttons other than page turning other: { // 显示指定绑定或者禁用全局按键 // Explicitly bind or disable global keys top: "#top", bottom: { // 显示指定绑定或者禁用全局按键 // Explicitly bind or disable global keys globalKeys: false, // disable child: "#bottomBtn", // 指定只对该网站生效的按键 // Specify keys that only take effect on this site keys: { code: ["KeyN"] } }, close: { child: "#closeBtn", keys: { code: ["KeyC"] } } } } } }; GM_setValue("pagepilot_options", options); GM_setValue("pagepilot_options_ver", optionsVer); } console.log({ pagepilot_options_ver: optionsVer, pagepilot_options: options }); window.addEventListener("keyup", function(e) { const ae = document.activeElement; if (ae && ["input", "select", "button", "textarea"].indexOf( ae.tagName.toLowerCase() ) !== -1) return; console.log({ key: e.key, code: e.code, event: e }); if (pilot(window.location.href, e, options)) { e.preventDefault(); } }); window.addEventListener("mouseup", function(e) { if (e.button < 2) return; console.log({ button: e.button, event: e }); if (pilot(window.location.href, e, options)) { e.preventDefault(); } }); GM_registerMenuCommand( "PagePilotSetting", (_event) => { let root = document.getElementById("pagepilot_options"); if (root) { root.setAttribute("hidden", "false"); return; } root = GM_addElement(document.body, "div", { id: "pagepilot_options" }); renderPilotConfig(root, { export() { return stringifyWithDepth(options, 4, 2); }, save() { GM_setValue("pagepilot_options", options); }, close() { root.setAttribute("hidden", "true"); }, import(json) { try { const j = JSON.parse(json); for (const [k, v] of Object.entries(j.globalKeys ?? {})) { options.globalKeys[k] = v; } for (const [k, v] of Object.entries(j.pilots ?? {})) { options.pilots[k] = v; } GM_setValue("pagepilot_options", options); return stringifyWithDepth(options, 4, 2); } catch (e) { console.error(e); return stringifyWithDepth(options, 4, 2); } }, clear() { options = { globalKeys: {}, pilots: {} }; }, sample() { const s = { globalKeys: { prev: { code: ["ArrowUp", "PageUp", "KeyP", "KeyD"] }, next: { code: ["ArrowDown", "PageDown", "KeyN", "KeyF"] }, top: { code: ["Home", "KeyT"] }, bottom: { code: ["End", "KeyB"] } }, pilots: { "http://example1.com(/.*)?": { prev: ".pg .prev_btn", next: ".pg .next_btn" }, "https://(.*.)?example2.org(/.*)?": { cur: "#cur_page" }, "https?://(.*.)?example3.org(/.*)?": { cur: "#cur_page", prev: "a", next: "a" }, "https?://(.*.)?example4.org(/.*)?": { prev: { child: ".nav", deep: { child: ".pg", sibling: -3, deep: { child: "a" } } }, next: { child: ".nav", deep: { child: ".pg", sibling: 5, deep: { child: "a" } } }, other: { top: "#top", bottom: { globalKeys: false, child: "#bottomBtn", keys: { code: ["KeyN"] } }, close: { child: "#closeBtn", keys: { code: ["KeyC"] } } } } } }; return JSON.stringify(s, null, 2); } }); } ); })();