- // ==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, XPathResult.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*1024)).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