Bobby's Pixiv Utils

7/2/2024, 8:37:14 PM

当前为 2025-03-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bobby's Pixiv Utils
  3. // @namespace https://github.com/BobbyWibowo
  4. // @match *://www.pixiv.net/*
  5. // @exclude-match *://www.pixiv.net/setting*
  6. // @exclude-match *://www.pixiv.net/manage*
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
  8. // @grant GM_addStyle
  9. // @grant GM_getValue
  10. // @run-at document-end
  11. // @version 1.3.8
  12. // @author Bobby Wibowo
  13. // @license MIT
  14. // @description 7/2/2024, 8:37:14 PM
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict'
  20.  
  21. /** CONFIG **/
  22.  
  23. const log = (message, ...args) => {
  24. return console.log(`[${Date.now()}]: ${message}`, ...args);
  25. }
  26.  
  27. const logError = (message, ...args) => {
  28. return console.error(`[${Date.now()}]: ${message}`, ...args);
  29. }
  30.  
  31. const ENV = {
  32. MODE: GM_getValue('MODE'),
  33.  
  34. TEXT_EDIT_BOOKMARK: GM_getValue('EDIT_BOOKMARK_TEXT', 'Edit bookmark'),
  35.  
  36. // The following options have preset values. Scroll further to find them.
  37. // Specifiying custom values will extend instead of replacing them.
  38. SELECTORS_IMAGE: GM_getValue('SELECTORS_IMAGE'),
  39. SELECTORS_IMAGE_TITLE: GM_getValue('SELECTORS_IMAGE_TITLE'),
  40. SELECTORS_IMAGE_ARTIST_AVATAR: GM_getValue('SELECTORS_IMAGE_ARTIST_AVATAR'),
  41. SELECTORS_IMAGE_ARTIST_NAME: GM_getValue('SELECTORS_IMAGE_ARTIST_NAME'),
  42. SELECTORS_IMAGE_CONTROLS: GM_getValue('SELECTORS_IMAGE_CONTROLS'),
  43. SELECTORS_EXPANDED_VIEW_CONTROLS: GM_getValue('SELECTORS_EXPANDED_VIEW_CONTROLS'),
  44. SELECTORS_MULTI_VIEW: GM_getValue('SELECTORS_MULTI_VIEW'),
  45. SELECTORS_MULTI_VIEW_CONTROLS: GM_getValue('SELECTORS_MULTI_VIEW_CONTROLS'),
  46.  
  47. DATE_CONVERSION: GM_getValue('DATE_CONVERSION', true),
  48. DATE_CONVERSION_LOCALES: GM_getValue('DATE_CONVERSION_LOCALES', 'en-GB'),
  49. DATE_CONVERSION_OPTIONS: GM_getValue('DATE_CONVERSION_OPTIONS', {
  50. hour12: true,
  51. year: 'numeric',
  52. month: 'long',
  53. day: 'numeric',
  54. hour: '2-digit',
  55. minute: '2-digit'
  56. }),
  57. // This has a preset value. Specifiying a custom value will extend instead of replacing it.
  58. SELECTORS_DATE: GM_getValue('SELECTORS_DATE'),
  59.  
  60. REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME: GM_getValue('REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME', false),
  61.  
  62. ENABLE_KEYBINDS: GM_getValue('ENABLE_KEYBINDS', true),
  63.  
  64. UTAGS_INTEGRATION: GM_getValue('UTAGS_INTEGRATION', true),
  65. // Presets "block" and "hide" tags. Specifying custom values will extend instead of replacing them.
  66. UTAGS_BLOCKED_TAGS: GM_getValue('UTAGS_BLOCKED_TAGS'),
  67. // Instead of merely hiding them à la Pixiv's built-in tags mute.
  68. UTAGS_REMOVE_BLOCKED: GM_getValue('UTAGS_REMOVE_BLOCKED', false)
  69. }
  70.  
  71. /* DOCUMENTATION
  72. * -------------
  73. * 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
  74. *
  75. * Home's recommended works grid:
  76. * Image: .fhUcsb > li
  77. * Title: [data-ga4-label="title_link"]
  78. * Artist avatar: [data-ga4-label="user_icon_link"]
  79. * Artist name: [data-ga4-label="user_name_link"]
  80. * Controls: .kmCXcW
  81. *
  82. * Home's latest works grid:
  83. * Image: li[data-ga4-label="thumbnail"]
  84. *
  85. * Discovery page's grid:
  86. * Title: .gtm-illust-recommend-title
  87. * Controls: .dVtEKY
  88. *
  89. * Artist page's grid:
  90. * Image: .jtUPOE > li
  91. * Controls: .iHfghO
  92. *
  93. * Expanded view's artist works bottom row:
  94. * Image: .boBnlf > div
  95. *
  96. * Expanded view's related works grid:
  97. * Artist avatar: .eMfHJB
  98. * Artist name: .gtm-illust-recommend-user-name
  99. *
  100. * Artist page's featured works:
  101. * Image: .gmoaNn > li
  102. * Controls: .cGfNRT
  103. *
  104. * Bookmarks page's grid:
  105. * Title: .bOcolJ
  106. * Artist name: .IYOBi
  107. *
  108. * Tag page's grid:
  109. * Image: .hdRpMN > li
  110. *
  111. * Rankings page:
  112. * Image: .ranking-item
  113. * Title: .title
  114. * Artist avatar: ._user-icon
  115. * Artist name: .user-name
  116. * Controls: ._layout-thumbnail
  117. */
  118. const CONFIG = {
  119. MODE: 'PROD',
  120. SELECTORS_IMAGE: '.fhUcsb > li, li[data-ga4-label="thumbnail"], .jtUPOE > li, .boBnlf > div, .gmoaNn > li, .hdRpMN > li, .ranking-item', // .hjtPnz > li, .hkzusx > div, .iXWLAI > li, .cgtmvA li
  121. SELECTORS_IMAGE_TITLE: '[data-ga4-label="title_link"], .gtm-illust-recommend-title, .bOcolJ, .title', // .hQOtRd
  122. SELECTORS_IMAGE_ARTIST_AVATAR: '[data-ga4-label="user_icon_link"], .eMfHJB, ._user-icon', // .bwTmGA
  123. SELECTORS_IMAGE_ARTIST_NAME: '[data-ga4-label="user_name_link"], .gtm-illust-recommend-user-name, .IYOBi, .user-name', // .jNkIXf
  124. SELECTORS_IMAGE_CONTROLS: '.kmCXcW, .dVtEKY, .iHfghO, .cGfNRT, ._layout-thumbnail',
  125. SELECTORS_EXPANDED_VIEW_CONTROLS: '.gMEAWM',
  126. SELECTORS_MULTI_VIEW: '[data-ga4-label="work_content"]',
  127. SELECTORS_MULTI_VIEW_CONTROLS: '& > .w-full:last-child > .flex:first-child > .flex-row:first-child',
  128. SELECTORS_DATE: '.dqHJfP',
  129. UTAGS_BLOCKED_TAGS: ['block', 'hide']
  130. }
  131.  
  132. // Extend preset values with user-defined custom values if applicable.
  133. for (const key of Object.keys(ENV)) {
  134. if (key.startsWith('SELECTORS_')) {
  135. if (ENV[key]) {
  136. CONFIG[key] += `, ${ENV[key]}`;
  137. }
  138. } else if (Array.isArray(CONFIG[key])) {
  139. if (ENV[key]) {
  140. const customValues = Array.isArray(ENV[key]) ? ENV[key] : ENV[key].split(',').map(s => s.trim())
  141. CONFIG[key].push(...customValues);
  142. }
  143. } else if (ENV[key] !== undefined) {
  144. CONFIG[key] = ENV[key];
  145. }
  146. }
  147.  
  148. let logKeys = Object.keys(CONFIG);
  149. if (CONFIG.MODE === 'PROD') {
  150. // In PROD mode, only print some.
  151. logKeys = ['DATE_CONVERSION', 'REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME', 'ENABLE_KEYBINDS', 'UTAGS_INTEGRATION'];
  152. }
  153.  
  154. for (const key of logKeys) {
  155. log(`${key}: ${CONFIG[key]}`);
  156. }
  157.  
  158. /** GLOBAL UTILS **/
  159.  
  160. const addPageDateStyle = /*css*/`
  161. .bookmark-detail-unit .meta {
  162. display: block;
  163. font-size: 16px;
  164. font-weight: bold;
  165. color: inherit;
  166. margin-left: 0;
  167. margin-top: 10px;
  168. }
  169. `;
  170.  
  171. const convertDate = (element, fixJapanTime = false) => {
  172. let date;
  173.  
  174. const attr = element.getAttribute('datetime');
  175. if (attr) {
  176. date = new Date(attr);
  177. } else {
  178. // For pages which have the date display hardcoded to Japan time.
  179. let dateText = element.innerText;
  180. if (fixJapanTime) {
  181. dateText += ' UTC+9';
  182. }
  183. date = new Date(dateText);
  184. }
  185.  
  186. if (!date) {
  187. return false;
  188. }
  189.  
  190. const timestamp = String(date.getTime());
  191. if (element.dataset.oldTimestamp && element.dataset.oldTimestamp === timestamp) {
  192. return false;
  193. }
  194.  
  195. element.dataset.oldTimestamp = timestamp;
  196. element.innerText = date.toLocaleString(CONFIG.DATE_CONVERSION_LOCALES, CONFIG.DATE_CONVERSION_OPTIONS);
  197. return true;
  198. }
  199.  
  200. /** INTERCEPT EARLY FOR CERTAIN ROUTES **/
  201.  
  202. const path = location.pathname;
  203.  
  204. // Codes beyond this block will not execute for this route (mainly for efficiency).
  205. if (path.startsWith('/bookmark_add.php')) {
  206. if (CONFIG.DATE_CONVERSION) {
  207. GM_addStyle(addPageDateStyle);
  208.  
  209. const date = document.querySelector('.bookmark-detail-unit .meta');
  210. // This page has the date display hardcoded to Japan time without an accompanying timestamp.
  211. convertDate(date, true);
  212. }
  213.  
  214. log(`/bookmark_add.php path detected. Excluding date conversion, script has terminated early.`);
  215. return;
  216. }
  217.  
  218. /** MAIN STYLES **/
  219.  
  220. // To properly handle "&" CSS keyword, in context of also having to support user-defined custom values.
  221. // Somewhat overkill, but I'm out of ideas.
  222. const _formatSelectorsMultiViewControls = () => {
  223. const multiViews = CONFIG.SELECTORS_MULTI_VIEW.split(', ');
  224. const multiViewsControls = CONFIG.SELECTORS_MULTI_VIEW_CONTROLS.split(', ');
  225.  
  226. const formatted = [];
  227. for (const x of multiViews) {
  228. for (const y of multiViewsControls) {
  229. let z = y;
  230. if (y.startsWith('&')) {
  231. z = y.substring(1)
  232. }
  233. formatted.push(`${x} ${z.trim()}`);
  234. }
  235. }
  236. return formatted;
  237. }
  238.  
  239. const mainStyle = /*css*/`
  240. .flex:has(+.pu_edit_bookmark_container) {
  241. flex-grow: 1;
  242. }
  243.  
  244. .pu_edit_bookmark {
  245. color: rgb(245, 245, 245);
  246. background: rgba(0, 0, 0, 0.32);
  247. display: block;
  248. box-sizing: border-box;
  249. padding: 0px 6px;
  250. margin-top: 7px;
  251. margin-right: 2px;
  252. border-radius: 10px;
  253. font-weight: bold;
  254. font-size: 10px;
  255. line-height: 20px;
  256. height: 20px;
  257. }
  258.  
  259. ${CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS.split(', ').map(s => `${s} .pu_edit_bookmark`).join(', ')},
  260. ${_formatSelectorsMultiViewControls().map(s => `${s} .pu_edit_bookmark`).join(', ')} {
  261. font-size: 12px;
  262. height: 24px;
  263. line-height: 24px;
  264. margin-top: 5px;
  265. margin-right: 7px;
  266. }
  267.  
  268. ._layout-thumbnail .pu_edit_bookmark {
  269. position: absolute;
  270. right: calc(50% - 71px);
  271. bottom: 4px;
  272. z-index: 2;
  273. }
  274.  
  275. .ranking-item.muted .pu_edit_bookmark {
  276. display: none;
  277. }
  278.  
  279. ${CONFIG.SELECTORS_IMAGE_CONTROLS} {
  280. display: flex;
  281. justify-content: flex-end;
  282. }
  283. `;
  284.  
  285. const mainDateStyle = /*css*/`
  286. .dqHJfP {
  287. font-size: 14px !important;
  288. font-weight: bold;
  289. color: rgb(214, 214, 214) !important;
  290. }
  291. `;
  292.  
  293. /** UTAGS INTEGRATION INIT **/
  294.  
  295. const mainUtagsStyle = /*css*/`
  296. .pu_blocked_image {
  297. display: flex;
  298. justify-content: center;
  299. align-items: center;
  300. width: 100%;
  301. height: 100%;
  302. border-radius: 4px;
  303. color: rgb(92, 92, 92);
  304. background-color: rgb(0, 0, 0);
  305. }
  306.  
  307. .pu_blocked_image svg {
  308. fill: currentcolor;
  309. }
  310.  
  311. .pu_image_is_blocked .earAVC {
  312. width: 184px;
  313. height: 184px;
  314. }
  315.  
  316. .ranking-item.pu_image_is_blocked .work {
  317. width: 150px;
  318. height: 150px;
  319. }
  320.  
  321. ${CONFIG.SELECTORS_IMAGE_TITLE.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} {
  322. color: rgb(133, 133, 133) !important;
  323. }
  324.  
  325. ${CONFIG.SELECTORS_IMAGE_ARTIST_AVATAR.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} {
  326. display: none;
  327. }
  328.  
  329. ${CONFIG.SELECTORS_IMAGE_CONTROLS.split(', ').map(s => `.pu_image_is_blocked ${s}`).join(', ')} {
  330. display: none;
  331. }
  332. `;
  333.  
  334. const SELECTORS_UTAGS = CONFIG.UTAGS_BLOCKED_TAGS.map(s => `[data-utags_tag="${s}"]`).join(', ');
  335. log(`SELECTORS_UTAGS: ${SELECTORS_UTAGS}`);
  336.  
  337. const BLOCKED_IMAGE_HTML = `
  338. <div radius="4" class="pu_blocked_image">
  339. <svg viewBox="0 0 24 24" style="width: 48px; height: 48px;">
  340. <path d="M5.26763775,4 L9.38623853,11.4134814 L5,14.3684211 L5,18 L13.0454155,18 L14.1565266,20 L5,20
  341. 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
  342. C20.1045695,4 21,4.8954305 21,6 L21,18 C21,19.1045695 20.1045695,20 19,20 L18.7323623,20 L17.6212511,18
  343. L19,18 L19,13 L16,15 L15.9278695,14.951913 L9.84347336,4 Z M16,7 C14.8954305,7 14,7.8954305 14,9
  344. 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
  345. M7.38851434,1.64019979 L18.3598002,21.3885143 L16.6114857,22.3598002 L5.64019979,2.61148566
  346. L7.38851434,1.64019979 Z"></path>
  347. </svg>
  348. </div>
  349. `;
  350.  
  351. /** MAIN **/
  352.  
  353. GM_addStyle(mainStyle);
  354.  
  355. if (CONFIG.DATE_CONVERSION) {
  356. GM_addStyle(mainDateStyle);
  357. }
  358.  
  359. if (CONFIG.UTAGS_INTEGRATION) {
  360. GM_addStyle(mainUtagsStyle);
  361. }
  362.  
  363. class FunctionQueue {
  364. constructor() {
  365. this.queue = [];
  366. this.running = false;
  367. }
  368.  
  369. async go() {
  370. if (this.queue.length) {
  371. this.running = true;
  372. const _func = this.queue.shift();
  373. await _func[0](..._func[1]);
  374. this.go();
  375. } else {
  376. this.running = false;
  377. }
  378. }
  379.  
  380. add (func, ...args) {
  381. this.queue.push([func, [...args]]);
  382.  
  383. if (!this.running) {
  384. this.go();
  385. }
  386. }
  387. }
  388.  
  389. const observerFactory = function (option) {
  390. let options;
  391. if (typeof option === 'function') {
  392. options = {
  393. callback: option,
  394. node: document.getElementsByTagName('body')[0],
  395. option: { childList: true, subtree: true }
  396. };
  397. } else {
  398. options = $.extend({
  399. callback: () => {},
  400. node: document.getElementsByTagName('body')[0],
  401. option: { childList: true, subtree: true }
  402. }, option);
  403. }
  404. const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  405.  
  406. const observer = new MutationObserver((mutations, observer) => {
  407. options.callback.call(this, mutations, observer);
  408. });
  409.  
  410. observer.observe(options.node, options.option);
  411. return observer;
  412. };
  413.  
  414. const editBookmarkButton = (id, isNovel = false) => {
  415. const buttonContainer = document.createElement('div');
  416. buttonContainer.className = 'pu_edit_bookmark_container';
  417.  
  418. const button = document.createElement('a');
  419. button.className = 'pu_edit_bookmark';
  420. button.innerText = CONFIG.TEXT_EDIT_BOOKMARK;
  421.  
  422. if (isNovel) {
  423. button.href = `https://www.pixiv.net/novel/bookmark_add.php?id=${id}`;
  424. } else {
  425. button.href = `https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=${id}`;
  426. }
  427.  
  428. buttonContainer.appendChild(button);
  429. return buttonContainer;
  430. }
  431.  
  432. const findLink = element => {
  433. return element.querySelector('a[href*="artworks/"]');
  434. }
  435.  
  436. const findNovelLink = element => {
  437. return element.querySelector('a[href*="novel/show.php?id="]');
  438. }
  439.  
  440. const findItemId = element => {
  441. let id = null;
  442. let isNovel = false;
  443.  
  444. let link = findLink(element);
  445. if (link) {
  446. const match = link.href.match(/artworks\/(\d+)/);
  447. id = match ? match[1] : null;
  448. } else {
  449. link = findNovelLink(element);
  450. if (link) {
  451. const match = link.href.match(/novel\/show\.php\?id=(\d+)/);
  452. id = match ? match[1] : null;
  453. isNovel = true;
  454. }
  455. }
  456.  
  457. return { id, isNovel };
  458. }
  459.  
  460. const isElementVisible = element => {
  461. if (!element || !element.isConnected) {
  462. return false;
  463. }
  464.  
  465. return element.checkVisibility();
  466. }
  467.  
  468. const doImage = (element, isHome = false) => {
  469. if (!isElementVisible(element)) {
  470. return false;
  471. }
  472.  
  473. if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) {
  474. if (findNovelLink(element)) {
  475. element.style.display = 'none';
  476. return true;
  477. }
  478. }
  479.  
  480. // Skip if edit bookmark button already inserted.
  481. if (element.querySelector('.pu_edit_bookmark')) {
  482. return false;
  483. }
  484.  
  485. const imageControls = element.querySelector(CONFIG.SELECTORS_IMAGE_CONTROLS);
  486. if (!imageControls) {
  487. return false;
  488. }
  489.  
  490. const { id, isNovel } = findItemId(element);
  491. if (id !== null) {
  492. imageControls.insertBefore(editBookmarkButton(id, isNovel), imageControls.firstChild);
  493. return true;
  494. }
  495.  
  496. return false;
  497. }
  498.  
  499. const doMultiView = (element, isHome = false) => {
  500. if (!isElementVisible(element)) {
  501. return false;
  502. }
  503.  
  504. if (CONFIG.REMOVE_NOVEL_RECOMMENDATIONS_FROM_HOME && isHome) {
  505. if (findNovelLink(element)) {
  506. element.parentNode.style.display = 'none';
  507. return true;
  508. }
  509. }
  510.  
  511. // Skip if edit bookmark button already inserted.
  512. if (element.querySelector('.pu_edit_bookmark')) {
  513. return false;
  514. }
  515.  
  516. const multiViewControls = element.querySelector(CONFIG.SELECTORS_MULTI_VIEW_CONTROLS);
  517. if (!multiViewControls) {
  518. return false;
  519. }
  520.  
  521. const { id, isNovel } = findItemId(element);
  522. if (id !== null) {
  523. multiViewControls.insertBefore(editBookmarkButton(id, isNovel), multiViewControls.lastChild);
  524. return true;
  525. }
  526.  
  527. return false;
  528. }
  529.  
  530. const doExpandedViewControls = element => {
  531. if (!isElementVisible(element)) {
  532. return false;
  533. }
  534.  
  535. // Skip if edit bookmark button already inserted.
  536. if (element.querySelector('.pu_edit_bookmark')) {
  537. return false;
  538. }
  539.  
  540. let id = null;
  541. let isNovel = false;
  542.  
  543. let match = window.location.href.match(/artworks\/(\d+)/);
  544. if (match && match[1]) {
  545. id = match[1];
  546. } else {
  547. match = window.location.href.match(/novel\/show\.php\?id=(\d+)/);
  548. if (match && match[1]) {
  549. id = match[1];
  550. isNovel = true;
  551. }
  552. }
  553.  
  554. if (id !== null) {
  555. element.appendChild(editBookmarkButton(id, isNovel));
  556. return true;
  557. }
  558.  
  559. return false;
  560. }
  561.  
  562. const doUtagsImage = element => {
  563. if (!isElementVisible(element)) {
  564. return false;
  565. }
  566.  
  567. const image = element.closest(CONFIG.SELECTORS_IMAGE);
  568. if (image) {
  569. const imageLink = image.querySelector('a[href*="artworks/"], a[href*="novel/"]');
  570. if (!imageLink) {
  571. return false;
  572. }
  573.  
  574. if (CONFIG.UTAGS_REMOVE_BLOCKED) {
  575. image.style.display = 'none';
  576. return true;
  577. }
  578.  
  579. // Skip if already blocked.
  580. if (image.classList.contains('pu_image_is_blocked')) {
  581. return false;
  582. }
  583.  
  584. image.classList.add('pu_image_is_blocked');
  585.  
  586. imageLink.innerHTML = BLOCKED_IMAGE_HTML;
  587.  
  588. const imageTitle = image.querySelector(CONFIG.SELECTORS_IMAGE_TITLE);
  589. if (imageTitle) {
  590. if (element.dataset.utags_tag === "hide") {
  591. imageTitle.innerText = 'Hidden';
  592. } else {
  593. // block tag and custom tags
  594. imageTitle.innerText = 'Blocked';
  595. }
  596. }
  597.  
  598. const artistLink = image.querySelector(CONFIG.SELECTORS_IMAGE_ARTIST_NAME);
  599. if (artistLink) {
  600. artistLink.innerText = '';
  601. }
  602.  
  603. return true;
  604. }
  605.  
  606. const multiView = element.closest('[data-ga4-label="work_content"]');
  607. if (multiView) {
  608. // For multi view artwork, just hide the whole entry instead.
  609. multiView.parentNode.style.display = 'none';
  610. return true;
  611. }
  612.  
  613. const artistHeader = element.closest('.ggHNyV');
  614. if (artistHeader) {
  615. const followButton = artistHeader.querySelector('.irfecv:not([disabled])');
  616. if (followButton) {
  617. // This does not disable Pixiv's built-in "F" keybind.
  618. followButton.disabled = true;
  619. return true;
  620. }
  621. }
  622.  
  623. return false;
  624. }
  625.  
  626. const triggerQueue = new FunctionQueue();
  627.  
  628. observerFactory((...args) => {
  629. triggerQueue.add((mutations, observer) => {
  630. for (let i = 0, len = mutations.length; i < len; i++) {
  631. const mutation = mutations[i];
  632.  
  633. // Whether to change nodes.
  634. if (mutation.type !== 'childList') {
  635. continue;
  636. }
  637.  
  638. // Always attempt to query from its parent, to allow the element itself to match the queries.
  639. const target = mutation.target.parentElement || mutation.target;
  640.  
  641. const isHome = Boolean(target.closest('[data-ga4-label="page_root"]'));
  642.  
  643. // Expanded View Controls
  644. const expandedViewControls = target.querySelector(CONFIG.SELECTORS_EXPANDED_VIEW_CONTROLS);
  645. if (expandedViewControls && doExpandedViewControls(expandedViewControls)) {
  646. log(`Processed expanded view controls.`);
  647. }
  648.  
  649. // Images
  650. let _image = 0;
  651.  
  652. const images = target.querySelectorAll(CONFIG.SELECTORS_IMAGE);
  653. for (const image of images) {
  654. if (doImage(image, isHome)) {
  655. _image++;
  656. }
  657. }
  658.  
  659. if (_image > 0) {
  660. log(`Processed ${_image} image(s).`);
  661. }
  662.  
  663. // Multi Views
  664. let _multiView = 0;
  665.  
  666. const multiViews = target.querySelectorAll(CONFIG.SELECTORS_MULTI_VIEW);
  667. for (const multiView of multiViews) {
  668. if (doMultiView(multiView, isHome)) {
  669. _multiView++;
  670. }
  671. }
  672.  
  673. if (_multiView > 0) {
  674. log(`Processed ${_multiView} multi view(s).`);
  675. }
  676.  
  677. // Dates
  678. if (CONFIG.DATE_CONVERSION) {
  679. let _date = 0;
  680.  
  681. const dates = target.querySelectorAll(CONFIG.SELECTORS_DATE);
  682. for (const date of dates) {
  683. if (convertDate(date)) {
  684. _date++;
  685. }
  686. }
  687.  
  688. if (_date > 0) {
  689. log(`Processed ${_date} date element(s).`);
  690. }
  691. }
  692.  
  693. // UTags integration
  694. if (CONFIG.UTAGS_INTEGRATION) {
  695. let _utag = 0;
  696.  
  697. const utags = target.querySelectorAll(SELECTORS_UTAGS);
  698. for (const utag of utags) {
  699. if (doUtagsImage(utag)) {
  700. _utag++;
  701. }
  702. }
  703.  
  704. if (_utag > 0) {
  705. log(`Processed ${_utag} UTag(s).`);
  706. }
  707. }
  708. }
  709. }, ...args);
  710. });
  711.  
  712. /** KEYBINDS **/
  713.  
  714. if (CONFIG.ENABLE_KEYBINDS) {
  715. let onCooldown = {};
  716.  
  717. const processKeyEvent = (id, element) => {
  718. if (!element) {
  719. return false;
  720. }
  721.  
  722. if (onCooldown[id]) {
  723. log(`"${id}" keybind still on cooldown.`);
  724. return false;
  725. }
  726.  
  727. onCooldown[id] = true;
  728. element.click();
  729. setTimeout(() => { onCooldown[id] = false }, 1000);
  730. }
  731.  
  732. document.addEventListener('keydown', event => {
  733. event = event || window.event;
  734.  
  735. // Ignore keybinds when currently focused to an input/textarea/editable element.
  736. if (document.activeElement && (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable)) {
  737. return;
  738. }
  739.  
  740. // "Shift+B" for Edit Bookmark.
  741. // Pixiv has built-in keybind "B" for just bookmarking.
  742. if (event.keyCode === 66) {
  743. if (event.ctrlKey || event.altKey) {
  744. // Ignore "Ctrl+B" or "Alt+B".
  745. return;
  746. }
  747. if (event.shiftKey) {
  748. event.stopPropagation();
  749. const element = document.querySelector('.gpoeGt .pu_edit_bookmark');
  750. return processKeyEvent('bookmarkEdit', element);
  751. }
  752. }
  753. });
  754.  
  755. log('Listening for keybinds.');
  756. } else {
  757. log('Keybinds disabled.');
  758. }
  759.  
  760. })()