// ==UserScript== // @name Patchouli // @name:ja パチュリー // @name:zh-TW 帕秋莉 // @name:zh-CN 帕秋莉 // @description An image searching/browsing tool on Pixiv // @description:ja Pixiv 検索機能強化 // @description:zh-TW Pixiv 搜尋/瀏覽 工具 // @description:zh-CN Pixiv 搜尋/瀏覽 工具 // @namespace https://github.com/FlandreDaisuki // @include *://www.pixiv.net/* // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js // @version 2017.06.03 // @icon http://i.imgur.com/VwoYc5w.png // @grant none // @noframes // meta keys for Greasy Fork // @author FlandreDaisuki // @license The MIT License (MIT) Copyright (c) 2016-2017 FlandreDaisuki // @compatible firefox 52+ // @compatible chrome 55+ // @downloadURL none // ==/UserScript== 'use strict'; console.log(`[${GM_info.script.name}] version: ${GM_info.script.version}`); class L10N { constructor() { this.lang = document.documentElement.lang; this.following = this._following(); this.bookmark = this._bookmark(); this.koakumaGo = this._koakumaGo(); this.koakumaPause = this._koakumaPause(); this.koakumaEnd = this._koakumaEnd(); this.koakumaFullwidth = this._koakumaFullwidth(); this.koakumaSort = this._koakumaSort(); } _following() { switch (this.lang) { case 'ja': return 'フォロー中'; case 'zh-tw': return '關注中'; case 'zh': return '关注中'; default: return 'following'; } } _bookmark() { switch (this.lang) { case 'ja': return 'ブックマーク'; case 'zh-tw': case 'zh': return '收藏'; default: return 'Bookmark'; } } _koakumaGo() { switch (this.lang) { case 'ja': return '捜す'; case 'zh-tw': case 'zh': return '找'; default: return 'Go'; } } _koakumaPause() { switch (this.lang) { case 'ja': return '中断'; case 'zh-tw': case 'zh': return '停'; default: return 'Pause'; } } _koakumaEnd() { switch (this.lang) { case 'ja': return '終了'; case 'zh-tw': case 'zh': return '完'; default: return 'End'; } } koakumaProcessed(n) { switch (this.lang) { case 'ja': return `${n} 件が処理された`; case 'zh-tw': return `已處理 ${n} 張`; case 'zh': return `已处理 ${n} 张`; default: return `${n} pics processed`; } } _koakumaFullwidth() { switch (this.lang) { case 'ja': return '全幅'; case 'zh-tw': return '全寬'; case 'zh': return '全宽'; default: return 'fullwidth'; } } _koakumaSort() { switch (this.lang) { case 'ja': return 'ソート'; case 'zh-tw': case 'zh': return '排序'; default: return 'sorted'; } } bookmarkTooltip(n) { switch (this.lang) { case 'ja': return `${n}件のブックマーク`; case 'zh-tw': return `${n}個收藏`; case 'zh': return `${n}个收藏`; default: return `${n} bookmarks`; } } } class PageType { constructor() { const path = location.pathname; const search = new URLSearchParams(location.search); const hasid = search.has('id'); this.DEFAULT = false; this.RECOMMEND = false; this.MEMBERILLIST = false; this.MYBOOKMARK = false; this.NOSUP = false; switch (path) { case '/search.php': case '/bookmark_new_illust.php': case '/new_illust.php': case '/mypixiv_new_illust.php': case '/new_illust_r18.php': case '/bookmark_new_illust_r18.php': this.DEFAULT = true; break; case '/recommended.php': this.RECOMMEND = true; break; case '/member_illust.php': this.MEMBERILLIST = hasid; this.NOSUP = !hasid; break; case '/bookmark.php': const t = search.get('type'); if (hasid) { this.DEFAULT = true; } else if (!t || t === 'illust_all') { this.MYBOOKMARK = true; } else { // e.g. http://www.pixiv.net/bookmark.php?type=reg_user this.NOSUP = true; } break; default: this.NOSUP = true; } } } class Pixiv { constructor() { this.tt = document.querySelector('input[name="tt"]').value; } static storageGet() { const storage = localStorage.getItem('むきゅー'); if (!storage || storage.version < GM_info.script.version) { Pixiv.storageSet({ version: GM_info.script.version }); } return JSON.parse(localStorage.getItem('むきゅー')); } static storageSet(obj) { localStorage.setItem('むきゅー', JSON.stringify(obj)); } static rmAnnoyance(doc = document) { [ 'iframe', //Ad '.ad', '.ads_area', '.ad-footer', '.ads_anchor', '.ads-top-info', '.comic-hot-works', '.user-ad-container', '.ads_area_no_margin', //Premium '.hover-item', '.ad-printservice', '.bookmark-ranges', '.require-premium', '.showcase-reminder', '.sample-user-search', '.popular-introduction', ].forEach(cl => [...doc.querySelectorAll(cl)].forEach(el => el.remove())); } async fetch(url) { try { if (url) { const res = await axios.get(url); if (res.statusText !== 'OK') { throw res; } else { return res.data; } } else { console.trace('Fetch has no url'); } } catch (e) { console.error(e); } } async getDetail(illust_ids, f) { const iids = []; for (let iid of illust_ids) { iids.push(f(iid)); } const processed = await Promise.all(iids); const ret = {}; for (let p of processed) { ret[p.illust_id] = p; } return ret; } async getBookmarkCount(illust_id) { const url = `/bookmark_detail.php?illust_id=${illust_id}`; try { const html = await this.fetch(url); const _a = html.match(/sprites-bookmark-badge[^\d]+(\d+)/); const bookmark_count = _a ? parseInt(_a[1]) : 0; return { bookmark_count, illust_id, }; } catch (e) { console.error(e); } } /** * Returns detail object that illust_id: detail object by DOM * * { '12345': {}, '12346': {}, ... } * @param {String[]} illust_ids * @return {{String: Object}} */ async getBookmarksDetail(illust_ids) { const _f = this.getBookmarkCount.bind(this); return await this.getDetail(illust_ids, _f); } async getIllustPageDetail(illust_id) { const url = `/member_illust.php?mode=medium&illust_id=${illust_id}`; try { const html = await this.fetch(url); const _a = html.match(/rated-count[^\d]+(\d+)/); const rating_score = _a ? parseInt(_a[1]) : 0; return { illust_id, rating_score, }; } catch (e) { console.error(e); } } /** * Returns detail object that illust_id: detail object by DOM * * { '12345': {}, '12346': {}, ... } * @param {String[]} illust_ids * @return {{String: Object}} */ async getIllustPagesDetail(illust_ids) { const _f = this.getIllustPageDetail.bind(this); return await this.getDetail(illust_ids, _f); } /** * Returns detail object that illust_id: detail object by Pixiv API * * { '12345': {}, '12346': {}, ... } * @param {String[]} illust_ids * @return {{String: Object}} */ getIllustsDetail(illust_ids) { const iids = illust_ids.join(','); const url = `/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids=${iids}&tt=${this.tt}`; return this.fetch(url) .then(json => json.body) .catch(console.error); } /** * Returns detail object that user_id: detail object by Pixiv API * * { '12345': {}, '12346': {}, ... } * @param {String[]} user_ids * @return {{String: Object}} */ getUsersDetail(user_ids) { const uids = user_ids.join(','); const url = `/rpc/get_profile.php?user_ids=${uids}&tt=${this.tt}`; return this.fetch(url) .then(json => { let ret = {}; for (let u of json.body) { ret[u.user_id] = u; } return ret; }) .catch(console.error); } postBookmarkadd(illust_id) { const data = [ 'mode=save_illust_bookmark', `illust_id=${illust_id}`, 'restrict=0', 'comment=', 'tags=', `tt=${this.tt}`, ].join('&'); const config = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }; return axios.post('/rpc/index.php', data, config) .then(res => { return new Promise((resolve, reject) => { (res.statusText === 'OK' && !res.data.error) ? resolve(true): reject(res); }); }) .catch(console.error); } /** * Returns array of recommend illust_id * @return {String[]} */ async getRecommendIllustids(illust_id = 'auto') { const param = [ 'type=illust', `sample_illusts=${illust_id}`, 'num_recommendations=500', `tt=${this.tt}`, ].join('&'); const url = `/rpc/recommender.php?${param}`; try { return await this.fetch(url).then(data => data.recommendations.map(x => `${x}`)); } catch (e) { console.error(e); } } /** * Returns array of recommend illust_id * @param {String} url * @return {{next_url: String, illust_ids: String[]}} */ async getPageIllustids(url, needBookId) { try { const html = await this.fetch(url); const next_tag = html.match(/class="next".+(?=<\/span>)/); let next_url = ''; if (next_tag) { const next_href = next_tag[0].match(/href="([^"]+)"/); if (next_href) { const query = next_href[1].replace(/&/g, '&'); if (query) { next_url = `${location.pathname}${query}`; } } } const iidHTMLs = html.match(/data-id="\d+"/g) || []; const illust_ids = []; for (let dataid of iidHTMLs) { const iid = dataid.replace(/\D+(\d+).*/, '$1'); if (!illust_ids.includes(iid) && iid !== '0') { illust_ids.push(iid); } } const ret = { next_url, illust_ids, }; if (needBookId) { const bookId = {}; const bimHTMLs = html.match(/name="book_id[^;]+;illust_id=\d+/g) || []; for (let bim of bimHTMLs) { const [iid, bid] = bim.replace(/\D+(\d+)\D+(\d+)/, '$2 $1').split(' '); if (illust_ids.includes(iid)) { bookId[iid] = bid; } } ret.bookmark_ids = bookId; } return ret; } catch (e) { console.error(e); } } } const utils = { linkStyle(url) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = url; document.head.appendChild(link); }, linkScript(url) { const script = document.createElement('script'); script.src = url; document.head.appendChild(script); }, createIcon(name, options = {}) { const el = document.createElement('i'); el.classList.add('fa'); el.classList.add(`fa-${name}`); el.setAttribute('aria-hidden', 'true'); return el; }, addStyle(text, id = '') { const style = document.createElement('style'); style.innerHTML = text; if (id) { style.id = id; } document.head.appendChild(style); }, }; const global = { api: new Pixiv(), l10n: new L10N(), pagetype: new PageType(), library: [], filters: { limit: 0, orderBy: 'illust_id', }, favorite: { fullwidth: 1, sort: 0, }, patchouliToMount: (() => { const _a = document.querySelector('li.image-item'); const _b = document.querySelector('ul._image-items'); return _a ? _a.parentElement : _b; })(), koakumaToMount: (() => { return document.querySelector('#toolbar-items'); })(), }; global.favorite = (() => { const _s = Object.assign(global.favorite, Pixiv.storageGet()); if (_s.fullwidth) { document.querySelector('#wrapper').classList.add('fullwidth'); } if (_s.sort) { global.filters.orderBy = 'bookmark_count'; } Pixiv.storageSet(_s); return _s; })(); Vue.component('koakuma-bookmark', { props: ['limit', 'l10n'], methods: { blur(event) { const self = event.target; if (!self.validity.valid) { console.error('koakuma-bookmark', self.validationMessage); } }, input(event) { let val = parseInt(event.target.value); val = Math.max(0, val); this.$emit('limitUpdate', val); }, wheel(event) { let val; if (event.deltaY < 0) { val = this.limit + 20; } else { val = Math.max(0, this.limit - 20); } this.$emit('limitUpdate', val); }, }, template: `