您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Compatible with mobile. "Edit bookmark" and "Toggle bookmarked" buttons, publish dates conversion, block AI-generated works, block by Pixiv tags, UTags integration, and more!
当前为
// ==UserScript== // @name Bobby's Pixiv Utils // @namespace https://github.com/BobbyWibowo // @version 1.6.1 // @description Compatible with mobile. "Edit bookmark" and "Toggle bookmarked" buttons, publish dates conversion, block AI-generated works, block by Pixiv tags, UTags integration, and more! // @author Bobby Wibowo // @license MIT // @match *://www.pixiv.net/* // @exclude *://www.pixiv.net/setting* // @exclude *://www.pixiv.net/manage* // @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant window.onurlchange // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/sentinel.min.js // @noframes // ==/UserScript== /* global sentinel */ (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 **/ /* It's recommended to edit these values through your userscript manager's storage/values editor. * Visit Pixiv once after installing the script to allow it to populate its storage with default values. * Especially necessary for Tampermonkey to show the script's Storage tab when Advanced mode is turned on. */ const ENV_DEFAULTS = { MODE: 'PROD', TEXT_EDIT_BOOKMARK: '✏️', TEXT_EDIT_BOOKMARK_TOOLTIP: 'Edit bookmark', TEXT_TOGGLE_BOOKMARKED: '❤️', TEXT_TOGGLE_BOOKMARKED_TOOLTIP: 'Cycle bookmarked display (Right-Click to cycle back)', TEXT_TOGGLE_BOOKMARKED_SHOW_ALL: 'Show all', TEXT_TOGGLE_BOOKMARKED_SHOW_BOOKMARKED: 'Show bookmarked', TEXT_TOGGLE_BOOKMARKED_SHOW_NOT_BOOKMARKED: 'Show not bookmarked', SELECTORS_HOME: null, SELECTORS_OWN_PROFILE: null, SELECTORS_IMAGE: null, SELECTORS_IMAGE_TITLE: null, SELECTORS_IMAGE_ARTIST_AVATAR: null, SELECTORS_IMAGE_ARTIST_NAME: null, SELECTORS_IMAGE_CONTROLS: null, SELECTORS_IMAGE_BOOKMARKED: null, SELECTORS_EXPANDED_VIEW_IMAGE: null, SELECTORS_EXPANDED_VIEW_CONTROLS: null, SELECTORS_EXPANDED_VIEW_ARTIST_BOTTOM_IMAGE: null, SELECTORS_MULTI_VIEW: null, SELECTORS_MULTI_VIEW_CONTROLS: null, SELECTORS_FOLLOW_BUTTON_CONTAINER: null, SELECTORS_FOLLOW_BUTTON: null, SELECTORS_RECOMMENDED_USER_CONTAINER: null, DATE_CONVERSION: true, DATE_CONVERSION_LOCALES: 'en-GB', DATE_CONVERSION_OPTIONS: { hour12: true, year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }, SELECTORS_DATE: null, REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME: false, SECTIONS_TOGGLE_BOOKMARKED: null, ENABLE_KEYBINDS: true, PIXIV_BLOCK_AI: false, PIXIV_BLOCKED_TAGS: null, // Instead of merely masking them à la Pixiv's built-in tags mute. PIXIV_REMOVE_BLOCKED: false, UTAGS_INTEGRATION: true, UTAGS_BLOCKED_TAGS: null, // Instead of merely masking them à la Pixiv's built-in tags mute. UTAGS_REMOVE_BLOCKED: false }; /* Hard-coded preset values. * Specifying custom values will extend instead of replacing them. */ const PRESETS = { // Keys that starts with "SELECTORS_", and in array, will automatically be converted to single-line strings. SELECTORS_HOME: '[data-ga4-label="page_root"]', SELECTORS_OWN_PROFILE: [ 'a[href*="settings/profile"]', // desktop '.ui-button[href*="setting_profile.php"]' // mobile ], SELECTORS_IMAGE: [ 'li[data-ga4-label="thumbnail"]', // home's latest works grid '.sc-96f10c4f-0 > li', // home's recommended works grid '.jELUak > li', // artist page's grid '.iHrRmI > li', // artist page's featured works '.ibaIoN > div:has(a[href])', // expanded view's recommended works after pop-in '.iwHaa-d > li', // tags page's grid '.jClpXN > li', // tags page's grid (novel) '.fhUcsb > li', // "newest by all" page '.buGhFj > li', // requests page '.dHJLGd > div', // novels page's ongoing contests '.ranking-item', // rankings page '._ranking-item', // rankings page (novel) '.works-item-illust:has(.thumb:not([src^="data"]))', // mobile '.works-item:not(.works-item-illust):has(.thumb:not([src^="data"]))', // mobile (novel) '.works-item-novel-editor-recommend:has(.cover:not([style^="data"]))', // mobile's novels page's editor's picks '.stacclist > li.illust' // mobile's feed page ], SELECTORS_IMAGE_TITLE: [ '[data-ga4-label="title_link"]', // home's recommended works grid '.gtm-illust-recommend-title', // discovery page's grid '.kmUlkw', // tags/bookmarks page's grid '.title', // rankings page '.illust-info > a[class*="c-text"]' // mobile list view ], SELECTORS_IMAGE_ARTIST_AVATAR: [ '[data-ga4-label="user_icon_link"]', // home's recommended works grid '.sc-1rx6dmq-1', // expanded view's related works grid '.lbFgXO', // tags/bookmarks page's grid '._user-icon' // rankings page ], SELECTORS_IMAGE_ARTIST_NAME: [ '[data-ga4-label="user_name_link"]', // home's recommended works grid '.gtm-illust-recommend-user-name', // expanded view's related works grid '.QzTPT', // tags/bookmarks page's grid '.user-name', // rankings page '.illust-author' // mobile list view ], SELECTORS_IMAGE_CONTROLS: [ '.ldNztP', // home's latest/recommended works grid '.ppQNN', // discovery page's grid '.btqmcy', // artist page's grid '.fRrNLv', // artist page's featured works '.cgYJXZ', // tags page's grid (novel) '.ZBDKi', // "newest by all" page '.byWzRq', // expanded view's artist bottom bar (novel) '.Yzjmx', // artist page's grid (novel) '.jVTssb', // artist page's featured works (novel) '.hFAmSK', // novels page '.djUdtd > div:last-child', // novels page's editor's picks '.gAyuNi', // novels page's ongoing contests '._layout-thumbnail', // rankings page '.novel-right-contents', // rankings page (novel) '.imgoverlay', // mobile's feed page '.bookmark', // mobile '.hSoPoc' // mobile ], SELECTORS_IMAGE_BOOKMARKED: [ '.epoVSE', // desktop '.wQCIS', // "newest by all" page '._one-click-bookmark.on', // rankings page '.works-bookmark-button svg path[fill="#FF4060"]' // mobile ], SELECTORS_EXPANDED_VIEW_IMAGE: [ '.cxsjmo', // desktop '.illust-details-view' // mobile ], SELECTORS_EXPANDED_VIEW_CONTROLS: [ '.inxZPA', // desktop '.work-interactions' // mobile ], SELECTORS_EXPANDED_VIEW_ARTIST_BOTTOM_IMAGE: '.eoaxji > div:has(a[href])', SELECTORS_MULTI_VIEW: '[data-ga4-label="work_content"]:has(a[href])', SELECTORS_MULTI_VIEW_CONTROLS: '& > .w-full:last-child > .flex:first-child > .flex-row:first-child', SELECTORS_FOLLOW_BUTTON_CONTAINER: [ '.XFDNu', // artist page's header '.kIkMnj', // artist hover popup '.gSkxA', // expanded view's artist bottom bar '.cmowxU', // expanded view's artist sidebar '.user-details' // mobile's artist page ], SELECTORS_FOLLOW_BUTTON: [ '[data-click-label="follow"]:not([disabled])', // desktop '.ui-button' // mobile ], SELECTORS_RECOMMENDED_USER_CONTAINER: [ // home's recommended users sidebar '.hSNbaL > .grid > :nth-child(2) .flex-col.gap-8:first-child .flex-row.items-center:not(.mr-auto)' ], SELECTORS_DATE: [ '.dgDuKx', // desktop '.kzGSfF', // desktop "updated on" popup '.times' // mobile ], // Selectors must be single-line strings. SECTIONS_TOGGLE_BOOKMARKED: [ // Following page { selectorParent: '.icUpwV', selectorHeader: '.fHQERN', selectorImagesContainer: '.fJdNho' }, // Artist page { selectorParent: '.gqvfWY:not(.bYCbxa)', selectorHeader: '.rXWMQ', selectorImagesContainer: '.rXWMQ ~ div:not([class])' }, // Artist page's bookmarks tab { selectorParent: '.gqvfWY.bYCbxa', selectorHeader: '.cfUrtF', selectorImagesContainer: '.cfUrtF ~ div:not([class])', sanityCheck: () => { // Skip if in own profile. return document.querySelector('a[href*="settings/profile"]'); } }, // Tags page { selectorParent: '.icUpwV', selectorHeader: '.dlidhK', selectorImagesContainer: '.fxjfKC' }, // "Newest by all" page { selectorParent: '.YXoqY', selectorHeader: '.cwGkEl', selectorImagesContainer: '.hairtM ' }, // Rankings page { selectorParent: '#wrapper ._unit', selectorHeader: '.ranking-menu', selectorImagesContainer: '.ranking-items-container' }, // Mobile artist page's illustrations/bookmarks tab, following page, tags page { selectorParent: '.v-nav-tabs + div:not(.header-buttons), ' + '.nav-tab + div, ' + '.search-nav-config + div', selectorHeader: '.pager-view-nav', selectorImagesContainer: '.works-grid-list', sanityCheck: () => { // Skip if in own profile (intended for bookmarks page). return document.querySelector('.ui-button[href*="setting_profile.php"]'); } }, // Mobile artist page's home tab { selectorParent: '.work-set > div', selectorHeader: '.title-line > div:last-child', selectorImagesContainer: '.works-grid-list' }, // Mobile rankings page { selectorParent: '.ranking-page', selectorHeader: '.header-buttons', selectorImagesContainer: '.works-grid-list' } ], // To ensure any custom values will be inserted into array, or combined together if also an array. PIXIV_BLOCKED_TAGS: [], UTAGS_BLOCKED_TAGS: ['block', 'hide'] }; const ENV = {}; // Store default values. for (const key of Object.keys(ENV_DEFAULTS)) { const stored = GM_getValue(key); if (stored === null || stored === undefined) { ENV[key] = ENV_DEFAULTS[key]; GM_setValue(key, ENV_DEFAULTS[key]); } else { ENV[key] = stored; } } const _DOCUMENT_FRAGMENT = document.createDocumentFragment(); const queryCheck = selector => _DOCUMENT_FRAGMENT.querySelector(selector); const isSelectorValid = selector => { try { queryCheck(selector); } catch { return false; } return true; }; const CONFIG = {}; // Extend hard-coded preset values with user-defined custom values, if applicable. for (const key of Object.keys(ENV)) { if (key.startsWith('SELECTORS_')) { if (Array.isArray(PRESETS[key])) { CONFIG[key] = PRESETS[key].join(', '); } else { CONFIG[key] = PRESETS[key] || ''; } if (ENV[key]) { CONFIG[key] += `, ${Array.isArray(ENV[key]) ? ENV[key].join(', ') : ENV[key]}`; } if (!isSelectorValid(CONFIG[key])) { console.error(`${key} contains invalid selector =`, CONFIG[key]); return; } } else if (Array.isArray(PRESETS[key])) { CONFIG[key] = PRESETS[key]; if (ENV[key]) { const customValues = Array.isArray(ENV[key]) ? ENV[key] : ENV[key].split(',').map(s => s.trim()); CONFIG[key].push(...customValues); } } else { CONFIG[key] = PRESETS[key] || null; if (ENV[key] !== null) { 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', 'PIXIV_BLOCK_AI', 'PIXIV_BLOCK_TAGS', 'PIXIV_REMOVE_BLOCKED', 'UTAGS_INTEGRATION', 'UTAGS_BLOCKED_TAGS', 'UTAGS_REMOVE_BLOCKED' ]; } 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) => { // Support "updated on" popups const updatedOnRegexes = [ /(^Image updated on )(.*)$/i // EN ]; let prefix = ''; 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; for (const regex of updatedOnRegexes) { const _match = dateText.match(regex); if (_match) { dateText = _match[2]; prefix = _match[1]; break; } } // For dates hard-coded to Japan locale. const match = dateText.match(/^(\d{4})年(\d{2})月(\d{2})日 (\d{2}:\d{2})$/); if (match) { dateText = `${match[2]}-${match[3]}-${match[1]} ${match[4]}`; } if (fixJapanTime) { dateText += ' UTC+9'; } date = new Date(dateText); } if (!date) { return false; } const timestamp = String(date.getTime()); if (element.dataset.oldTimestamp === timestamp) { return false; } element.dataset.oldTimestamp = timestamp; element.innerText = prefix + date.toLocaleString(CONFIG.DATE_CONVERSION_LOCALES, CONFIG.DATE_CONVERSION_OPTIONS); return true; }; /** INTERCEPT EARLY FOR CERTAIN ROUTES **/ const waitPageLoaded = () => { return new Promise(resolve => { if (document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') { resolve(); } else { document.addEventListener('DOMContentLoaded', resolve); } }); }; const path = location.pathname; // Codes beyond this block will not execute for these routes (mainly for efficiency). if (path.startsWith('/bookmark_add.php') || path.startsWith('/novel/bookmark_add.php')) { if (CONFIG.DATE_CONVERSION) { waitPageLoaded().then(() => { GM_addStyle(addPageDateStyle); const date = document.querySelector('.bookmark-detail-unit .meta'); if (date) { // This page has the date display hardcoded to Japan time without an accompanying timestamp. convertDate(date, true); } }); } log('bookmark_add.php detected. Excluding date conversion, script has terminated early.'); return; } /** MAIN UTILS */ const SELECTORS_IMAGE_MOBILE = '.works-item-illust'; const PIXIV_BLOCKED_TAGS_STRING = []; const PIXIV_BLOCKED_TAGS_REGEXP = []; for (const tag of CONFIG.PIXIV_BLOCKED_TAGS) { if (typeof tag === 'string') { PIXIV_BLOCKED_TAGS_STRING.push(String(tag)); } else if (Array.isArray(tag)) { PIXIV_BLOCKED_TAGS_REGEXP.push(new RegExp(tag[0], tag[1] || '')); } } logDebug('PIXIV_BLOCKED_TAGS_STRING = ', PIXIV_BLOCKED_TAGS_STRING); logDebug('PIXIV_BLOCKED_TAGS_REGEXP = ', PIXIV_BLOCKED_TAGS_REGEXP); const PIXIV_BLOCKED_TAGS_VALIDATED = PIXIV_BLOCKED_TAGS_STRING.length || PIXIV_BLOCKED_TAGS_REGEXP.length; let currentUrl = new URL(window.location.href, window.location.origin).href; const notify = (method, url) => { const newUrl = new URL(url || window.location.href, window.location.origin).href; if (currentUrl !== newUrl) { const event = new CustomEvent('detectnavigate'); window.dispatchEvent(event); currentUrl = newUrl; } }; if (window.onurlchange === null) { window.addEventListener('urlchange', event => { notify('urlchange', event.url); }); logDebug('Using window.onurlchange.'); } else { const oldMethods = {}; ['pushState', 'replaceState'].forEach(method => { oldMethods[method] = history[method]; history[method] = function (...args) { oldMethods[method].apply(this, args); notify(method, args[2]); }; }); window.addEventListener('popstate', event => { notify(event.type); }); logDebug('Using window.onurlchange polyfill.'); } /** MAIN STYLES **/ const formatChildSelector = (parentSelector, childSelector) => { let child = childSelector; if (childSelector.startsWith('&')) { child = childSelector.substring(1).trimStart(); } const formatted = []; const parents = parentSelector.split(', '); for (const parent of parents) { formatted.push(`${parent} ${child}`); } return formatted.join(', '); }; const _formatSelectorsMultiViewControls = () => { const multiViews = CONFIG.SELECTORS_MULTI_VIEW.split(', '); const multiViewsControls = CONFIG.SELECTORS_MULTI_VIEW_CONTROLS.split(', '); const formatted = []; for (const parent of multiViews) { for (const child of multiViewsControls) { formatted.push(formatChildSelector(parent, child)); } } return formatted; }; const _SELECTORS_IMAGE_CONTROLS = CONFIG.SELECTORS_IMAGE_CONTROLS.split(', '); const _FILTERED_SELECTORS_IMAGE_CONTROLS = _SELECTORS_IMAGE_CONTROLS .filter(s => !['._layout-thumbnail', '.novel-right-contents'].includes(s)) .join(', '); const mainStyle = /*css*/` .flex:has(+ .pixiv_utils_edit_bookmark_container) { flex-grow: 1; } .ranking-item.muted .pixiv_utils_edit_bookmark_container { display: none; } :is(.byWzRq, .hFAmSK, .gAyuNi, .cgYJXZ, .Yzjmx) .pixiv_utils_edit_bookmark { margin-top: -26px; } .pixiv_utils_edit_bookmark { color: rgb(245, 245, 245); background: rgba(0, 0, 0, 0.5); display: block; box-sizing: border-box; padding: 0px 8px; 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; position: relative; z-index: 1; } :is(${CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS}) .pixiv_utils_edit_bookmark, :is(${_formatSelectorsMultiViewControls().join(', ')}) .pixiv_utils_edit_bookmark { font-size: 12px; height: 24px; line-height: 24px; margin-top: 5px; margin-right: 7px; } :is(._layout-thumbnail, .novel-right-contents, .imgoverlay) .pixiv_utils_edit_bookmark { position: absolute !important; right: calc(50% - 71px); bottom: 4px; z-index: 2; } .novel-right-contents .pixiv_utils_edit_bookmark { right: 50px; } .imgoverlay .pixiv_utils_edit_bookmark { right: 40px; bottom: 15px; } *:has(> .pixiv_utils_image_artist_container) { position: relative !important; } .pixiv_utils_image_artist_container { position: absolute; padding: 5px; bottom: 0; left: 0; max-width: calc(100% - 76px); } .pixiv_utils_image_artist { color: rgb(245, 245, 245); background: rgba(0, 0, 0, 0.5); box-sizing: border-box; padding: 0px 8px; border-radius: 10px; font-weight: bold; font-size: 14px; line-height: 20px; height: 20px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; float: left; width: 100%; } :is(.bXtqby, .eEVUIK):has(+ .pixiv_utils_toggle_bookmarked_container) { flex-grow: 1; justify-content: flex-end; } .pixiv_utils_toggle_bookmarked_container { text-align: center; } .pixiv_utils_toggle_bookmarked { color: rgb(245, 245, 245); background: rgb(58, 58, 58); display: inline-block; box-sizing: border-box; padding: 6px; border-radius: 10px; font-weight: bold; margin-left: 12px; cursor: pointer; user-select: none; } .pixiv_utils_toggle_bookmarked:hover { text-decoration: none; } .pixiv_utils_toggle_bookmarked span { padding-left: 6px; } ${_FILTERED_SELECTORS_IMAGE_CONTROLS} { display: flex; justify-content: flex-end; } [data-pixiv_utils_expanded_view_blocked] :is([role="presentation"], .work-main-image) :is(img, canvas) { filter: blur(32px); } [data-pixiv_utils_expanded_view_blocked] :is([role="presentation"], .work-main-image):hover :is(img, canvas) { filter: unset; } `; const mainDateStyle = /*css*/` .dqHJfP { font-size: 14px !important; font-weight: bold; color: rgb(214, 214, 214) !important; } `; /** UTAGS INTEGRATION INIT **/ const mainUtagsStyle = /*css*/` :not(#higher_specificity) *:has(+ .pixiv_utils_blocked_image_container) { display: none !important; } .pixiv_utils_blocked_image { display: flex; justify-content: center; align-items: center; width: 100%; color: rgb(92, 92, 92); min-width: 93px; aspect-ratio: 1 / 1; } .pixiv_utils_blocked_image svg { fill: currentcolor; } .ranking-item .pixiv_utils_blocked_image { max-width: 150px; margin: 0 auto; border: 1px solid rgb(242, 242, 242); } /* Pixiv's built-in tags mute. */ .ranking-item.muted .work img { filter: brightness(50%); } .ranking-item.muted .muted-thumbnail .negative { position: relative; z-index: 1; color: rgb(92, 92, 92); } /* Only use black background on desktop layout. */ body > div:not(#wrapper) .pixiv_utils_blocked_image, body > div:not(#wrapper) .ranking-item.muted .work img { background: rgb(0, 0, 0); } [data-pixiv_utils_blocked] :is(.series-title, .tag-container, .show-more-creator-works-button), [data-pixiv_utils_blocked] .pqkmS, /* desktop: show more creator works button */ [data-pixiv_utils_blocked] ._illust-series-title-text { display: none !important; } [data-pixiv_utils_blocked] :is(${CONFIG.SELECTORS_IMAGE_TITLE}) { display: none !important; } [data-pixiv_utils_blocked] :is(${CONFIG.SELECTORS_IMAGE_ARTIST_AVATAR}) { display: none !important; } [data-pixiv_utils_blocked] :is(${CONFIG.SELECTORS_IMAGE_ARTIST_NAME}) { display: none !important; } [data-pixiv_utils_blocked] :is(${_SELECTORS_IMAGE_CONTROLS}) { display: none !important; } [data-pixiv_utils_blocked] .pixiv_utils_image_artist_container { max-width: calc(100% - 10px); } [data-pixiv_utils_blocked] .pixiv_utils_image_artist { background: none; padding: 0; width: 0; } `; const SELECTORS_UTAGS = CONFIG.UTAGS_BLOCKED_TAGS.map(s => `[data-utags_tag="${s}"]`).join(', '); log('SELECTORS_UTAGS =', SELECTORS_UTAGS); const BLOCKED_IMAGE_HTML = /*html*/` <div radius="4" class="pixiv_utils_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); } const uuidv4 = () => { return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) ); }; const waitForIntervals = {}; const waitFor = (func, element = document) => { if (typeof func !== 'function') { return false; } return new Promise(resolve => { let interval = null; const find = () => { const result = func(element); if (result) { if (interval) { delete waitForIntervals[interval]; clearInterval(interval); } return resolve(result); } }; find(); interval = setInterval(find, 100); waitForIntervals[interval] = { func, element, resolve }; }); }; const initElementObserver = (element, callback, options = {}) => { if (!element || typeof callback !== 'function' || typeof options !== 'object' || !Object.keys(options).length) { return false; } // Skip if already observing. if (element.dataset.pixiv_utils_observing) { return false; } if (options.attributes && (!options.attributeFilter || options.attributeFilter.includes('pixiv_utils_observing'))) { console.error('initElementObserver cannot be initiated without proper attributes filtering', element); return false; } // Mark as observing. element.dataset.pixiv_utils_observing = true; const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; const observer = new MutationObserver((mutations, observer) => { callback.call(this, mutations, observer); }); observer.observe(element, options); return observer; }; const editBookmarkButton = (id, isNovel = false) => { const buttonContainer = document.createElement('div'); buttonContainer.className = 'pixiv_utils_edit_bookmark_container'; const button = document.createElement('a'); button.className = 'pixiv_utils_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.append(button); return buttonContainer; }; const findArtworkUrl = element => { return element.querySelector('a[href*="artworks/"]'); }; const findIllustUrl = element => { return element.querySelector('a[href*="illust_id="]'); }; const findNovelUrl = element => { return element.querySelector('a[href*="novel/show.php?id="]'); }; const findItemData = element => { const methods = [ { func: findArtworkUrl, regex: /artworks\/(\d+)/ }, { func: findIllustUrl, regex: /illust_id=(\d+)/ }, { func: findNovelUrl, regex: /novel\/show\.php\?id=(\d+)/, novel: true } ]; const result = { id: null, novel: false }; for (const method of methods) { result.link = method.func(element); if (result.link) { const match = result.link.href.match(method.regex); if (match) { result.id = match[1]; result.novel = Boolean(method.novel); } break; } } return result; }; // Toggle Bookmarked Modes. // 0 = Show all // 1 = Show not bookmarked // 2 = Show bookmarked const _TB_MIN = 0; const _TB_MAX = 2; const isImageBookmarked = element => { return element.querySelector(CONFIG.SELECTORS_IMAGE_BOOKMARKED) !== null; }; const getImagePixivData = async element => { let ai = false; let tags = null; if (element.__vue__) { for (const key of ['item', 'illustDetails']) { const data = element.__vue__._props?.[key]; if (!data) { continue; } if (key === 'item') { const awaited = await waitFor(() => !data.notLoaded, element); if (!awaited) { return false; } } ai = data.ai_type === 2; tags = data.tags; } } else { const reactFiberKey = Object.keys(element).find(k => k.startsWith('__reactFiber')); if (!reactFiberKey) { return false; } const MAX_STEPS = 2; let step = 0; const traverseChild = obj => { if (!obj || !obj.memoizedProps) { return null; } step++; const props = obj.memoizedProps; if (props.tags) { ai = props.aiType === 2; tags = props.tags; } else if (props.content?.access?.tgs) { tags = props.content.access.tgs; } else { for (const key of ['thumbnail', 'rawThumbnail']) { if (props[key]) { ai = props[key].aiType === 2; tags = props[key].tags; break; } } } if (tags === null && step < MAX_STEPS) { traverseChild(obj.child); } }; traverseChild(element[reactFiberKey].child); } if (!tags) { tags = []; } // Re-map extended tags data. tags = tags.map(tag => typeof tag !== 'string' ? tag.name : tag); return { ai, tags }; }; const isImageBlockedByData = data => { const blockedAI = CONFIG.PIXIV_BLOCK_AI && data.ai; const blockedTags = []; for (const tag of data.tags) { if (PIXIV_BLOCKED_TAGS_STRING.includes(tag) || PIXIV_BLOCKED_TAGS_REGEXP.some(t => t.test(tag))) { blockedTags.push(tag); } } if (!blockedAI && !blockedTags.length) { return false; } let hint = ''; if (CONFIG.PIXIV_BLOCK_AI) { hint = `AI-generated: ${blockedAI}`; } const blockedTagsStr = blockedTags.join(', '); if (blockedTagsStr) { hint += `\nTags: ${blockedTagsStr}`; hint = hint.trim(); } return { hint }; }; const setImageBlocked = (element, options = {}) => { const data = findItemData(element); if (!data.link) { return false; } // Skip if already blocked. if (element.dataset.pixiv_utils_blocked) { return false; } element.dataset.pixiv_utils_blocked = true; // For mobile, never remove blocked, as it does not behave well with Pixiv's in-place navigation. if (options.remove && !options.mobile) { element.style.display = 'none'; return true; } const blockedThumb = document.createElement('a'); blockedThumb.className = 'pixiv_utils_blocked_image_container'; blockedThumb.href = data.link.href; blockedThumb.innerHTML = BLOCKED_IMAGE_HTML; data.link.after(blockedThumb); // Tooltip. if (options.hint) { element.title = options.hint; } return true; }; const doBlockImage = async element => { const data = await getImagePixivData(element); if (!data.tags?.length) { return false; } const blocked = isImageBlockedByData(data); if (!blocked) { return false; } const status = setImageBlocked(element, { mobile: element.matches(SELECTORS_IMAGE_MOBILE), remove: CONFIG.PIXIV_REMOVE_BLOCKED, hint: blocked.hint }); if (status) { logDebug(`Image blocked (${blocked.hint.replace('\n', ', ')})`, element); } return status; }; const addImageArtist = async element => { let userId = null; let userName = null; if (element.__vue__) { const awaited = await waitFor(() => !element.__vue__._props?.item?.notLoaded, element); if (!awaited) { return false; } userId = element.__vue__._props.item.user_id; userName = element.__vue__._props.item.author_details.user_name; } else { const reactPropsKey = Object.keys(element).find(k => k.startsWith('__reactProps')); if (!reactPropsKey) { return false; } for (const key of ['thumbnail', 'rawThumbnail']) { if (element[reactPropsKey].children?.props?.[key]) { userId = element[reactPropsKey].children.props[key].userId; userName = element[reactPropsKey].children.props[key].userName; break; } } } if (!userId || !userName) { return false; } const div = document.createElement('div'); div.className = 'pixiv_utils_image_artist_container'; div.innerHTML = /*html*/` <a class="pixiv_utils_image_artist" href="https://www.pixiv.net/users/${userId}">${userName}</a> `; element.append(div); return true; }; const doImage = async (element, options = {}) => { // Skip if invalid. if (!element.querySelector('a[href]')) { return false; } if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && options.isHome) { if (findNovelUrl(element)) { element.style.display = 'none'; logDebug('Novel recommendation removed from home', element); return true; } } // Process new entries in toggled bookmarked sections. if (element.closest('[data-pixiv_utils_toggle_bookmarked_section]')) { const mode = GM_getValue('PREF_TOGGLE_BOOKMARKED_MODE', _TB_MIN); 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, unless forced. if (element.querySelector('.pixiv_utils_edit_bookmark') && !options.forced) { return false; } // Init MutationObserver for mobile images. if (element.__vue__) { if (!element.dataset.pixiv_utils_last_tx) { initElementObserver(element, () => { const lastGrid = element.dataset.pixiv_utils_last_grid === 'true'; if (element.dataset.tx !== element.dataset.pixiv_utils_last_tx || element.classList.contains('grid') !== lastGrid) { options.forced = true; doImage(element, options); } }, { attributes: true, // Monitor class tag to also detect list/grid view change. attributeFilter: ['class', 'data-tx'] }); } element.dataset.pixiv_utils_last_tx = element.dataset.tx; element.dataset.pixiv_utils_last_grid = element.classList.contains('grid'); } // Reset blocked status if necessary. if (options.forced && element.dataset.pixiv_utils_blocked) { delete element.title; delete element.dataset.pixiv_utils_blocked; const blockedThumb = element.querySelector('.pixiv_utils_blocked_image_container'); if (blockedThumb) { blockedThumb.remove(); } } // Only block images if not in own profile. if (PIXIV_BLOCKED_TAGS_VALIDATED && !options.isOwnProfile) { const blocked = await doBlockImage(element); if (blocked) { return true; } } // Exit early if in own profile, and not in bookmarks tab. if (options.isOwnProfile && currentUrl.indexOf('/bookmarks') === -1) { return false; } const oldImageArtist = element.querySelector('.pixiv_utils_image_artist_container'); if (oldImageArtist) { oldImageArtist.remove(); } const data = findItemData(element); if (data.id === null) { return false; } let imageControls = null; if (data.novel) { imageControls = element.querySelector(CONFIG.SELECTORS_IMAGE_CONTROLS); } else { // If it's not a novel, assume image controls may be delayed due to still being generated. imageControls = await waitFor(() => { return element.querySelector(CONFIG.SELECTORS_IMAGE_CONTROLS); }, element); } if (!imageControls) { return false; } const artistTag = element.querySelector('a[href*="users/"]'); let hasVisibleArtistTag = Boolean(artistTag); if (hasVisibleArtistTag && element.parentOffset !== null) { // If the image itself is visible, but its built-in artist tag is not. hasVisibleArtistTag = artistTag.offsetParent !== null; } // Add artist tag if necessary. if (!hasVisibleArtistTag && !element.closest('.user-badge .works-horizontal-list') && // never in mobile expanded view's artist bottom bar (currentUrl.indexOf('users/') === -1 || // never in artist page (except bookmarks tab) (currentUrl.indexOf('users/') !== -1 && currentUrl.indexOf('/bookmarks') !== -1))) { await addImageArtist(element); } const oldEditBookmarkButton = imageControls.querySelector('.pixiv_utils_edit_bookmark_container'); if (oldEditBookmarkButton) { oldEditBookmarkButton.remove(); } imageControls.prepend(editBookmarkButton(data.id, data.novel)); return true; }; const doBlockMultiView = async element => { const target = element.querySelector('div[data-ga4-label="thumbnail_link"]'); if (!target) { return false; } const data = await getImagePixivData(target); if (!data.tags?.length) { return false; } const blocked = isImageBlockedByData(data); if (!blocked) { return false; } // For multi view artwork, always hide the whole entry instead. element.parentNode.style.display = 'none'; logDebug(`Multi view entry removed (${blocked.hint})`, element); return true; }; const doMultiView = async (element, options = {}) => { if (PIXIV_BLOCKED_TAGS_VALIDATED) { const blocked = await doBlockMultiView(element); if (blocked) { return true; } } if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && options.isHome) { if (findNovelUrl(element)) { element.parentNode.style.display = 'none'; logDebug('Novel recommendation removed from home', element); return true; } } // Skip if edit bookmark button already inserted. if (element.querySelector('.pixiv_utils_edit_bookmark')) { return false; } const multiViewControls = element.querySelector(CONFIG.SELECTORS_MULTI_VIEW_CONTROLS); if (!multiViewControls) { return false; } const data = findItemData(element); if (data.id !== null) { multiViewControls.lastChild.before(editBookmarkButton(data.id, data.novel)); return true; } return false; }; const doBlockExpandedView = async element => { const image = element.closest(CONFIG.SELECTORS_EXPANDED_VIEW_IMAGE); if (!image) { return false; } // Reset blocked status if necessary. delete image.dataset.pixiv_utils_expanded_view_blocked; // Init MutationObserver for mobile expanded view. if (image.__vue__) { const target = image.querySelector('.work-main-image a'); if (!image.dataset.pixiv_utils_last_id) { initElementObserver(target, () => { const data = findItemData(image); if (data.id !== image.dataset.pixiv_utils_last_id) { doBlockExpandedView(image); } }, { attributes: true, attributeFilter: ['href'] }); } const data = findItemData(image); image.dataset.pixiv_utils_last_id = data.id; } const data = await getImagePixivData(image); if (!data.tags?.length) { return false; } const blocked = isImageBlockedByData(data); if (!blocked) { return false; } image.dataset.pixiv_utils_expanded_view_blocked = true; logDebug(`Expanded view blocked (${blocked.hint})`, image); return true; }; const doExpandedViewControls = async element => { if (PIXIV_BLOCKED_TAGS_VALIDATED) { await doBlockExpandedView(element); } // Skip if edit bookmark button already inserted. if (element.querySelector('.pixiv_utils_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.append(editBookmarkButton(id, isNovel)); // Re-process expanded view's artist bottom bar. const images = document.querySelectorAll(CONFIG.SELECTORS_EXPANDED_VIEW_ARTIST_BOTTOM_IMAGE); for (const image of images) { await doImage(image); } 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', _TB_MIN); if (rightClick) { mode--; } else { mode++; } if (mode > _TB_MAX) { mode = _TB_MIN; } else if (mode < _TB_MIN) { mode = _TB_MAX; } 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.PIXIV_REMOVE_BLOCKED || CONFIG.UTAGS_REMOVE_BLOCKED) { images = images.filter(image => !image.dataset.pixiv_utils_blocked); } if (mode === 0) { for (const image of images) { image.style.display = ''; } } else if (mode === 1) { for (const image of images) { if (image.dataset.pixiv_utils_blocked || isImageBookmarked(image)) { image.style.display = 'none'; } else { image.style.display = ''; } } } else if (mode === 2) { for (const image of images) { if (image.dataset.pixiv_utils_blocked || !isImageBookmarked(image)) { image.style.display = 'none'; } else { image.style.display = ''; } } } GM_setValue('PREF_TOGGLE_BOOKMARKED_MODE', mode); toggling = false; return true; }; const doToggleBookmarkedSection = async (element, sectionConfig) => { // Skip if this config has a sanity check function, and it passes. if (typeof sectionConfig.sanityCheck === 'function' && sectionConfig.sanityCheck()) { return false; } const imagesContainer = element.querySelector(sectionConfig.selectorImagesContainer); if (!imagesContainer) { return false; } // Skip if already processed. if (element.dataset.pixiv_utils_toggle_bookmarked_section) { if (element.dataset.pixiv_utils_toggle_bookmarked_section === imagesContainer.dataset.pixiv_utils_toggle_bookmarked_section) { return false; } logDebug('Refreshing toggle bookmarked section due to images container update', element); } const header = element.querySelector(sectionConfig.selectorHeader); if (!header) { return false; } // Mark as processed. const uuid = element.dataset.pixiv_utils_toggle_bookmarked_section || uuidv4(); element.dataset.pixiv_utils_toggle_bookmarked_section = imagesContainer.dataset.pixiv_utils_toggle_bookmarked_section = uuid; // Clear old button if it's being refreshed. const oldButtonContainer = element.querySelector('.pixiv_utils_toggle_bookmarked_container'); if (oldButtonContainer) { oldButtonContainer.remove(); } const buttonContainer = document.createElement('div'); buttonContainer.className = 'pixiv_utils_toggle_bookmarked_container'; const button = document.createElement('a'); button.className = 'pixiv_utils_toggle_bookmarked'; button.innerHTML = formatToggleBookmarkedButtonHtml(GM_getValue('PREF_TOGGLE_BOOKMARKED_MODE', _TB_MIN)); if (CONFIG.TEXT_TOGGLE_BOOKMARKED_TOOLTIP) { button.title = CONFIG.TEXT_TOGGLE_BOOKMARKED_TOOLTIP; } // Left click. button.addEventListener('click', event => toggleBookmarked(button, element, header, imagesContainer)); // Right click. button.addEventListener('contextmenu', event => { event.preventDefault(); toggleBookmarked(button, element, header, imagesContainer, true); }); buttonContainer.append(button); header.append(buttonContainer); return true; }; const doUtags = async element => { let image = element.closest(CONFIG.SELECTORS_IMAGE); let mobile = false; if (image) { mobile = image.matches(SELECTORS_IMAGE_MOBILE); } else { // For mobile images, re-attempt query with some patience. image = element.closest(SELECTORS_IMAGE_MOBILE); if (image) { mobile = true; const awaited = await waitFor(() => image.querySelector('.thumb:not([src^="data"])'), image); if (!awaited) { return false; } } } const utag = element.dataset.utags_tag; if (image) { const status = setImageBlocked(image, { mobile, remove: CONFIG.UTAGS_REMOVE_BLOCKED, hint: `UTag: ${utag}` }); if (status) { logDebug(`Image blocked (UTag: ${utag})`, image); } return status; } const multiView = element.closest(CONFIG.SELECTORS_MULTI_VIEW); if (multiView) { // For multi view artwork, always hide the whole entry instead. multiView.parentNode.style.display = 'none'; logDebug(`Multi view entry removed (UTag: ${utag})`, multiView); return true; } const recommendedUserContainer = element.closest(CONFIG.SELECTORS_RECOMMENDED_USER_CONTAINER); if (recommendedUserContainer) { recommendedUserContainer.style.display = 'none'; logDebug(`Recommended user removed (UTag: ${utag})`, recommendedUserContainer); return true; } const followButtonContainer = element.closest(CONFIG.SELECTORS_FOLLOW_BUTTON_CONTAINER); if (followButtonContainer) { const followButton = followButtonContainer.querySelector(CONFIG.SELECTORS_FOLLOW_BUTTON); if (followButton) { // Cosmetic only. This will not disable Pixiv's built-in "F" keybind. followButton.classList.add('disabled'); followButton.disabled = true; logDebug(`Follow button disabled (UTag: ${utag})`, followButtonContainer); // Return early since there will only be one follow button per container. return true; } } return false; }; let isHome = false; let isOwnProfile = false; const determinePageType = () => { isHome = Boolean(document.querySelector(CONFIG.SELECTORS_HOME)); isOwnProfile = Boolean(document.querySelector(CONFIG.SELECTORS_OWN_PROFILE)); logDebug(`isHome: ${isHome}, isOwnProfile: ${isOwnProfile}`); }; window.addEventListener('detectnavigate', event => { const intervals = Object.keys(waitForIntervals); for (const interval of intervals) { clearInterval(interval); waitForIntervals[interval].resolve(); delete waitForIntervals[interval]; } if (intervals.length > 0) { logDebug(`Cleared ${intervals.length} pending waitFor interval(s).`); } // Reset page type. isHome = isOwnProfile = false; }); /** SENTINEL */ waitPageLoaded().then(() => { // Immediately attempt to determine page type. determinePageType(); sentinel.on(CONFIG.SELECTORS_HOME, () => { isHome = true; logDebug(`isHome: ${isHome}`); }); sentinel.on(CONFIG.SELECTORS_OWN_PROFILE, () => { isOwnProfile = true; logDebug(`isOwnProfile: ${isOwnProfile}`); }); // Expanded View Controls sentinel.on(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS, element => { doExpandedViewControls(element); }); // Images sentinel.on([ CONFIG.SELECTORS_IMAGE, CONFIG.SELECTORS_EXPANDED_VIEW_ARTIST_BOTTOM_IMAGE ], element => { doImage(element, { isHome, isOwnProfile }); }); // Multi View Entries sentinel.on(CONFIG.SELECTORS_MULTI_VIEW, element => { doMultiView(element, { isHome }); }); // Toggle Bookmarked Sections for (const sectionConfig of CONFIG.SECTIONS_TOGGLE_BOOKMARKED) { let configValid = true; for (const key of ['selectorParent', 'selectorHeader', 'selectorImagesContainer']) { if (!sectionConfig[key] || !isSelectorValid(sectionConfig[key])) { console.error(`SECTIONS_TOGGLE_BOOKMARKED contains invalid ${key} =`, sectionConfig[key]); configValid = false; break; } } if (!configValid) { continue; } sentinel.on(sectionConfig.selectorParent, element => { doToggleBookmarkedSection(element, sectionConfig); }); const formattedSelector = formatChildSelector( sectionConfig.selectorParent, sectionConfig.selectorImagesContainer ); sentinel.on(formattedSelector, element => { const parent = element.closest(sectionConfig.selectorParent); if (parent && !element.dataset.pixiv_utils_toggle_bookmarked_section) { doToggleBookmarkedSection(parent, sectionConfig); } }); } // Dates sentinel.on(CONFIG.SELECTORS_DATE, element => { convertDate(element); }); // UTags Integration if (CONFIG.UTAGS_INTEGRATION) { sentinel.on(SELECTORS_UTAGS, element => { doUtags(element); }); } if (CONFIG.MODE !== 'PROD') { setInterval(() => { const intervals = Object.keys(waitForIntervals); if (intervals.length > 0) { // Debug first pending interval. logDebug('waitFor', waitForIntervals[intervals[0]].element); } }, 2500); } }); /** KEYBINDS **/ if (CONFIG.ENABLE_KEYBINDS) { const selectors = { editBookmark: CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS .split(', ').map(s => `${s} .pixiv_utils_edit_bookmark`).join(', ') }; const 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(selectors.editBookmark); return processKeyEvent('bookmarkEdit', element); } } }); logDebug('Listening for keybinds.'); } else { logDebug('Keybinds disabled.'); } })();