Greasy Fork

Patchouli

An image searching/browsing tool on Pixiv

当前为 2017-02-05 提交的版本,查看 最新版本

// ==UserScript==
// @name        Patchouli
// @description An image searching/browsing tool on Pixiv
// @namespace   https://github.com/FlandreDaisuki
// @include     http://www.pixiv.net/*
// @require     https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.25/vue.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/URI.js/1.18.1/URI.min.js
// @version     2017.02.06
// @author      FlandreDaisuki
// @grant       none
// @noframes
// ==/UserScript==
/* jshint esnext: true */

function fetchWithcookie(url) {
    return fetch(url, {credentials: 'same-origin'})
        .then(response => response.text())
        .catch(err => { console.error(err); });
}

function getBookmarkCountAndTags(illust_id) {
    const url = `http://www.pixiv.net/bookmark_detail.php?illust_id=${illust_id}`;
    
    return fetchWithcookie(url)
        .then(html => parseToDOM(html))
        .then(doc => $(doc))
        .then($doc => {
            let m = $doc.find('a.bookmark-count').text();
            const bookmark_count =  m ? parseInt(m) : 0;
            const tags = Array.from($doc.find('ul.tags:first a')).map(x => x.innerText);

            return {
                bookmark_count,
                illust_id,
                tags,
            };
        })
        .catch(err => { console.error(err); });
}

function getBatch(url) {
    return fetchWithcookie(url)
        .then(html => parseToDOM(html))
        .then(doc => $(doc))
        .then($doc => {
            removeAnnoyance($doc);
            const next = $doc.find('.next a').attr('href');
            const nextLink = (next) ? new URI(BASE.baseURI).query(next).toString() : null;
            
            const illust_ids = $doc
                .find('li.image-item > a.work')
                .toArray()
                .map(x => URI.parseQuery($(x).attr('href')).illust_id);

            return {
                nextLink,
                illust_ids,
            };
        })
        .catch(err => { console.error(err); });
}

/**
 * return object which key is illust_id
 */
function getIllustsDetails(illust_ids) {
    const api = `http://www.pixiv.net/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids=${illust_ids.join(',')}&tt=${BASE.tt}`;
    
    return fetchWithcookie(api).then(json => JSON.parse(json).body).catch(err => { console.error(err); });
}

/**
 * return an array
 */
function getUsersDetails(user_ids) {
    const api = `http://www.pixiv.net/rpc/get_profile.php?user_ids=${user_ids.join(',')}&tt=${BASE.tt}`;
    return fetchWithcookie(api).then(json => JSON.parse(json).body).catch(err => { console.error(err); });
}

function parseDataFromBatch(batch) {
    const illust_d = batch.illust_d;
    const user_d = batch.user_d;
    const bookmark_d = batch.bookmark_d;

    return batch.illust_ids
        .filter(x => x)
        .map(x => {
            const iinfo = illust_d[x];
            const uinfo = user_d[iinfo.user_id];
            const binfo = bookmark_d[x];
            const is_ugoira = iinfo.illust_type === '2';
            const is_manga = iinfo.illust_type === '1';
            const src150 = (is_ugoira) ?
                iinfo.url.big.replace(/([^-]+)(?:-original)([^_]+)(?:[^\.]+)(.+)/,'$1-inf$2_s$3') : 
                iinfo.url.m.replace(/600x600/,'150x150');
            
            return {
                is_ugoira,
                is_manga,
                src150,
                srcbig: iinfo.url.big,
                is_multiple: iinfo.is_multiple,
                illust_id: iinfo.illust_id,
                illust_title: iinfo.illust_title,
                user_id: uinfo.user_id,
                user_name: uinfo.user_name,
                is_follow: uinfo.is_follow,
                tags: binfo.tags,
                bookmark_count: binfo.bookmark_count,
            };
        });
}

function parseToDOM(html) {
    return (new DOMParser()).parseFromString(html, 'text/html');
}

function removeAnnoyance($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
        '.ad-printservice',
        '.bookmark-ranges',
        '.require-premium',
        '.showcase-reminder',
        '.sample-user-search',
        '.popular-introduction',
    ].forEach((e) => {
        $doc.find(e).remove();
    });
}

const BASE = (() => {
    const bu = new URI(document.baseURI);
    const pn = bu.pathname();
    const ss = URI.parseQuery(bu.query());
    const baseURI = bu.toString();
    const tt = $('input[name="tt"]').val();
    const container = $('li.image-item').parent()[0];
    const $fullwidthElement = $('#wrapper div:first');

    let supported = true;
    let li_type = 'search';
    /** li_type - the DOM type to show li.image-item
     *
     *  'search'(default) : illust_150 + illust_title + user_name + bookmark_count
     *  'member-illust'   : illust_150 + illust_title +           + bookmark_count
     *  'mybookmark'      : illust_150 + illust_title + user_name + bookmark_count + checkbox + editlink
     */

    if (pn === '/member_illust.php' && ss.id) {
        li_type = 'member-illust';
    } else if (pn === '/search.php') {
    } else if (pn === '/bookmark.php' && !ss.type) {
        if (!ss.id) {
            li_type = 'mybookmark';
        }
    } else if (pn === '/bookmark_new_illust.php') {
    } else if (pn === '/new_illust.php') {
    } else if (pn === '/mypixiv_new_illust.php') {
    } else if (pn === '/new_illust_r18.php') {
    } else if (pn === '/bookmark_new_illust_r18.php') {
    } else {
        supported = false;
    }

    return {
        tt,
        baseURI,
        li_type,
        supported,
        container,
        $fullwidthElement,
    };
})();



Vue.filter('illust_href', function(illust_id) {
    return 'http://www.pixiv.net/member_illust.php?mode=medium&illust_id='+illust_id;
});

Vue.component('img150', {
    props:['thd'],
    template: `<a class="work _work"
                 :href="thd.illust_id | illust_href"
                 :class="{
                    'ugoku-illust': thd.is_ugoira,
                    'manga': thd.is_manga,
                    'multiple': thd.is_multiple}">
                    <div class="_layout-thumbnail"><img class="_thumbnail" :src="thd.src150"></img></div></a>`,

});

Vue.component('illust-title', {
    props:['thd'],
    template: '<a :href="thd.illust_id | illust_href"><h1 class="title" :title="thd.illust_title">{{thd.illust_title}}</h1></a>',
});

Vue.component('user-name', {
    props:['thd'],
    template: `<a class="user ui-profile-popup"
                 :href="thd.user_id | user_href"
                 :title="thd.user_name"
                 :data-user_id="thd.user_id"
                 :data-user_name="thd.user_name"
                 :class="{'followed': thd.is_follow}"> {{thd.user_name}}</a>`,
    filters: {
        user_href: function(user_id) {
            return 'http://www.pixiv.net/member_illust.php?id='+user_id;
        },
    },
});

Vue.component('count-list', {
    props:['thd'],
    template: `<ul class="count-list">
                    <li v-if="thd.bookmark_count > 0">
                        <a class="bookmark-count _ui-tooltip"
                          :href="thd.illust_id | bookmark_detail_href"
                          :data-tooltip="thd.bookmark_count | datatooltip">
                            <i class="_icon sprites-bookmark-badge"></i>{{thd.bookmark_count}}</a></li></ul>`,
    filters: {
        datatooltip: function(bookmark_count) {
            return bookmark_count+'件のブックマーク';
        },
        bookmark_detail_href: function(illust_id) {
            return 'http://www.pixiv.net/bookmark_detail.php?illust_id='+illust_id;
        },
    },
});

Vue.component('edit-link', {
    props:['thd'],
    template: '<a :href="thd.illust_id | edit_href" class="edit-work"><span class="edit-bookmark edit_link">編集</span></a>',
    filters: {
        edit_href: function(illust_id) {
            return `http://www.pixiv.net/bookmark_add.php?type=illust&illust_id=${ illust_id }&tag=&rest=show&p=1`;
        },
    },
});

Vue.component('imageitem-search', {
    props:['thdata'],
    template: `<li class="image-item">
                   <img150 :thd="thdata"></img150>
                   <illust-title :thd="thdata"></illust-title>
                   <user-name :thd="thdata"></user-name>
                   <count-list :thd="thdata"></count-list>
               </li>`,
});

Vue.component('imageitem-member-illust', {
    props:['thdata'],
    template: `<li class="image-item">
                    <img150 :thd="thdata"></img150>
                    <illust-title :thd="thdata"></illust-title>
                    <count-list :thd="thdata"></count-list>
               </li>`,
});

Vue.component('imageitem-mybookmark', {
    props:['thdata'],
    template: `<li class="image-item">
                    <bookmark-checkbox v-if="false" :thd="thdata"></bookmark-checkbox>
                    <img150 :thd="thdata"></img150>
                    <illust-title :thd="thdata"></illust-title>
                    <user-name :thd="thdata"></user-name>
                    <count-list :thd="thdata"></count-list>
                    <edit-link :thd="thdata"></edit-link>
               </li>`,
});

function setupHTML() {
    $(`
    <div id="Koa-controller" class="tachi">
        <div id="Koa-controller-child">
            <div id="Koa-found"><span id="Koa-found-value">0</span></div>
            <div id="Koa-bookmark">
                <span>★書籤</span>:
                <input id="Koa-bookmark-input" type="number" min="0" step="1" value="0" required/>
            </div>
            <div id="Koa-btn">
                <input id="Koa-btn-input" type="button" value="找"/>
            </div>
            <div id="Koa-options">
                全<input id="Koa-fullwidth-input" type="checkbox"/>
                排序<input id="Koa-ordering-input" type="checkbox"/>
            </div>
        </div>
    </div>`).appendTo('body');

    $(`
    <style>
    #Koa-controller.tachi {
        background-color: #8F2019;
        position: fixed;
        bottom: 0px;
        left: 0px;
        margin: 10px 30px;
        padding: 15px 8px 0px;
        border-radius: 15px 15px 8px 8px;
        font-size: 16px;
        cursor: default;
        z-index: 10;
    }

    #Koa-controller::before {
        content: '';
        position: absolute;
        width: 0;
        height: 0;
        border-style: solid;
        border-width: 30px 0 0 100px;
        border-color: transparent transparent transparent #000000;
        left: 0px;
        top: 0px;
        z-index: -1;
        transform-origin: 50% 50%;
        transform: translate(-45px,40px) skew(20deg, 0deg) rotate(-20deg);
    }

    #Koa-controller::after {
        border-width: 0 0 30px 100px;
        border-color: transparent transparent #000000 transparent;
        content: '';
        position: absolute;
        width: 0;
        height: 0;
        border-style: solid;
        right: 0px;
        top: 0px;
        z-index: -1;
        transform-origin: 50% 50%;
        transform: translate(45px,40px) skew(-20deg, 0deg) rotate(20deg);
    }

    #Koa-controller.chibi {
        width: 60px;
        background-color: #8F2019;
        position: fixed;
        bottom: 0px;
        left: 0px;
        margin: 10px 30px;
        padding: 10px 4px 15px;
        border-radius: 50%;
        font-size: 16px;
        cursor: default;
        z-index: 10;
        color: white;
    }
    
    #Koa-controller.chibi::before {
        transform: translate(-45px,10px) skew(20deg, 0deg) rotate(-10deg) scale(0.4);
    }

    #Koa-controller.chibi::after {
        transform: translate(45px,10px) skew(-20deg, 0deg) rotate(10deg) scale(0.4);
    }
    
    #Koa-controller.tachi #Koa-controller-child {
        background-color: #FEE6CA;
        border-bottom: 30px solid black;
        border-radius: 10px 10px 20px 20px;
    }

    #Koa-controller.chibi #Koa-controller-child {
        background-color: #8F2019;
        width: 100%;
        height: 30px;
        border-radius: 50%;
        margin-top: -2px;
    }

    #Koa-controller.chibi #Koa-controller-child::after {
        content: " ' ' ";
        display: block;
        text-align: center;
        font-size: 32px;
        padding-right: 5px;
        transform: rotate(10deg);
    }
    
    #Koa-controller.chibi #Koa-controller-child > div {
        display: none;
    }

    #Koa-found,
    #Koa-btn {
        text-align: center;
    }

    #Koa-controller.tachi #Koa-btn {
        border-left: 15px solid white;
        border-right: 15px solid white;
        background-color: black;
        color: white;
    }

    #Koa-controller.tachi #Koa-options {
        border-left: 15px solid white;
        border-right: 15px solid white;
        background-color: black;
        color: white;
    }

    #Koa-bookmark > span {
        color: #0069b1;
        background-color: #cceeff;
    }

    input#Koa-bookmark-input::-webkit-inner-spin-button, 
    input#Koa-bookmark-input::-webkit-outer-spin-button {
        -webkit-appearance: none;
        margin: 0;
    }

    input#Koa-bookmark-input {
        -moz-appearance: textfield;
    }

    input#Koa-bookmark-input {
        border: none;
        background-color: transparent;
        padding: 0px;
        color: blue;
        font-size: 16px;
        display: inline-block;
        width: 50px;
        cursor: ns-resize;
        text-align: center;
    }

    input#Koa-bookmark-input:focus {
        cursor: initial;
    }

    #Koa-controller.tachi #Koa-btn-input {
        border: none;
        background-color: #FFFF00;
        height: 100%;
        margin: 0px auto;
        display: block;
        border-radius: 0% 0% 50% 50%;
        font-size: 22px;
    }

    #Koa-container {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-around;
        margin-left: initial;
    }

    .fullwidth {
        width: initial;
    }

    .layout-a.fullwidth {
        display: flex;
        flex-direction: row-reverse;
    }

    .layout-a.fullwidth .layout-column-2{
        flex: 1;
        margin-left: 20px;
    }

    .layout-body.fullwidth,
    .layout-a.fullwidth {
        margin: 10px 20px;
    }
    .followed.followed.followed {
        font-weight: bold;
        color: red;
    }
    </style>`).appendTo('head');
}

function setupEvent() {

    const $KoaController = $('#Koa-controller');
    const $KoaBookmarkInput = $('#Koa-bookmark-input');
    const $KoaBtnInput = $('#Koa-btn-input');
    const $KoaFullwidthInput = $('#Koa-fullwidth-input');
    const $KoaOrderingInput = $('#Koa-ordering-input');

    $KoaController
        .on('click', function() {
            if($(this).hasClass('chibi')){
                $(this).removeClass('chibi');
                $(this).addClass('tachi');
            }
        })
        .on('mouseleave', function() {
            $(this).addClass('chibi');
            $(this).removeClass('tachi');
            $KoaBookmarkInput.focusout();
        });


    $KoaBookmarkInput
        .on('wheel', function(event) {
            this.blur();
            if(event.originalEvent.deltaY > 0) {
                this.stepDown(20);
            } else {
                this.stepUp(20);
            }
            MuQ.Koakuma.$data.limit = parseInt(this.value);
            return false;
        })
        .on('focusout', function(event) {
            this.blur();
            if(!this.validity.valid){
                console.log(this.validationMessage);
            } else {
                MuQ.Koakuma.$data.limit = parseInt(this.value);
            }
        });

    $KoaBtnInput.click(function(event) {
        if(!$KoaBookmarkInput[0].validity.valid){
            console.log($KoaBookmarkInput[0].validationMessage);
        } else {
            if (MuQ.intervalID) {
                clearInterval(MuQ.intervalID);
                MuQ.intervalID = null;
                this.value = '找';
                $KoaController.removeClass('working');
            } else {
                MuQ.intervalID = setInterval(function(){
                    MuQ.nextPage();
                }, 1000);
                this.value = '停';
                $KoaController.addClass('working');
            }
        }
        return false;
    });

    $KoaFullwidthInput.click(function(event){
        const node = BASE.$fullwidthElement;
        if (this.checked) {
            node.addClass('fullwidth');
        } else {
            node.removeClass('fullwidth');
        }
    });

    $KoaOrderingInput.click(function(event){
        const order_t = this.checked ? 'bookmark_count' : '';
        MuQ.Koakuma.$data.order_t = order_t;
    });
}

const MuQ = {
    nextLink: location.href,
    intervalID: null,
    init: function() {
        if(BASE.supported){
            if(BASE.container) {
                BASE.container.id = 'Koa-container';
            }
            $('#wrapper').width('initial');
            setupHTML();
            setupEvent();

            this.Koakuma.$watch('thumbs', function(newVal, oldVal) {
                $('#Koa-found-value').text(newVal.length);
            });

            this.page = this.np_gen();
            this.np = this.page.next().value.then(v => {
                this.Koakuma.$mount('#Koa-container');
                return v;
            });
        }
    },
    nextPage: function() {
        this.np = this.np
            .then(n => this.page.next(n.nextLink).value )
            .catch(err => {
                console.error(err);
                clearInterval(this.intervalID);
                this.intervalID = null;
                $('#Koa-btn-input').attr('disabled', true).val('完');
            });
    },
    np_gen: function* () {
        while(this.nextLink) {
            this.nextLink = yield getBatch(this.nextLink)
                .then(bat => {
                    return getIllustsDetails(bat.illust_ids)
                        .then(illust_d => {
                            bat.illust_d = illust_d;
                            return bat;
                        });
                })
                .then(bat => {
                    return getUsersDetails(Object.keys(bat.illust_d)
                        .map((k) => bat.illust_d[k].user_id))
                        .then(user_d => {
                            bat.user_d = {};
                            user_d.forEach(x => bat.user_d[x.user_id] = x);
                            return bat;
                        });
                })
                .then(bat => {
                    return Promise.all(Object.keys(bat.illust_d)
                        .map((k) => bat.illust_d[k])
                        .map(x => getBookmarkCountAndTags(x.illust_id)))
                        .then(bookmark_d => {
                            bat.bookmark_d = {};
                            bookmark_d.forEach(x => bat.bookmark_d[x.illust_id] = x);
                            return bat;
                        });
                })
                .then(bat => {
                    this.Koakuma.$data.thumbs.push(...parseDataFromBatch(bat));
                    return bat;
                }).catch(err => {
                    console.error(err);
                });
        }
    },
    Koakuma: new Vue({
        template: `<ul><component :is="li_type" v-for="th in thumbs | bookmark_gt limit | orderBy order_t -1" :thdata="th"></component></ul>`,
        data: {
            thumbs: [],
            order_t: '',
            limit: 0,
        },
        computed: {
            li_type: function() {
                return 'imageitem-' + BASE.li_type;
            },
        },
        filters: {
            bookmark_gt: function(data, limit) {
                return data.filter(x => x.bookmark_count >= limit);
            },
        },
    }),
};

removeAnnoyance();
MuQ.init();

//Debugging
window.fetchWithcookie = fetchWithcookie;
window.getBookmarkCountAndTags = getBookmarkCountAndTags;
window.getBatch = getBatch;
window.getIllustsDetails = getIllustsDetails;
window.getUsersDetails = getUsersDetails;
window.parseToDOM = parseToDOM;
window.parseDataFromBatch = parseDataFromBatch;
window.removeAnnoyance = removeAnnoyance;
window.BASE = BASE;
window.MuQ = MuQ;
window.Vue = Vue;
window.URI = URI;