您需要先安装一个扩展,例如 篡改猴、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 // @grant GM_setValue // @grant unsafeWindow // @run-at document-end // @version 1.4.1 // @author Bobby Wibowo // @license MIT // @description 7/2/2024, 8:37:14 PM // @noframes // ==/UserScript== /* global document, console, location, setTimeout, unsafeWindow, window, Array, CustomEvent, URL, $, GM_addStyle, GM_getValue, GM_setValue */ (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); } }; /** CONFIG **/ const ENV = { MODE: GM_getValue('MODE'), TEXT_EDIT_BOOKMARK: GM_getValue('TEXT_EDIT_BOOKMARK', '✏️'), TEXT_EDIT_BOOKMARK_TOOLTIP: GM_getValue('TEXT_EDIT_BOOKMARK_TOOLTIP', 'Edit bookmark'), TEXT_TOGGLE_BOOKMARKED: GM_getValue('TEXT_TOGGLE_BOOKMARKED', '🔖'), TEXT_TOGGLE_BOOKMARKED_TOOLTIP: GM_getValue('TEXT_TOGGLE_BOOKMARKED', 'Cycle bookmarked display (Right-Click to cycle back)'), TEXT_TOGGLE_BOOKMARKED_SHOW_ALL: GM_getValue('TEXT_TOGGLE_BOOKMARKED', 'Show All'), TEXT_TOGGLE_BOOKMARKED_SHOW_BOOKMARKED: GM_getValue('TEXT_TOGGLE_BOOKMARKED', 'Show Bookmarked'), TEXT_TOGGLE_BOOKMARKED_SHOW_NOT_BOOKMARKED: GM_getValue('TEXT_TOGGLE_BOOKMARKED', 'Show Not Bookmarked'), // 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), // This has a preset value. Specifiying a custom value will extend instead of replacing it. SECTIONS_TOGGLE_BOOKMARKED: GM_getValue('SECTIONS_TOGGLE_BOOKMARKED'), 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. * NOTE: Figure out selectors that are more update-proof. * Class names that are formatted as 5 random letters (e.g., hSoPoc) are known to be dynamically generated by Pixiv's stylesheet framework. * Whenever they do any updates, significant or otherwise, will cause them to be regenerated. * * Home's recommended works grid: * Image: .sc-96f10c4f-0 > li * Title: [data-ga4-label="title_link"] * Artist avatar: [data-ga4-label="user_icon_link"] * Artist name: [data-ga4-label="user_name_link"] * Controls: .sc-eacaaccb-9 * * Home's latest works grid: * Image: li[data-ga4-label="thumbnail"] * * Discovery page's grid: * Title: .gtm-illust-recommend-title * Controls: .sc-e33a5c4-2 * * Artist page's grid: * Image: .sc-9y4be5-1 > li * Controls: .sc-iasfms-4 * * Expanded view's artist works bottom row: * Image: .sc-1nhgff6-4 > div * * Expanded view's related works grid: * Artist avatar: .sc-1rx6dmq-1 * Artist name: .gtm-illust-recommend-user-name * * Artist page's featured works: * Image: .sc-1sxj2bl-5 > li * Controls: .sc-xsxgxe-3 * * Bookmarks page's grid: * Title: .sc-iasfms-6 * Artist name: .sc-1rx6dmq-2 * * Tag page's grid: * Image: .sc-l7cibp-1 > 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: '.sc-96f10c4f-0 > li, li[data-ga4-label="thumbnail"], .sc-9y4be5-1 > li, .sc-1nhgff6-4 > div, .sc-1sxj2bl-5 > li, .sc-l7cibp-1 > li, .ranking-item', SELECTORS_IMAGE_TITLE: '[data-ga4-label="title_link"], .gtm-illust-recommend-title, .sc-iasfms-6, .title', SELECTORS_IMAGE_ARTIST_AVATAR: '[data-ga4-label="user_icon_link"], .sc-1rx6dmq-1, ._user-icon', SELECTORS_IMAGE_ARTIST_NAME: '[data-ga4-label="user_name_link"], .gtm-illust-recommend-user-name, .sc-1rx6dmq-2, .user-name', SELECTORS_IMAGE_CONTROLS: '.sc-eacaaccb-9, .sc-e33a5c4-2, .sc-iasfms-4, .sc-xsxgxe-3, ._layout-thumbnail', SELECTORS_EXPANDED_VIEW_CONTROLS: '.sc-181ts2x-0', 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', SECTIONS_TOGGLE_BOOKMARKED: [ // Bookmarks page { selectorParent: '.sc-jgyytr-0', selectorHeader: '.sc-s8zj3z-2', selectorImagesContainer: '.sc-s8zj3z-4' }, // Artist page { selectorParent: '.sc-1xj6el2-3', selectorHeader: '.sc-1xj6el2-2', selectorImagesContainer: '& > div:last-child' }, // Tag page { selectorParent: '.sc-jgyytr-0', selectorHeader: '.sc-7zddlj-0', selectorImagesContainer: '.sc-l7cibp-0' } ], 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 logDebug = () => {}; 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']; } else { logDebug = log; } 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 UTILS **/ // GMCompat compatibility shim // adapted from https://github.com/chocolateboy/gm-compat const $unsafeWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow.wrappedJSObject || unsafeWindow : window; const GMCompat = Object.freeze({ unsafeWindow: $unsafeWindow, CLONE_INTO_OPTIONS: { cloneFunctions: true, target: $unsafeWindow, wrapReflectors: true }, EXPORT_FUNCTION_OPTIONS: { target: $unsafeWindow }, apply: function ($this, fn, _args) { const args = [].slice.call(_args); return fn.apply($this, this.cloneInto(args)); }, call: function ($this, fn, ..._args) { const args = this.cloneInto(_args); return fn.call($this, ...args); }, cloneInto: function (object, _options) { const options = Object.assign({}, this.CLONE_INTO_OPTIONS, _options); const _cloneInto = (typeof cloneInto === 'function') ? cloneInto : object => object; return _cloneInto(object, options.target, options); }, export: function (value, options) { return (typeof value === 'function') ? this.exportFunction(value, options) : this.cloneInto(value, options); }, exportFunction: function (fn, _options) { const options = Object.assign({}, this.EXPORT_FUNCTION_OPTIONS, _options); const _exportFunction = (typeof exportFunction === 'function') ? exportFunction : (fn, { defineAs, target = this.unsafeWindow } = {}) => { return defineAs ? (target[defineAs] = fn) : fn }; return _exportFunction(fn, options.target, options); }, unwrap: function (value) { return value ? (value.wrappedJSObject || value) : value; } }); const patchHistory = () => { ['pushState', 'replaceState'].forEach(method => { const original = GMCompat.unsafeWindow.history[method]; const patched = function () { GMCompat.apply(this, original, arguments); notify(method, arguments[2]); }; GMCompat.unsafeWindow.history[method] = GMCompat.export(patched); }); window.addEventListener('popstate', e => { notify(e.type); }); }; // Navigation detection & CustomEvent dispatch. let _OLD_URL; const notify = (method, url) => { const absUrl = new URL(url || window.location.href, window.location.origin).href; const detail = GMCompat.export({ method: method, oldUrl: _OLD_URL, newUrl: absUrl }); const event = new CustomEvent('detectnavigate', { bubbles: true, detail: detail }); document.dispatchEvent(event); _OLD_URL = absUrl; }; if (window.navigation) { logDebug('Using Navigation API.'); window.navigation.addEventListener('navigatesuccess', e => { notify(e.type, e.currentTarget.currentEntry.url); }); } else if (window.onurlchange === null) { logDebug('Using window.onurlchange.'); window.addEventListener('urlchange', e => { notify('urlchange', e.url); }); } else { logDebug('Using patchHistory().'); patchHistory(); } /** 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; cursor: pointer; user-select: none; } ${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; } .sc-s8zj3z-3:has(+ .pu_toggle_bookmarked_container) { flex-grow: 1; justify-content: flex-end; } .pu_toggle_bookmarked { color: rgb(245, 245, 245); background: rgb(58, 58, 58); display: block; box-sizing: border-box; padding: 6px; border-radius: 10px; font-weight: bold; margin-left: 12px; cursor: pointer; user-select: none; } .pu_toggle_bookmarked span { padding-left: 6px; } ${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 .sc-eacaaccb-1 { 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; } .ranking-item.pu_image_is_blocked ._illust-series-title-text { display: none; } ${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(); } } 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 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 (CONFIG.TEXT_EDIT_BOOKMARK_TOOLTIP) { button.title = CONFIG.TEXT_EDIT_BOOKMARK_TOOLTIP; } 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(); }; // 0 = show all // 1 = show not bookmarked // 2 = show bookmarked // bookmarked: .bXjFLc // not bookmarked: .fYcrPo const isImageBookmarked = element => { return Boolean(element.querySelector('.bXjFLc')); }; 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; } } // Process new entries in toggled bookmarked sections. if (element.closest('.pu_toggle_bookmarked_section')) { const mode = GM_getValue('PREF_TOGGLE_BOOKMARKED_MODE', 0); if (mode === 1) { element.style.display = isImageBookmarked(element) ? 'none': ''; } else if (mode === 2) { element.style.display = isImageBookmarked(element) ? '': 'none'; } } // 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 formatToggleBookmarkedButtonHtml = mode => { if (mode === 0) { return /*html*/`${CONFIG.TEXT_TOGGLE_BOOKMARKED}<span>${CONFIG.TEXT_TOGGLE_BOOKMARKED_SHOW_ALL}<span>`; } else if (mode === 1) { return /*html*/`${CONFIG.TEXT_TOGGLE_BOOKMARKED}<span>${CONFIG.TEXT_TOGGLE_BOOKMARKED_SHOW_NOT_BOOKMARKED}<span>`; } else if (mode === 2) { return /*html*/`${CONFIG.TEXT_TOGGLE_BOOKMARKED}<span>${CONFIG.TEXT_TOGGLE_BOOKMARKED_SHOW_BOOKMARKED}<span>`; } } let toggling = false; const toggleBookmarked = (button, parent, header, imagesContainer, rightClick = false) => { if (toggling) { return false; } toggling = true; let mode = GM_getValue('PREF_TOGGLE_BOOKMARKED_MODE', 0); if (rightClick) { mode--; } else { mode++; } if (mode > 2) { mode = 0; } else if (mode < 0) { mode = 2; } button.innerHTML = formatToggleBookmarkedButtonHtml(mode); let images = Array.from(imagesContainer.querySelectorAll(CONFIG.SELECTORS_IMAGE)); // Do not process blocked images if they are already forcefully hidden. if (CONFIG.UTAGS_REMOVE_BLOCKED) { images = images.filter(image => !image.classList.contains('pu_image_is_blocked')); } if (mode === 0) { for (const image of images) { image.style.display = ''; } } else if (mode === 1) { for (const image of images) { if (image.classList.contains('pu_image_is_blocked') || isImageBookmarked(image)) { image.style.display = 'none'; } else { image.style.display = ''; } } } else if (mode === 2) { for (const image of images) { if (image.classList.contains('pu_image_is_blocked') || !isImageBookmarked(image)) { image.style.display = 'none'; } else { image.style.display = ''; } } } GM_setValue('PREF_TOGGLE_BOOKMARKED_MODE', mode); toggling = false; return true; }; const doToggleBookmarkedSection = (element, sectionConfig) => { // Skip if already processed. if (element.classList.contains('pu_toggle_bookmarked_section')) { return false; } const header = element.querySelector(sectionConfig.selectorHeader); const imagesContainer = element.querySelector(sectionConfig.selectorImagesContainer); if (!header || !imagesContainer) { return false; } // Mark as processed. element.classList.add('pu_toggle_bookmarked_section'); const buttonContainer = document.createElement('div'); buttonContainer.className = 'pu_toggle_bookmarked_container'; const button = document.createElement('a'); button.className = 'pu_toggle_bookmarked'; button.innerHTML = formatToggleBookmarkedButtonHtml(GM_getValue('PREF_TOGGLE_BOOKMARKED_MODE', 0)); if (CONFIG.TEXT_TOGGLE_BOOKMARKED_TOOLTIP) { button.title = CONFIG.TEXT_TOGGLE_BOOKMARKED_TOOLTIP; } button.addEventListener('click', event => toggleBookmarked(button, element, header, imagesContainer)); button.addEventListener('contextmenu', event => { event.preventDefault(); toggleBookmarked(button, element, header, imagesContainer, true); }); buttonContainer.appendChild(button); header.appendChild(buttonContainer); return true; }; 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; } // Skip if already blocked. if (image.classList.contains('pu_image_is_blocked')) { return false; } image.classList.add('pu_image_is_blocked'); if (CONFIG.UTAGS_REMOVE_BLOCKED) { image.style.display = 'none'; return true; } 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'; } } // Empty the text instead of hiding it, so that the utags will still display properly to provide context. const artistLink = image.querySelector(CONFIG.SELECTORS_IMAGE_ARTIST_NAME); if (artistLink) { artistLink.innerText = ''; } return true; } const multiView = element.closest(CONFIG.SELECTORS_MULTI_VIEW); 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(); window.addEventListener('detectnavigate', event => { triggerQueue.clear(); logDebug('Cleared pending trigger queue.'); }); 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; } //const targetParent = mutation.target.parentElement || mutation.target; const isHome = Boolean(mutation.target.closest('[data-ga4-label="page_root"]')); // Expanded View Controls let expandedViewControls = null; if (mutation.target.matches(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS)) { expandedViewControls = mutation.target; } else { expandedViewControls = mutation.target.querySelector(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS); } if (expandedViewControls && doExpandedViewControls(expandedViewControls)) { log(`Processed expanded view controls.`); } // Images let _image = 0; if (mutation.target.matches(CONFIG.SELECTORS_IMAGE)) { if (doImage(mutation.target, isHome)) { _image++; } } else { const images = mutation.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; if (mutation.target.matches(CONFIG.SELECTORS_MULTI_VIEW)) { if (doMultiView(mutation.target, isHome)) { _multiView++; } } else { const multiViews = mutation.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).`); } // Toggle Bookmarked let _toggleBookmarked = 0; for (const sectionConfig of CONFIG.SECTIONS_TOGGLE_BOOKMARKED) { if (!sectionConfig.selectorParent || !sectionConfig.selectorHeader || !sectionConfig.selectorImagesContainer) { logDebug('Invalid "SECTIONS_TOGGLE_BOOKMARKED" config', sectionConfig); continue; } let parents = []; if (mutation.target.matches(sectionConfig.selectorParent)) { parents = [mutation.target]; } else { parents = mutation.target.querySelectorAll(sectionConfig.selectorParent); } if (!parents || !parents.length) { continue; } for (const parent of parents) { if (doToggleBookmarkedSection(parent, sectionConfig)) { _toggleBookmarked++; } } } if (_toggleBookmarked > 0) { log(`Processed ${_toggleBookmarked} toggle bookmarked section(s).`); } // Dates if (CONFIG.DATE_CONVERSION) { let _date = 0; const dates = mutation.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; if (mutation.target.matches(SELECTORS_UTAGS)) { if (doUtagsImage(mutation.target)) { _utag++; } } else { const utags = mutation.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(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS + ' .pu_edit_bookmark'); return processKeyEvent('bookmarkEdit', element); } } }); logDebug('Listening for keybinds.'); } else { logDebug('Keybinds disabled.'); } })()