您需要先安装一个扩展,例如 篡改猴、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 *://www.pixiv.net/setting* // @exclude *://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 window.onurlchange // @run-at document-start // @version 1.5.1 // @author Bobby Wibowo // @license MIT // @description 7/2/2024, 8:37:14 PM // @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. // For Tampermonkey users, load Pixiv once after installing the userscript, // to allow it to populate its storage with default values. 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', // The following options have hard-coded preset values. Scroll further to find them. // Specifiying custom values will extend instead of replacing them. SELECTORS_HOME: 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_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, 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' }, // This has a hard-coded preset value. Specifiying a custom value will extend instead of replacing it. SELECTORS_DATE: null, REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME: false, // This has a hard-coded preset value. Specifiying a custom value will extend instead of replacing it. SECTIONS_TOGGLE_BOOKMARKED: null, ENABLE_KEYBINDS: true, UTAGS_INTEGRATION: true, // Hard-coded presets of "block" and "hide" tags. Specifying custom values will extend instead of replacing them. UTAGS_BLOCKED_TAGS: null, // Instead of merely hiding them à la Pixiv's built-in tags mute. UTAGS_REMOVE_BLOCKED: false }; const ENV = {}; // Store preset 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; } } /* 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. * * 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 * Bookmarked: .bXjFLc * * Home's latest works grid: * Image: li[data-ga4-label="thumbnail"] * * Discovery page's grid: * Title: .gtm-illust-recommend-title * Controls: .ppQNN * * 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:has(a[href]) * * 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 * Bookmarked: ._one-click-bookmark.on * * Newest by all page: * Image: .sc-e6de33c8-0 > li * Bookmarked: .epoVSE * * General mobile page: * Image: .works-item-illust:has(.thumb:not([src^=data])) * Controls: .bookmark, .hSoPoc */ const PRESETS = { SELECTORS_HOME: '[data-ga4-label="page_root"]', SELECTORS_IMAGE: '.sc-96f10c4f-0 > li, li[data-ga4-label="thumbnail"], .sc-9y4be5-1 > li, .sc-1sxj2bl-5 > li, .sc-l7cibp-1 > li, .ranking-item, .sc-e6de33c8-0 > li, .works-item-illust:has(.thumb:not([src^=data]))', 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, .ppQNN, .sc-iasfms-4, .sc-xsxgxe-3, ._layout-thumbnail, .bookmark, .hSoPoc', SELECTORS_IMAGE_BOOKMARKED: '.bXjFLc, ._one-click-bookmark.on, .epoVSE', SELECTORS_EXPANDED_VIEW_CONTROLS: '.sc-181ts2x-0, .work-interactions', SELECTORS_EXPANDED_VIEW_ARTIST_BOTTOM_IMAGE: '.sc-1nhgff6-4 > 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: '.sc-gulj4d-2, .sc-k3uf3r-3, .sc-10gpz4q-3, .sc-f30yhg-3', SELECTORS_FOLLOW_BUTTON: '[data-click-label="follow"]:not([disabled])', SELECTORS_DATE: '.sc-5981ly-1', 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' }, // Rankings page { selectorParent: '#wrapper ._unit', selectorHeader: '.ranking-menu', selectorImagesContainer: '.ranking-items-container' }, // Newest by all page { selectorParent: '.sc-7b5ed552-0', selectorHeader: '.sc-f08ce4e3-2', selectorImagesContainer: '.sc-a7a11491-1' } ], UTAGS_BLOCKED_TAGS: ['block', 'hide'] }; 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_')) { CONFIG[key] = PRESETS[key] || ''; if (ENV[key]) { CONFIG[key] += `, ${Array.isArray(ENV[key]) ? ENV[key].join(', ') : ENV[key]}`; } } 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', '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; // 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 && 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 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 */ 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 **/ // 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(+ .pixiv_utils_edit_bookmark_container) { flex-grow: 1; } .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; } ${CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS.split(', ').map(s => `${s} .pixiv_utils_edit_bookmark`).join(', ')}, ${_formatSelectorsMultiViewControls().map(s => `${s} .pixiv_utils_edit_bookmark`).join(', ')} { font-size: 12px; height: 24px; line-height: 24px; margin-top: 5px; margin-right: 7px; } ._layout-thumbnail .pixiv_utils_edit_bookmark { position: absolute; right: calc(50% - 71px); bottom: 4px; z-index: 2; } .ranking-item.muted .pixiv_utils_edit_bookmark { display: none; } *:has(> .pixiv_utils_image_artist_container) { position: relative; } .pixiv_utils_image_artist_container { color: rgb(245, 245, 245); background: rgba(0, 0, 0, 0.5); display: inline-block; box-sizing: border-box; padding: 0px 8px; border-radius: 10px; font-weight: bold; font-size: 14px; line-height: 20px; height: 20px; position: absolute; bottom: 6px; left: 6px; } .pixiv_utils_image_artist_container a { color: inherit; } .sc-s8zj3z-3:has(+ .pixiv_utils_toggle_bookmarked_container), .sc-7c5ab71e-2: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; } ${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 */` .pixiv_utils_blocked_image { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; border-radius: 4px; color: rgb(92, 92, 92); min-width: 96px; min-height: 96px; } .pixiv_utils_blocked_image svg { fill: currentcolor; } ${CONFIG.SELECTORS_IMAGE_TITLE.split(', ').map(s => `[data-pixiv_utils_blocked] ${s}`).join(', ')} { color: rgb(133, 133, 133) !important; } .ranking-item[data-pixiv_utils_blocked] ._illust-series-title-text { display: none; } ${CONFIG.SELECTORS_IMAGE_ARTIST_AVATAR.split(', ').map(s => `[data-pixiv_utils_blocked] ${s}`).join(', ')} { display: none; } ${CONFIG.SELECTORS_IMAGE_CONTROLS.split(', ').map(s => `[data-pixiv_utils_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="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 waitForIntervals = {}; const waitFor = (element = document, func) => { 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] = { element, func, resolve }; }); }; 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 findUrl = element => { return element.querySelector('a[href*="artworks/"]'); }; const findNovelUrl = element => { return element.querySelector('a[href*="novel/show.php?id="]'); }; const findItemId = element => { let id = null; let isNovel = false; let link = findUrl(element); if (link) { const match = link.href.match(/artworks\/(\d+)/); id = match ? match[1] : null; } else { link = findNovelUrl(element); if (link) { const match = link.href.match(/novel\/show\.php\?id=(\d+)/); id = match ? match[1] : null; isNovel = true; } } return { id, isNovel }; }; // 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 addImageArtist = async element => { let userId = null; let userName = null; if (element.__vue__) { await waitFor(element, () => !element.__vue__._props.item.notLoaded); 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 || !element[reactPropsKey].children.props.thumbnail) { return false; } userId = element[reactPropsKey].children.props.thumbnail.userId; userName = element[reactPropsKey].children.props.thumbnail.userName; } const div = document.createElement('div'); div.className = 'pixiv_utils_image_artist_container'; div.innerHTML = /* html */` <a href="https://www.pixiv.net/users/${userId}">${userName}</a> `; element.append(div); return true; }; const doImage = async (element, isHome = false) => { // Skip if invalid. if (!element.querySelector('a[href]')) { return false; } if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) { if (findNovelUrl(element)) { element.style.display = 'none'; logDebug('Removed novel recommendation 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. if (element.querySelector('.pixiv_utils_edit_bookmark')) { return false; } // Add artist tag if necessary, but never in artist page, or mobile expanded view's artist works bottom row. if (!element.querySelector('a[href*="users/"]') && currentUrl.indexOf('users/') === -1 && !element.closest('.user-badge')) { await addImageArtist(element); } // Wait if image controls is still being generated. const imageControls = await waitFor(element, () => { return element.querySelector(CONFIG.SELECTORS_IMAGE_CONTROLS); }); if (!imageControls) { return false; } const { id, isNovel } = findItemId(element); if (id !== null) { imageControls.prepend(editBookmarkButton(id, isNovel)); return true; } return false; }; const doMultiView = async (element, isHome = false) => { if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) { if (findNovelUrl(element)) { element.parentNode.style.display = 'none'; logDebug('Removed novel recommendation 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 { id, isNovel } = findItemId(element); if (id !== null) { multiViewControls.lastChild.before(editBookmarkButton(id, isNovel)); return true; } return false; }; const doExpandedViewControls = async 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 works bottom row. 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.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 = (element, sectionConfig) => { // Skip if already processed. if (element.dataset.pixiv_utils_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.dataset.pixiv_utils_toggle_bookmarked_section = true; 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 = element => { 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.dataset.pixiv_utils_blocked) { return false; } image.dataset.pixiv_utils_blocked = true; 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 + ', .pixiv_utils_image_artist_container a'); 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'; logDebug('Removed multi view entry due to UTag', element); 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.disabled = true; // Return early since there will only be one follow button per container. return true; } } return false; }; let isHome = false; window.addEventListener('detectnavigate', event => { const intervals = Object.keys(waitForIntervals); if (intervals.length) { logDebug(`Clearing ${intervals.length} pending waitFor interval(s).`); } for (const interval of intervals) { clearInterval(interval); waitForIntervals[interval].resolve(); delete waitForIntervals[interval]; } isHome = Boolean(document.querySelector(CONFIG.SELECTORS_HOME)); }); /** SENTINEL */ waitPageLoaded().then(() => { isHome = Boolean(document.querySelector(CONFIG.SELECTORS_HOME)); // 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); }); // Multi View Entries sentinel.on(CONFIG.SELECTORS_MULTI_VIEW, element => { doMultiView(element, isHome); }); // Toggle Bookmarked Sections for (const sectionConfig of CONFIG.SECTIONS_TOGGLE_BOOKMARKED) { if (!sectionConfig.selectorParent || !sectionConfig.selectorHeader || !sectionConfig.selectorImagesContainer) { log('Invalid "SECTIONS_TOGGLE_BOOKMARKED" config', sectionConfig); continue; } sentinel.on(sectionConfig.selectorParent, element => { doToggleBookmarkedSection(element, 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); } }, 1000); } }); /** 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.'); } })();