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.
// ==UserScript== // @name Pixiv Arts Preview & Followed Atrists Coloring & Extended History // @namespace Pixiv // @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. // @author NightLancerX // @version 3.93.1 // @match https://www.pixiv.net/bookmark_new_illust* // @match https://www.pixiv.net/discovery* // @match https://www.pixiv.net/ranking.php* // @match https://www.pixiv.net/*artworks/* // @match https://www.pixiv.net/*users/* // @match https://www.pixiv.net/history.php* // @match https://www.pixiv.net/bookmark_detail.php?illust_id=* // @match https://www.pixiv.net/*tags/* // @match https://www.pixiv.net/* // @connect i.pximg.net // @connect i-f.pximg.net // @connect i-cf.pximg.net // @connect techorus-cdn.com // @homepageURL https://github.com/NightLancer/PixivPreview // @supportURL https://greasyfork.org/users/167506-nightlancerx // @license MIT // @copyright NightLancerX // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_setValue // @grant GM.setValue // @grant GM_getValue // @grant GM.getValue // @require https://code.jquery.com/jquery-3.3.1.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js // @compatible firefox >= 74 // @compatible chrome >= 80 // @noframes // ==/UserScript== //======================================================================================= (function () { 'use strict'; if (window.top == window.self && window.jQuery) jQuery(function($) //window.top check may be useless because of @noframes { console.log('MyPixivJS'); //---------------------------***CUSTOM PREFERENCES***-------------------------------- let propList = [ {paramIndex:0, array:[false,true], name:"PREVIEW_ON_CLICK"}, {paramIndex:2, array:[0, 100, 200, 300, 500, 1000, 1500], name:"DELAY_BEFORE_PREVIEW"}, {paramIndex:0, array:["auto", 600, 1200], name:"PREVIEW_SIZE"}, {paramIndex:1, array:[false,true], name:"ENABLE_AUTO_PAGINATION"}, {paramIndex:0, array:[false,true], name:"DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"}, {paramIndex:1, array:[false,true], name:"SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"}, {paramIndex:0, array:[false,true], name:"DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"}, {paramIndex:0, array:[false,true], name:"HIDE_PEOPLE_WHO_BOOKMARKED_THIS"}, {paramIndex:0, array:[false,true], name:"KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"} ]; //---------------------------------DEFAULT VALUES------------------------------------ // ■ PREVIEW_ON_CLICK = // false : showing preview on mouseover (default) // true : showing preview after LMB-click // // ■ DELAY_BEFORE_PREVIEW = // 0 : no delay before preview // 100 : 0.1 second delay (1000 for 1 second, etc) (default) // // ■ PREVIEW_SIZE = // auto : automatically calculate preview size (1200 or 600) depending of current screen size (default) // 600 : up to 600px x 600px // 1200 : up to 1200px x 1200px // // ■ ENABLE_AUTO_PAGINATION = // false: disable auto pagination // true: enable auto-pagination on Following and Users pages (default) // // ■ DISABLE_MANGA_PREVIEW_SCROLLLING_PROPAGATION = // false : keeping page scrolling after end of manga preview scrolling (default) // true : disable page scrolling when viewing manga preview (move mouse out of preview to re-enable scrolling) // // ■ SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE = // true : preview of single image will smoothly fit to vertical screen border after one scroll (default) // false : manually scrolling (may need in case of forced 1200px vertical preview with small user screen) // // ■ DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING = // false: standard behavior (default) // true : disable page scrolling when viewing single preview (works only if previous setting set to true) // // ■ HIDE_PEOPLE_WHO_BOOKMARKED_THIS = // false: don't change `bookmark_detail.php` page (default) // true: hide "People who bookmarked this" section // // ■ KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS = // false: update date every time artwork page opens (default) // true: don't renew date and keep first one (NOTE: art will not appear at the top of the history) let currentSettings = {}; //----------------------------------------------------------------------------------- let hoverImg = document.createElement('img'); hoverImg.style = 'display: block;' let imgContainer = document.createElement('div'); imgContainer.id = 'imgPreview'; imgContainer.style = 'position:absolute; display:block; visibility:visible; z-index:1000; background:#222; padding:5px; margin:-5px;'; imgContainer.appendChild(hoverImg); let mangaContainer = document.createElement('div'); mangaContainer.id = 'mangaContainer'; mangaContainer.style = 'display:block; overflow-x:auto; white-space:nowrap; maxWidth:1200px; z-index:1500; background:#111; font-size: 0;'; let mangaMiddleContainer = document.createElement('div'); mangaMiddleContainer.style = 'display:block; visibility:inherit; z-index:1250;'; mangaMiddleContainer.appendChild(mangaContainer); let mangaOuterContainer = document.createElement('div'); mangaOuterContainer.id = 'mangaOuterContainer'; 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;'; mangaOuterContainer.appendChild(mangaMiddleContainer); let imgsArr = [], //for manga-style image packs... followedUsersId = {}, //storing followed users pixiv ID BOOKMARK_URL = 'https://www.pixiv.net/ajax/user/XXXXXXXX/following?limit=100&tag=&lang=en',//&offset=0&rest=show' USER_ID, totalHits = 0, lastImgId = -1, PREVIEWSIZE, siteImgMaxWidth = 184, //2,7,12 [NEW]| quite useless on this pages because of square previews... mangaWidth = 1200, maxRequestTime = 30000, bookmarkContainer, pageNumber, DELTASCALE = +navigator.userAgent.match(/(?<=Firefox\/)\d+/)?.[0]<83?70:4, //older than 83.0 FF uses different scrolling scale //[temporary...] previewEventType, PAGETYPE = checkPageType(), followedCheck = { id:0, //backuping user id in case of cookie errors status:0, //-1: error, 0:default, 1:in progress, 2:done date:0, //date of last successful check saveState(){ localStorage.setObj('followedCheck', this); }, loadState(){ this.id = localStorage.getObj('followedCheck')?.id || 0; this.status = localStorage.getObj('followedCheck')?.status || 0; this.date = localStorage.getObj('followedCheck')?.date || 0; } }; var timerId, tInt, menuTimer; //----------------------------------------------------------------------------------- Storage.prototype.setObj = function(key, obj){ return this.setItem(key, JSON.stringify(obj)) } Storage.prototype.getObj = function(key){ return JSON.parse(this.getItem(key)) } //----------------------------------------------------------------------------------- const GM_setV = (typeof(GM_setValue)==='function')?GM_setValue:GM.setValue; const GM_getV = (typeof(GM_getValue)==='function')?GM_getValue:GM.getValue; //=================================================================================== //************************************PageType*************************************** //=================================================================================== function checkPageType() { if (document.URL.match(/https:\/\/www.pixiv.net\/bookmark_new_illust(?:_r18)?.php/)) return 0; //New illustrations - New + if (document.URL.match(/^https:\/\/www.pixiv.net\/discovery(?:\?mode=(safe|r18))?$/)) return 1; //Discovery page(works) - New + if (document.URL.match('https://www.pixiv.net/bookmark_detail.php?')) return 4; //Bookmark information - Old + if (document.URL.match('https://www.pixiv.net/ranking.php?')) return 6; //Daily rankings - Old + if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users\/\d+\/bookmarks\/artworks/)) return 7; //Bookmarks page - New + if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users/)) return 2; //Artist works page - New + if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?tags/)) return 8; //Search page - New + if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?artworks/)) return 12; //Illust page - New* + if (document.URL.match('https://www.pixiv.net/discovery/users?')) return 13; //Discovery page(users) New + if (document.URL.match('https://www.pixiv.net/history.php')) return 14; //History - Old + if (document.URL.match(/^https:\/\/www\.pixiv\.net\/(?:en\/)?$/)) return 10; //Home page - New + return -1; } console.log('PAGETYPE:',PAGETYPE); //----------------------------------------------------------------------------------- //Old: 4 6 14 //New: 0 1 2 7 8 10 12 13 //==============---------------------------- //Coloring: = 1 = 4 6 7 8 10 12 == ~~ // //Profile card: 0 1 = 4 6 7 8 10 12 == == // //On following: = 1 2 4 6 7 8 ?? 12 13 == // //=================================================================================== function setCurrentSettings(){ for (let i = 0; i < propList.length; i++){ currentSettings[propList[i].name] = propList[i].array[propList[i].paramIndex]; //only for options checking, actual settings contains in propList[] } resetPreviewSize(); //needed because of "auto" feature resetPreviewEventType(); } //----------------------------------------------------------------------------------- function saveSettings(){ for (let i = 0; i < propList.length; i++){ localStorage.setObj(propList[i].name, propList[i].paramIndex); } console.log("Settings saved"); } //----------------------------------------------------------------------------------- function loadSavedSettings(){ for (let i = 0; i < propList.length; i++){ propList[i].paramIndex = localStorage.getObj(propList[i].name) ?? propList[i].paramIndex; //load saved setting value, or let default if not found if ((propList[i].paramIndex < 0) || (propList[i].paramIndex >= propList[i].array.length)){ propList[i].paramIndex = 0; // "0" is not default for all settings... console.error(`localStorage error! Setting ${propList[i].name} has been reset to default value! [${propList[i].array[propList[i].paramIndex]}]`); } } console.log("Settings loaded"); } //----------------------------------------------------------------------------------- loadSavedSettings(); setCurrentSettings(); //----------------------------------------------------------------------------------- function resetPreviewSize(){PREVIEWSIZE = (currentSettings["PREVIEW_SIZE"] > 0)?currentSettings["PREVIEW_SIZE"]:(window.innerHeight>1200 & document.body.clientWidth>1200)?1200:600} function resetPreviewEventType(){previewEventType = (currentSettings["PREVIEW_ON_CLICK"])?'click':'mouseenter'; console.log(previewEventType)} //=================================================================================== //**********************************ColorFollowed************************************ //=================================================================================== function makeArgs(baseUrl, total){ let arr = []; for(let i = 1; i < Math.ceil(total / 100); i++){ //from 1 - because we already have first 100 users arr.push(baseUrl + "&offset=" + i + "00"); } return arr; } //----------------------------------------------------------------------------------- async function getUserId(){ USER_ID = USER_ID || followedCheck && followedCheck?.id || document.cookie.match(/user_id=\d+/)?.[0].split("=").pop() || Object.keys(localStorage).filter(e => e.match(/viewed_illust_ids_\d+/)).map(a => a.match(/\d+/))[0] || (await fetch('https://www.pixiv.net/bookmark.php')).url.match(/\d{3,}/)[0]; if (!USER_ID) return Promise.reject('FATAL ERROR in obtaining user ID! Please report this on GitHub "Issues"'); } //----------------------------------------------------------------------------------- async function checkFollowedArtists() { followedCheck.loadState(); if (((Date.now()-23*60*60*1000) > followedCheck.date) || (followedCheck.status < 2) || !localStorage['followedUsersId']){ console.log('*Followed check started*'); followedCheck.status = 1; followedCheck.saveState(); await getUserId().catch(e => followedCheckError(e)); if (USER_ID>0){ BOOKMARK_URL = BOOKMARK_URL.replace("XXXXXXXX", USER_ID); } else return -1; //make first request separately for obtaining count of followed users, both public/private let response0 = []; try{ response0 = await Promise.all([request(BOOKMARK_URL+'&rest=show&offset=0'), request(BOOKMARK_URL+'&rest=hide&offset=0')]); } catch(error){ console.error("Error with initial bookmark url!"); followedCheckError(error); return -1; } for(const i of response0) i.body.users.forEach(user => followedUsersId[user.userId] = 1); let args = []; let len = response0.map(r => r.body.total); args = makeArgs(BOOKMARK_URL+'&rest=show', len[0]); //public args.concat(makeArgs(BOOKMARK_URL+'&rest=hide', len[1])); //private //100 parallel requests in case of 10K users. TODO: find maximum amount and part requests let responseArray = []; try{ responseArray = await Promise.all(args.map(e => request(e))); } catch(error){ followedCheckError(error); return -1; } for(const r of responseArray) r.body.users.forEach(user => followedUsersId[user.userId] = 1); localStorage.setObj('followedUsersId', followedUsersId); followedCheck.id = USER_ID; followedCheck.status = 2; followedCheck.date = Date.now(); followedCheck.saveState(); console.log('*Followed check completed*'); console.log('Obtained', Object.keys(followedUsersId).length, 'followed users'); } else{ followedUsersId = localStorage.getObj('followedUsersId'); console.log(`followedCheck is up to date of %c${new Date(followedCheck.date).toLocaleString()}`, 'color:violet;'); } } checkFollowedArtists(); //----------------------------------------------------------------------------------- async function request(url, responseType) { return new Promise(function (resolve, reject){ let xhr = new XMLHttpRequest(); xhr.responseType = responseType || 'json'; xhr.timeout = 10000; xhr.open('GET', url, true); xhr.onload = function (){ resolve(this.response); }; xhr.onerror = xhr.ontimeout = function(){ reject(this); }; xhr.send(); }); } //----------------------------------------------------------------------------------- function followedCheckError(error){ console.error(error); followedCheck.status = -1; followedCheck.saveState(); } //----------------------------------------------------------------------------------- async function colorFollowed(artsContainers, delay) { let c = 0, d = 0; while (!artsContainers || artsContainers.length === 0) //first call -> daily rankings, illust page { console.log('waiting for arts...'); await sleep(delay ?? 2000); artsContainers = getAllArtsContainers(); ++c; if (c>5) {console.error('Error while waiting for arts loading! [Timeout 10s]'); return} } let artsContainersLength = artsContainers.length; //wait until last XHR completed if it is not--------------------------------------- followedCheck.loadState(); if (followedCheck.status == 1){ while (followedCheck.status !== 2){ console.log("waiting for followed users..."); //this could happen in case of huge amount of followed users await sleep(2000); followedCheck.loadState(); ++d; if (d*2000 > maxRequestTime || followedCheck.status == -1){ console.error(`ERROR while EXPECTING for subscriptions list! [${d*2000/1000}s]`); break; } } } //load from localStorage on any errors if (followedCheck.status <= 0 || Object.keys(followedUsersId).length == 0){ console.error(`There was some error during followed users check [Error Code: ${followedCheck.status}]`); console.log(`Trying to load cached followedUsersId by date of ${new Date(followedCheck.date).toLocaleString()} ...`); followedUsersId = localStorage.getObj('followedUsersId'); if (followedUsersId && Object.keys(followedUsersId).length > 0){ console.log("Loaded cached", Object.keys(followedUsersId).length, "followed users"); } else{ console.error('There is no locally stored followed users entries!'); return -1; } } //--------------------------------------------------------------------------------- if (PAGETYPE!==1) console.log('arts loaded:', artsContainersLength, 'Total:', getAllArtsContainers().length); let hitContainers = []; let currentHits = 0; if (PAGETYPE == 12){ let authorId = +document.querySelector("aside").querySelector("[href*=users]").href.match(/\d+/)[0]; [].filter.call(artsContainers, container => getAuthorIdFromContainer(container) == authorId) //color current authors arts among suggested .forEach(container => container.setAttribute("style", "background-color: deepskyblue; !important")); } hitContainers = [].filter.call(artsContainers, container => followedUsersId[getAuthorIdFromContainer(container)] == 1); hitContainers.forEach(container => container.setAttribute("style", "background-color: green; !important")); currentHits = hitContainers.length; totalHits += currentHits; if (PAGETYPE!==1) console.log('hits: '+currentHits + ' (Total: '+(totalHits)+')'); //containers are constantly being replaced on this page } //----------------------------------------------------------------------------------- function getAllArtsContainers() { switch (PAGETYPE){ case 1: case 7: case 8: case 10: return [...document.querySelectorAll('li > div')].filter(e => (e.querySelector('a[href*="/artworks/"]'))); case 4: case 6: return document.querySelectorAll('.ui-profile-popup'); case 12: return document.querySelectorAll('.gtm-illust-recommend-title'); default: console.error('Unprocessed PAGETYPE in getAllArtsContainers()!'); } return null; } //----------------------------------------------------------------------------------- function getAuthorIdFromContainer(artContainer) { let authorId = -1; //console.log(artContainer); if (!artContainer){ console.error('UNPROCESSED getAuthorIdFromContainer() call!'); } else if (typeof artContainer.hasAttribute !== 'function'){ console.log(artContainer, 'has been filtered out.'); } else if ([1,7,10,12].includes(PAGETYPE)){ authorId = searchNearestNode(artContainer,'[href*="/users/"]').getAttribute('href').split('/').pop(); } else if (PAGETYPE===4 || PAGETYPE===6){ authorId = artContainer.getAttribute('data-user_id') || artContainer.querySelector('.ui-profile-popup').getAttribute('data-user_id'); } else if (PAGETYPE===8){ let node = searchNearestNode(artContainer,'[href*="/users/"]'); authorId = (node)? node.getAttribute('href').split('/').pop(): -8; } return +authorId; } //----------------------------------------------------------------------------------- function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } //----------------------------------------------------------------------------------- function getElementByXpath(path) { return document.evaluate(path, document, null, XPathR###lt.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } //----------------------------------------------------------------------------------- function getArtSectionContainers() { switch(PAGETYPE) { case 0: case 2: case 7: return document.querySelector('a[data-gtm-user-id][href*="/artworks/"]')?.closest('ul') case 1: case 4: return $('.gtm-illust-recommend-zone')[0] case 6: return $('.ranking-items')[0] case 8: return $('body')[0] //$("section>div>ul")[0] case 10: return document.querySelector('div[style*=--columns]') //fckng sick of this-_- case 12: return $('.gtm-illust-recommend-zone ul')[0] default: return 0; } } //----------------------------------------------------------------------------------- let observer = { mutationObserver: null, init(func){ this.mutationObserver = new MutationObserver((mutations)=>{func(mutations)}); }, observe(mainDiv, options){ this.mutationObserver.observe(mainDiv, options); console.log('Observer has been set'); }, disconnect(){ this.mutationObserver.disconnect(); } } //----------------------------------------------------------------------------------- let renewObserver = Object.assign({}, observer); //copy new instance of object //----------------------------------------------------------------------------------- function observerBody(mutations) { let arr = []; mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (PAGETYPE == 10){ if (node.nodeName == "IMG" && node.matches('img:not([class*=" "])')){ //todo: very unstable condition(>=2 img classes) arr.push(searchNearestNode(node, 'a[href*="/artworks/"]')); } else if (node.nodeName == "DIV"){ node.querySelectorAll('a[href*="/artworks/"]:only-child').forEach((el)=>arr.push(el)); } } else if (PAGETYPE == 12 && (!!node.querySelector('iframe'))){ node.remove(); //filtering ads } else if (PAGETYPE == 1 || PAGETYPE == 7){ node.querySelectorAll('li > div').forEach((el) => arr.push(el)); } else if (PAGETYPE == 8){ //console.log(node); if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='UL'){ node.querySelectorAll('li > div').forEach((el) => arr.push(el)); } if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='LI' && node.querySelector('div [href*="/users/"]')){ arr.push(node); } } else{ arr.push(node); } }); }); if (arr.length>0) colorFollowed(arr); } //----------------------------------------------------------------------------------- observer.init(observerBody); //----------------------------------------------------------------------------------- async function waitForArtSectionContainers() { let mainDiv = getArtSectionContainers(); let count = 0; while(!mainDiv) { console.log('Waiting for arts container...'); await sleep(1000); mainDiv = getArtSectionContainers(); ++count; if (count>10) {console.error('Error while waiting for arts containers! [Timeout 10s]'); return -1} } console.log(mainDiv); return mainDiv; } //----------------------------------------------------------------------------------- async function initMutationObject(options) { observer.observe(await waitForArtSectionContainers(), options); } //----------------------------------------------------------------------------------- function searchNearestNode(el, selector) { let nearestNode = el.querySelector(selector); while ((!nearestNode) && (el != document.body)){ el = el.parentNode; nearestNode = el.querySelector(selector); } return nearestNode; } //----------------------------------------------------------------------------------- function followage(thisObj, toFollow) //In case of followed check lasting too long, async queue may be a solution { console.log('toFollow: '+ toFollow); let userId = searchNearestNode(thisObj, '[href*="/users/"]').getAttribute('href').split('/').filter(el => el.match(/\d+/))[0]; if (!(userId>0)) {console.error(`Wrong userId! ${userId}`); return} if (localStorage.getObj('followedCheck').status == 2) { if (Object.keys(followedUsersId).length == 0) followedUsersId = localStorage.getObj('followedUsersId'); if (toFollow){ followedUsersId[userId] = 1; if ([2,12].includes(PAGETYPE)){ followagePreview(); } } else delete followedUsersId[userId]; localStorage.setObj('followedUsersId', followedUsersId); console.log('userId ' + userId + [(toFollow)?' added to':' deleted from'] + ' localStorage. Followed: '+ Object.keys(followedUsersId).length); } 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`); } //-------------------------------------Followage------------------------------------- async function followagePreview() { let recommendationBlock; let c = 0; while(!recommendationBlock) { console.log('Waiting for FollowagePreview'); await sleep(1000); recommendationBlock = getElementByXpath("//div[contains(., 'Recommended users')]"); ++c; if (c>10) {console.error("Error while waiting for recommendationBlock! [Timeout 10s]"); return -1} } console.log('*FollowagePreview loaded*'); //let recommendationObserver = Object.assign({}, observer); let scrollBackward = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(1)'); let scrollForward = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(2)'); recommendationBlock.onwheel = function(e){ e.preventDefault(); //no need if (e.deltaY > 0) scrollForward.click() else scrollBackward.click(); }; $(recommendationBlock).on(previewEventType, 'a:not([href*="/users/"]) img', function(e) { e.preventDefault(); //let top = window.innerHeight - PREVIEWSIZE - 5 + window.scrollY + 'px'; let top = window.scrollY + 5 + 'px'; checkDelay(setHover, this, top); }); } //---------------------------------------History------------------------------------- let illust_history = { ids: [], timestamps: {}, load(){ this.ids = localStorage.getObj('viewed_illust_ids') || GM_getV("viewed_illust_ids") || localStorage.getObj('viewed_illust_ids_' + USER_ID)?.data || []; this.timestamps = localStorage.getObj('viewed_illust_timestamps') || GM_getV("viewed_illust_timestamps") || localStorage.getObj('viewed_illust_timestamp_' + USER_ID)?.data || {}; }, save(){ localStorage.setObj('viewed_illust_ids', this.ids); //viewed_illust_ids localStorage.setObj('viewed_illust_timestamps', this.timestamps); //viewed_illust_timestamp }, add_record(illust_id){ this.load(); if (this.ids.indexOf(illust_id.toString()) == -1){ this.ids.push(illust_id.toString()); this.timestamps[illust_id] = Date.now()/1000; console.log(+illust_id, "has been added to history"); } else if (currentSettings["KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"] == false){ this.timestamps[illust_id] = Date.now()/1000; console.log(+illust_id, ": updated view date"); } 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:;'); this.save(); }, delete_record(illust_id){ this.load(); let index = this.ids.indexOf(illust_id.toString()); if (index > -1){ this.ids.splice(index, 1); } delete this.timestamps[illust_id]; this.save(); console.log(+illust_id, "has been deleted from history"); }, override(){ this.load(); let date = Date.now()+365*24*60*60*1000; localStorage.setObj('viewed_illust_ids_' + USER_ID, {data:this.ids, expires:date}); localStorage.setObj('viewed_illust_timestamp_' + USER_ID, {data:this.timestamps, expires:date}); console.info(`History overridden [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;'); let count = 0, t = setInterval(()=>{ document.querySelectorAll('._history-item.trial').forEach(e => { e.querySelector('img').style.opacity = 1; e.classList.remove("trial"); }); ++count; if (count>10) clearInterval(t); }, 1000); }, export(){ this.load(); //TODO:check history records integrity before export GM_setV("viewed_illust_ids", this.ids); GM_setV("viewed_illust_timestamps", this.timestamps); console.info(`History was exported to script manager storage [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;'); }, check_space(){ let spaceConsumed = +((new Blob([Object.values(localStorage), Object.keys(localStorage), localStorage.viewed_illust_ids, localStorage.viewed_illust_timestamps]).size)/(5000*####)).toFixed(3); //duplicating records not the best solution... but simplest [solve this later if needed] if (spaceConsumed > 0.95){ this.add_record = this.override = ()=>{}; return Promise.reject(`Too much space consumed [${spaceConsumed*100}%] — history is disabled`); //~100.000 entries } else console.log('History initialized'); } }; getUserId().then(() => illust_history.check_space()).catch(e => console.error('History not initialized —', e)); //=================================================================================== if (PAGETYPE===0) siteImgMaxWidth = 198; else if (PAGETYPE===4) siteImgMaxWidth = 150; else if (PAGETYPE===6 || PAGETYPE===14) siteImgMaxWidth = 240; //----------------------------------------------------------------------------------- $(document).ready(function () { console.log('$(document).ready'); mangaWidth = document.body.clientWidth - 60; mangaContainer.style.maxWidth = mangaWidth+'px'; document.body.appendChild(imgContainer); document.body.appendChild(mangaOuterContainer); //---------------------------------Settings menu----------------------------------- let menu = document.createElement("div"); menu.id = "menu"; menu.style = ` position: absolute; display: block; visibility: hidden; top: 60px; left: 10px; padding: 5px 5px 5px 20px; border: 2px solid deepskyblue; border-radius: 15px; background: white; font-size: 14px; line-height: 17px; color: rgb(0, 0, 0); border-radius: 15px; word-wrap: normal; `; //filling menu fields with values and property names for (let i = 0; i < propList.length; i++){ 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>` } document.body.appendChild(menu); //--------------------------------------------------------------------------------- function changeMenuValues(menuDiv){ let index = Array.prototype.indexOf.call(menuDiv.parentNode.parentNode.children, menuDiv.parentNode); propList[index].paramIndex+=1; if (propList[index].paramIndex >= propList[index].array.length) propList[index].paramIndex = 0; menuDiv.textContent = propList[index].array[propList[index].paramIndex]; //foolproof protection if (propList[0].paramIndex == 1){ menu.childNodes[1].childNodes[0].disabled = true; propList[1].paramIndex = 0; menu.childNodes[1].childNodes[0].textContent = "0"; } else{ menu.childNodes[1].childNodes[0].disabled = false; } } $('#menu').on('click', 'button', function(){ changeMenuValues(this); }); //--------------------------------------------------------------------------------- async function initMenu(){ if ([-1,14].includes(PAGETYPE)) return; let buttons, menuButton; //put to global scope if (menuButton) is needed elsewhere let count = 0; while (!menuButton && count<5){ if ([0,1,2,7,8,10,12].includes(PAGETYPE)) buttons = document.querySelectorAll('body > div#root > div.charcoal-token button[title]') else buttons = document.querySelectorAll('body > div#js-mount-point-header > div:nth-child(1) button'); menuButton = buttons[buttons.length - 1]; // last is the menu button console.log(menuButton); await sleep(1000); ++count; } if (menuButton) menuButton.addEventListener("click", function(){ menu.style.visibility = 'visible'; clearTimeout(menuTimer); menuTimer = setTimeout(()=>{menu.style.visibility = 'hidden'}, 60*1000); //closing menu after 60s to prevent "hanging" it in one tab }); else console.error("menuButton is undefined!"); } //--------------------------------------------------------------------------------- $(document).mouseup(function (e){ if (!($(menu).has(e.target).length) && (menu.style.visibility == 'visible')){ menu.style.visibility = 'hidden'; saveSettings(); if (currentSettings[propList[0].name] !== propList[0].array[propList[0].paramIndex]) setTimeout(()=>{initPreviewListeners(); initProfileCard()}, 0); //reset event listeners only after settings are applied setCurrentSettings(); clearTimeout(menuTimer); } }); //--------------------------------------------------------------------------------- initMenu(); //-------------------------------Follow onclick------------------------------------ let toFollow, followSelector; //--------------------------------------------------------------------------------- function initFollowagePreview() { if ([1,2,7,8,12].includes(PAGETYPE)){ followSelector = 'button:contains("Follow")'; } else if ([4,6,13].includes(PAGETYPE)){ followSelector = '.follow-button'; } else return 0; $('body').off('mouseup', followSelector); //clearing previous events if ([1,2,4,6,7,8,12,13].includes(PAGETYPE)) { $('body').on('mouseup', followSelector, function(){ toFollow = (this.textContent == 'Follow'); //~mustn't work on non-English locale| todo: add some locale-specific text condition? followage(this, toFollow); }); } if ([2,7].includes(PAGETYPE)){ $('body').off('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem'); $('body').on('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem', followagePreview); } } //--------------------------------------------------------------------------------- initFollowagePreview(); //====================================PAGINATION=================================== async function autoPagination(){ $('section ul').off('click', 'button'); window.onscroll = null; //------------------------------------------------------------------------------- if (!currentSettings['ENABLE_AUTO_PAGINATION'] || ![0,2,7].includes(PAGETYPE)) return -1; let pageCount = location.href.match(/(?<=[?|&]p=)\d+/)?.[0] || 1; let mode = location.href.match(/r18/)?.[0] || "All"; let maxPageCount = 35; //limit for Following is 35 pages let authorId = location.href.match(/(?<=users\/)\d+/)?.[0]; let artworks = !!location.href.match(/\d+\/artworks/)?.[0]; let illusts = !!location.href.match(/illustrations/)?.[0]; let manga = !!location.href.match(/manga/)?.[0]; let rest = location.href.match(/rest=hide/)?.[0] && "hide" || "show"; //let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || ''; //------------------------------------------------------------------------------- let x_csrf_token; //for bookmarks request('/en/', 'document').then(response => x_csrf_token = response.documentElement.innerHTML.match(/(?<=token":")[\dA-z]+/)); //------------------------------------------------------------------------------- let artsSection = await waitForArtSectionContainers(); await sleep(2000); let art = $(artsSection.querySelector('a[href*="artworks"]')).parents('li')[0].cloneNode(true); let mangaCount = document.createElement('div'); 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);"; mangaCount.appendChild(document.createElement('span')); mangaCount.querySelector('span').style = "font-size: 10px; line-height: 10px; color: rgb(255, 255, 255); font-family: inherit; font-weight: bold;"; if (!art.querySelector('span')) art.querySelector('[href]').appendChild(mangaCount); art.querySelectorAll('img').forEach(el => el.src=''); art.classList.add("paginated"); //------------------------------------------------------------------------------- pageNumber = pageNumber ?? mangaCount.cloneNode(true); pageNumber.className = "pageNumber"; 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;"; document.body.appendChild(pageNumber); //------------------------------------------------------------------------------- let running = false, urls = []; window.onscroll = async function(){ if ((window.innerHeight*1.8 + window.scrollY) >= document.body.scrollHeight){ if (running || pageCount>=maxPageCount) return; running = true; pageCount++; let url; let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || ''; if (PAGETYPE == 0){ url = `https:\/\/www\.pixiv\.net\/ajax\/follow_latest\/illust\?p=${pageCount}\&mode=${mode}\&lang=en`; } if (PAGETYPE == 2){ if (!urls.length && !tags){ urls = []; await fetch(`https://www.pixiv.net/ajax/user/${authorId}/profile/all?lang=en`).then(r => r.json()).then(response => { let iArr = (illusts || artworks) && Object.keys(response.body.illusts) || []; let mArr = (manga || artworks) && Object.keys(response.body.manga) || []; let arr = iArr.concat(mArr).sort(function(a,b){return a-b}).reverse(); for(let i=(pageCount-1)*48; i<arr.length; i+=48){ urls.push(`https://www.pixiv.net/ajax/user/${authorId}/profile/illusts?ids[]=` + arr.slice(i, i+48).join('&ids[]=') + "&work_category=illustManga&is_first_page=0&lang=en" ); } }); if (!urls.length) return; //maybe check nav element before fetching instead maxPageCount = urls.length + 1; } if (tags){ let illustManga = artworks && 'illustmanga' || illusts && 'illusts' || manga && 'manga'; url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/${illustManga}\/tag\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&lang=en`; } else{ url = urls.shift(); } } if (PAGETYPE == 7){ url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/illusts\/bookmarks\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&rest=${rest}\&lang=en` } console.log('Loading', pageCount, 'page...'); fetch(url).then(r => r.json()).then(response => { let fragment = new DocumentFragment(); Array.prototype.forEach.call(response.body?.thumbnails?.illust || Object.values(response.body.works).reverse(), (obj) => { let el = art.cloneNode(true); if (obj.pageCount > 1) [...(el.querySelectorAll('span'))].pop().textContent = obj.pageCount; else $(el.querySelector('span')).parents('a > div')[0].remove(); //----------------------------------------------------------------------- let s = el.querySelector('[href]').href.match('/en/')?.[0] || '/'; let hrefs = el.querySelectorAll('[href]'); hrefs[0].setAttribute('data-gtm-value', obj.id); hrefs[0].href = s + "artworks/" + obj.id; hrefs[1].href = s + "artworks/" + obj.id; hrefs[1].textContent = obj.title; el.querySelector('img').src = obj.url; if (hrefs.length == 4){ hrefs[2].setAttribute('data-gtm-value', obj.userId); hrefs[2].href = s + "users/" + obj.userId; hrefs[3].setAttribute('data-gtm-value', obj.userId); hrefs[3].href = s + "users/" + obj.userId; hrefs[3].textContent = obj.userName; el.querySelectorAll('img')[1].src = obj?.profileImageUrl || ''; //for deleted bookmarks } if (obj.bookmarkData) el.querySelectorAll('path:not(:only-child)').forEach(e => { e.setAttribute("style", "fill: rgb(255, 64, 96); !important") }); //----------------------------------------------------------------------- el.style.display = "list-item"; //needed - 'none' otherwise fragment.appendChild(el); }); if (PAGETYPE==7 || tags) maxPageCount = Math.ceil(response.body.total/48); artsSection.appendChild(fragment); running = false; }); if (pageCount>=maxPageCount){ console.log('*All pages loaded*'); [...document.querySelectorAll("nav")].pop().style.opacity = 0.3; pageNumber.querySelector('span').textContent = `All pages loaded [${pageCount}]`; pageNumber.style.opacity = "100%"; setTimeout(()=>pageNumber.style.opacity = "0%", 3000); } else{ pageNumber.querySelector('span').textContent = pageCount; pageNumber.style.opacity = "100%"; setTimeout(()=>pageNumber.style.opacity = "0%", 1500); } } //endif } //onscroll //------------------------------------------------------------------------------- $(artsSection).on('click', 'button', function(event){ event.preventDefault(); let illust_id = searchNearestNode(this,'[href*="/artworks/"]').href.match(/\d+/)[0]; fetch('/ajax/illusts/bookmarks/add', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=utf-8', 'x-csrf-token': x_csrf_token }, body: JSON.stringify({"illust_id":illust_id,"restrict":0,"comment":"","tags":[]}) }) .then(() => this.querySelectorAll('path:not(:only-child)').forEach(e => e.setAttribute("style", "fill: rgb(255, 64, 96);"))) .catch(err => console.log(err)); }); //------------------------------------------------------------------------------- return 0; } //================================================================================= function initMutationObservers() { observer.disconnect(); $('body').off('mouseup', 'a[href*="/discovery"]'); $('body').off('mouseup', 'a[href*="bookmarks/artworks"]'); $('body').off('mouseup', 'section>div>a[href*="/artworks"], a[href*="/illustrations"], a[href*="/manga"]'); //------------------------------------------------------------------------------- if (PAGETYPE===0){ autoPagination().then(v => { if (v === 0){ $('body').on('mouseup', 'a[href*="/bookmark_new_illust"]', function(e){ e.preventDefault(); location.href = this.href; }); } }) } //------------------------------------------------------------------------------- if (PAGETYPE===1){ colorFollowed(); initMutationObject({'childList': true, 'subtree': true}); let timeout_1; $('body').on('mouseup', 'a[href*="/discovery"]', function(){ clearTimeout(timeout_1); timeout_1 = setTimeout(() => { if (PAGETYPE===1){ colorFollowed(); initMutationObject({'childList': true, 'subtree': true}); } }, 2000); }); } //---------------------------Bookmark detail page cleaning----------------------- if (PAGETYPE===4) { if (currentSettings["HIDE_PEOPLE_WHO_BOOKMARKED_THIS"]) $('.bookmark-list-unit')[0].remove(); initMutationObject({'childList': true}); } //-----------------------------Daily rankings ad cleaning------------------------ if (PAGETYPE===6) { colorFollowed(); $('.ad-printservice').remove(); initMutationObject({'childList': true}); } //----------------------------------Artwork page--------------------------------- if (PAGETYPE===12) { initMutationObject({'childList': true}); illust_history.add_record(location.href.match(/\d+/)[0]); } //----------------------------------Search page---------------------------------- if (PAGETYPE===8) { initMutationObject({'childList': true, 'subtree': true}); } //----------------------------------Main page------------------------------------ if (PAGETYPE===10) { colorFollowed(); initMutationObject({'childList': true, 'subtree': true}); } //-------------------------------Pixiv User pages-------------------------------- if (PAGETYPE===2 || PAGETYPE===7) { let pagination = autoPagination(); //2,7 $('body').on('mouseup', 'a[href*="bookmarks/artworks"]', function(){ console.log('PAGETYPE: '+ PAGETYPE+' -> 7'); PAGETYPE = 7; sleep(5000).then(() => { initMutationObject({'childList': true}); autoPagination().then((v)=>{colorFollowed(null, v && 2000)}); //success(0) -> already waited 2+ secs; disabled(-1) -> need to wait initProfileCard(); }); }); $('body').on('mouseup', 'section>div>a[href$="/artworks"], a[href$="/illustrations"], a[href$="/manga"]', function(){ console.log('PAGETYPE: '+ PAGETYPE+' -> 2'); PAGETYPE = 2; sleep(2500).then(autoPagination); observer.disconnect(); }); if (PAGETYPE===7){ initMutationObject({'childList': true}); pagination.then((v)=>{colorFollowed(null, v && 2000)}); //if pagination is enabled we need to wait before it completes*, but no more initProfileCard(); } //clearing "cache" of autopaged arts $('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(){ let artsSection = getArtSectionContainers(); [...artsSection.querySelectorAll('.paginated')].forEach(el => el.remove()); }); } //------------------------------------History------------------------------------ if (PAGETYPE===14){ let trial = document.querySelector('span.trial'); //indicator of non-premium account if (trial){ getUserId().then(() => illust_history.override()); trial.textContent = "Extended Version"; } //export with Shift+E document.onkeyup = function(e){ if (e.key.toUpperCase() == "E" && e.shiftKey){ illust_history.export(); } }; } //------------------------------------------------------------------------------- } //--------------------------------------------------------------------------------- initMutationObservers(); //================================================================================= //***************************************HOVER************************************* //================================================================================= //------------------------------------Profile card--------------------------------- //4,6,9 [~0,1,~7,8,10,12] function initProfileCard() { $('body').off('mouseenter click', 'section._profile-popup a[href*="/artworks/"]'); $('body').off("mouseenter", '.paginated a[href*="/users/"]'); $('body').off("mouseleave", '.paginated a[href*="/users/"]'); //------------------------------------------------------------------------------- if ([4,6].includes(PAGETYPE)) //rankings { $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e) { console.log('Profile card'); e.preventDefault(); checkDelay(setHover, this, getOffsetRect(this).top+200+'px', true); }); } //------------------------------------------------------------------------------- if ([0,7].includes(PAGETYPE) && currentSettings['ENABLE_AUTO_PAGINATION']) //patch for profile preview with pagination { //creating profile card(for last 3 arts) let profilePopup = document.createElement('section'); profilePopup.className = '_profile-popup'; profilePopup.style = `visibility:hidden; position:absolute; height:128px; z-index:10001; padding: 0px;`; profilePopup.onmouseleave = function(e){ profilePopup.style.visibility = "hidden"; if (e.relatedTarget?.id != 'imgPreview') imgContainer.style.visibility = "hidden"; } let profileImagesDiv = document.createElement('div'); profileImagesDiv.style = `overflow:hidden; height:128px; border-radius:5px; border: 1px solid #c7d2dc; padding: 0px; background-color: rgb(255,255,255);`; profilePopup.appendChild(profileImagesDiv); for (let i=0; i<3; i++){ var a = document.createElement('a'); a.className = `item_${i}`; a.style = `display: inline-block !important; width: 128px; height: 128px;`; a.target = "_blank"; profileImagesDiv.appendChild(a); } document.body.appendChild(profilePopup); let profileCard_timeout, previous_id; //handler for showing paginated profile card $('body').on("mouseenter", '.paginated a[href*="/users/"]', function(e){ e.preventDefault(); let user_id = this.href.match(/\d+/)[0]; if (user_id == 0) return; if (previous_id == user_id){ profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px"; profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px"; profilePopup.style.visibility = "visible"; return; } clearTimeout(profileCard_timeout); //cancelling previous event profilePopup.firstChild.childNodes.forEach(el => el.style.backgroundImage = ''); profileCard_timeout = setTimeout(fillProfileCard.bind(this, user_id), 500); }); function fillProfileCard(user_id){ if (!([].indexOf.call(document.querySelectorAll(':hover'), this) > -1)) return; //need to check whether mouse is still over user profile after 500ms profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px"; profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px"; //-sq.preview +icon profilePopup.style.visibility = "visible"; 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 => { response.body[0].illusts.forEach((el,i) => { profilePopup.querySelector(`a.item_${i}`).style.backgroundImage = `url(${el.url["128x128"]})`; profilePopup.querySelector(`a.item_${i}`).href = `/artworks/${el.illust_id}`; }) }); previous_id = user_id; } //actual art preview $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e){ e.preventDefault(); checkDelay(setHover, this, getOffsetRect(this).top+128+5+'px', true); }); $('body').on("mouseleave", `.paginated div[aria-haspopup]`, function(e){ if (!e.relatedTarget?.closest('._profile-popup')) profilePopup.style.visibility = "hidden"; }); } } initProfileCard(); //================================================================================= //*******************************Initialize Preview Listeners********************** //================================================================================= function initPreviewListeners() { //clearing----------------------------------------------------------------------- $('body').off('click mouseenter', 'a[href*="/artworks/"]'); $('body').off('click mouseenter', 'a[href*="/artworks/"] img'); $('body').off('click', '[role="presentation"] img'); //document.removeEventListener('click'); //not worth bothering //------------------------------------------------------------------------------- if (previewEventType == 'click'){ document.addEventListener('click', (e)=>{ if (e.target.nodeName==="IMG") e.preventDefault(); }, {capture: true}) //site uses event capturing now which jQuery can't cover } //------------------------------------------------------------------------------- //New illustrations, Discovery[Artworks], Artist pages, Bookmarks, Search, Home page, Artwork page //0,1,2,7,8,10,12 if (PAGETYPE === 0 || PAGETYPE === 1 || PAGETYPE === 2 || PAGETYPE === 7 || PAGETYPE === 8 || PAGETYPE === 10 || PAGETYPE === 12) { console.info('new'); $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e) { e.preventDefault(); //---------------------------filtering preview card-------------------------- if (getElementByXpath("//a[text()='View Profile']")){ if (this.closest('a').querySelector('span')) checkDelay(setMangaHover, this, this.closest('a').textContent, getOffsetRect(this).top+112+'px'); else checkDelay(setHover, this, getOffsetRect(this).top+112+5+'px', true); } //-------------------------filtering recommended users----------------------- else if (getElementByXpath("//div[text()='Recommended users']")){ let top = window.scrollY + 5 + 'px'; checkDelay(setHover, this, top); } //--------------------------------Normal case-------------------------------- else{ //console.log(this); //multiple if (this.closest('a').querySelector('span')) checkDelay(setMangaHover, this, this.closest('a').textContent.replace(/R-18(G)?/,"")); //single else checkDelay(setHover, this); } //--------------------------------------------------------------------------- }); //----------------------------------------------------------------------------- if (PAGETYPE === 12) $('body').on('click', '[role="presentation"] img', function(event){ if (event.ctrlKey){ event.preventDefault(); event.stopPropagation(); let isManga = !!document.querySelector('.gtm-manga-viewer-preview-modal-open'); onClickActions(this, event, isManga); } }); } //----------------------DAILY RANKINGS & BOOKMARK INFORMATION PAGES-------------- //4,6 else if (PAGETYPE === 4 || PAGETYPE === 6) { $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e) //direct div selector works badly with "::before" { e.preventDefault(); //console.log(this); //single if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV"){ checkDelay(setHover, this.querySelector('img')); } //multiple else if (this.children[1] && this.children[1].className == 'page-count'){ checkDelay(setMangaHover, this.querySelector('img'), this.querySelector('.page-count').textContent); } }); } //----------------------------------DISCOVERY[USERS]----------------------------- //13 else if (PAGETYPE === 13) { $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e){ e.preventDefault(); if (this.childNodes.length == 0) checkDelay(setHover, this); //single art else if (this.childNodes.length == 1) checkDelay(setMangaHover, this, this.firstChild.textContent); //manga }); } //-------------------------------------History----------------------------------- //14 else if (PAGETYPE === 14) { $('body').on(previewEventType, '._history-item', function(e){ e.preventDefault(); checkDelay(setHover, this.querySelector('img'), getOffsetRect(this).top + 'px'); }); } } //--------------------------------------------------------------------------------- initPreviewListeners(); //================================================================================= if (currentSettings["DELAY_BEFORE_PREVIEW"]>0) $('body').on('mouseleave', 'a[href*="/artworks/"]', function() { clearTimeout(timerId); clearInterval(tInt); }); //---------------------------------Async page change------------------------------- function renewAll() { if (PAGETYPE != checkPageType()) { console.log('PAGETYPE:', PAGETYPE, '->', PAGETYPE = checkPageType()); clearTimeout(timerId); clearInterval(tInt); if (PAGETYPE === -1) return; initPreviewListeners(); initMutationObservers(); initMenu(); autoPagination(); initFollowagePreview(); initProfileCard(); } } //--------------------------------------------------------------------------------- renewObserver.init(renewAll); renewObserver.observe($('body')[0], {childList: true, subtree: true}); //--------------------------------------------------------------------------------- }); //end of document.ready //=================================================================================== //----------------------------------------------------------------------------------- function checkDelay(func, ...args) { if (currentSettings["DELAY_BEFORE_PREVIEW"]>0){ clearTimeout(timerId); timerId = setTimeout(()=>{ if ([].indexOf.call(document.querySelectorAll(':hover'), (PAGETYPE!=6)? args[0] : args[0].parentNode.parentNode) > -1) func(...args) }, currentSettings["DELAY_BEFORE_PREVIEW"]); } else func(...args) } //----------------------------------------------------------------------------------- function setHover(thisObj, top, profileCard) { clearInterval(tInt); imgContainer.style.visibility = 'hidden'; mangaOuterContainer.style.visibility = 'hidden'; hoverImg.src=''; //just in case hoverImg.src = parseImgUrl(thisObj); imgContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px'; //adjusting preview position considering expected image width //--------------------------------------------------------------------------------- let l = ([4,6,14].includes(PAGETYPE)) ?getOffsetRect(thisObj.parentNode.parentNode).left //more accurate on discovery users and history :getOffsetRect(thisObj).left; let dcw = document.body.clientWidth; let previewWidth = PREVIEWSIZE; if (hoverImg.naturalWidth>0){ //cached (previously viewed) adjustSinglePreview(dcw, l, hoverImg.naturalWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode); //console.log("cached"); } else{ //on old pages width can be pre-calculated if ([4,6,14].includes(PAGETYPE) && !profileCard){ previewWidth = PREVIEWSIZE*(((PAGETYPE==6 || PAGETYPE==14)?thisObj.clientWidth:thisObj.parentNode.parentNode.clientWidth)/siteImgMaxWidth)+5; adjustSinglePreview(dcw, l, previewWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode); //console.log("count"); } 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) if (dcw - l - PREVIEWSIZE - 5 > 0){ imgContainer.style.left = l+'px'; imgContainer.style.visibility = 'visible'; //console.log("excessive"); } else{ //when on NEW layout - need to wait until image width is received let tLimit = 0; tInt = setInterval(function(){ if (hoverImg.naturalWidth>0){ clearInterval(tInt); adjustSinglePreview(dcw, l, hoverImg.naturalWidth, thisObj); //position mismatching due to old `thisObj` => clearing in hoverImg.mouseleave } ++tLimit; //console.log(tInt, tLimit); if (tLimit*40>5000){ //timeout 5s in case of loading errors clearInterval(tInt); hoverImg.src=''; console.error('setInterval error'); return; } }, 40); } } } //--------------------------------------------------------------------------------- checkBookmark(thisObj, imgContainer); } //----------------------------------------------------------------------------------- function adjustSinglePreview(dcw, l, contentWidth) { if (l<0) l = 5; //followage preview let d = dcw - l - contentWidth - 5; //5 - padding - todo... imgContainer.style.left = (d>=0)?l+'px':l+d+'px'; imgContainer.style.visibility = 'visible'; } //----------------------------------------------------------------------------------- function setMangaHover(thisObj, count, top) { clearInterval(tInt); imgContainer.style.visibility = 'hidden'; //just in case mangaOuterContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px'; checkBookmark(thisObj, mangaOuterContainer); imgsArrInit(thisObj, +count); } //----------------------------------------------------------------------------------- function imgsArrInit(thisObj, count) { let primaryLink = parseImgUrl(thisObj); let currentImgId = getImgId(primaryLink); //--------------------------------------------------------------------------------- if (currentImgId != lastImgId) { for(let j=0; j<imgsArr.length; j++) { imgsArr[j].src = ''; } lastImgId = currentImgId; for(let i=0; i<count; i++) { if (!(!!imgsArr[i])) //if [i] img element doesn't exist { imgsArr[i] = document.createElement('img'); mangaContainer.appendChild(imgsArr[i]); } imgsArr[i].src = primaryLink.replace('p0','p'+i); } } //--------------------------------------------------------------------------------- mangaOuterContainer.style.visibility = 'visible'; } //----------------------------------------------------------------------------------- function parseImgUrl(thisObj) { let url = (thisObj.src)? thisObj.src: thisObj.style.backgroundImage.slice(5,-2); url = url.replace(/\/...x..[0|8]/, '/'+PREVIEWSIZE+'x'+PREVIEWSIZE). replace('_square1200','_master1200'). replace('_custom1200','_master1200'). replace('custom-thumb','img-master'). replace('_80_a2',''). replace('_70','') ; return url; } //----------------------------------------------------------------------------------- function checkBookmark(thisContainer, previewContainer) { if ([0,1,2,7,8,10,12].includes(PAGETYPE)) bookmarkContainer = searchNearestNode(thisContainer, 'button'); else if ([4,6].includes(PAGETYPE)) bookmarkContainer = searchNearestNode(thisContainer, "._one-click-bookmark") else return; //no favourite button if ($(bookmarkContainer).hasClass("on")) $(previewContainer).css("background", "rgb(255, 64, 96)"); //purple else $(previewContainer).css("background", "rgb(34, 34, 34)"); //grey } //----------------------------------------------------------------------------------- function getImgId(str) { return str.substring(str.lastIndexOf("/")+1, str.indexOf("_")); } //----------------------------------------------------------------------------------- function getOffsetRect(elem) { // (1) let box = elem.getBoundingClientRect(); // (2) let body = document.body; let docElem = document.documentElement; // (3) let scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; let scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; // (4) let clientTop = docElem.clientTop || body.clientTop || 0; let clientLeft = docElem.clientLeft || body.clientLeft || 0; // (5) let top = box.top + scrollTop - clientTop; let left = box.left + scrollLeft - clientLeft; return { top: Math.round(top), left: Math.round(left) }; } //=================================================================================== //**************************************Hide***************************************** //=================================================================================== imgContainer.onmouseleave = function() { imgContainer.style.visibility = 'hidden'; hoverImg.src=''; clearTimeout(timerId); clearInterval(tInt); }; //----------------------------------------------------------------------------------- mangaOuterContainer.onmouseleave = function() { mangaOuterContainer.style.visibility = 'hidden'; clearTimeout(timerId); }; //=================================================================================== //***********************************Art Clicks************************************** //=================================================================================== //-----------------------------Single arts onclick actions--------------------------- hoverImg.onmouseup = function (event) { onClickActions(this, event, false); }; //-----------------------------Manga arts onclick actions---------------------------- $('body').on('mouseup', 'div#mangaContainer > img', function(event) { onClickActions(this, event, true); }); //---------------------------------onClickActions------------------------------------ async function onClickActions(imgContainerObj, event, isManga) { event.preventDefault(); let illustId = getImgId(imgContainerObj.src); //----------------------------Middle Mouse Button click---------------------------- if (event.button == 1) { let illustPageUrl = 'https://www.pixiv.net/artworks/' + illustId; window.open(illustPageUrl,'_blank'); //open illust page in new tab(in background — with FF pref "browser.tabs.loadDivertedInBackground" set to "true") } //----------------------------Left Mouse Button clicks...-------------------------- else if (event.button == 0) { //----------------------------Single LMB-click----------------------------------- if (event.shiftKey){ illust_history.delete_record(illustId); //Shift + LMB-click -> delete record from history document.querySelector(`[style*="/${illustId}_"]`).style.opacity = ".25"; } else if (!event.altKey) //need to be this way. Don't change. { let toSave = event.ctrlKey; //Ctrl + LMB-click -> saving image let pageNum = 0; //Single (general url) let ajaxIllustUrl = 'https://www.pixiv.net/ajax/illust/' + illustId; //https://www.pixiv.net/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids= //Manga if (isManga) { let src = imgContainerObj.src; pageNum = src.match(/(?<=\/\d+_p)\d+(?=[_|.])/)[0]; } getOriginalUrl(ajaxIllustUrl, pageNum, toSave); } //-----------------------------Alt + LMB-click----------------------------------- else if (event.altKey){ $(bookmarkContainer).click(); if (!isManga) $(imgContainerObj).parent().css("background", "rgb(255, 64, 96)"); else $(mangaOuterContainer).css("background", "rgb(255, 64, 96)"); } //------------------------------------------------------------------------------- } //--------------------------------------------------------------------------------- } //---------------------------------getOriginalUrl------------------------------------ async function getOriginalUrl(illustPageUrl, pageNum, toSave) { let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open("GET", illustPageUrl, true); xhr.onload = function () { let originalArtUrl = this.response.body.urls.original; //this.response.body.url.big; if (pageNum>0) originalArtUrl = originalArtUrl.replace('p0','p'+pageNum); if (toSave) saveImgByUrl(originalArtUrl); else window.open(originalArtUrl, '_blank'); }; xhr.send(); } //----------------------------------------------------------------------------------- async function saveImgByUrl(sourceUrl) { const filename = sourceUrl.split('/').pop(); const illustId = filename.split('_')[0]; const ext = filename.split('.').pop().toLowerCase(); const GMR = (typeof(GM_xmlhttpRequest)==='function')?GM_xmlhttpRequest:GM.xmlHttpRequest; //Thanx to FlandreKawaii(c) GMR({ method: 'GET', url: sourceUrl, responseType: 'arraybuffer', //TM binary: true, //GM headers: { Referer: `https://www.pixiv.net/en/artworks/${illustId}`, }, onload: function(response) { if (ext === 'jpg' || ext === 'jpeg') saveAs(new File([response.response], filename, { type: 'image/jpeg' })); else if (ext === 'png') saveAs(new File([response.response], filename, { type: 'image/png' })); } }); } //=================================================================================== //**************************************Other**************************************** //=================================================================================== mangaContainer.onwheel = function(e) { if (e.deltaY<0 && (mangaOuterContainer.getBoundingClientRect().top < 0)) { setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "start", behavior: "smooth"}), 0); //aligning to top screen side on scrollUp if needed } else if (e.deltaY>0 && (mangaOuterContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight)) { setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "end", behavior: "smooth"}), 0); //aligning to bottom screen side on scrollDown if needed } let scrlLft = mangaContainer.scrollLeft; if ((currentSettings["DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"]) || ((scrlLft>0 && e.deltaY<0) || ((scrlLft<(mangaContainer.scrollWidth-mangaContainer.clientWidth)) && e.deltaY>0))) { e.preventDefault(); mangaContainer.scrollLeft += e.deltaY*DELTASCALE; } }; //----------------------------------------------------------------------------------- if (currentSettings["SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"]) imgContainer.onwheel = function(e) { if (currentSettings["DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"]) e.preventDefault(); if (e.deltaY<0 && (imgContainer.getBoundingClientRect().top < 0)) { imgContainer.scrollIntoView({block: "start", behavior: "smooth"}); //aligning to top screen side on scrollUp if needed } else if (e.deltaY>0 && (imgContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight)) { imgContainer.scrollIntoView({block: "end", behavior: "smooth"}); //aligning to bottom screen side on scrollDown if needed } }; //----------------------------------------------------------------------------------- window.onresize = function() { mangaWidth = document.body.clientWidth - 60; mangaContainer.style.maxWidth = mangaWidth+'px'; resetPreviewSize(); }; //-------------------------------fix for Chrome panoraming--------------------------- if (navigator.userAgent.indexOf("Chrome") != -1){ hoverImg.onmousedown = function(e){if (e.button == 2) e.preventDefault()}; $('body').on('mousedown', 'div#mangaContainer > img', (e)=>{if (e.button == 2) e.preventDefault()}); } //=================================================================================== //*********************************************************************************** //=================================================================================== }); }) (); //function