Patchouli

An image searching/browsing tool on Pixiv

Versión del día 27/06/2016. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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;
}