// ==UserScript== // @name Patchouli // @name:en Patchouli // @name:ja パチュリー // @name:zh-CN 帕秋莉 // @name:zh-TW 帕秋莉 // @namespace https://github.com/FlandreDaisuki // @description An image searching/browsing tool on pixiv // @description:en An image searching/browsing tool on pixiv // @description:ja pixiv 検索機能強化 // @description:zh-CN pixiv 搜寻/浏览 工具 // @description:zh-TW pixiv 搜尋/瀏覽 工具 // @include *://www.pixiv.net/* // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/vue-i18n/7.4.2/vue-i18n.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.1/axios.min.js // @icon http://i.imgur.com/VwoYc5w.png // @noframes // @author FlandreDaisuki // @license The MIT License (MIT) Copyright (c) 2016-2018 FlandreDaisuki // @compatible firefox >=52 // @compatible chrome >=55 // @version 4.0.9 // @grant none // @downloadURL none // ==/UserScript== (function (Vue,Vuex,VueI18n) { 'use strict'; function __$styleInject( css ) { if(!css) return ; if(typeof(window) == 'undefined') return ; let style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); return css; } Vue = Vue && Vue.hasOwnProperty('default') ? Vue['default'] : Vue; Vuex = Vuex && Vuex.hasOwnProperty('default') ? Vuex['default'] : Vuex; VueI18n = VueI18n && VueI18n.hasOwnProperty('default') ? VueI18n['default'] : VueI18n; __$styleInject("._global-header {\n /* FIXME: Chrome will show #koakuma on top of header */\n z-index: 4;\n transform: translate(0);\n}\n._global-header.koakuma-placeholder {\n /* I don't know why #koakuma just 32px\n but it should preserve 42px to keep all spacing correct */\n margin-bottom: 42px;\n}\n.ω {\n display: -webkit-box;\n display: -ms-flexbox;\n display: flex;\n -webkit-box-orient: horizontal;\n -webkit-box-direction: normal;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -webkit-box-pack: center;\n -ms-flex-pack: center;\n justify-content: center;\n}\n.ω,\n.ω .layout-a,\n.ω .layout-body {\n -webkit-transition: width 0.2s;\n transition: width 0.2s;\n}\n.ω.↔,\n.ω.↔ .layout-a,\n.ω.↔ .layout-body {\n width: 100% !important;\n}\n.ω.↔ .layout-a {\n display: -webkit-box;\n display: -ms-flexbox;\n display: flex;\n -webkit-box-orient: horizontal;\n -webkit-box-direction: reverse;\n -ms-flex-direction: row-reverse;\n flex-direction: row-reverse;\n}\n.ω.↔ .layout-column-2 {\n -webkit-box-flex: 1;\n -ms-flex: 1;\n flex: 1;\n margin-left: 20px;\n}\n.ω.↔ .layout-body,\n.ω.↔ .layout-a {\n margin: 10px 20px;\n}\n"); function $(selector) { return document.querySelector(selector); } function $$(selector) { return [...document.querySelectorAll(selector)]; } function $$find(doc, selector) { return [...doc.querySelectorAll(selector)]; } function $el(tag, attr = {}, cb = () => {}) { const el = document.createElement(tag); Object.assign(el, attr); cb(el); return el; } function $error(...args) { console.error.apply(console, args); } (() => { Math.clamp = (val, min, max) => Math.min(Math.max(min, val), max); Number.toInt = (s) => (isNaN(~~s) ? 0 : ~~s); (function(arr) { arr.forEach(function(item) { if (item.hasOwnProperty('after')) { return; } Object.defineProperty(item, 'after', { configurable: true, enumerable: true, writable: true, value: function after() { const argArr = Array.prototype.slice.call(arguments); const docFrag = document.createDocumentFragment(); argArr.forEach(function(argItem) { const isNode = argItem instanceof Node; docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem))); }); this.parentNode.insertBefore(docFrag, this.nextSibling); } }); }); })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); })(); class Pixiv { constructor() { this.tt = $('input[name="tt"]').value; } async fetch(url) { try { if (url) { const res = await axios.get(url); if (res.status !== 200) { throw new Error(`${res.status} ${res.statusText}`); } else { return res.data; } } else { $error('Pixiv#fetch has no url'); } } catch (error) { $error('Pixiv#fetch: error:', error); } } async getLegacyPageHTMLIllustIds(url, { needBookmarkId } = { needBookmarkId: false }) { try { const html = await this.fetch(url); const nextTag = html.match(/class="next"[^/]*/); let nextUrl = ''; if (nextTag) { const nextHref = nextTag[0].match(/href="([^"]+)"/); if (nextHref) { const query = nextHref[1].replace(/&/g, '&'); if (query) { nextUrl = `${location.pathname}${query}`; } } } const iidHTMLs = html.match(/;illust_id=\d+"\s*class="work/g) || []; const illustIds = []; for (const dataid of iidHTMLs) { const iid = dataid.replace(/\D+(\d+).*/, '$1'); if (!illustIds.includes(iid) && iid !== '0') { illustIds.push(iid); } } const ret = { nextUrl, illustIds }; if (needBookmarkId) { ret.bookmarkIds = {}; const bimHTMLs = html.match(/name="book_id[^;]+;illust_id=\d+/g) || []; for (const bim of bimHTMLs) { const [illustId, bookmarkId] = bim.replace(/\D+(\d+)\D+(\d+)/, '$2 $1').split(' '); if (illustIds.includes(illustId)) { ret.bookmarkIds[illustId] = { illustId, bookmarkId }; } } } return ret; } catch (error) { $error('Pixiv#getLegacyPageHTMLIllustIds: error:', error); } } async getPageHTMLIllustIds(url) { try { const html = await this.fetch(url); const nextTag = html.match(/class="next"[^/]*/); let nextUrl = ''; if (nextTag) { const nextHref = nextTag[0].match(/href="([^"]+)"/); if (nextHref) { const query = nextHref[1].replace(/&/g, '&'); if (query) { nextUrl = `${location.pathname}${query}`; } } } const iidHTMLs = html.match(/illustId":"(\d+)"/g) || []; const illustIds = []; for (const dataid of iidHTMLs) { const iid = dataid.replace(/\D+(\d+).*/, '$1'); if (!illustIds.includes(iid) && iid !== '0') { illustIds.push(iid); } } const ret = { nextUrl, illustIds }; return ret; } catch (error) { $error('Pixiv#getPageHTMLIllustIds: error:', error); } } async getBookmarkHTMLDetails(illustIds) { const bookmarkHTMLDetails = illustIds.map(id => this.getBookmarkHTMLDetail(id)); const bookmarkDetails = await Promise.all(bookmarkHTMLDetails); const detail = {}; for (const d of bookmarkDetails) { detail[d.illustId] = d; } return detail; } async getBookmarkHTMLDetail(illustId) { const url = `/bookmark_detail.php?illust_id=${illustId}`; try { const html = await this.fetch(url); const bkMatches = html.match(/<\/i>(\d+)/); const bookmarkCount = bkMatches ? parseInt(bkMatches[1]) : 0; const tagsListHTML = html.match(/