Patchouli

An image searching/browsing tool on Pixiv

目前為 2016-06-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.09
// @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()
					.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();
if (Patchouli.supported) {
	setupHTML();
	setupEvent();
	removeJama();
	Patchouli.page = Patchouli.pageGenerator();
	Patchouli.np = Patchouli.page.next().value;
}