Patchouli

An image searching/browsing tool on Pixiv

Per 27-06-2016. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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/URI.js/1.18.1/URI.min.js
// @version     2016.06.27
// @author      FlandreDaisuki
// @grant       none
// @noframes
// ==/UserScript==
/* jshint esnext: true */

const baseURI = document.baseURI;

console.log('jQuery version:', $.fn.jquery);
console.log('URI version:', URI.version);
console.log('baseURI:', baseURI);

function getBaseInfo() {
	const retval = {
		supported: true,
		imgitemConf: {
			selector: 'li.image-item',
			hasAuthor: true,
			hasBookmark: false,
			needFillwidth: true,
		},
	};
	retval.$container = $(retval.imgitemConf.selector).parent();

	const pbu = new URI(baseURI);
	const pn = pbu.pathname();
	const ss = URI.parseQuery(pbu.query());

	if (pn === '/member_illust.php' && ss.id) {
		//作品一覧
		retval.imgitemConf.hasAuthor = false;
		retval.imgitemConf.needFillwidth = false;
	} else if (pn === '/search.php') {
		//作品の検索
		retval.imgitemConf.hasBookmark = true;
	} else if (pn === '/bookmark.php' && !ss.type) {
		//ブックマーク
		retval.imgitemConf.hasBookmark = true;
		retval.imgitemConf.needFillwidth = false;
	} 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') {
		//みんなのR-18新着作品
	} else if (pn === '/bookmark_new_illust_r18.php') {
		//フォローユーザーR-18新着イラスト
	} else {
		//Not Support Pages
		retval.supported = false;
	}
	return retval;
}

function removeJama($doc = $(document)) {
	[
		'iframe',
		'aside',
		//Ad
		'.ad',
		'.ads_area',
		'.ad-footer',
		'.ads_anchor',
		'.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();
	});
}

function getThumbPageInfo(tpageURI) {
	// complete URI String
	return fetch(tpageURI, {
			credentials: 'same-origin' // with cookies
		})
		.then(response => response.text())
		.then(html => new DOMParser().parseFromString(html, 'text/html'))
		.then(doc => $(doc))
		.then(($doc) => {
			removeJama($doc);

			const tpage = {};
			tpage.followed = $doc.find('#favorite-button').hasClass('following');
			tpage.tags = $doc.find('li.tag .text').toArray().map(e => e.innerText);
			tpage.type = (function($wd) {
				if ($wd.find('._work.multiple').length > 0) {
					return 'manga';
				} else if ($wd.find('._ugoku-illust-player-container').length > 0) {
					return 'ugoira';
				} else {
					return 'illust';
				}
			})($doc.find('.works_display'));
			tpage.rating = ~~$doc.find('dd.score-count').text();
			return tpage;
		})
		.catch((err) => {
			console.error(err);
		});
}

// Class Thumb
function Thumb(imgitem) {
	const workURI = new URI(imgitem.querySelector('a.work').getAttribute('href'));
	const bk = imgitem.querySelector('.bookmark-count');

	this.elem = imgitem;
	this.elem.removeAttribute('style');
	this.URI = workURI.absoluteTo(baseURI);
	this.workId = URI.parseQuery(this.URI.query()).illust_id;
	this.autherId = Patchouli.conf.hasAuthor ? imgitem.querySelector('.user').dataset.user_id : null;
	this.bookmark = bk ? ~~bk.innerText : 0;
	this.hide = false;
}

//Main Class
const Patchouli = {
	init: function() {
		this.Tbooks = [];
		const BI = getBaseInfo();
		this.conf = BI.imgitemConf;
		this.supported = BI.supported;
		this.$container = BI.$container;
		this.koakuma = {
			ratingStep: 1000,
			bookmarkStep: 50,
			keywords: '',
			rating: 0,
			bookmark: 0,
			autosort: false,
			markfollowed: true,
		};
		this.intervalFetcherId = null;
		console.log('Patchouli', this);
	},

	getPageInfo: function(pageURI) {
		// complete URI String
		console.log(URI.decode(pageURI));
		return new Promise((resolve, reject) => {
				if (pageURI) {
					resolve();
				} else {
					reject('pageURI is null');
				}
			})
			.then(() => {
				return fetch(pageURI.toString(), {
						credentials: 'same-origin' // with cookies
					})
					.then(response => response.text())
					.then(html => new DOMParser().parseFromString(html, 'text/html'))
					.then(doc => $(doc));
			})
			.then(($doc) => {
				removeJama($doc);
				const page = {};

				page.URI = pageURI;
				const next = $doc.find('.next a').attr('href');
				page.nextURI = (next) ? new URI(baseURI).query(next).toString() : null;

				page.thumbs = $doc.find(this.conf.selector)
					.toArray()
					.filter((elem) => elem.querySelector('a.work'))
					.map((elem) => new Thumb(elem))
					.filter((elem) => {
						// Pixiv inner error, like image been deleted
						return elem.workId;
					});
				return page;
			})
			.then((page) => {
				// valid imgitems

				let innerp = page.thumbs.map((thumb) => {
					return Promise.resolve(getThumbPageInfo(thumb.URI))
						.then((tpi)=> {
							for(let k in tpi) {
								thumb[k] = tpi[k];
							}
							return thumb;
						});
				});

				return Promise.all(innerp).then(() => {
					return page;
				});
			})
			.catch((err) => {
				console.error(err);
			});
	},

	pageGenerator: function* () {
		let next = baseURI;
		while(true) {
			next = yield this.getPageInfo(next)
					.then((p)=> {
						const filtered = this.koakumaFilter(p.thumbs);
						
						[].push.apply(this.Tbooks, filtered);

						filtered.forEach((e) => {
							//append elem reference, Do NOT use jQuery
							this.$container[0].appendChild(e.elem);
						});

						$('#Koakuma-processed').text(this.Tbooks.length);
						return p.nextURI;
					}).catch((err) => {
						console.log(err);
					});
		}
	},

	koakumaFilter: function(thumbs) {
		if (this.koakuma.keywords) {
			const kw = this.koakuma.keywords;

			thumbs.forEach(e => {
				e.hide = true;
				for(let t of e.tags) {
					if(kw.indexOf(t) >= 0) {
						e.hide = false;
						break;
					}
				}
			});
		} else {
			thumbs.forEach(e => e.hide = false);
		}
		thumbs.forEach(e => e.hide = e.hide || e.rating < this.koakuma.rating);
		thumbs.forEach(e => e.hide = e.hide || e.bookmark < this.koakuma.bookmark);
		thumbs.forEach(e => {
			if (e.hide) {
				e.elem.classList.add('filter-hidden');
			} else {
				e.elem.classList.remove('filter-hidden');
			}

			e.elem.style.order = (this.koakuma.autosort) ? -e.bookmark : 0;

			if(this.conf.hasAuthor && this.koakuma.markfollowed && e.followed) {
				e.elem.querySelector('a.user').style.color = 'red';
			}
		});
		return thumbs;
	}
};

function setupHTML() {
	$(`
	<div id="Koakuma">
		<div id="Koakuma-processed-box">
			已處理 <strong id="Koakuma-processed">0</strong> 張 
		</div>
		<div id="Koakuma-main-box">
			<div class="keyword">
				<span>標籤過濾:</span>
				<input type="text" id="Koakuma-keyword">
			</div>
			<div class="rating">
				<span>★評分</span>:
				<span id="Koakuma-rating">0</span>
				以上
			</div>
			<div class="bookmark">
				<span>★書籤</span>:
				<span id="Koakuma-bookmark">0</span>
				以上
			</div>
			<div class="func">
				<a href="javascript:;" id="Koakuma-fetcher">找</a>
				<a href="javascript:;" id="Koakuma-stop">停</a>
			</div>
			<div class="config">
				<a href="javascript:;" id="Koakuma-config">⚒更多選項</a>
			</div>
		</div>
		<div id="Koakuma-config-box">
			<div>標示已追蹤 <input type="checkbox" id="Koakuma-config-markfollowed"/></div>
			<div>滿版頁寬 <input type="checkbox" id="Koakuma-config-fillwidth"/></div>
			<div>書籤排序 <input type="checkbox" id="Koakuma-config-autosort"/></div>
		</div>
	</div>`).appendTo('body');

	$(`
	<style>
	/* Koakuma */
	#Koakuma {
		width: 150px;
		position: fixed;
		left: 10px;
		bottom: 10px;
		background-color: #E4E7EE;
		box-shadow: 0px 0px 3px 0px;
		font-size: 16px;
		padding: 10px;
		border-radius: 10px;
		z-index: 10;
	}

	#Koakuma a{
		text-decoration: none;
		display: inline-block;
	}

	#Koakuma-processed-box,
	#Koakuma-rating,
	#Koakuma-bookmark {
		cursor: pointer;
	}
	#Koakuma-processed-box,
	#Koakuma-main-box > div.func {
		text-align: center;
	}

	#Koakuma-main-box > div {
		margin: 3px auto;
	}

	#Koakuma-keyword {
		width: 140px;
	}

	#Koakuma-main-box .rating span:nth-of-type(1){
		color: #F18300;
	}

	#Koakuma-main-box .bookmark span:nth-of-type(1){
		color: #0069b1;
		background-color: #cceeff;
	}

	#Koakuma-main-box > div.func > a {
		padding: 5px 50px;
		border-radius: 5px;
		font-size: 20px;
		color: black;
	}

	#Koakuma-main-box > div.func > a:hover {
		transform: translate(-2px, -2px);
		box-shadow: 1px 1px 1px #533;
	}

	#Koakuma-main-box > div.func > a:active {
		transform: translate(0px, 0px);
		box-shadow: none;
	}

	#Koakuma-stop {
		background-color: rgb(230, 160, 160);
	}

	#Koakuma-stop:hover {
		background-color: rgb(240, 170, 170);
	}

	#Koakuma-stop:active {
		background-color: rgb(220, 150, 150);
	}

	#Koakuma-fetcher {
		background-color: rgb(160, 230, 160);
	}

	#Koakuma-fetcher:hover {
		background-color: rgb(170, 240, 170);
	}

	#Koakuma-fetcher:active {
		background-color: rgb(150, 220, 150);
	}

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

	.Tbooks-container > li.image-item {
		margin: 21px 4px 0;
		padding: 0 8px 0;
	}

	.filter-editing {
		color: red;
		font-size: 18px;
	}

	.filter-hidden {
		display: none;
	}

	/* Pixiv Better */
	#wrapper {
		width: initial;
	}

	.fillwidth {
		width: initial;
	}
	</style>`).appendTo('head');

	Patchouli.$container.children().remove();
	Patchouli.$container.addClass('Tbooks-container');
}

function setupEvent() {
	$('#Koakuma-config-box').hide();
	$('#Koakuma-stop').hide();

	if (!Patchouli.conf.hasAuthor) {
		$('#Koakuma-config-markfollowed').parent().remove();
	} else {
		$('#Koakuma-config-markfollowed').attr('checked', true);
	}

	if (!Patchouli.conf.hasBookmark) {
		$('#Koakuma-config-autosort').parent().remove();
		$('#Koakuma-main-box > .bookmark').remove();
	}

	if (!Patchouli.conf.needFillwidth) {
		$('#Koakuma-config-fillwidth').parent().remove();
	}

	$('#Koakuma-processed-box').on('click', function(event) {
		$('#Koakuma-main-box').toggle(300);
		$('#Koakuma-config-box').hide(300);
	});

	$('#Koakuma-config').on('click', function(event) {
		$('#Koakuma-config-box').toggle(300);
	});

	$('#Koakuma-rating').on('click', function(event) {
		if (!Patchouli.intervalFetcherId) {
			$('#Koakuma-rating').toggleClass('filter-editing');
		}
		$('#Koakuma-bookmark').removeClass('filter-editing');
	});

	$('#Koakuma-bookmark').on('click', function(event) {
		if (!Patchouli.intervalFetcherId) {
			$('#Koakuma-bookmark').toggleClass('filter-editing');
		}
		$('#Koakuma-rating').removeClass('filter-editing');
	});

	$('#Koakuma-stop').on('click', function(event) {
		$('#Koakuma-stop').hide();
		$('#Koakuma-fetcher').show();
		$('#Koakuma-main-box > div:not(.func)').show();

		clearInterval(Patchouli.intervalFetcherId);
		Patchouli.intervalFetcherId = null;
	});

	$('#Koakuma-fetcher').on('click', function(event) {
		$('#Koakuma-fetcher').hide();
		$('#Koakuma-stop').show();
		$('.filter-editing').removeClass('filter-editing');
		$('#Koakuma-main-box > div:not(.func)').hide();
		$('#Koakuma-config-box').hide();

		Patchouli.koakuma.keywords = $('#Koakuma-keyword').val();
		Patchouli.koakumaFilter(Patchouli.Tbooks);
		Patchouli.intervalFetcherId = setInterval(function() {
			Patchouli.np = Patchouli.np.then((n) => Patchouli.page.next(n).value);
		}, 1000);
	});

	$('#Koakuma-config-markfollowed').on('click', function(event) {
		if (event.target.checked) {
			Patchouli.koakuma.markfollowed = true;
			Patchouli.Tbooks.forEach((e) => {
				if (e.followed) {
					e.elem.querySelector('a.user').style.color = 'red';
				}
			});
		} else {
			Patchouli.koakuma.markfollowed = false;
			Patchouli.Tbooks.forEach((e) => {
				e.elem.querySelector('a.user').style.color = '';
			});
		}
	});

	$('#Koakuma-config-autosort').on('click', function(event) {
		if (event.target.checked) {
			Patchouli.koakuma.autosort = true;
		} else {
			Patchouli.koakuma.autosort = false;
		}
	});

	$('#Koakuma-config-fillwidth').on('click', function(event) {
		if (event.target.checked) {
			$('.layout-body').addClass('fillwidth');
		} else {
			$('.layout-body').removeClass('fillwidth');
		}
	});

	$('#Koakuma').on('wheel', function(event) {
		const t = event.originalEvent.deltaY < 0 ? 1 : -1;
		const fe = $('.filter-editing');
		if(fe.length > 0) {
			const feid = fe[0].id;
			if (feid === 'Koakuma-rating') {
				const tt = Patchouli.koakuma.ratingStep * t;
				fe.text(Math.max(0, ~~fe.text() + tt));
				Patchouli.koakuma.rating = ~~fe.text();
			} else if (feid === 'Koakuma-bookmark') {
				const tt = Patchouli.koakuma.bookmarkStep * t;
				fe.text(Math.max(0, ~~fe.text() + tt));
				Patchouli.koakuma.bookmark = ~~fe.text();
			} else {
				console.log(feid);
			}
		} else {
			return;
		}
	});
}

//for debugging
// window.Patchouli = Patchouli;
// window.URI = URI;

// Program Entry Point
Patchouli.init();
removeJama();
if (Patchouli.supported) {
	setupHTML();
	setupEvent();
	Patchouli.page = Patchouli.pageGenerator();
	Patchouli.np = Patchouli.page.next().value;
}