您需要先安装一个扩展,例如 篡改猴、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/*
- // @exclude-match *://www.pixiv.net/setting*
- // @exclude-match *://www.pixiv.net/manage*
- // @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.8
- // @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'),
- TEXT_EDIT_BOOKMARK: GM_getValue('EDIT_BOOKMARK_TEXT', 'Edit bookmark'),
- // The following options have preset values. Scroll further to find them.
- // Specifiying custom values will extend instead of replacing them.
- 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),
- DATE_CONVERSION_LOCALES: GM_getValue('DATE_CONVERSION_LOCALES', 'en-GB'),
- DATE_CONVERSION_OPTIONS: GM_getValue('DATE_CONVERSION_OPTIONS', {
- hour12: true,
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- }),
- // This has a preset value. Specifiying a custom value will extend instead of replacing it.
- SELECTORS_DATE: GM_getValue('SELECTORS_DATE'),
- REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME: GM_getValue('REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME', false),
- ENABLE_KEYBINDS: GM_getValue('ENABLE_KEYBINDS', true),
- UTAGS_INTEGRATION: GM_getValue('UTAGS_INTEGRATION', true),
- // Presets "block" and "hide" tags. Specifying custom values will extend instead of replacing them.
- UTAGS_BLOCKED_TAGS: GM_getValue('UTAGS_BLOCKED_TAGS'),
- // Instead of merely hiding them à la Pixiv's built-in tags mute.
- UTAGS_REMOVE_BLOCKED: GM_getValue('UTAGS_REMOVE_BLOCKED', false)
- }
- /* 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
- *
- * Rankings page:
- * Image: .ranking-item
- * Title: .title
- * Artist avatar: ._user-icon
- * Artist name: .user-name
- * Controls: ._layout-thumbnail
- */
- const CONFIG = {
- MODE: 'PROD',
- SELECTORS_IMAGE: '.fhUcsb > li, li[data-ga4-label="thumbnail"], .jtUPOE > li, .boBnlf > div, .gmoaNn > li, .hdRpMN > li, .ranking-item', // .hjtPnz > li, .hkzusx > div, .iXWLAI > li, .cgtmvA li
- SELECTORS_IMAGE_TITLE: '[data-ga4-label="title_link"], .gtm-illust-recommend-title, .bOcolJ, .title', // .hQOtRd
- SELECTORS_IMAGE_ARTIST_AVATAR: '[data-ga4-label="user_icon_link"], .eMfHJB, ._user-icon', // .bwTmGA
- SELECTORS_IMAGE_ARTIST_NAME: '[data-ga4-label="user_name_link"], .gtm-illust-recommend-user-name, .IYOBi, .user-name', // .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']
- }
- // Extend preset values with user-defined custom values if applicable.
- 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', 'REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME', 'ENABLE_KEYBINDS', 'UTAGS_INTEGRATION'];
- }
- for (const key of logKeys) {
- log(`${key}: ${CONFIG[key]}`);
- }
- /** GLOBAL UTILS **/
- const addPageDateStyle = /*css*/`
- .bookmark-detail-unit .meta {
- display: block;
- font-size: 16px;
- font-weight: bold;
- color: inherit;
- margin-left: 0;
- margin-top: 10px;
- }
- `;
- const convertDate = (element, fixJapanTime = false) => {
- let date;
- const attr = element.getAttribute('datetime');
- if (attr) {
- date = new Date(attr);
- } else {
- // For pages which have the date display hardcoded to Japan time.
- let dateText = element.innerText;
- if (fixJapanTime) {
- dateText += ' UTC+9';
- }
- date = new Date(dateText);
- }
- if (!date) {
- return false;
- }
- const timestamp = String(date.getTime());
- if (element.dataset.oldTimestamp && element.dataset.oldTimestamp === timestamp) {
- return false;
- }
- element.dataset.oldTimestamp = timestamp;
- element.innerText = date.toLocaleString(CONFIG.DATE_CONVERSION_LOCALES, CONFIG.DATE_CONVERSION_OPTIONS);
- return true;
- }
- /** INTERCEPT EARLY FOR CERTAIN ROUTES **/
- const path = location.pathname;
- // Codes beyond this block will not execute for this route (mainly for efficiency).
- if (path.startsWith('/bookmark_add.php')) {
- if (CONFIG.DATE_CONVERSION) {
- GM_addStyle(addPageDateStyle);
- const date = document.querySelector('.bookmark-detail-unit .meta');
- // This page has the date display hardcoded to Japan time without an accompanying timestamp.
- convertDate(date, true);
- }
- log(`/bookmark_add.php path detected. Excluding date conversion, script has terminated early.`);
- return;
- }
- /** MAIN STYLES **/
- // To properly handle "&" CSS keyword, in context of also having to support user-defined custom values.
- // Somewhat overkill, but I'm out of ideas.
- 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;
- }
- .ranking-item.muted .pu_edit_bookmark {
- display: none;
- }
- ${CONFIG.SELECTORS_IMAGE_CONTROLS} {
- display: flex;
- justify-content: flex-end;
- }
- `;
- const mainDateStyle = /*css*/`
- .dqHJfP {
- font-size: 14px !important;
- font-weight: bold;
- color: rgb(214, 214, 214) !important;
- }
- `;
- /** UTAGS INTEGRATION INIT **/
- const mainUtagsStyle = /*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;
- }
- .ranking-item.pu_image_is_blocked .work {
- width: 150px;
- height: 150px;
- }
- ${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>
- `;
- /** MAIN **/
- GM_addStyle(mainStyle);
- if (CONFIG.DATE_CONVERSION) {
- GM_addStyle(mainDateStyle);
- }
- if (CONFIG.UTAGS_INTEGRATION) {
- GM_addStyle(mainUtagsStyle);
- }
- 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 = CONFIG.TEXT_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 findLink = element => {
- return element.querySelector('a[href*="artworks/"]');
- }
- const findNovelLink = element => {
- return element.querySelector('a[href*="novel/show.php?id="]');
- }
- const findItemId = element => {
- let id = null;
- let isNovel = false;
- let link = findLink(element);
- if (link) {
- const match = link.href.match(/artworks\/(\d+)/);
- id = match ? match[1] : null;
- } else {
- link = findNovelLink(element);
- 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, isHome = false) => {
- if (!isElementVisible(element)) {
- return false;
- }
- if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) {
- if (findNovelLink(element)) {
- element.style.display = 'none';
- return true;
- }
- }
- // 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, isHome = false) => {
- if (!isElementVisible(element)) {
- return false;
- }
- if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) {
- if (findNovelLink(element)) {
- element.parentNode.style.display = 'none';
- return true;
- }
- }
- // 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;
- }
- const image = element.closest(CONFIG.SELECTORS_IMAGE);
- if (image) {
- const imageLink = image.querySelector('a[href*="artworks/"], a[href*="novel/"]');
- if (!imageLink) {
- return false;
- }
- if (CONFIG.UTAGS_REMOVE_BLOCKED) {
- image.style.display = 'none';
- return true;
- }
- // 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;
- const isHome = Boolean(target.closest('[data-ga4-label="page_root"]'));
- // 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, isHome)) {
- _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, isHome)) {
- _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.');
- }
- })()