Greasy Fork is available in English.

Pixiv Arts Preview & Followed Atrists Coloring & Extended History

Enlarged preview of arts and manga on mouse hovering. Extended history for non-premium users. Auto-Pagination on Following and Users pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to add art to bookmarks, Ctrl+LMB-click for saving originals of artworks. The names of the authors you are already subscribed to are highlighted with green. Settings can be changed in proper menu.

Ajankohdalta 20.1.2025. Katso uusin versio.

  1. // ==UserScript==
  2. // @name Pixiv Arts Preview & Followed Atrists Coloring & Extended History
  3. // @namespace Pixiv
  4. // @description Enlarged preview of arts and manga on mouse hovering. Extended history for non-premium users. Auto-Pagination on Following and Users pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to add art to bookmarks, Ctrl+LMB-click for saving originals of artworks. The names of the authors you are already subscribed to are highlighted with green. Settings can be changed in proper menu.
  5. // @author NightLancerX
  6. // @version 3.93.1
  7. // @match https://www.pixiv.net/bookmark_new_illust*
  8. // @match https://www.pixiv.net/discovery*
  9. // @match https://www.pixiv.net/ranking.php*
  10. // @match https://www.pixiv.net/*artworks/*
  11. // @match https://www.pixiv.net/*users/*
  12. // @match https://www.pixiv.net/history.php*
  13. // @match https://www.pixiv.net/bookmark_detail.php?illust_id=*
  14. // @match https://www.pixiv.net/*tags/*
  15. // @match https://www.pixiv.net/*
  16. // @connect i.pximg.net
  17. // @connect i-f.pximg.net
  18. // @connect i-cf.pximg.net
  19. // @connect techorus-cdn.com
  20. // @homepageURL https://github.com/NightLancer/PixivPreview
  21. // @supportURL https://greasyfork.org/users/167506-nightlancerx
  22. // @license MIT
  23. // @copyright NightLancerX
  24. // @grant GM_xmlhttpRequest
  25. // @grant GM.xmlHttpRequest
  26. // @grant GM_setValue
  27. // @grant GM.setValue
  28. // @grant GM_getValue
  29. // @grant GM.getValue
  30. // @require https://code.jquery.com/jquery-3.3.1.min.js
  31. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
  32. // @compatible firefox >= 74
  33. // @compatible chrome >= 80
  34. // @noframes
  35. // ==/UserScript==
  36. //=======================================================================================
  37. (function ()
  38. {
  39. 'use strict';
  40.  
  41. if (window.top == window.self && window.jQuery) jQuery(function($) //window.top check may be useless because of @noframes
  42. {
  43. console.log('MyPixivJS');
  44.  
  45. //---------------------------***CUSTOM PREFERENCES***--------------------------------
  46. let propList = [
  47. {paramIndex:0, array:[false,true], name:"PREVIEW_ON_CLICK"},
  48. {paramIndex:2, array:[0, 100, 200, 300, 500, 1000, 1500], name:"DELAY_BEFORE_PREVIEW"},
  49. {paramIndex:0, array:["auto", 600, 1200], name:"PREVIEW_SIZE"},
  50. {paramIndex:1, array:[false,true], name:"ENABLE_AUTO_PAGINATION"},
  51. {paramIndex:0, array:[false,true], name:"DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"},
  52. {paramIndex:1, array:[false,true], name:"SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"},
  53. {paramIndex:0, array:[false,true], name:"DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"},
  54. {paramIndex:0, array:[false,true], name:"HIDE_PEOPLE_WHO_BOOKMARKED_THIS"},
  55. {paramIndex:0, array:[false,true], name:"KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"}
  56. ];
  57. //---------------------------------DEFAULT VALUES------------------------------------
  58. // ■ PREVIEW_ON_CLICK =
  59. // false : showing preview on mouseover (default)
  60. // true : showing preview after LMB-click
  61. //
  62. // ■ DELAY_BEFORE_PREVIEW =
  63. // 0 : no delay before preview
  64. // 100 : 0.1 second delay (1000 for 1 second, etc) (default)
  65. //
  66. // ■ PREVIEW_SIZE =
  67. // auto : automatically calculate preview size (1200 or 600) depending of current screen size (default)
  68. // 600 : up to 600px x 600px
  69. // 1200 : up to 1200px x 1200px
  70. //
  71. // ■ ENABLE_AUTO_PAGINATION =
  72. // false: disable auto pagination
  73. // true: enable auto-pagination on Following and Users pages (default)
  74. //
  75. // ■ DISABLE_MANGA_PREVIEW_SCROLLLING_PROPAGATION =
  76. // false : keeping page scrolling after end of manga preview scrolling (default)
  77. // true : disable page scrolling when viewing manga preview (move mouse out of preview to re-enable scrolling)
  78. //
  79. // ■ SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE =
  80. // true : preview of single image will smoothly fit to vertical screen border after one scroll (default)
  81. // false : manually scrolling (may need in case of forced 1200px vertical preview with small user screen)
  82. //
  83. // ■ DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING =
  84. // false: standard behavior (default)
  85. // true : disable page scrolling when viewing single preview (works only if previous setting set to true)
  86. //
  87. // ■ HIDE_PEOPLE_WHO_BOOKMARKED_THIS =
  88. // false: don't change `bookmark_detail.php` page (default)
  89. // true: hide "People who bookmarked this" section
  90. //
  91. // ■ KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS =
  92. // false: update date every time artwork page opens (default)
  93. // true: don't renew date and keep first one (NOTE: art will not appear at the top of the history)
  94.  
  95. let currentSettings = {};
  96. //-----------------------------------------------------------------------------------
  97. let hoverImg = document.createElement('img');
  98. hoverImg.style = 'display: block;'
  99.  
  100. let imgContainer = document.createElement('div');
  101. imgContainer.id = 'imgPreview';
  102. imgContainer.style = 'position:absolute; display:block; visibility:visible; z-index:1000; background:#222; padding:5px; margin:-5px;';
  103. imgContainer.appendChild(hoverImg);
  104.  
  105. let mangaContainer = document.createElement('div');
  106. mangaContainer.id = 'mangaContainer';
  107. mangaContainer.style = 'display:block; overflow-x:auto; white-space:nowrap; maxWidth:1200px; z-index:1500; background:#111; font-size: 0;';
  108.  
  109. let mangaMiddleContainer = document.createElement('div');
  110. mangaMiddleContainer.style = 'display:block; visibility:inherit; z-index:1250;';
  111. mangaMiddleContainer.appendChild(mangaContainer);
  112.  
  113. let mangaOuterContainer = document.createElement('div');
  114. mangaOuterContainer.id = 'mangaOuterContainer';
  115. mangaOuterContainer.style = 'position:absolute; display:block; visibility:hidden; left:0px; right:0px; width:max-content; margin: 0px auto; padding:5px; background:#111; z-index:1000;';
  116. mangaOuterContainer.appendChild(mangaMiddleContainer);
  117.  
  118. let imgsArr = [], //for manga-style image packs...
  119. followedUsersId = {}, //storing followed users pixiv ID
  120. BOOKMARK_URL = 'https://www.pixiv.net/ajax/user/XXXXXXXX/following?limit=100&tag=&lang=en',//&offset=0&rest=show'
  121. USER_ID,
  122. totalHits = 0,
  123. lastImgId = -1,
  124. PREVIEWSIZE,
  125. siteImgMaxWidth = 184, //2,7,12 [NEW]| quite useless on this pages because of square previews...
  126. mangaWidth = 1200,
  127. maxRequestTime = 30000,
  128. bookmarkContainer,
  129. pageNumber,
  130. DELTASCALE = +navigator.userAgent.match(/(?<=Firefox\/)\d+/)?.[0]<83?70:4, //older than 83.0 FF uses different scrolling scale //[temporary...]
  131. previewEventType,
  132. PAGETYPE = checkPageType(),
  133. followedCheck = {
  134. id:0, //backuping user id in case of cookie errors
  135. status:0, //-1: error, 0:default, 1:in progress, 2:done
  136. date:0, //date of last successful check
  137. saveState(){
  138. localStorage.setObj('followedCheck', this);
  139. },
  140. loadState(){
  141. this.id = localStorage.getObj('followedCheck')?.id || 0;
  142. this.status = localStorage.getObj('followedCheck')?.status || 0;
  143. this.date = localStorage.getObj('followedCheck')?.date || 0;
  144. }
  145. };
  146.  
  147. var timerId, tInt, menuTimer;
  148. //-----------------------------------------------------------------------------------
  149. Storage.prototype.setObj = function(key, obj){
  150. return this.setItem(key, JSON.stringify(obj))
  151. }
  152. Storage.prototype.getObj = function(key){
  153. return JSON.parse(this.getItem(key))
  154. }
  155. //-----------------------------------------------------------------------------------
  156. const GM_setV = (typeof(GM_setValue)==='function')?GM_setValue:GM.setValue;
  157. const GM_getV = (typeof(GM_getValue)==='function')?GM_getValue:GM.getValue;
  158. //===================================================================================
  159. //************************************PageType***************************************
  160. //===================================================================================
  161. function checkPageType()
  162. {
  163. if (document.URL.match(/https:\/\/www.pixiv.net\/bookmark_new_illust(?:_r18)?.php/)) return 0; //New illustrations - New +
  164. if (document.URL.match(/^https:\/\/www.pixiv.net\/discovery(?:\?mode=(safe|r18))?$/)) return 1; //Discovery page(works) - New +
  165. if (document.URL.match('https://www.pixiv.net/bookmark_detail.php?')) return 4; //Bookmark information - Old +
  166. if (document.URL.match('https://www.pixiv.net/ranking.php?')) return 6; //Daily rankings - Old +
  167. if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users\/\d+\/bookmarks\/artworks/)) return 7; //Bookmarks page - New +
  168. if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users/)) return 2; //Artist works page - New +
  169. if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?tags/)) return 8; //Search page - New +
  170. if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?artworks/)) return 12; //Illust page - New* +
  171. if (document.URL.match('https://www.pixiv.net/discovery/users?')) return 13; //Discovery page(users) New +
  172. if (document.URL.match('https://www.pixiv.net/history.php')) return 14; //History - Old +
  173. if (document.URL.match(/^https:\/\/www\.pixiv\.net\/(?:en\/)?$/)) return 10; //Home page - New +
  174.  
  175. return -1;
  176. }
  177. console.log('PAGETYPE:',PAGETYPE);
  178. //-----------------------------------------------------------------------------------
  179. //Old: 4 6 14
  180. //New: 0 1 2 7 8 10 12 13
  181. //==============----------------------------
  182. //Coloring: = 1 = 4 6 7 8 10 12 == ~~ //
  183. //Profile card: 0 1 = 4 6 7 8 10 12 == == //
  184. //On following: = 1 2 4 6 7 8 ?? 12 13 == //
  185. //===================================================================================
  186. function setCurrentSettings(){
  187. for (let i = 0; i < propList.length; i++){
  188. currentSettings[propList[i].name] = propList[i].array[propList[i].paramIndex]; //only for options checking, actual settings contains in propList[]
  189. }
  190. resetPreviewSize(); //needed because of "auto" feature
  191. resetPreviewEventType();
  192. }
  193. //-----------------------------------------------------------------------------------
  194. function saveSettings(){
  195. for (let i = 0; i < propList.length; i++){
  196. localStorage.setObj(propList[i].name, propList[i].paramIndex);
  197. }
  198. console.log("Settings saved");
  199. }
  200. //-----------------------------------------------------------------------------------
  201. function loadSavedSettings(){
  202. for (let i = 0; i < propList.length; i++){
  203. propList[i].paramIndex = localStorage.getObj(propList[i].name) ?? propList[i].paramIndex; //load saved setting value, or let default if not found
  204.  
  205. if ((propList[i].paramIndex < 0) || (propList[i].paramIndex >= propList[i].array.length)){
  206. propList[i].paramIndex = 0; // "0" is not default for all settings...
  207. console.error(`localStorage error! Setting ${propList[i].name} has been reset to default value! [${propList[i].array[propList[i].paramIndex]}]`);
  208. }
  209. }
  210. console.log("Settings loaded");
  211. }
  212. //-----------------------------------------------------------------------------------
  213. loadSavedSettings();
  214. setCurrentSettings();
  215. //-----------------------------------------------------------------------------------
  216. function resetPreviewSize(){PREVIEWSIZE = (currentSettings["PREVIEW_SIZE"] > 0)?currentSettings["PREVIEW_SIZE"]:(window.innerHeight>1200 & document.body.clientWidth>1200)?1200:600}
  217. function resetPreviewEventType(){previewEventType = (currentSettings["PREVIEW_ON_CLICK"])?'click':'mouseenter'; console.log(previewEventType)}
  218. //===================================================================================
  219. //**********************************ColorFollowed************************************
  220. //===================================================================================
  221. function makeArgs(baseUrl, total){
  222. let arr = [];
  223. for(let i = 1; i < Math.ceil(total / 100); i++){ //from 1 - because we already have first 100 users
  224. arr.push(baseUrl + "&offset=" + i + "00");
  225. }
  226. return arr;
  227. }
  228. //-----------------------------------------------------------------------------------
  229. async function getUserId(){
  230. USER_ID = USER_ID
  231. || followedCheck && followedCheck?.id
  232. || document.cookie.match(/user_id=\d+/)?.[0].split("=").pop()
  233. || Object.keys(localStorage).filter(e => e.match(/viewed_illust_ids_\d+/)).map(a => a.match(/\d+/))[0]
  234. || (await fetch('https://www.pixiv.net/bookmark.php')).url.match(/\d{3,}/)[0];
  235.  
  236. if (!USER_ID) return Promise.reject('FATAL ERROR in obtaining user ID! Please report this on GitHub "Issues"');
  237. }
  238. //-----------------------------------------------------------------------------------
  239. async function checkFollowedArtists()
  240. {
  241. followedCheck.loadState();
  242.  
  243. if (((Date.now()-23*60*60*1000) > followedCheck.date) || (followedCheck.status < 2) || !localStorage['followedUsersId']){
  244. console.log('*Followed check started*');
  245.  
  246. followedCheck.status = 1;
  247. followedCheck.saveState();
  248.  
  249. await getUserId().catch(e => followedCheckError(e));
  250. if (USER_ID>0){
  251. BOOKMARK_URL = BOOKMARK_URL.replace("XXXXXXXX", USER_ID);
  252. }
  253. else return -1;
  254.  
  255. //make first request separately for obtaining count of followed users, both public/private
  256. let response0 = [];
  257. try{
  258. response0 = await Promise.all([request(BOOKMARK_URL+'&rest=show&offset=0'), request(BOOKMARK_URL+'&rest=hide&offset=0')]);
  259. }
  260. catch(error){
  261. console.error("Error with initial bookmark url!");
  262. followedCheckError(error);
  263. return -1;
  264. }
  265. for(const i of response0) i.body.users.forEach(user => followedUsersId[user.userId] = 1);
  266.  
  267. let args = [];
  268. let len = response0.map(r => r.body.total);
  269.  
  270. args = makeArgs(BOOKMARK_URL+'&rest=show', len[0]); //public
  271. args.concat(makeArgs(BOOKMARK_URL+'&rest=hide', len[1])); //private
  272.  
  273. //100 parallel requests in case of 10K users. TODO: find maximum amount and part requests
  274. let responseArray = [];
  275. try{
  276. responseArray = await Promise.all(args.map(e => request(e)));
  277. }
  278. catch(error){
  279. followedCheckError(error);
  280. return -1;
  281. }
  282. for(const r of responseArray) r.body.users.forEach(user => followedUsersId[user.userId] = 1);
  283.  
  284. localStorage.setObj('followedUsersId', followedUsersId);
  285. followedCheck.id = USER_ID;
  286. followedCheck.status = 2;
  287. followedCheck.date = Date.now();
  288. followedCheck.saveState();
  289. console.log('*Followed check completed*');
  290. console.log('Obtained', Object.keys(followedUsersId).length, 'followed users');
  291. }
  292. else{
  293. followedUsersId = localStorage.getObj('followedUsersId');
  294. console.log(`followedCheck is up to date of %c${new Date(followedCheck.date).toLocaleString()}`, 'color:violet;');
  295. }
  296. }
  297. checkFollowedArtists();
  298. //-----------------------------------------------------------------------------------
  299. async function request(url, responseType)
  300. {
  301. return new Promise(function (resolve, reject){
  302. let xhr = new XMLHttpRequest();
  303. xhr.responseType = responseType || 'json';
  304. xhr.timeout = 10000;
  305. xhr.open('GET', url, true);
  306. xhr.onload = function (){
  307. resolve(this.response);
  308. };
  309. xhr.onerror = xhr.ontimeout = function(){
  310. reject(this);
  311. };
  312. xhr.send();
  313. });
  314. }
  315. //-----------------------------------------------------------------------------------
  316. function followedCheckError(error){
  317. console.error(error);
  318. followedCheck.status = -1;
  319. followedCheck.saveState();
  320. }
  321. //-----------------------------------------------------------------------------------
  322. async function colorFollowed(artsContainers, delay)
  323. {
  324. let c = 0, d = 0;
  325. while (!artsContainers || artsContainers.length === 0) //first call -> daily rankings, illust page
  326. {
  327. console.log('waiting for arts...');
  328. await sleep(delay ?? 2000);
  329.  
  330. artsContainers = getAllArtsContainers();
  331. ++c;
  332. if (c>5) {console.error('Error while waiting for arts loading! [Timeout 10s]'); return}
  333. }
  334.  
  335. let artsContainersLength = artsContainers.length;
  336.  
  337. //wait until last XHR completed if it is not---------------------------------------
  338. followedCheck.loadState();
  339.  
  340. if (followedCheck.status == 1){
  341. while (followedCheck.status !== 2){
  342. console.log("waiting for followed users..."); //this could happen in case of huge amount of followed users
  343. await sleep(2000);
  344. followedCheck.loadState();
  345.  
  346. ++d;
  347. if (d*2000 > maxRequestTime || followedCheck.status == -1){
  348. console.error(`ERROR while EXPECTING for subscriptions list! [${d*2000/1000}s]`);
  349. break;
  350. }
  351. }
  352. }
  353.  
  354. //load from localStorage on any errors
  355. if (followedCheck.status <= 0 || Object.keys(followedUsersId).length == 0){
  356. console.error(`There was some error during followed users check [Error Code: ${followedCheck.status}]`);
  357. console.log(`Trying to load cached followedUsersId by date of ${new Date(followedCheck.date).toLocaleString()} ...`);
  358.  
  359. followedUsersId = localStorage.getObj('followedUsersId');
  360. if (followedUsersId && Object.keys(followedUsersId).length > 0){
  361. console.log("Loaded cached", Object.keys(followedUsersId).length, "followed users");
  362. }
  363. else{
  364. console.error('There is no locally stored followed users entries!');
  365. return -1;
  366. }
  367. }
  368. //---------------------------------------------------------------------------------
  369. if (PAGETYPE!==1) console.log('arts loaded:', artsContainersLength, 'Total:', getAllArtsContainers().length);
  370.  
  371. let hitContainers = [];
  372. let currentHits = 0;
  373.  
  374. if (PAGETYPE == 12){
  375. let authorId = +document.querySelector("aside").querySelector("[href*=users]").href.match(/\d+/)[0];
  376. [].filter.call(artsContainers, container => getAuthorIdFromContainer(container) == authorId) //color current authors arts among suggested
  377. .forEach(container => container.setAttribute("style", "background-color: deepskyblue; !important"));
  378. }
  379.  
  380. hitContainers = [].filter.call(artsContainers, container => followedUsersId[getAuthorIdFromContainer(container)] == 1);
  381. hitContainers.forEach(container => container.setAttribute("style", "background-color: green; !important"));
  382.  
  383. currentHits = hitContainers.length;
  384. totalHits += currentHits;
  385.  
  386. if (PAGETYPE!==1) console.log('hits: '+currentHits + ' (Total: '+(totalHits)+')'); //containers are constantly being replaced on this page
  387. }
  388. //-----------------------------------------------------------------------------------
  389. function getAllArtsContainers()
  390. {
  391. switch (PAGETYPE){
  392. case 1:
  393. case 7:
  394. case 8:
  395. case 10: return [...document.querySelectorAll('li > div')].filter(e => (e.querySelector('a[href*="/artworks/"]')));
  396.  
  397. case 4:
  398. case 6: return document.querySelectorAll('.ui-profile-popup');
  399.  
  400. case 12: return document.querySelectorAll('.gtm-illust-recommend-title');
  401.  
  402. default: console.error('Unprocessed PAGETYPE in getAllArtsContainers()!');
  403. }
  404. return null;
  405. }
  406. //-----------------------------------------------------------------------------------
  407. function getAuthorIdFromContainer(artContainer)
  408. {
  409. let authorId = -1;
  410. //console.log(artContainer);
  411.  
  412. if (!artContainer){
  413. console.error('UNPROCESSED getAuthorIdFromContainer() call!');
  414. }
  415. else if (typeof artContainer.hasAttribute !== 'function'){
  416. console.log(artContainer, 'has been filtered out.');
  417. }
  418. else if ([1,7,10,12].includes(PAGETYPE)){
  419. authorId = searchNearestNode(artContainer,'[href*="/users/"]').getAttribute('href').split('/').pop();
  420. }
  421. else if (PAGETYPE===4 || PAGETYPE===6){
  422. authorId = artContainer.getAttribute('data-user_id') || artContainer.querySelector('.ui-profile-popup').getAttribute('data-user_id');
  423. }
  424. else if (PAGETYPE===8){
  425. let node = searchNearestNode(artContainer,'[href*="/users/"]');
  426. authorId = (node)? node.getAttribute('href').split('/').pop(): -8;
  427. }
  428.  
  429. return +authorId;
  430. }
  431. //-----------------------------------------------------------------------------------
  432. function sleep(ms)
  433. {
  434. return new Promise(resolve => setTimeout(resolve, ms));
  435. }
  436. //-----------------------------------------------------------------------------------
  437. function getElementByXpath(path)
  438. {
  439. return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  440. }
  441. //-----------------------------------------------------------------------------------
  442. function getArtSectionContainers()
  443. {
  444. switch(PAGETYPE)
  445. {
  446. case 0:
  447. case 2:
  448. case 7: return document.querySelector('a[data-gtm-user-id][href*="/artworks/"]')?.closest('ul')
  449. case 1:
  450. case 4: return $('.gtm-illust-recommend-zone')[0]
  451. case 6: return $('.ranking-items')[0]
  452. case 8: return $('body')[0] //$("section>div>ul")[0]
  453. case 10: return document.querySelector('div[style*=--columns]') //fckng sick of this-_-
  454. case 12: return $('.gtm-illust-recommend-zone ul')[0]
  455.  
  456. default: return 0;
  457. }
  458. }
  459. //-----------------------------------------------------------------------------------
  460. let observer = {
  461. mutationObserver: null,
  462.  
  463. init(func){
  464. this.mutationObserver = new MutationObserver((mutations)=>{func(mutations)});
  465. },
  466.  
  467. observe(mainDiv, options){
  468. this.mutationObserver.observe(mainDiv, options);
  469. console.log('Observer has been set');
  470. },
  471.  
  472. disconnect(){
  473. this.mutationObserver.disconnect();
  474. }
  475. }
  476. //-----------------------------------------------------------------------------------
  477. let renewObserver = Object.assign({}, observer); //copy new instance of object
  478. //-----------------------------------------------------------------------------------
  479. function observerBody(mutations)
  480. {
  481. let arr = [];
  482. mutations.forEach(function(mutation)
  483. {
  484. mutation.addedNodes.forEach(function(node)
  485. {
  486. if (PAGETYPE == 10){
  487. if (node.nodeName == "IMG" && node.matches('img:not([class*=" "])')){ //todo: very unstable condition(>=2 img classes)
  488. arr.push(searchNearestNode(node, 'a[href*="/artworks/"]'));
  489. }
  490. else if (node.nodeName == "DIV"){
  491. node.querySelectorAll('a[href*="/artworks/"]:only-child').forEach((el)=>arr.push(el));
  492. }
  493. }
  494. else if (PAGETYPE == 12 && (!!node.querySelector('iframe'))){
  495. node.remove(); //filtering ads
  496. }
  497. else if (PAGETYPE == 1 || PAGETYPE == 7){
  498. node.querySelectorAll('li > div').forEach((el) => arr.push(el));
  499. }
  500. else if (PAGETYPE == 8){
  501. //console.log(node);
  502. if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='UL'){
  503. node.querySelectorAll('li > div').forEach((el) => arr.push(el));
  504. }
  505. if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='LI' && node.querySelector('div [href*="/users/"]')){
  506. arr.push(node);
  507. }
  508. }
  509. else{
  510. arr.push(node);
  511. }
  512.  
  513. });
  514. });
  515.  
  516. if (arr.length>0) colorFollowed(arr);
  517. }
  518. //-----------------------------------------------------------------------------------
  519. observer.init(observerBody);
  520. //-----------------------------------------------------------------------------------
  521. async function waitForArtSectionContainers()
  522. {
  523. let mainDiv = getArtSectionContainers();
  524. let count = 0;
  525. while(!mainDiv)
  526. {
  527. console.log('Waiting for arts container...');
  528. await sleep(1000);
  529. mainDiv = getArtSectionContainers();
  530.  
  531. ++count;
  532. if (count>10) {console.error('Error while waiting for arts containers! [Timeout 10s]'); return -1}
  533. }
  534. console.log(mainDiv);
  535.  
  536. return mainDiv;
  537. }
  538. //-----------------------------------------------------------------------------------
  539. async function initMutationObject(options)
  540. {
  541. observer.observe(await waitForArtSectionContainers(), options);
  542. }
  543. //-----------------------------------------------------------------------------------
  544. function searchNearestNode(el, selector)
  545. {
  546. let nearestNode = el.querySelector(selector);
  547. while ((!nearestNode) && (el != document.body)){
  548. el = el.parentNode;
  549. nearestNode = el.querySelector(selector);
  550. }
  551. return nearestNode;
  552. }
  553. //-----------------------------------------------------------------------------------
  554. function followage(thisObj, toFollow) //In case of followed check lasting too long, async queue may be a solution
  555. {
  556. console.log('toFollow: '+ toFollow);
  557. let userId = searchNearestNode(thisObj, '[href*="/users/"]').getAttribute('href').split('/').filter(el => el.match(/\d+/))[0];
  558.  
  559. if (!(userId>0)) {console.error(`Wrong userId! ${userId}`); return}
  560.  
  561. if (localStorage.getObj('followedCheck').status == 2)
  562. {
  563. if (Object.keys(followedUsersId).length == 0)
  564. followedUsersId = localStorage.getObj('followedUsersId');
  565.  
  566. if (toFollow){
  567. followedUsersId[userId] = 1;
  568. if ([2,12].includes(PAGETYPE)){
  569. followagePreview();
  570. }
  571. }
  572. else
  573. delete followedUsersId[userId];
  574.  
  575. localStorage.setObj('followedUsersId', followedUsersId);
  576. console.log('userId ' + userId + [(toFollow)?' added to':' deleted from'] + ' localStorage. Followed: '+ Object.keys(followedUsersId).length);
  577. }
  578. else console.info(`${userId} will not be highlighted too quick subscription before followedCheck is completed. It will be updated in 24 hours, but if you want you can report this on GitHub`);
  579. }
  580. //-------------------------------------Followage-------------------------------------
  581. async function followagePreview()
  582. {
  583. let recommendationBlock;
  584. let c = 0;
  585.  
  586. while(!recommendationBlock)
  587. {
  588. console.log('Waiting for FollowagePreview');
  589. await sleep(1000);
  590. recommendationBlock = getElementByXpath("//div[contains(., 'Recommended users')]");
  591.  
  592. ++c;
  593. if (c>10) {console.error("Error while waiting for recommendationBlock! [Timeout 10s]"); return -1}
  594. }
  595. console.log('*FollowagePreview loaded*');
  596.  
  597. //let recommendationObserver = Object.assign({}, observer);
  598.  
  599. let scrollBackward = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(1)');
  600. let scrollForward = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(2)');
  601.  
  602. recommendationBlock.onwheel = function(e){
  603. e.preventDefault(); //no need
  604. if (e.deltaY > 0) scrollForward.click()
  605. else scrollBackward.click();
  606. };
  607.  
  608. $(recommendationBlock).on(previewEventType, 'a:not([href*="/users/"]) img', function(e)
  609. {
  610. e.preventDefault();
  611. //let top = window.innerHeight - PREVIEWSIZE - 5 + window.scrollY + 'px';
  612. let top = window.scrollY + 5 + 'px';
  613. checkDelay(setHover, this, top);
  614. });
  615. }
  616. //---------------------------------------History-------------------------------------
  617. let illust_history = {
  618. ids: [],
  619. timestamps: {},
  620.  
  621. load(){
  622. this.ids = localStorage.getObj('viewed_illust_ids') || GM_getV("viewed_illust_ids") || localStorage.getObj('viewed_illust_ids_' + USER_ID)?.data || [];
  623. this.timestamps = localStorage.getObj('viewed_illust_timestamps') || GM_getV("viewed_illust_timestamps") || localStorage.getObj('viewed_illust_timestamp_' + USER_ID)?.data || {};
  624. },
  625.  
  626. save(){
  627. localStorage.setObj('viewed_illust_ids', this.ids); //viewed_illust_ids
  628. localStorage.setObj('viewed_illust_timestamps', this.timestamps); //viewed_illust_timestamp
  629. },
  630.  
  631. add_record(illust_id){
  632. this.load();
  633.  
  634. if (this.ids.indexOf(illust_id.toString()) == -1){
  635. this.ids.push(illust_id.toString());
  636. this.timestamps[illust_id] = Date.now()/1000;
  637. console.log(+illust_id, "has been added to history");
  638. }
  639. else if (currentSettings["KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"] == false){
  640. this.timestamps[illust_id] = Date.now()/1000;
  641. console.log(+illust_id, ": updated view date");
  642. }
  643. else console.log(`%c${illust_id}%c already in history (%c${new Date(this.timestamps[illust_id]*1000).toLocaleString()}%c)`, 'color:lime;', 'color:;', 'color:violet;', 'color:;');
  644.  
  645. this.save();
  646. },
  647.  
  648. delete_record(illust_id){
  649. this.load();
  650.  
  651. let index = this.ids.indexOf(illust_id.toString());
  652. if (index > -1){
  653. this.ids.splice(index, 1);
  654. }
  655. delete this.timestamps[illust_id];
  656.  
  657. this.save();
  658. console.log(+illust_id, "has been deleted from history");
  659. },
  660.  
  661. override(){
  662. this.load();
  663. let date = Date.now()+365*24*60*60*1000;
  664. localStorage.setObj('viewed_illust_ids_' + USER_ID, {data:this.ids, expires:date});
  665. localStorage.setObj('viewed_illust_timestamp_' + USER_ID, {data:this.timestamps, expires:date});
  666. console.info(`History overridden [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;');
  667.  
  668. let count = 0, t = setInterval(()=>{
  669. document.querySelectorAll('._history-item.trial').forEach(e => {
  670. e.querySelector('img').style.opacity = 1;
  671. e.classList.remove("trial");
  672. });
  673. ++count;
  674. if (count>10) clearInterval(t);
  675. }, 1000);
  676. },
  677.  
  678. export(){
  679. this.load(); //TODO:check history records integrity before export
  680. GM_setV("viewed_illust_ids", this.ids);
  681. GM_setV("viewed_illust_timestamps", this.timestamps);
  682. console.info(`History was exported to script manager storage [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;');
  683. },
  684.  
  685. check_space(){
  686. let spaceConsumed = +((new Blob([Object.values(localStorage), Object.keys(localStorage),
  687. localStorage.viewed_illust_ids, localStorage.viewed_illust_timestamps]).size)/(5000*1024)).toFixed(3); //duplicating records not the best solution... but simplest [solve this later if needed]
  688. if (spaceConsumed > 0.95){
  689. this.add_record = this.override = ()=>{};
  690. return Promise.reject(`Too much space consumed [${spaceConsumed*100}%] history is disabled`); //~100.000 entries
  691. }
  692. else console.log('History initialized');
  693. }
  694. };
  695. getUserId().then(() => illust_history.check_space()).catch(e => console.error('History not initialized —', e));
  696. //===================================================================================
  697. if (PAGETYPE===0) siteImgMaxWidth = 198;
  698. else if (PAGETYPE===4) siteImgMaxWidth = 150;
  699. else if (PAGETYPE===6 || PAGETYPE===14) siteImgMaxWidth = 240;
  700. //-----------------------------------------------------------------------------------
  701. $(document).ready(function ()
  702. {
  703. console.log('$(document).ready');
  704. mangaWidth = document.body.clientWidth - 60;
  705. mangaContainer.style.maxWidth = mangaWidth+'px';
  706. document.body.appendChild(imgContainer);
  707. document.body.appendChild(mangaOuterContainer);
  708. //---------------------------------Settings menu-----------------------------------
  709. let menu = document.createElement("div");
  710. menu.id = "menu";
  711. menu.style = `
  712. position: absolute;
  713. display: block;
  714. visibility: hidden;
  715. top: 60px;
  716. left: 10px;
  717. padding: 5px 5px 5px 20px;
  718. border: 2px solid deepskyblue;
  719. border-radius: 15px;
  720. background: white;
  721. font-size: 14px;
  722. line-height: 17px;
  723. color: rgb(0, 0, 0);
  724. border-radius: 15px;
  725. word-wrap: normal;
  726. `;
  727.  
  728. //filling menu fields with values and property names
  729. for (let i = 0; i < propList.length; i++){
  730. menu.innerHTML += `<li style = 'font:inherit;'><button style = 'width: 40px; padding: 0px; margin-right: 5px;'>${propList[i].array[propList[i].paramIndex]}</button>${propList[i].name}</li>`
  731. }
  732.  
  733. document.body.appendChild(menu);
  734. //---------------------------------------------------------------------------------
  735. function changeMenuValues(menuDiv){
  736. let index = Array.prototype.indexOf.call(menuDiv.parentNode.parentNode.children, menuDiv.parentNode);
  737. propList[index].paramIndex+=1;
  738. if (propList[index].paramIndex >= propList[index].array.length) propList[index].paramIndex = 0;
  739. menuDiv.textContent = propList[index].array[propList[index].paramIndex];
  740.  
  741. //foolproof protection
  742. if (propList[0].paramIndex == 1){
  743. menu.childNodes[1].childNodes[0].disabled = true;
  744. propList[1].paramIndex = 0;
  745. menu.childNodes[1].childNodes[0].textContent = "0";
  746. }
  747. else{
  748. menu.childNodes[1].childNodes[0].disabled = false;
  749. }
  750. }
  751.  
  752. $('#menu').on('click', 'button', function(){
  753. changeMenuValues(this);
  754. });
  755. //---------------------------------------------------------------------------------
  756. async function initMenu(){
  757. if ([-1,14].includes(PAGETYPE)) return;
  758.  
  759. let buttons, menuButton; //put to global scope if (menuButton) is needed elsewhere
  760.  
  761. let count = 0;
  762. while (!menuButton && count<5){
  763. if ([0,1,2,7,8,10,12].includes(PAGETYPE))
  764. buttons = document.querySelectorAll('body > div#root > div.charcoal-token button[title]')
  765. else
  766. buttons = document.querySelectorAll('body > div#js-mount-point-header > div:nth-child(1) button');
  767. menuButton = buttons[buttons.length - 1]; // last is the menu button
  768. console.log(menuButton);
  769. await sleep(1000);
  770. ++count;
  771. }
  772.  
  773. if (menuButton)
  774. menuButton.addEventListener("click", function(){
  775. menu.style.visibility = 'visible';
  776. clearTimeout(menuTimer);
  777. menuTimer = setTimeout(()=>{menu.style.visibility = 'hidden'}, 60*1000); //closing menu after 60s to prevent "hanging" it in one tab
  778. });
  779. else
  780. console.error("menuButton is undefined!");
  781. }
  782. //---------------------------------------------------------------------------------
  783. $(document).mouseup(function (e){
  784. if (!($(menu).has(e.target).length) && (menu.style.visibility == 'visible')){
  785. menu.style.visibility = 'hidden';
  786. saveSettings();
  787. if (currentSettings[propList[0].name] !== propList[0].array[propList[0].paramIndex]) setTimeout(()=>{initPreviewListeners(); initProfileCard()}, 0); //reset event listeners only after settings are applied
  788. setCurrentSettings();
  789. clearTimeout(menuTimer);
  790. }
  791. });
  792. //---------------------------------------------------------------------------------
  793. initMenu();
  794. //-------------------------------Follow onclick------------------------------------
  795. let toFollow, followSelector;
  796. //---------------------------------------------------------------------------------
  797. function initFollowagePreview()
  798. {
  799. if ([1,2,7,8,12].includes(PAGETYPE)){
  800. followSelector = 'button:contains("Follow")';
  801. }
  802. else if ([4,6,13].includes(PAGETYPE)){
  803. followSelector = '.follow-button';
  804. }
  805. else return 0;
  806.  
  807. $('body').off('mouseup', followSelector); //clearing previous events
  808.  
  809. if ([1,2,4,6,7,8,12,13].includes(PAGETYPE))
  810. {
  811. $('body').on('mouseup', followSelector, function(){
  812. toFollow = (this.textContent == 'Follow'); //~mustn't work on non-English locale| todo: add some locale-specific text condition?
  813. followage(this, toFollow);
  814. });
  815. }
  816.  
  817. if ([2,7].includes(PAGETYPE)){
  818. $('body').off('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem');
  819. $('body').on('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem', followagePreview);
  820. }
  821. }
  822. //---------------------------------------------------------------------------------
  823. initFollowagePreview();
  824. //====================================PAGINATION===================================
  825. async function autoPagination(){
  826. $('section ul').off('click', 'button');
  827. window.onscroll = null;
  828. //-------------------------------------------------------------------------------
  829. if (!currentSettings['ENABLE_AUTO_PAGINATION'] || ![0,2,7].includes(PAGETYPE)) return -1;
  830.  
  831. let pageCount = location.href.match(/(?<=[?|&]p=)\d+/)?.[0] || 1;
  832. let mode = location.href.match(/r18/)?.[0] || "All";
  833. let maxPageCount = 35; //limit for Following is 35 pages
  834.  
  835. let authorId = location.href.match(/(?<=users\/)\d+/)?.[0];
  836. let artworks = !!location.href.match(/\d+\/artworks/)?.[0];
  837. let illusts = !!location.href.match(/illustrations/)?.[0];
  838. let manga = !!location.href.match(/manga/)?.[0];
  839. let rest = location.href.match(/rest=hide/)?.[0] && "hide" || "show";
  840. //let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || '';
  841. //-------------------------------------------------------------------------------
  842. let x_csrf_token; //for bookmarks
  843. request('/en/', 'document').then(response => x_csrf_token = response.documentElement.innerHTML.match(/(?<=token&quot;:&quot;)[\dA-z]+/));
  844. //-------------------------------------------------------------------------------
  845. let artsSection = await waitForArtSectionContainers();
  846. await sleep(2000);
  847. let art = $(artsSection.querySelector('a[href*="artworks"]')).parents('li')[0].cloneNode(true);
  848. let mangaCount = document.createElement('div');
  849. mangaCount.style = "position: absolute; right: 0px; top: 0px; z-index: 1; display: flex; justify-content: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; height: 20px; min-width: 20px; font-weight: bold; padding: 0px 6px; background: rgba(0, 0, 0, 0.32) none repeat scroll 0% 0%; border-radius: 10px; font-size: 10px; line-height: 10px; color: rgb(255, 255, 255);";
  850. mangaCount.appendChild(document.createElement('span'));
  851. mangaCount.querySelector('span').style = "font-size: 10px; line-height: 10px; color: rgb(255, 255, 255); font-family: inherit; font-weight: bold;";
  852. if (!art.querySelector('span')) art.querySelector('[href]').appendChild(mangaCount);
  853. art.querySelectorAll('img').forEach(el => el.src='');
  854. art.classList.add("paginated");
  855. //-------------------------------------------------------------------------------
  856. pageNumber = pageNumber ?? mangaCount.cloneNode(true);
  857. pageNumber.className = "pageNumber";
  858. pageNumber.style = "position: fixed; right: 5px; bottom: 5px; height: 16px; min-width: 16px; width: max-content; padding: 0px 4px; z-index: 1; display: flex; justify-content: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; font-weight: bold; background: rgba(0, 0, 0, 0.32) none repeat scroll 0% 0%; border-radius: 16px; font-size: 10px; line-height: 10px; color: rgb(255, 255, 255); opacity: 0%; transition: opacity 1s;";
  859. document.body.appendChild(pageNumber);
  860. //-------------------------------------------------------------------------------
  861. let running = false, urls = [];
  862.  
  863. window.onscroll = async function(){
  864. if ((window.innerHeight*1.8 + window.scrollY) >= document.body.scrollHeight){
  865. if (running || pageCount>=maxPageCount) return;
  866.  
  867. running = true;
  868. pageCount++;
  869.  
  870. let url;
  871. let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || '';
  872.  
  873. if (PAGETYPE == 0){
  874. url = `https:\/\/www\.pixiv\.net\/ajax\/follow_latest\/illust\?p=${pageCount}\&mode=${mode}\&lang=en`;
  875. }
  876. if (PAGETYPE == 2){
  877. if (!urls.length && !tags){
  878. urls = [];
  879. await fetch(`https://www.pixiv.net/ajax/user/${authorId}/profile/all?lang=en`).then(r => r.json()).then(response => {
  880. let iArr = (illusts || artworks) && Object.keys(response.body.illusts) || [];
  881. let mArr = (manga || artworks) && Object.keys(response.body.manga) || [];
  882. let arr = iArr.concat(mArr).sort(function(a,b){return a-b}).reverse();
  883. for(let i=(pageCount-1)*48; i<arr.length; i+=48){
  884. urls.push(`https://www.pixiv.net/ajax/user/${authorId}/profile/illusts?ids[]=`
  885. + arr.slice(i, i+48).join('&ids[]=')
  886. + "&work_category=illustManga&is_first_page=0&lang=en"
  887. );
  888. }
  889. });
  890. if (!urls.length) return; //maybe check nav element before fetching instead
  891. maxPageCount = urls.length + 1;
  892. }
  893.  
  894. if (tags){
  895. let illustManga = artworks && 'illustmanga' || illusts && 'illusts' || manga && 'manga';
  896. url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/${illustManga}\/tag\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&lang=en`;
  897. }
  898. else{
  899. url = urls.shift();
  900. }
  901. }
  902. if (PAGETYPE == 7){
  903. url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/illusts\/bookmarks\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&rest=${rest}\&lang=en`
  904. }
  905.  
  906. console.log('Loading', pageCount, 'page...');
  907.  
  908. fetch(url).then(r => r.json()).then(response => {
  909. let fragment = new DocumentFragment();
  910. Array.prototype.forEach.call(response.body?.thumbnails?.illust || Object.values(response.body.works).reverse(), (obj) => {
  911. let el = art.cloneNode(true);
  912. if (obj.pageCount > 1) [...(el.querySelectorAll('span'))].pop().textContent = obj.pageCount;
  913. else $(el.querySelector('span')).parents('a > div')[0].remove();
  914. //-----------------------------------------------------------------------
  915. let s = el.querySelector('[href]').href.match('/en/')?.[0] || '/';
  916. let hrefs = el.querySelectorAll('[href]');
  917.  
  918. hrefs[0].setAttribute('data-gtm-value', obj.id);
  919. hrefs[0].href = s + "artworks/" + obj.id;
  920.  
  921. hrefs[1].href = s + "artworks/" + obj.id;
  922. hrefs[1].textContent = obj.title;
  923.  
  924. el.querySelector('img').src = obj.url;
  925.  
  926. if (hrefs.length == 4){
  927. hrefs[2].setAttribute('data-gtm-value', obj.userId);
  928. hrefs[2].href = s + "users/" + obj.userId;
  929.  
  930. hrefs[3].setAttribute('data-gtm-value', obj.userId);
  931. hrefs[3].href = s + "users/" + obj.userId;
  932. hrefs[3].textContent = obj.userName;
  933.  
  934. el.querySelectorAll('img')[1].src = obj?.profileImageUrl || ''; //for deleted bookmarks
  935. }
  936.  
  937. if (obj.bookmarkData) el.querySelectorAll('path:not(:only-child)').forEach(e => {
  938. e.setAttribute("style", "fill: rgb(255, 64, 96); !important")
  939. });
  940. //-----------------------------------------------------------------------
  941. el.style.display = "list-item"; //needed - 'none' otherwise
  942. fragment.appendChild(el);
  943. });
  944. if (PAGETYPE==7 || tags) maxPageCount = Math.ceil(response.body.total/48);
  945.  
  946. artsSection.appendChild(fragment);
  947. running = false;
  948. });
  949.  
  950. if (pageCount>=maxPageCount){
  951. console.log('*All pages loaded*');
  952. [...document.querySelectorAll("nav")].pop().style.opacity = 0.3;
  953.  
  954. pageNumber.querySelector('span').textContent = `All pages loaded [${pageCount}]`;
  955. pageNumber.style.opacity = "100%";
  956. setTimeout(()=>pageNumber.style.opacity = "0%", 3000);
  957. }
  958. else{
  959. pageNumber.querySelector('span').textContent = pageCount;
  960. pageNumber.style.opacity = "100%";
  961. setTimeout(()=>pageNumber.style.opacity = "0%", 1500);
  962. }
  963. } //endif
  964. } //onscroll
  965. //-------------------------------------------------------------------------------
  966. $(artsSection).on('click', 'button', function(event){
  967. event.preventDefault();
  968. let illust_id = searchNearestNode(this,'[href*="/artworks/"]').href.match(/\d+/)[0];
  969.  
  970. fetch('/ajax/illusts/bookmarks/add', {
  971. method: 'POST',
  972. headers: {
  973. 'Accept': 'application/json',
  974. 'Content-Type': 'application/json; charset=utf-8',
  975. 'x-csrf-token': x_csrf_token
  976. },
  977. body: JSON.stringify({"illust_id":illust_id,"restrict":0,"comment":"","tags":[]})
  978. })
  979. .then(() => this.querySelectorAll('path:not(:only-child)').forEach(e => e.setAttribute("style", "fill: rgb(255, 64, 96);")))
  980. .catch(err => console.log(err));
  981. });
  982. //-------------------------------------------------------------------------------
  983. return 0;
  984. }
  985. //=================================================================================
  986. function initMutationObservers()
  987. {
  988. observer.disconnect();
  989. $('body').off('mouseup', 'a[href*="/discovery"]');
  990. $('body').off('mouseup', 'a[href*="bookmarks/artworks"]');
  991. $('body').off('mouseup', 'section>div>a[href*="/artworks"], a[href*="/illustrations"], a[href*="/manga"]');
  992. //-------------------------------------------------------------------------------
  993. if (PAGETYPE===0){
  994. autoPagination().then(v => {
  995. if (v === 0){
  996. $('body').on('mouseup', 'a[href*="/bookmark_new_illust"]', function(e){
  997. e.preventDefault();
  998. location.href = this.href;
  999. });
  1000. }
  1001. })
  1002. }
  1003. //-------------------------------------------------------------------------------
  1004. if (PAGETYPE===1){
  1005. colorFollowed();
  1006. initMutationObject({'childList': true, 'subtree': true});
  1007.  
  1008. let timeout_1;
  1009. $('body').on('mouseup', 'a[href*="/discovery"]', function(){
  1010. clearTimeout(timeout_1);
  1011. timeout_1 = setTimeout(() => {
  1012. if (PAGETYPE===1){
  1013. colorFollowed();
  1014. initMutationObject({'childList': true, 'subtree': true});
  1015. }
  1016. }, 2000);
  1017. });
  1018. }
  1019. //---------------------------Bookmark detail page cleaning-----------------------
  1020. if (PAGETYPE===4)
  1021. {
  1022. if (currentSettings["HIDE_PEOPLE_WHO_BOOKMARKED_THIS"])
  1023. $('.bookmark-list-unit')[0].remove();
  1024.  
  1025. initMutationObject({'childList': true});
  1026. }
  1027. //-----------------------------Daily rankings ad cleaning------------------------
  1028. if (PAGETYPE===6)
  1029. {
  1030. colorFollowed();
  1031. $('.ad-printservice').remove();
  1032.  
  1033. initMutationObject({'childList': true});
  1034. }
  1035. //----------------------------------Artwork page---------------------------------
  1036. if (PAGETYPE===12)
  1037. {
  1038. initMutationObject({'childList': true});
  1039. illust_history.add_record(location.href.match(/\d+/)[0]);
  1040. }
  1041. //----------------------------------Search page----------------------------------
  1042. if (PAGETYPE===8)
  1043. {
  1044. initMutationObject({'childList': true, 'subtree': true});
  1045. }
  1046. //----------------------------------Main page------------------------------------
  1047. if (PAGETYPE===10)
  1048. {
  1049. colorFollowed();
  1050. initMutationObject({'childList': true, 'subtree': true});
  1051. }
  1052. //-------------------------------Pixiv User pages--------------------------------
  1053. if (PAGETYPE===2 || PAGETYPE===7)
  1054. {
  1055. let pagination = autoPagination(); //2,7
  1056.  
  1057. $('body').on('mouseup', 'a[href*="bookmarks/artworks"]', function(){
  1058. console.log('PAGETYPE: '+ PAGETYPE+' -> 7');
  1059. PAGETYPE = 7;
  1060.  
  1061. sleep(5000).then(() => {
  1062. initMutationObject({'childList': true});
  1063. autoPagination().then((v)=>{colorFollowed(null, v && 2000)}); //success(0) -> already waited 2+ secs; disabled(-1) -> need to wait
  1064. initProfileCard();
  1065. });
  1066. });
  1067.  
  1068. $('body').on('mouseup', 'section>div>a[href$="/artworks"], a[href$="/illustrations"], a[href$="/manga"]', function(){
  1069. console.log('PAGETYPE: '+ PAGETYPE+' -> 2');
  1070. PAGETYPE = 2;
  1071. sleep(2500).then(autoPagination);
  1072. observer.disconnect();
  1073. });
  1074.  
  1075. if (PAGETYPE===7){
  1076. initMutationObject({'childList': true});
  1077. pagination.then((v)=>{colorFollowed(null, v && 2000)}); //if pagination is enabled we need to wait before it completes*, but no more
  1078. initProfileCard();
  1079. }
  1080.  
  1081. //clearing "cache" of autopaged arts
  1082. $('body').on('mouseup', 'section>div>div>div>a[href*="/illustrations/"], section>div>div>div>a[href*="/artworks/"], section>div>div>div>a[href*="/manga/"]', function(){
  1083. let artsSection = getArtSectionContainers();
  1084. [...artsSection.querySelectorAll('.paginated')].forEach(el => el.remove());
  1085. });
  1086. }
  1087. //------------------------------------History------------------------------------
  1088. if (PAGETYPE===14){
  1089. let trial = document.querySelector('span.trial'); //indicator of non-premium account
  1090. if (trial){
  1091. getUserId().then(() => illust_history.override());
  1092. trial.textContent = "Extended Version";
  1093. }
  1094.  
  1095. //export with Shift+E
  1096. document.onkeyup = function(e){
  1097. if (e.key.toUpperCase() == "E" && e.shiftKey){
  1098. illust_history.export();
  1099. }
  1100. };
  1101. }
  1102. //-------------------------------------------------------------------------------
  1103. }
  1104. //---------------------------------------------------------------------------------
  1105. initMutationObservers();
  1106. //=================================================================================
  1107. //***************************************HOVER*************************************
  1108. //=================================================================================
  1109. //------------------------------------Profile card--------------------------------- //4,6,9 [~0,1,~7,8,10,12]
  1110. function initProfileCard()
  1111. {
  1112. $('body').off('mouseenter click', 'section._profile-popup a[href*="/artworks/"]');
  1113. $('body').off("mouseenter", '.paginated a[href*="/users/"]');
  1114. $('body').off("mouseleave", '.paginated a[href*="/users/"]');
  1115. //-------------------------------------------------------------------------------
  1116. if ([4,6].includes(PAGETYPE)) //rankings
  1117. {
  1118. $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e)
  1119. {
  1120. console.log('Profile card');
  1121. e.preventDefault();
  1122. checkDelay(setHover, this, getOffsetRect(this).top+200+'px', true);
  1123. });
  1124. }
  1125. //-------------------------------------------------------------------------------
  1126. if ([0,7].includes(PAGETYPE) && currentSettings['ENABLE_AUTO_PAGINATION']) //patch for profile preview with pagination
  1127. {
  1128. //creating profile card(for last 3 arts)
  1129. let profilePopup = document.createElement('section');
  1130. profilePopup.className = '_profile-popup';
  1131. profilePopup.style = `visibility:hidden; position:absolute; height:128px; z-index:10001; padding: 0px;`;
  1132. profilePopup.onmouseleave = function(e){
  1133. profilePopup.style.visibility = "hidden";
  1134. if (e.relatedTarget?.id != 'imgPreview') imgContainer.style.visibility = "hidden";
  1135. }
  1136. let profileImagesDiv = document.createElement('div');
  1137. profileImagesDiv.style = `overflow:hidden; height:128px; border-radius:5px; border: 1px solid #c7d2dc; padding: 0px; background-color: rgb(255,255,255);`;
  1138. profilePopup.appendChild(profileImagesDiv);
  1139.  
  1140. for (let i=0; i<3; i++){
  1141. var a = document.createElement('a');
  1142. a.className = `item_${i}`;
  1143. a.style = `display: inline-block !important; width: 128px; height: 128px;`;
  1144. a.target = "_blank";
  1145. profileImagesDiv.appendChild(a);
  1146. }
  1147. document.body.appendChild(profilePopup);
  1148.  
  1149. let profileCard_timeout, previous_id;
  1150. //handler for showing paginated profile card
  1151. $('body').on("mouseenter", '.paginated a[href*="/users/"]', function(e){
  1152. e.preventDefault();
  1153. let user_id = this.href.match(/\d+/)[0];
  1154. if (user_id == 0) return;
  1155. if (previous_id == user_id){
  1156. profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px";
  1157. profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px";
  1158. profilePopup.style.visibility = "visible";
  1159. return;
  1160. }
  1161. clearTimeout(profileCard_timeout); //cancelling previous event
  1162. profilePopup.firstChild.childNodes.forEach(el => el.style.backgroundImage = '');
  1163.  
  1164. profileCard_timeout = setTimeout(fillProfileCard.bind(this, user_id), 500);
  1165. });
  1166.  
  1167. function fillProfileCard(user_id){
  1168. if (!([].indexOf.call(document.querySelectorAll(':hover'), this) > -1)) return; //need to check whether mouse is still over user profile after 500ms
  1169.  
  1170. profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px";
  1171. profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px"; //-sq.preview +icon
  1172. profilePopup.style.visibility = "visible";
  1173.  
  1174. fetch(`https://www.pixiv.net/rpc/get_profile.php?user_ids=${user_id}&illust_num=3&novel_num=0`).then(r => r.json()).then(response => {
  1175. response.body[0].illusts.forEach((el,i) => {
  1176. profilePopup.querySelector(`a.item_${i}`).style.backgroundImage = `url(${el.url["128x128"]})`;
  1177. profilePopup.querySelector(`a.item_${i}`).href = `/artworks/${el.illust_id}`;
  1178. })
  1179. });
  1180. previous_id = user_id;
  1181. }
  1182. //actual art preview
  1183. $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e){
  1184. e.preventDefault();
  1185. checkDelay(setHover, this, getOffsetRect(this).top+128+5+'px', true);
  1186. });
  1187.  
  1188. $('body').on("mouseleave", `.paginated div[aria-haspopup]`, function(e){
  1189. if (!e.relatedTarget?.closest('._profile-popup')) profilePopup.style.visibility = "hidden";
  1190. });
  1191. }
  1192. }
  1193.  
  1194. initProfileCard();
  1195. //=================================================================================
  1196. //*******************************Initialize Preview Listeners**********************
  1197. //=================================================================================
  1198. function initPreviewListeners()
  1199. {
  1200. //clearing-----------------------------------------------------------------------
  1201. $('body').off('click mouseenter', 'a[href*="/artworks/"]');
  1202. $('body').off('click mouseenter', 'a[href*="/artworks/"] img');
  1203. $('body').off('click', '[role="presentation"] img');
  1204. //document.removeEventListener('click'); //not worth bothering
  1205. //-------------------------------------------------------------------------------
  1206. if (previewEventType == 'click'){
  1207. document.addEventListener('click', (e)=>{
  1208. if (e.target.nodeName==="IMG") e.preventDefault();
  1209. }, {capture: true}) //site uses event capturing now which jQuery can't cover
  1210. }
  1211. //-------------------------------------------------------------------------------
  1212. //New illustrations, Discovery[Artworks], Artist pages, Bookmarks, Search, Home page, Artwork page //0,1,2,7,8,10,12
  1213. if (PAGETYPE === 0 || PAGETYPE === 1 || PAGETYPE === 2 || PAGETYPE === 7 || PAGETYPE === 8 || PAGETYPE === 10 || PAGETYPE === 12)
  1214. {
  1215. console.info('new');
  1216. $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e)
  1217. {
  1218. e.preventDefault();
  1219. //---------------------------filtering preview card--------------------------
  1220. if (getElementByXpath("//a[text()='View Profile']")){
  1221. if (this.closest('a').querySelector('span'))
  1222. checkDelay(setMangaHover, this, this.closest('a').textContent, getOffsetRect(this).top+112+'px');
  1223. else
  1224. checkDelay(setHover, this, getOffsetRect(this).top+112+5+'px', true);
  1225. }
  1226. //-------------------------filtering recommended users-----------------------
  1227. else if (getElementByXpath("//div[text()='Recommended users']")){
  1228. let top = window.scrollY + 5 + 'px';
  1229. checkDelay(setHover, this, top);
  1230. }
  1231. //--------------------------------Normal case--------------------------------
  1232. else{
  1233. //console.log(this);
  1234. //multiple
  1235. if (this.closest('a').querySelector('span'))
  1236. checkDelay(setMangaHover, this, this.closest('a').textContent.replace(/R-18(G)?/,""));
  1237. //single
  1238. else checkDelay(setHover, this);
  1239. }
  1240. //---------------------------------------------------------------------------
  1241. });
  1242. //-----------------------------------------------------------------------------
  1243. if (PAGETYPE === 12) $('body').on('click', '[role="presentation"] img', function(event){
  1244. if (event.ctrlKey){
  1245. event.preventDefault();
  1246. event.stopPropagation();
  1247. let isManga = !!document.querySelector('.gtm-manga-viewer-preview-modal-open');
  1248. onClickActions(this, event, isManga);
  1249. }
  1250. });
  1251. }
  1252. //----------------------DAILY RANKINGS & BOOKMARK INFORMATION PAGES-------------- //4,6
  1253. else if (PAGETYPE === 4 || PAGETYPE === 6)
  1254. {
  1255. $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e) //direct div selector works badly with "::before"
  1256. {
  1257. e.preventDefault();
  1258. //console.log(this);
  1259. //single
  1260. if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV"){
  1261. checkDelay(setHover, this.querySelector('img'));
  1262. }
  1263. //multiple
  1264. else if (this.children[1] && this.children[1].className == 'page-count'){
  1265. checkDelay(setMangaHover, this.querySelector('img'), this.querySelector('.page-count').textContent);
  1266. }
  1267. });
  1268. }
  1269. //----------------------------------DISCOVERY[USERS]----------------------------- //13
  1270. else if (PAGETYPE === 13)
  1271. {
  1272. $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e){
  1273. e.preventDefault();
  1274. if (this.childNodes.length == 0) checkDelay(setHover, this); //single art
  1275. else if (this.childNodes.length == 1) checkDelay(setMangaHover, this, this.firstChild.textContent); //manga
  1276. });
  1277. }
  1278. //-------------------------------------History----------------------------------- //14
  1279. else if (PAGETYPE === 14)
  1280. {
  1281. $('body').on(previewEventType, '._history-item', function(e){
  1282. e.preventDefault();
  1283. checkDelay(setHover, this.querySelector('img'), getOffsetRect(this).top + 'px');
  1284. });
  1285. }
  1286. }
  1287. //---------------------------------------------------------------------------------
  1288. initPreviewListeners();
  1289. //=================================================================================
  1290. if (currentSettings["DELAY_BEFORE_PREVIEW"]>0) $('body').on('mouseleave', 'a[href*="/artworks/"]', function()
  1291. {
  1292. clearTimeout(timerId);
  1293. clearInterval(tInt);
  1294. });
  1295. //---------------------------------Async page change-------------------------------
  1296. function renewAll()
  1297. {
  1298. if (PAGETYPE != checkPageType())
  1299. {
  1300. console.log('PAGETYPE:', PAGETYPE, '->', PAGETYPE = checkPageType());
  1301.  
  1302. clearTimeout(timerId);
  1303. clearInterval(tInt);
  1304.  
  1305. if (PAGETYPE === -1) return;
  1306.  
  1307. initPreviewListeners();
  1308. initMutationObservers();
  1309. initMenu();
  1310. autoPagination();
  1311.  
  1312. initFollowagePreview();
  1313. initProfileCard();
  1314. }
  1315. }
  1316. //---------------------------------------------------------------------------------
  1317. renewObserver.init(renewAll);
  1318. renewObserver.observe($('body')[0], {childList: true, subtree: true});
  1319. //---------------------------------------------------------------------------------
  1320. }); //end of document.ready
  1321. //===================================================================================
  1322. //-----------------------------------------------------------------------------------
  1323. function checkDelay(func, ...args)
  1324. {
  1325. if (currentSettings["DELAY_BEFORE_PREVIEW"]>0){
  1326. clearTimeout(timerId);
  1327. timerId = setTimeout(()=>{
  1328. if ([].indexOf.call(document.querySelectorAll(':hover'), (PAGETYPE!=6)? args[0] : args[0].parentNode.parentNode) > -1) func(...args)
  1329. }, currentSettings["DELAY_BEFORE_PREVIEW"]);
  1330. }
  1331. else func(...args)
  1332. }
  1333. //-----------------------------------------------------------------------------------
  1334. function setHover(thisObj, top, profileCard)
  1335. {
  1336. clearInterval(tInt);
  1337. imgContainer.style.visibility = 'hidden';
  1338. mangaOuterContainer.style.visibility = 'hidden';
  1339. hoverImg.src=''; //just in case
  1340.  
  1341. hoverImg.src = parseImgUrl(thisObj);
  1342. imgContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px';
  1343.  
  1344. //adjusting preview position considering expected image width
  1345. //---------------------------------------------------------------------------------
  1346. let l = ([4,6,14].includes(PAGETYPE))
  1347. ?getOffsetRect(thisObj.parentNode.parentNode).left //more accurate on discovery users and history
  1348. :getOffsetRect(thisObj).left;
  1349. let dcw = document.body.clientWidth;
  1350. let previewWidth = PREVIEWSIZE;
  1351.  
  1352. if (hoverImg.naturalWidth>0){ //cached (previously viewed)
  1353. adjustSinglePreview(dcw, l, hoverImg.naturalWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode);
  1354. //console.log("cached");
  1355. }
  1356. else{ //on old pages width can be pre-calculated
  1357. if ([4,6,14].includes(PAGETYPE) && !profileCard){
  1358. previewWidth = PREVIEWSIZE*(((PAGETYPE==6 || PAGETYPE==14)?thisObj.clientWidth:thisObj.parentNode.parentNode.clientWidth)/siteImgMaxWidth)+5;
  1359. adjustSinglePreview(dcw, l, previewWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode);
  1360. //console.log("count");
  1361. }
  1362. else{ //if it is obvious that preview will fit on the screen then there is no need in setInterval(trying to use as minimun setInterval`s as possible)
  1363. if (dcw - l - PREVIEWSIZE - 5 > 0){
  1364. imgContainer.style.left = l+'px';
  1365. imgContainer.style.visibility = 'visible';
  1366. //console.log("excessive");
  1367. }
  1368. else{ //when on NEW layout - need to wait until image width is received
  1369. let tLimit = 0;
  1370.  
  1371. tInt = setInterval(function(){
  1372. if (hoverImg.naturalWidth>0){
  1373. clearInterval(tInt);
  1374. adjustSinglePreview(dcw, l, hoverImg.naturalWidth, thisObj); //position mismatching due to old `thisObj` => clearing in hoverImg.mouseleave
  1375. }
  1376. ++tLimit;
  1377. //console.log(tInt, tLimit);
  1378.  
  1379. if (tLimit*40>5000){ //timeout 5s in case of loading errors
  1380. clearInterval(tInt);
  1381. hoverImg.src='';
  1382. console.error('setInterval error');
  1383. return;
  1384. }
  1385. }, 40);
  1386. }
  1387. }
  1388. }
  1389. //---------------------------------------------------------------------------------
  1390. checkBookmark(thisObj, imgContainer);
  1391. }
  1392. //-----------------------------------------------------------------------------------
  1393. function adjustSinglePreview(dcw, l, contentWidth)
  1394. {
  1395. if (l<0) l = 5; //followage preview
  1396. let d = dcw - l - contentWidth - 5; //5 - padding - todo...
  1397. imgContainer.style.left = (d>=0)?l+'px':l+d+'px';
  1398. imgContainer.style.visibility = 'visible';
  1399. }
  1400. //-----------------------------------------------------------------------------------
  1401. function setMangaHover(thisObj, count, top)
  1402. {
  1403. clearInterval(tInt);
  1404. imgContainer.style.visibility = 'hidden'; //just in case
  1405.  
  1406. mangaOuterContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px';
  1407.  
  1408. checkBookmark(thisObj, mangaOuterContainer);
  1409.  
  1410. imgsArrInit(thisObj, +count);
  1411. }
  1412. //-----------------------------------------------------------------------------------
  1413. function imgsArrInit(thisObj, count)
  1414. {
  1415. let primaryLink = parseImgUrl(thisObj);
  1416. let currentImgId = getImgId(primaryLink);
  1417. //---------------------------------------------------------------------------------
  1418. if (currentImgId != lastImgId)
  1419. {
  1420. for(let j=0; j<imgsArr.length; j++)
  1421. {
  1422. imgsArr[j].src = '';
  1423. }
  1424.  
  1425. lastImgId = currentImgId;
  1426.  
  1427. for(let i=0; i<count; i++)
  1428. {
  1429. if (!(!!imgsArr[i])) //if [i] img element doesn't exist
  1430. {
  1431. imgsArr[i] = document.createElement('img');
  1432. mangaContainer.appendChild(imgsArr[i]);
  1433. }
  1434. imgsArr[i].src = primaryLink.replace('p0','p'+i);
  1435. }
  1436. }
  1437. //---------------------------------------------------------------------------------
  1438. mangaOuterContainer.style.visibility = 'visible';
  1439. }
  1440. //-----------------------------------------------------------------------------------
  1441. function parseImgUrl(thisObj)
  1442. {
  1443. let url = (thisObj.src)? thisObj.src: thisObj.style.backgroundImage.slice(5,-2);
  1444. url = url.replace(/\/...x..[0|8]/, '/'+PREVIEWSIZE+'x'+PREVIEWSIZE).
  1445. replace('_square1200','_master1200').
  1446. replace('_custom1200','_master1200').
  1447. replace('custom-thumb','img-master').
  1448. replace('_80_a2','').
  1449. replace('_70','')
  1450. ;
  1451. return url;
  1452. }
  1453. //-----------------------------------------------------------------------------------
  1454. function checkBookmark(thisContainer, previewContainer)
  1455. {
  1456. if ([0,1,2,7,8,10,12].includes(PAGETYPE))
  1457. bookmarkContainer = searchNearestNode(thisContainer, 'button');
  1458. else if ([4,6].includes(PAGETYPE))
  1459. bookmarkContainer = searchNearestNode(thisContainer, "._one-click-bookmark")
  1460. else return; //no favourite button
  1461.  
  1462. if ($(bookmarkContainer).hasClass("on"))
  1463. $(previewContainer).css("background", "rgb(255, 64, 96)"); //purple
  1464. else
  1465. $(previewContainer).css("background", "rgb(34, 34, 34)"); //grey
  1466. }
  1467. //-----------------------------------------------------------------------------------
  1468. function getImgId(str)
  1469. {
  1470. return str.substring(str.lastIndexOf("/")+1, str.indexOf("_"));
  1471. }
  1472. //-----------------------------------------------------------------------------------
  1473. function getOffsetRect(elem)
  1474. {
  1475. // (1)
  1476. let box = elem.getBoundingClientRect();
  1477. // (2)
  1478. let body = document.body;
  1479. let docElem = document.documentElement;
  1480. // (3)
  1481. let scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
  1482. let scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
  1483. // (4)
  1484. let clientTop = docElem.clientTop || body.clientTop || 0;
  1485. let clientLeft = docElem.clientLeft || body.clientLeft || 0;
  1486. // (5)
  1487. let top = box.top + scrollTop - clientTop;
  1488. let left = box.left + scrollLeft - clientLeft;
  1489.  
  1490. return { top: Math.round(top), left: Math.round(left) };
  1491. }
  1492. //===================================================================================
  1493. //**************************************Hide*****************************************
  1494. //===================================================================================
  1495. imgContainer.onmouseleave = function()
  1496. {
  1497. imgContainer.style.visibility = 'hidden';
  1498. hoverImg.src='';
  1499. clearTimeout(timerId);
  1500. clearInterval(tInt);
  1501. };
  1502. //-----------------------------------------------------------------------------------
  1503. mangaOuterContainer.onmouseleave = function()
  1504. {
  1505. mangaOuterContainer.style.visibility = 'hidden';
  1506. clearTimeout(timerId);
  1507. };
  1508. //===================================================================================
  1509. //***********************************Art Clicks**************************************
  1510. //===================================================================================
  1511. //-----------------------------Single arts onclick actions---------------------------
  1512. hoverImg.onmouseup = function (event)
  1513. {
  1514. onClickActions(this, event, false);
  1515. };
  1516. //-----------------------------Manga arts onclick actions----------------------------
  1517. $('body').on('mouseup', 'div#mangaContainer > img', function(event)
  1518. {
  1519. onClickActions(this, event, true);
  1520. });
  1521. //---------------------------------onClickActions------------------------------------
  1522. async function onClickActions(imgContainerObj, event, isManga)
  1523. {
  1524. event.preventDefault();
  1525. let illustId = getImgId(imgContainerObj.src);
  1526. //----------------------------Middle Mouse Button click----------------------------
  1527. if (event.button == 1)
  1528. {
  1529. let illustPageUrl = 'https://www.pixiv.net/artworks/' + illustId;
  1530. window.open(illustPageUrl,'_blank'); //open illust page in new tab(in background — with FF pref "browser.tabs.loadDivertedInBackground" set to "true")
  1531. }
  1532. //----------------------------Left Mouse Button clicks...--------------------------
  1533. else if (event.button == 0)
  1534. {
  1535. //----------------------------Single LMB-click-----------------------------------
  1536. if (event.shiftKey){
  1537. illust_history.delete_record(illustId); //Shift + LMB-click -> delete record from history
  1538. document.querySelector(`[style*="/${illustId}_"]`).style.opacity = ".25";
  1539. }
  1540. else if (!event.altKey) //need to be this way. Don't change.
  1541. {
  1542. let toSave = event.ctrlKey; //Ctrl + LMB-click -> saving image
  1543. let pageNum = 0;
  1544.  
  1545. //Single (general url)
  1546. let ajaxIllustUrl = 'https://www.pixiv.net/ajax/illust/' + illustId;
  1547. //https://www.pixiv.net/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids=
  1548.  
  1549. //Manga
  1550. if (isManga)
  1551. {
  1552. let src = imgContainerObj.src;
  1553. pageNum = src.match(/(?<=\/\d+_p)\d+(?=[_|.])/)[0];
  1554. }
  1555.  
  1556. getOriginalUrl(ajaxIllustUrl, pageNum, toSave);
  1557. }
  1558. //-----------------------------Alt + LMB-click-----------------------------------
  1559. else if (event.altKey){
  1560. $(bookmarkContainer).click();
  1561. if (!isManga) $(imgContainerObj).parent().css("background", "rgb(255, 64, 96)");
  1562. else $(mangaOuterContainer).css("background", "rgb(255, 64, 96)");
  1563. }
  1564. //-------------------------------------------------------------------------------
  1565. }
  1566. //---------------------------------------------------------------------------------
  1567. }
  1568. //---------------------------------getOriginalUrl------------------------------------
  1569. async function getOriginalUrl(illustPageUrl, pageNum, toSave)
  1570. {
  1571. let xhr = new XMLHttpRequest();
  1572. xhr.responseType = 'json';
  1573. xhr.open("GET", illustPageUrl, true);
  1574. xhr.onload = function ()
  1575. {
  1576. let originalArtUrl = this.response.body.urls.original; //this.response.body.url.big;
  1577. if (pageNum>0) originalArtUrl = originalArtUrl.replace('p0','p'+pageNum);
  1578.  
  1579. if (toSave) saveImgByUrl(originalArtUrl);
  1580. else window.open(originalArtUrl, '_blank');
  1581. };
  1582. xhr.send();
  1583. }
  1584. //-----------------------------------------------------------------------------------
  1585. async function saveImgByUrl(sourceUrl)
  1586. {
  1587. const filename = sourceUrl.split('/').pop();
  1588. const illustId = filename.split('_')[0];
  1589. const ext = filename.split('.').pop().toLowerCase();
  1590. const GMR = (typeof(GM_xmlhttpRequest)==='function')?GM_xmlhttpRequest:GM.xmlHttpRequest;
  1591.  
  1592. //Thanx to FlandreKawaii(c)
  1593. GMR({
  1594. method: 'GET',
  1595. url: sourceUrl,
  1596. responseType: 'arraybuffer', //TM
  1597. binary: true, //GM
  1598. headers: {
  1599. Referer: `https://www.pixiv.net/en/artworks/${illustId}`,
  1600. },
  1601. onload: function(response)
  1602. {
  1603. if (ext === 'jpg' || ext === 'jpeg')
  1604. saveAs(new File([response.response], filename, { type: 'image/jpeg' }));
  1605. else if (ext === 'png')
  1606. saveAs(new File([response.response], filename, { type: 'image/png' }));
  1607. }
  1608. });
  1609. }
  1610. //===================================================================================
  1611. //**************************************Other****************************************
  1612. //===================================================================================
  1613. mangaContainer.onwheel = function(e)
  1614. {
  1615. if (e.deltaY<0 && (mangaOuterContainer.getBoundingClientRect().top < 0))
  1616. {
  1617. setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "start", behavior: "smooth"}), 0); //aligning to top screen side on scrollUp if needed
  1618. }
  1619. else if (e.deltaY>0 && (mangaOuterContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight))
  1620. {
  1621. setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "end", behavior: "smooth"}), 0); //aligning to bottom screen side on scrollDown if needed
  1622. }
  1623.  
  1624. let scrlLft = mangaContainer.scrollLeft;
  1625. if ((currentSettings["DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"]) || ((scrlLft>0 && e.deltaY<0) || ((scrlLft<(mangaContainer.scrollWidth-mangaContainer.clientWidth)) && e.deltaY>0)))
  1626. {
  1627. e.preventDefault();
  1628. mangaContainer.scrollLeft += e.deltaY*DELTASCALE;
  1629. }
  1630. };
  1631. //-----------------------------------------------------------------------------------
  1632. if (currentSettings["SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"]) imgContainer.onwheel = function(e)
  1633. {
  1634. if (currentSettings["DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"]) e.preventDefault();
  1635.  
  1636. if (e.deltaY<0 && (imgContainer.getBoundingClientRect().top < 0))
  1637. {
  1638. imgContainer.scrollIntoView({block: "start", behavior: "smooth"}); //aligning to top screen side on scrollUp if needed
  1639. }
  1640. else if (e.deltaY>0 && (imgContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight))
  1641. {
  1642. imgContainer.scrollIntoView({block: "end", behavior: "smooth"}); //aligning to bottom screen side on scrollDown if needed
  1643. }
  1644. };
  1645. //-----------------------------------------------------------------------------------
  1646. window.onresize = function()
  1647. {
  1648. mangaWidth = document.body.clientWidth - 60;
  1649. mangaContainer.style.maxWidth = mangaWidth+'px';
  1650. resetPreviewSize();
  1651. };
  1652. //-------------------------------fix for Chrome panoraming---------------------------
  1653. if (navigator.userAgent.indexOf("Chrome") != -1){
  1654. hoverImg.onmousedown = function(e){if (e.button == 2) e.preventDefault()};
  1655. $('body').on('mousedown', 'div#mangaContainer > img', (e)=>{if (e.button == 2) e.preventDefault()});
  1656. }
  1657. //===================================================================================
  1658. //***********************************************************************************
  1659. //===================================================================================
  1660. });
  1661. }) (); //function