Greasy Fork

来自缓存

Greasy Fork is available in English.

帕秋莉

Pixiv 搜尋/瀏覽 工具

当前为 2017-08-15 提交的版本,查看 最新版本

// ==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.08.15
// @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+
// ==/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.tag = this._tag();
        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';
        }
    }

    _tag() {
        switch (this.lang) {
            case 'ja':
                return 'タグフィルター';
            case 'zh-tw':
                return '標籤過濾器';
            case 'zh':
                return '标签过滤器';
            default:
                return 'Tag filter';
        }
    }

    _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 getBookmarkCountAndTags(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;
            const _b = html.match(/<ul class="tags[^>]+>.*?(?=<\/ul>)/);
            const _c = _b ? _b[0].match(/>[^<]+?(?=<\/a>)/g) : [];
            const tags = _c ? _c.map(x => x.slice(1)) : [];
            return {
                bookmark_count,
                illust_id,
                tags,
            };
        } 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.getBookmarkCountAndTags.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"[^\/]*/);

            let next_url = '';
            if (next_tag) {
                const next_href = next_tag[0].match(/href="([^"]+)"/);
                if (next_href) {
                    const query = next_href[1].replace(/&amp;/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',
        tag: new RegExp('', 'i'),
    },
    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 value = Math.max(0, parseInt(event.target.value));
            global.filters.limit = isNaN(value) ? 0 : value;
        },
        wheel(event) {
            let value;
            if (event.deltaY < 0) {
                value = this.limit + 20;
            } else {
                value = Math.max(0, this.limit - 20);
            }
            global.filters.limit = isNaN(value) ? 0 : value;
        },
    },
    template: `
	<div id="koakuma-bookmark">
		<label for="koakuma-bookmark-input">★{{ l10n.bookmark }}</label>
		<input id="koakuma-bookmark-input"
			type="number" min="0" step="1"
			:value="limit"
			@wheel.stop.prevent="wheel"
			@blur="blur"
			@input="input"/>
	</div>`,
});const koakumaTemplate = `
<div id="こあくま">
	<div>{{ l10n.koakumaProcessed(library.length) }}</div>
	<koakuma-bookmark :l10n="l10n"
		:limit="filters.limit"></koakuma-bookmark>
	<input id="koakuma-tag-input"
			:placeholder="l10n.tag"
			@input="tagUpdate"/>
	<button id="koakuma-switch"
		@click="switchSearching"
		:disabled="isEnded"
		:class="switchStyle">{{ switchText }}</button>
	<div>
		<input id="koakuma-settings-fullwidth" type="checkbox"
			:checked="favorite.fullwidth"
			@click="fullwidthClick"> {{ l10n.koakumaFullwidth }}
		<input id="koakuma-settings-sort" type="checkbox"
			:checked="favorite.sort"
			@click="sortClick"> {{ l10n.koakumaSort }}
	</div>
</div>`;
const koakuma = new Vue({
    data: {
        l10n: global.l10n,
        library: global.library,
        filters: global.filters,
        api: global.api,
        favorite: global.favorite,
        pagetype: global.pagetype,
        next_url: location.href,
        isStoped: true,
        isEnded: false,
        localIdsQueue: [],
        bookmark_ids: {},
    },
    computed: {
        library_iids() {
            return this.library.map(x => x.illust_id);
        },
        switchText() {
            return this.isEnded ? this.l10n.koakumaEnd : this.isStoped ? this.l10n.koakumaGo : this.l10n.koakumaPause;
        },
        switchStyle() {
            return {
                ended: this.isEnded,
                toSearch: !this.isEnded && this.isStoped,
                toStop: !this.isEnded && !this.isStoped,
            };
        },
    },
    methods: {
        async start(times = Infinity) {
            this.isStoped = false;
            const toContinue = () => {
                return !this.isEnded && !this.isStoped && times > 0 && (this.next_url || this.localIdsQueue.length);
            };
            while (toContinue()) {
                // get illust_ids and next_url
                if (this.next_url) {
                    if (this.pagetype.RECOMMEND) {
                        if (this.next_url !== '') {
                            const res = await this.api.getRecommendIllustids();
                            this.next_url = '';
                            this.localIdsQueue.push(...res);
                        }
                    } else {
                        const res = await this.api.getPageIllustids(this.next_url, this.pagetype.MYBOOKMARK);
                        if (res.next_url === this.next_url) {
                            // debounce
                            this.stop();
                            break;
                        }
                        this.next_url = res.next_url;
                        this.localIdsQueue.push(...res.illust_ids);
                        if (this.pagetype.MYBOOKMARK) {
                            Object.assign(this.bookmark_ids, res.bookmark_ids);
                        }
                    }
                }

                //get illust_ids from localIdsQueue
                const process_ids = [];
                while (this.localIdsQueue.length && process_ids.length < 20) {
                    const id = this.localIdsQueue.shift();
                    if (!this.library_iids.includes(id)) {
                        process_ids.push(id);
                    }
                }

                if (process_ids.length) {
                    const ild = await this.api.getIllustsDetail(process_ids);
                    for (let k in ild) {
                        if (ild[k].error) {
                            delete ild[k];
                        }
                    }
                    const iids = Object.values(ild).map(x => x.illust_id);
                    // const ipd = await this.api.getIllustPagesDetail(iids);
                    const bd = await this.api.getBookmarksDetail(iids);

                    const uids = [];
                    for (let d of Object.values(ild)) {
                        if (!uids.includes(d.user_id)) {
                            uids.push(d.user_id);
                        }
                    }
                    const ud = await this.api.getUsersDetail(uids);

                    for (let iid of iids) {
                        const illust = ild[iid];
                        const book = {
                            illust_id: iid,
                            thumb_src: illust.url['240mw'].replace('240x480', '150x150'),
                            user_id: illust.user_id,
                            user_name: illust.user_name,
                            illust_title: illust.illust_title,
                            illust_page_count: parseInt(illust.illust_page_count),
                            is_multiple: illust.is_multiple,
                            is_bookmarked: illust.is_bookmarked,
                            is_manga: illust.illust_type === '1',
                            is_ugoira: !!illust.ugoira_meta,
                            is_follow: ud[illust.user_id].is_follow,
                            bookmark_count: bd[iid].bookmark_count,
                            tags_str: bd[iid].tags.join(' '),
                            // rating_score: ipd[iid].rating_score,
                        };
                        if (this.pagetype.MYBOOKMARK) {
                            book.bookmark_id = this.bookmark_ids[iid];
                            delete this.bookmark_ids[iid];
                        }
                        this.library.push(book);
                    }
                }
                times--;
            }
            // End of while
            if (this.next_url === '') {
                this.stop();
                this.isEnded = this.localIdsQueue.length <= 0;
                if (this.isEnded) {
                    delete this.bookmark_ids;
                    delete this.localIdsQueue;
                }
            }
            if (times <= 0) {
                this.stop();
            }
        },
        stop() {
            this.isStoped = true;
        },
        switchSearching() {
            if (this.isStoped) {
                this.start();
            } else {
                this.stop();
            }
        },
        tagUpdate(event) {
            global.filters.tag = new RegExp(event.target.value, 'i');
        },
        fullwidthClick(event) {
            if (event.target.checked) {
                document.querySelector('#wrapper').classList.add('fullwidth');
                global.favorite.fullwidth = 1;
            } else {
                document.querySelector('#wrapper').classList.remove('fullwidth');
                global.favorite.fullwidth = 0;
            }
            Pixiv.storageSet(global.favorite);
        },
        sortClick(event) {
            if (event.target.checked) {
                global.filters.orderBy = 'bookmark_count';
                global.favorite.sort = 1;
            } else {
                global.filters.orderBy = 'illust_id';
                global.favorite.sort = 0;
            }
            Pixiv.storageSet(global.favorite);
        },
    },
    template: koakumaTemplate,
});
if (!global.pagetype.NOSUP) {
    utils.addStyle(`
	#wrapper.fullwidth,
	#wrapper.fullwidth .layout-a,
	#wrapper.fullwidth .layout-body {
		width: initial;
	}
	#wrapper.fullwidth .layout-a {
		display: flex;
		flex-direction: row-reverse;
	}
	#wrapper.fullwidth .layout-column-2{
		flex: 1;
		margin-left: 20px;
	}
	#wrapper.fullwidth .layout-body,
	#wrapper.fullwidth .layout-a {
		margin: 10px 20px;
	}

	#koakuma-bookmark {
		display: flex;
	}
	#koakuma-bookmark label{
		white-space: nowrap;
		color: #0069b1 !important;
		background-color: #cceeff;
		border-radius: 3px;
		padding: 0 6px;
	}
	#koakuma-bookmark-input::-webkit-inner-spin-button,
	#koakuma-bookmark-input::-webkit-outer-spin-button {
		-webkit-appearance: none;
		margin: 0;
	}
	#koakuma-bookmark-input {
		-moz-appearance: textfield;
		border: none;
		background-color: transparent;
		padding: 0px;
		color: blue;
		font-size: 16px;
		display: inline-block;
		cursor: ns-resize;
		text-align: center;
		min-width: 0;
	}
	#koakuma-bookmark-input:focus {
		cursor: initial;
	}
	#koakuma-tag-input {
		width: 150px;
	}
	#koakuma-tag-input:focus {
		box-shadow: 0 0 1px 1px cornflowerblue;
	}
	#koakuma-switch {
		border: 0;
		padding: 3px 20px;
		border-radius: 3px;
		font-size: 16px;
	}
	#koakuma-switch:hover {
		box-shadow: 1px 1px gray;
	}
	#koakuma-switch:active {
		box-shadow: 1px 1px gray inset;
	}
	#koakuma-switch:focus {
		outline: 0;
	}
	#koakuma-switch.toSearch {
		background-color: lightgreen;
	}
	#koakuma-switch.toStop {
		background-color: lightpink;
	}
	#koakuma-switch.ended {
		background-color: lightgrey;
	}
	#koakuma-switch.ended:hover,
	#koakuma-switch.ended:hover {
		box-shadow: unset;
	}
	#こあくま {
		position: fixed;
		left: 22px;
		bottom: 10px;
		z-index: 1;
		background-color: aliceblue;
		border-radius: 10px;
		padding: 5px;
		font-size: 16px;
		text-align: center;
		max-width: 162px;
		display: flex;
		flex-direction: column;
		align-items: center;
	}
	#こあくま > * {
		margin: 2px 0;
		max-width: 100%;
	}
	#こあくま input::-moz-placeholder {
		color: initial;
	}
	#こあくま *::-moz-selection {
		background-color: initial;
	}`);
}const patchouliImageItemTemplate = `
<li class="image-item">
	<a class="work _work" :href="illust_page_href" :class="thumbStyle">
		<div><img :src="detail.thumb_src"></div>
		<div v-if="detail.is_multiple" class="page-count">
			<div class="icon"></div><span>{{detail.illust_page_count}}</span>
		</div>
	</a>
	<a :href="illust_page_href">
		<h1 class="title" :title="detail.illust_title">{{ detail.illust_title }}</h1>
	</a>
	<span v-if="!pagetype.MEMBERILLIST">
		<a class="user ui-profile-popup"
			:href="user_page_href"
			:title="detail.user_name"
			:data-user_id="detail.user_id">{{ detail.user_name }}</a>
		<i class="fa fa-feed" aria-hidden="true" v-show="detail.is_follow"></i>
	</span>
	<ul class="count-list">
		<li v-if="detail.bookmark_count > 0">
			<a class="bookmark-count _ui-tooltip"
				:href="bookmark_detail_href"
				:data-tooltip="tooltip">
				<i class="_icon sprites-bookmark-badge"></i>{{ detail.bookmark_count }}</a>
		</li>
		<li>
			<input v-if="pagetype.MYBOOKMARK" name="book_id[]" :value="detail.bookmark_id" type="checkbox">
			<a v-else class="is-bookmarked" @click.prevent="bookmarkClick">
				<i class="fa" :class="bookmarkStyle" aria-hidden="true"></i>
			</a>
		</li>
	</ul>
</li>`;
Vue.component('image-item', {
    props: ['api', 'l10n', 'detail', 'pagetype'],
    data() {
        return {
            bookmarked: this.detail.is_bookmarked,
        };
    },
    computed: {
        illust_page_href() {
            return `/member_illust.php?mode=medium&illust_id=${this.detail.illust_id}`;
        },
        bookmark_detail_href() {
            return `/bookmark_detail.php?illust_id=${this.detail.illust_id}`;
        },
        user_page_href() {
            return `/member_illust.php?id=${this.detail.user_id}`;
        },
        thumbStyle() {
            return {
                multiple: this.detail.is_multiple,
                manga: this.detail.is_manga,
                'ugoku-illust': this.detail.is_ugoira,
            };
        },
        bookmarkStyle() {
            return this.bookmarked ? 'fa-bookmark' : 'fa-bookmark-o';
        },
        tooltip() {
            return this.l10n.bookmarkTooltip(this.detail.bookmark_count);
        },
    },
    methods: {
        bookmarkClick(event) {
            if (!this.bookmarked) {
                this.api.postBookmarkadd(this.detail.illust_id);
                this.$emit('bookmarkUpdate', this.detail.illust_id);
                this.bookmarked = true;
            }
        },
    },
    template: patchouliImageItemTemplate,
});const patchouli = new Vue({
    data: {
        api: global.api,
        l10n: global.l10n,
        library: global.library,
        filters: global.filters,
        pagetype: global.pagetype,
    },
    methods: {
        bookmarkUpdate(illust_id) {
            for (let book of this.library) {
                if (book.illust_id === illust_id) {
                    book.is_bookmarked = true;
                }
            }
        },
        sortedBooks(library) {
            const books = Array.from(library);
            const order = this.filters.orderBy;
            const int = parseInt;
            // https://jsperf.com/javascript-sort/
            for (let i = 1; i < books.length; i++) {
                const b = books[i];
                let j = i;
                while (j > 0 && int(books[j - 1][order]) < int(b[order])) {
                    books[j] = books[j - 1];
                    --j;
                }
                books[j] = b;
            }
            return books;
        },
    },
    template: `
	<ul id="パチュリー">
		<image-item v-for="book in sortedBooks(library)"
			v-show="book.bookmark_count >= filters.limit && filters.tag.test(book.tags_str)"
			:key="book.illust_id"
			:api="api"
			:l10n="l10n"
			:detail="book"
			:pagetype="pagetype"></image-item>
	</ul>`,
});
if (!global.pagetype.NOSUP) {
    utils.addStyle(`
	.fa-feed {
		color: dodgerblue;
		cursor: default;
	}
	.fa-feed:hover::after {
		content:'${global.l10n.following}';
		position: absolute;
		color: white;
		white-space: nowrap;
		background-color: dodgerblue;
		padding: 2px;
		border-radius: 3px;
		margin-top: -2px;
		margin-left: 8px;
		font-family: "Helvetica Neue","arial","Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
	}
	.rating-score {
		background-color: #FFEE88;
		color: #FF7700;
		border-radius: 3px;
		display: inline-block !important;
		margin: 0 1px;
		padding: 0 6px !important;
		font: bold 10px/18px "lucida grande", sans-serif !important;
		text-decoration: none;
		cursor: default;
	}
	.is-bookmarked {
		cursor: pointer;
		font-size: 1rem;
	}
	.image-item .count-list {
		display: flex;
		flex-direction: row;
		justify-content: center;
		padding: 0 8px;
	}
	.image-item .count-list li {
		margin: 0 2px !important;
	}
	#パチュリー {
		display: flex;
		flex-wrap: wrap;
		align-items: flex-end;
		justify-content: space-around;
	}`);
}if (!global.pagetype.NOSUP) {
    utils.linkStyle('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
    koakuma.$mount(global.koakumaToMount);
    koakuma.start(1).then(() => {
        patchouli.$mount(global.patchouliToMount);
    });
}
Pixiv.rmAnnoyance();
if (global.pagetype.MYBOOKMARK) {
    // bind select-all and select-none event
    document.querySelectorAll('.select-none, .select-all').forEach(sel => {
        sel.addEventListener('click', (e) => {
            for (let checkbox of [...document.querySelectorAll('input[name="book_id[]"]')]) {
                checkbox.checked = e.target.classList.contains('select-all');
            }
        });
    })
}