您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
7/2/2024, 8:37:14 PM
当前为
// ==UserScript== // @name Bobby's Pixiv Utils // @namespace https://github.com/BobbyWibowo // @match *://www.pixiv.net/* // @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net // @grant GM_addStyle // @grant GM_getValue // @run-at document-end // @version 1.3.5 // @author Bobby Wibowo // @license MIT // @description 7/2/2024, 8:37:14 PM // @noframes // ==/UserScript== (function () { 'use strict' /** CONFIG **/ const log = (message, ...args) => { return console.log(`[${Date.now()}]: ${message}`, ...args); } const logError = (message, ...args) => { return console.error(`[${Date.now()}]: ${message}`, ...args); } const ENV = { MODE: GM_getValue('MODE'), SELECTORS_IMAGE: GM_getValue('SELECTORS_IMAGE'), SELECTORS_IMAGE_TITLE: GM_getValue('SELECTORS_IMAGE_TITLE'), SELECTORS_IMAGE_ARTIST_AVATAR: GM_getValue('SELECTORS_IMAGE_ARTIST_AVATAR'), SELECTORS_IMAGE_ARTIST_NAME: GM_getValue('SELECTORS_IMAGE_ARTIST_NAME'), SELECTORS_IMAGE_CONTROLS: GM_getValue('SELECTORS_IMAGE_CONTROLS'), SELECTORS_EXPANDED_VIEW_CONTROLS: GM_getValue('SELECTORS_EXPANDED_VIEW_CONTROLS'), SELECTORS_MULTI_VIEW: GM_getValue('SELECTORS_MULTI_VIEW'), SELECTORS_MULTI_VIEW_CONTROLS: GM_getValue('SELECTORS_MULTI_VIEW_CONTROLS'), DATE_CONVERSION: GM_getValue('DATE_CONVERSION', true), SELECTORS_DATE: GM_getValue('SELECTORS_DATE'), ENABLE_KEYBINDS: GM_getValue('ENABLE_KEYBINDS', true), UTAGS_INTEGRATION: GM_getValue('UTAGS_INTEGRATION', true), UTAGS_BLOCKED_TAGS: GM_getValue('UTAGS_BLOCKED_TAGS') } /* DOCUMENTATION * ------------- * For any section that does not have complete selectors, it's implied that they are already matched using selectors contained in sections that preceded it * * Home's recommended works grid: * Image: .fhUcsb > li * Title: [data-ga4-label="title_link"] * Artist avatar: [data-ga4-label="user_icon_link"] * Artist name: [data-ga4-label="user_name_link"] * Controls: .kmCXcW * * Home's latest works grid: * Image: li[data-ga4-label="thumbnail"] * * Discovery page's grid: * Title: .gtm-illust-recommend-title * Controls: .dVtEKY * * Artist page's grid: * Image: .jtUPOE > li * Controls: .iHfghO * * Expanded view's artist works bottom row: * Image: .boBnlf > div * * Expanded view's related works grid: * Artist avatar: .eMfHJB * Artist name: .gtm-illust-recommend-user-name * * Artist page's featured works: * Image: .gmoaNn > li * Controls: .cGfNRT * * Bookmarks page's grid: * Title: .bOcolJ * Artist name: .IYOBi * * Tag page's grid: * Image: .hdRpMN > li */ // Preset values, then append with custom values if available const CONFIG = { MODE: 'PROD', SELECTORS_IMAGE: '.fhUcsb > li, li[data-ga4-label="thumbnail"], .jtUPOE > li, .boBnlf > div, .gmoaNn > li, .hdRpMN > li', // .hjtPnz > li, .hkzusx > div, .ranking-item, .iXWLAI > li, .cgtmvA li SELECTORS_IMAGE_TITLE: '[data-ga4-label="title_link"], .gtm-illust-recommend-title, .bOcolJ', // .hQOtRd SELECTORS_IMAGE_ARTIST_AVATAR: '[data-ga4-label="user_icon_link"], .eMfHJB', // .bwTmGA SELECTORS_IMAGE_ARTIST_NAME: '[data-ga4-label="user_name_link"], .gtm-illust-recommend-user-name, .IYOBi', // .jNkIXf SELECTORS_IMAGE_CONTROLS: '.kmCXcW, .dVtEKY, .iHfghO, .cGfNRT', // ._layout-thumbnail SELECTORS_EXPANDED_VIEW_CONTROLS: '.gMEAWM', SELECTORS_MULTI_VIEW: '[data-ga4-label="work_content"]', SELECTORS_MULTI_VIEW_CONTROLS: '& > .w-full:last-child > .flex:first-child > .flex-row:first-child', SELECTORS_DATE: '.dqHJfP', UTAGS_BLOCKED_TAGS: ['block', 'hide'] } for (const key of Object.keys(ENV)) { if (key.startsWith('SELECTORS_')) { if (ENV[key]) { CONFIG[key] += `, ${ENV[key]}`; } } else if (Array.isArray(CONFIG[key])) { if (ENV[key]) { const customValues = Array.isArray(ENV[key]) ? ENV[key] : ENV[key].split(',').map(s => s.trim()) CONFIG[key].push(...customValues); } } else if (ENV[key] !== undefined) { CONFIG[key] = ENV[key]; } } let logKeys = Object.keys(CONFIG); if (CONFIG.MODE === 'PROD') { // In PROD mode, only print some logKeys = ['DATE_CONVERSION', 'ENABLE_KEYBINDS', 'UTAGS_INTEGRATION']; } for (const key of logKeys) { log(`${key}: ${CONFIG[key]}`); } /** STYLES **/ const _formatSelectorsMultiViewControls = () => { const multiViews = CONFIG.SELECTORS_MULTI_VIEW.split(', '); const multiViewsControls = CONFIG.SELECTORS_MULTI_VIEW_CONTROLS.split(', '); const formatted = []; for (const x of multiViews) { for (const y of multiViewsControls) { let z = y; if (y.startsWith('&')) { z = y.substring(1) } formatted.push(`${x} ${z.trim()}`); } } return formatted; } const mainStyle = /*css*/` .flex:has(+.pu_edit_bookmark_container) { flex-grow: 1; } .pu_edit_bookmark { color: rgb(245, 245, 245); background: rgba(0, 0, 0, 0.32); display: block; box-sizing: border-box; padding: 0px 6px; margin-top: 7px; margin-right: 2px; border-radius: 10px; font-weight: bold; font-size: 10px; line-height: 20px; height: 20px; } ${CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS.split(', ').map(s => `${s} .pu_edit_bookmark`).join(', ')}, ${_formatSelectorsMultiViewControls().map(s => `${s} .pu_edit_bookmark`).join(', ')} { font-size: 12px; height: 24px; line-height: 24px; margin-top: 5px; margin-right: 7px; } ._layout-thumbnail .pu_edit_bookmark { position: absolute; right: calc(50% - 71px); bottom: 4px; z-index: 2; } ${CONFIG.SELECTORS_IMAGE_CONTROLS} { display: flex; justify-content: flex-end; } `; const globalDateStyle = /*css*/` .dqHJfP { font-size: 14px !important; font-weight: bold; color: rgb(214, 214, 214) !important; } `; const addPageDateStyle = /*css*/` .bookmark-detail-unit .meta { display: block; font-size: 16px; font-weight: bold; color: inherit; margin-left: 0; margin-top: 10px; } `; /** UTAGS INTEGRATION INIT **/ const globalUtagsStyle = /*css*/` .pu_blocked_image { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; border-radius: 4px; color: rgb(92, 92, 92); background-color: rgb(0, 0, 0); } .pu_blocked_image svg { fill: currentcolor; } .pu_image_is_blocked .earAVC { width: 184px; height: 184px; } ${CONFIG.SELECTORS_IMAGE_TITLE.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} { color: rgb(133, 133, 133) !important; } ${CONFIG.SELECTORS_IMAGE_ARTIST_AVATAR.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} { display: none; } ${CONFIG.SELECTORS_IMAGE_CONTROLS.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} { display: none; } `; const SELECTORS_UTAGS = CONFIG.UTAGS_BLOCKED_TAGS.map(s => `[data-utags_tag="${s}"]`).join(', '); log(`SELECTORS_UTAGS: ${SELECTORS_UTAGS}`); const BLOCKED_IMAGE_HTML = ` <div radius="4" class="pu_blocked_image"> <svg viewBox="0 0 24 24" style="width: 48px; height: 48px;"> <path d="M5.26763775,4 L9.38623853,11.4134814 L5,14.3684211 L5,18 L13.0454155,18 L14.1565266,20 L5,20 C3.8954305,20 3,19.1045695 3,18 L3,6 C3,4.8954305 3.8954305,4 5,4 L5.26763775,4 Z M9.84347336,4 L19,4 C20.1045695,4 21,4.8954305 21,6 L21,18 C21,19.1045695 20.1045695,20 19,20 L18.7323623,20 L17.6212511,18 L19,18 L19,13 L16,15 L15.9278695,14.951913 L9.84347336,4 Z M16,7 C14.8954305,7 14,7.8954305 14,9 C14,10.1045695 14.8954305,11 16,11 C17.1045695,11 18,10.1045695 18,9 C18,7.8954305 17.1045695,7 16,7 Z M7.38851434,1.64019979 L18.3598002,21.3885143 L16.6114857,22.3598002 L5.64019979,2.61148566 L7.38851434,1.64019979 Z"></path> </svg> </div> `; /** UTILS **/ const convertDate = elem => { const date = new Date(elem.getAttribute('datetime') || elem.innerText); if (!date) { return false; } const timestamp = String(date.getTime()); if (elem.dataset.oldTimestamp && elem.dataset.oldTimestamp === timestamp) { return false; } elem.dataset.oldTimestamp = timestamp; elem.innerText = date.toLocaleString("en-GB", { hour12: true, year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return true; } /** INTERCEPT SOME PAGES **/ const path = location.pathname; if (path.startsWith('/bookmark_add.php')) { if (CONFIG.DATE_CONVERSION) { GM_addStyle(addPageDateStyle); const date = document.querySelector('.bookmark-detail-unit .meta'); convertDate(date); } log(`/bookmark_add.php path detected, disabled mutation observer.`); return; } /** MAIN **/ GM_addStyle(mainStyle); if (CONFIG.DATE_CONVERSION) { GM_addStyle(globalDateStyle); } if (CONFIG.UTAGS_INTEGRATION) { GM_addStyle(globalUtagsStyle); } 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(); } } } const observerFactory = function (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 editBookmarkButton = (id, isNovel = false) => { const buttonContainer = document.createElement('div'); buttonContainer.className = 'pu_edit_bookmark_container'; const button = document.createElement('a'); button.className = 'pu_edit_bookmark'; button.innerText = 'Edit bookmark'; if (isNovel) { button.href = `https://www.pixiv.net/novel/bookmark_add.php?id=${id}`; } else { button.href = `https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=${id}`; } buttonContainer.appendChild(button); return buttonContainer; } const findItemId = element => { let id = null; let isNovel = false; let link = element.querySelector('a[href*="artworks/"]'); if (link) { const match = link.href.match(/artworks\/(\d+)/); id = match ? match[1] : null; } else { link = element.querySelector('a[href*="novel/show.php?id="]'); if (link) { const match = link.href.match(/novel\/show\.php\?id=(\d+)/); id = match ? match[1] : null; isNovel = true; } } return { id, isNovel }; } const isElementVisible = element => { if (!element || !element.isConnected) { return false; } return element.checkVisibility(); } const doImage = element => { if (!isElementVisible(element)) { return false; } // Skip if edit bookmark button already inserted if (element.querySelector('.pu_edit_bookmark')) { return false; } const imageControls = element.querySelector(CONFIG.SELECTORS_IMAGE_CONTROLS); if (!imageControls) { return false; } const { id, isNovel } = findItemId(element); if (id !== null) { imageControls.insertBefore(editBookmarkButton(id, isNovel), imageControls.firstChild); return true; } return false; } const doMultiView = element => { if (!isElementVisible(element)) { return false; } // Skip if edit bookmark button already inserted if (element.querySelector('.pu_edit_bookmark')) { return false; } const multiViewControls = element.querySelector(CONFIG.SELECTORS_MULTI_VIEW_CONTROLS); if (!multiViewControls) { return false; } const { id, isNovel } = findItemId(element); if (id !== null) { multiViewControls.insertBefore(editBookmarkButton(id, isNovel), multiViewControls.lastChild); return true; } return false; } const doExpandedViewControls = element => { if (!isElementVisible(element)) { return false; } // Skip if edit bookmark button already inserted if (element.querySelector('.pu_edit_bookmark')) { return false; } let id = null; let isNovel = false; let match = window.location.href.match(/artworks\/(\d+)/); if (match && match[1]) { id = match[1]; } else { match = window.location.href.match(/novel\/show\.php\?id=(\d+)/); if (match && match[1]) { id = match[1]; isNovel = true; } } if (id !== null) { element.appendChild(editBookmarkButton(id, isNovel)); return true; } return false; } const doUtagsImage = element => { if (!isElementVisible(element)) { return false; } // Append multi view artwork selector const image = element.closest(CONFIG.SELECTORS_IMAGE); if (image) { const imageLink = image.querySelector('a[href*="artworks/"], a[href*="novel/"]'); if (!imageLink) { return false; } // Skip if already blocked if (image.classList.contains('pu_image_is_blocked')) { return false; } image.classList.add('pu_image_is_blocked'); imageLink.innerHTML = BLOCKED_IMAGE_HTML; const imageTitle = image.querySelector(CONFIG.SELECTORS_IMAGE_TITLE); if (imageTitle) { if (element.dataset.utags_tag === "hide") { imageTitle.innerText = 'Hidden'; } else { // block tag and custom tags imageTitle.innerText = 'Blocked'; } } const artistLink = image.querySelector(CONFIG.SELECTORS_IMAGE_ARTIST_NAME); if (artistLink) { artistLink.innerText = ''; } return true; } const multiView = element.closest('[data-ga4-label="work_content"]'); if (multiView) { // For multi view artwork, just hide the whole entry instead multiView.parentNode.style.display = 'none'; return true; } const artistHeader = element.closest('.ggHNyV'); if (artistHeader) { const followButton = artistHeader.querySelector('.irfecv:not([disabled])'); if (followButton) { // This does not disable Pixiv's built-in F keybind followButton.disabled = true; return true; } } return false; } const 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; } // Always attempt to query from its parent, to allow the element itself to match the queries const target = mutation.target.parentElement || mutation.target; // Expanded View Controls const expandedViewControls = target.querySelector(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS); if (expandedViewControls && doExpandedViewControls(expandedViewControls)) { log(`Processed expanded view controls.`); } // Images let _image = 0; const images = target.querySelectorAll(CONFIG.SELECTORS_IMAGE); for (const image of images) { if (doImage(image)) { _image++; } } if (_image > 0) { log(`Processed ${_image} image(s).`); } // Multi Views let _multiView = 0; const multiViews = target.querySelectorAll(CONFIG.SELECTORS_MULTI_VIEW); for (const multiView of multiViews) { if (doMultiView(multiView)) { _multiView++; } } if (_multiView > 0) { log(`Processed ${_multiView} multi view(s).`); } // Dates if (CONFIG.DATE_CONVERSION) { let _date = 0; const dates = target.querySelectorAll(CONFIG.SELECTORS_DATE); for (const date of dates) { if (convertDate(date)) { _date++; } } if (_date > 0) { log(`Processed ${_date} date element(s).`); } } // UTags integration if (CONFIG.UTAGS_INTEGRATION) { let _utag = 0; const utags = target.querySelectorAll(SELECTORS_UTAGS); for (const utag of utags) { if (doUtagsImage(utag)) { _utag++; } } if (_utag > 0) { log(`Processed ${_utag} UTag(s).`); } } } }, ...args); }); /** KEYBINDS **/ if (CONFIG.ENABLE_KEYBINDS) { let onCooldown = {}; const processKeyEvent = (id, element) => { if (!element) { return false; } if (onCooldown[id]) { log(`"${id}" keybind still on cooldown.`); return false; } onCooldown[id] = true; element.click(); setTimeout(() => { onCooldown[id] = false }, 1000); } document.addEventListener('keydown', event => { event = event || window.event; // Ignore keybinds when currently focused to an input/textarea/editable element if (document.activeElement && (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable)) { return; } // Shift+B for Edit Bookmark // Pixiv has built-in keybind B for just bookmarking if (event.keyCode === 66) { if (event.ctrlKey || event.altKey) { // Ignore Ctrl+B or Alt+B return; } if (event.shiftKey) { event.stopPropagation(); const element = document.querySelector('.gpoeGt .pu_edit_bookmark'); return processKeyEvent('bookmarkEdit', element); } } }); log('Listening for keybinds.'); } else { log('Keybinds disabled.'); } })()