您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
09/02/2025, 16.57.34
当前为
// ==UserScript== // @name Article Copy Button // @namespace https://github.com/BobbyWibowo // @include http://* // @include https://* // @grant GM_addStyle // @grant GM_getValue // @version 1.1.5 // @author Bobby Wibowo // @license MIT // @run-at document-end // @description 09/02/2025, 16.57.34 // ==/UserScript== (function () { 'use strict' const _logTime = () => { return new Date().toLocaleTimeString([], { hourCycle: 'h12', hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) .replaceAll('.', ':') .replace(',', '.') .toLocaleUpperCase(); }; const log = (message, ...args) => { const prefix = `[${_logTime()}]: `; if (typeof message === 'string') { return console.log(prefix + message, ...args); } else { return console.log(prefix, message, ...args); } }; const ARTICLES_CONFIG = [ { whitelistedHosts: [ 'quanben-xiaoshuo.com' ], parentSelector: '.wrapper .box', titleSelector: '.title', articleSelector: '#articlebody', AIPromptPrefix: 'Display the translation of this web novel chapter, retaining all original text without any loss in translation, and incorporating the remembered glossary:\n\n' }, { parentSelector: '.wp-manga-page', titleSelector: '.breadcrumb .active', articleSelector: '.reading-content div[class*="text-"]', AIPromptPrefix: 'Improve readability of this web novel chapter, without any loss:\n\n' }, { parentSelector: '#chapter', titleSelector: '.chapter-title', articleSelector: '.chapter-c' }, { whitelistedHosts: [ 'www.hoyolab.com' ], mutationObserver: true, // this option is ignored without whitelistedHosts for performance parentSelector: '.mhy-article-page', titleSelector: '.mhy-article-page__title h1', articleSelector: '.mhy-article-page__content' }, { whitelistedHosts: [ 'www.reddit.com' ], mutationObserver: true, parentSelector: 'shreddit-post', titleSelector: '[id^="post-title-"]', articleSelector: 'div[slot="text-body"], div[slot="expando-content"]' }, { whitelistedHosts: [ 'old.reddit.com' ], mutationObserver: true, parentSelector: 'div[id^="thing_"]', titleSelector: 'a.title', articleSelector: '.expando:not(.expando-uninitialized)' } ]; const ENV = { MODE: GM_getValue('MODE', 'PROD') }; let logDebug = () => {}; if (ENV.MODE !== 'PROD') { log(`MODE =`, ENV.MODE); logDebug = log; } const GLOBAL_STYLE = /*css*/` .copy-article-button-container { display: flex; justify-content: center; gap: 4px; margin-bottom: 16px; } .copy-article-button { --button-bg: #e5e6eb; --button-hover-bg: #d7dbe2; --button-text-color: #4e5969; --button-hover-text-color: #164de5; --button-border-radius: 6px; --button-diameter: 24px; --button-outline-width: 2px; --button-outline-color: #9f9f9f; --tooltip-bg: #1d2129; --toolptip-border-radius: 4px; --tooltip-font-family: JetBrains Mono, Consolas, Menlo, Roboto Mono, monospace; --tooltip-font-size: 12px; --tootip-text-color: #fff; --tooltip-padding-x: 7px; --tooltip-padding-y: 7px; --tooltip-offset: 8px; /* --tooltip-transition-duration: 0.3s; */ } html[data-darkreader-scheme="dark"] .copy-article-button, body[class*="text-ui-light"] .copy-article-button, main[class*="dark-background"] .copy-article-button { --button-bg: #353434; --button-hover-bg: #464646; --button-text-color: #ccc; --button-outline-color: #999; --button-hover-text-color: #8bb9fe; --tooltip-bg: #f4f3f3; --tootip-text-color: #111; } .copy-article-button { box-sizing: border-box; width: var(--button-diameter); height: var(--button-diameter); border-radius: var(--button-border-radius); background-color: var(--button-bg); color: var(--button-text-color); border: none; cursor: pointer; position: relative; outline: var(--button-outline-width) solid transparent; transition: all 0.2s ease; } .tooltip { position: absolute; opacity: 0; left: calc(100% + var(--tooltip-offset)); top: 50%; transform: translateY(-50%); white-space: nowrap; font: var(--tooltip-font-size) var(--tooltip-font-family); color: var(--tootip-text-color); background: var(--tooltip-bg); padding: var(--tooltip-padding-y) var(--tooltip-padding-x); border-radius: var(--toolptip-border-radius); pointer-events: none; transition: all var(--tooltip-transition-duration) cubic-bezier(0.68, -0.55, 0.265, 1.55); z-index: 1; } .tooltip::before { content: attr(data-text-initial); } .tooltip::after { content: ""; width: var(--tooltip-padding-y); height: var(--tooltip-padding-y); background: inherit; position: absolute; top: 50%; left: calc(var(--tooltip-padding-y) / 2 * -1); transform: translateY(-50%) rotate(45deg); z-index: -999; pointer-events: none; } .copy-article-button svg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .checkmark, .failedmark { display: none; } .copy-article-button:hover .tooltip, .copy-article-button:focus:not(:focus-visible) .tooltip { opacity: 1; visibility: visible; } .copy-article-button:focus:not(:focus-visible) .tooltip::before { content: attr(data-text-end); } .copy-article-button.copy-failed:focus:not(:focus-visible) .tooltip::before { content: attr(data-text-failed); } .copy-article-button:focus:not(:focus-visible) .clipboard { display: none; } .copy-article-button:focus:not(:focus-visible) .checkmark { display: block; } .copy-article-button.copy-failed:focus:not(:focus-visible) .checkmark { display: none; } .copy-article-button.copy-failed:focus:not(:focus-visible) .failedmark { display: block; } .copy-article-button:hover, .copy-article-button:focus { background-color: var(--button-hover-bg); } .copy-article-button:active { outline: var(--button-outline-width) solid var(--button-outline-color); } .copy-article-button:hover svg { color: var(--button-hover-text-color); } `; let globalStyleAdded = false; let observedConfigs = []; let observerInitiated = false; let triggerQueue = null; function formatArticle({ title, article, AIPromptPrefix = '' }) { const header = title ? title.innerText.trim() : null; const body = article.innerText.trim(); const formatted = (header ? `${header}\n\n` : '') + body; return AIPromptPrefix + formatted; } function handleCopyError({ event, title, article, error = new Error() }) { logError('Could not copy: ', error); event.element.classList.add('copy-failed'); } function copyTextToClipboard({ event, title, article, text }) { navigator.clipboard.writeText(text).then(function () { log('Article copied to clipboard.\n\n' + text); }).catch(function (error) { handleCopyError({ event, title, article, error }) }); } function handleArticleCopyClick({ event, title, article, AIPromptPrefix }) { event.stopPropagation(); try { const text = formatArticle({ title, article, AIPromptPrefix }); copyTextToClipboard({ event, title, article, text }); } catch (error) { handleCopyError({ event, title, article, error }) } } const BUTTON_INNER_TEMPLATE = /*html*/` <span data-text-initial="Copy to clipboard" data-text-end="Copied" data-text-failed="Copy failed, open the console for details!" class="tooltip"></span> <span> <svg xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 6.35 6.35" y="0" x="0" height="14" width="14" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" xmlns="http://www.w3.org/2000/svg" class="clipboard"> <g> <path fill="currentColor" d="M2.43.265c-.3 0-.548.236-.573.53h-.328a.74.74 0 0 0-.735.734v3.822a.74.74 0 0 0 .735.734H4.82a.74.74 0 0 0 .735-.734V1.529a.74.74 0 0 0-.735-.735h-.328a.58.58 0 0 0-.573-.53zm0 .529h1.49c.032 0 .049.017.049.049v.431c0 .032-.017.049-.049.049H2.43c-.032 0-.05-.017-.05-.049V.843c0-.032.018-.05.05-.05zm-.901.53h.328c.026.292.274.528.573.528h1.49a.58.58 0 0 0 .573-.529h.328a.2.2 0 0 1 .206.206v3.822a.2.2 0 0 1-.206.205H1.53a.2.2 0 0 1-.206-.205V1.529a.2.2 0 0 1 .206-.206z"> </path> </g> </svg> <svg xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 24 24" y="0" x="0" height="14" width="14" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" xmlns="http://www.w3.org/2000/svg" class="checkmark"> <g> <path data-original="#000000" fill="currentColor" d="M9.707 19.121a.997.997 0 0 1-1.414 0l-5.646-5.647a1.5 1.5 0 0 1 0-2.121l.707-.707a1.5 1.5 0 0 1 2.121 0L9 14.171l9.525-9.525a1.5 1.5 0 0 1 2.121 0l.707.707a1.5 1.5 0 0 1 0 2.121z"> </path> </g> </svg> <svg class="failedmark" xmlns="http://www.w3.org/2000/svg" height="14" width="14" viewBox="0 0 512 512"> <path fill="#FF473E" d="m330.443 256l136.765-136.765c14.058-14.058 14.058-36.85 0-50.908l-23.535-23.535c-14.058-14.058-36.85-14.058-50.908 0L256 181.557L119.235 44.792c-14.058-14.058-36.85-14.058-50.908 0L44.792 68.327c-14.058 14.058-14.058 36.85 0 50.908L181.557 256L44.792 392.765c-14.058 14.058-14.058 36.85 0 50.908l23.535 23.535c14.058 14.058 36.85 14.058 50.908 0L256 330.443l136.765 136.765c14.058 14.058 36.85 14.058 50.908 0l23.535-23.535c14.058-14.058 14.058-36.85 0-50.908z" /> </svg> </span> `; class FunctionQueue { constructor() { this.queue = []; this.running = false; } async go() { if (this.queue.length) { this.running = true; const _func = this.queue.shift(); await _func[0](..._func[1]); this.go(); } else { this.running = false; } } add(func, ...args) { this.queue.push([func, [...args]]); if (!this.running) { this.go(); } } clear() { this.queue.length = 0; } }; const observerFactory = option => { let options; if (typeof option === 'function') { options = { callback: option, node: document.getElementsByTagName('body')[0], option: { childList: true, subtree: true } }; } else { options = $.extend({ callback: () => {}, node: document.getElementsByTagName('body')[0], option: { childList: true, subtree: true } }, option); } const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; const observer = new MutationObserver((mutations, observer) => { options.callback.call(this, mutations, observer); }); observer.observe(options.node, options.option); return observer; }; const findArticles = (config, source = document.body) => { let parents = []; if (source.matches(config.parentSelector)) { parents = [source]; } else { // Loop through query results of parents, to support multiple articles at once. parents = source.querySelectorAll(config.parentSelector); // Also try to look up. if (!parents.length) { const up = source.closest(config.parentSelector); if (up) { parents = [up]; } } } if (!parents.length) { return null; } logDebug(`Found ${parents.length} element(s) matching parent selector: ${config.parentSelector}`); let _done = 0; for (const parent of parents) { const article = parent.querySelector(config.articleSelector); if (!article) { continue; } // Skip if already processed. if (article.querySelector('.copy-article-button-container')) { continue; } logDebug(`Found element matching article selector: ${config.articleSelector}`); const title = parent.querySelector(config.titleSelector); const copyButtonContainer = document.createElement('div'); copyButtonContainer.className = 'copy-article-button-container'; const copyButton = document.createElement('button'); copyButton.className = 'copy-article-button'; copyButton.innerHTML = BUTTON_INNER_TEMPLATE; copyButton.addEventListener('click', event => handleArticleCopyClick({ event, title, article })); copyButtonContainer.appendChild(copyButton); if (config.AIPromptPrefix) { const copyButtonAI = document.createElement('button'); copyButtonAI.className = 'copy-article-button'; copyButtonAI.innerHTML = BUTTON_INNER_TEMPLATE; copyButtonAI.querySelector(':first-child').dataset.textInitial += ' (with AI prompt prefix)'; copyButtonAI.addEventListener('click', event => handleArticleCopyClick({ event, title, article, AIPromptPrefix: config.AIPromptPrefix })); copyButtonContainer.appendChild(copyButtonAI); } if (!globalStyleAdded) { GM_addStyle(GLOBAL_STYLE); globalStyleAdded = true; } article.insertAdjacentElement('afterbegin', copyButtonContainer); _done++; } return _done; }; const initObserver = config => { observedConfigs.push(config); if (!observerInitiated) { log('Initiating Mutation Observer for this host.'); observerInitiated = true; triggerQueue = new FunctionQueue(); observerFactory((...args) => { triggerQueue.add((mutations, observer) => { for (let i = 0, len = mutations.length; i < len; i++) { const mutation = mutations[i]; // Whether to change nodes. if (mutation.type !== 'childList') { continue; } let _observerDone = 0; for (const config of observedConfigs) { const done = findArticles(config, mutation.target); if (done !== null) { _observerDone += done; } } if (_observerDone > 0) { log(`Added ${_observerDone} copy button(s) via Mutation Observer.`); } } }, ...args); }); } }; let _rootDone = 0; for (const config of ARTICLES_CONFIG) { let initMutationObsever = false; // Skip config if it's whitelisted for specific hosts yet it doesn't match current host. if (Array.isArray(config.whitelistedHosts)) { let hostPassed = false; for (const host of config.whitelistedHosts) { let _pass = true; if (host instanceof RegExp) { if (host.test(window.location.hostname)) { hostPassed = true; } } else if (host === window.location.hostname) { hostPassed = true; } } if (!hostPassed) { continue; } log(`Host whitelisted for parent selector: ${config.parentSelector}`); if (config.mutationObserver) { initMutationObsever = true; } } if (!initMutationObsever && config.mutationObserver) { log(`Mutation Observer can only be used for config with whitelisted hosts.`); } if (initMutationObsever) { initObserver(config); } else { const done = findArticles(config); if (done !== null) { _rootDone += done; } } } if (_rootDone > 0) { log(`Added ${_rootDone} copy button(s).`); } })()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址