pixivイラストページ改善

pixivイラストページのタグに作者マーカーと百科事典アイコン、ユーザー名の列に作品タグを復活させます

Pada tanggal 27 Desember 2018. Lihat %(latest_version_link).

// ==UserScript==
// @name        pixivイラストページ改善
// @description pixivイラストページのタグに作者マーカーと百科事典アイコン、ユーザー名の列に作品タグを復活させます
// @namespace   Aime
// @match       https://www.pixiv.net/*
// @version     1.1.2
// @grant       none
// @run-at      document-end
// @noframes
// @note        2018/06/22 1.0.1 作者アイコンを大サイズに差し替え
// @note        2018/07/18 1.0.2 pixiv側のclass変更に対応
// @note        2018/07/26 1.0.3 アイコン差し替えを修正
// @note        2018/09/23 1.1.0 新プロフィールページのサムネイルをトリミングなしに差し替え。作品タグをapiから取得
// @note        2018/10/03 1.1.1 サムネイル差し替え修正
// @note        2018/12/28 1.1.2 アイコン差し替え修正
// ==/UserScript==
// jshint esversion:6
(() => {
"use strict";
const pixivService = {
	enablePixpediaIcon		: true,		// 百科事典アイコンを付けるか?
	enableTagCloud			: true,		// 作品タグを表示するか?
	tagCloudDisplayCount	: 30,		// 作品タグの表示数(目安)
	tagCloudSortByName		: true,		// 作品タグのソート順 (true:名前順, false:多い順)
	enableReplaceAuthorIcon	: true,		// 作者アイコンを大サイズに差し替える?
	enableReplaceThumbnail	: true,		// サムネイルをトリミングなしに差し替える?

	_existPixpedia	: {},
	_illustTags		: {},
	_currenAuthorId	: -1,
	_tagCloud		: null,
	_delayTimer		: null,

	run: function() {
		const root = document.getElementById("root");
		if (!root)
			return;

		const style = this.$C("style");
		style.textContent = this._style;
		document.querySelector("head").appendChild(style);

		root.addEventListener("click", this, true);

		new MutationObserver(records => {
			let thumbs = [],
				tagModified = false;

			records.forEach(record => {
				if (record.type === "attributes") {
					const target = record.target;
					if (target.classList.contains("_2lyPnMP")) {
						this.replaceAuthorIcon();
					}

					if (this.enableReplaceThumbnail && /1200\.jpg/.test(target.style.backgroundImage)) {
						thumbs.push(target);
					}

				} else {
					record.addedNodes.forEach(node => {
						if (!node.querySelectorAll)
							return;

						// 増えたタグに百科事典アイコンを付ける
						const tags = node.querySelectorAll("a.gtm-new-work-tag-event-click:not([pixpedia]");
						if (tags.length) {
							if (this.enablePixpediaIcon)
								tags.forEach(node => this.insertPixpedia(node));
							tagModified = true;
							return;
						}
						// タグが減ったか変わらない場合は画像が変わったかで判断
						// イラスト:img.sc-hMrMfs img.sc-fjhmcy, うごイラ:div._2UMFAz4, 閲覧注意:div.sc-jkCMRl, 閲覧注意解除:div.sc-erNlkL
						if (node.classList.contains("sc-fjhmcy") || node.classList.contains("_2UMFAz4") || node.classList.contains("sc-bbkauy") || node.classList.contains("sc-jkCMRl")) {
							this.stopDelayTimer();
							this._delayTimer = setTimeout(this.illustChanged.bind(this), 1000);
						}
					});
				}
			});

			if (thumbs.length) {
				this.replaceThumbnail(thumbs);
			}
			if (tagModified) {
				this.authorTags();
			}

		}).observe(root, {
			childList		: true,
			subtree			: true,
			attributes		: true,
			attributeFilter	: [ "style" ]
		});
	},

	stopDelayTimer: function() {
		if (this._delayTimer) {
			clearTimeout(this._delayTimer);
			this._delayTimer = null;
		}
	},
	illustChanged: function() {
		this.stopDelayTimer();
		this.authorTags();
	},

	insertPixpedia: async function(node) {
		if (node.hasAttribute("pixpedia"))
			return;
		node.setAttribute("pixpedia", "true");
		node.addEventListener("mouseover", this, true);
		try {
			const eTag = encodeURIComponent(node.textContent.trim());
			if (!(eTag in this._existPixpedia))
				this._existPixpedia[eTag] = !!await this.fetchJSON("https://www.pixiv.net/ajax/tag/" + eTag + "/info");
			node.parentElement.appendChild(this.$C("a", {
				class:	"pixpedia-icon" + (this._existPixpedia[eTag]? "": " pixpedia-icon-no-item"),
				href:	"https://dic.pixiv.net/a/" + eTag
			}));
		} catch (e) {
			console.error(e);
		}
	},

	handleEvent: function(event) {
		switch (event.type) {
			case "mouseover":
				event.stopPropagation();
				break;
			case "click":
				if (event.target.classList.contains("gm-profile-work-list-tag-filter-click")) {
					this.openTagPage(event);
				}
				break;
		}
	},

	openTagPage: function(event) {
		const url = location.href;
		[
			{ re: /member_illust\.php\?id=(\d+)/,	url: "https://www.pixiv.net/member_tag_all.php?id=" },
			{ re: /novel\/member\.php\?id=(\d+)/,	url: "https://www.pixiv.net/novel/member_tag_all.php?id=" },
		].forEach(p => {
			const match = p.re.exec(url);
			if (match) {
				event.stopPropagation();
				event.preventDefault();
				location.href = p.url + match[1];
				return;
			}
		});
	},

	authorTags: async function() {
		this.replaceAuthorIcon();

		const match = /illust_id=(\d+)/.exec(location.href);
		if (!match)
			return;
		const illustId = match[1];
		let authorId;
		try {
			if (!(illustId in this._illustTags))
				this._illustTags[illustId] = await this.fetchJSON("https://www.pixiv.net/ajax/tags/illust/" + illustId);
			const tagData = this._illustTags[illustId];
			if (!tagData)
				return;
			authorId = tagData.authorId;

			document.querySelectorAll("figcaption footer > ul > li").forEach(elem => {
				let isOwn = false;
				const a = elem.querySelector("a.gtm-new-work-tag-event-click");
				if (a) {
					const tag = a.textContent.trim();
					const find = tagData.tags.find(t => t.tag == tag);
					isOwn = find && find.userId === authorId;
				}
				if (isOwn)
					elem.classList.add("author-tag-marker");
				else
					elem.classList.remove("author-tag-marker");
			});
		} catch (e) {
			console.error(e);
		}

		if (this.enableTagCloud && authorId && (this._currenAuthorId !== authorId || !document.getElementById("author-tags"))) {
			this.authorTagCloud(authorId);
		}
	},

	authorTagCloud: async function(authorId) {
		const aside = document.querySelector("article + aside");
		if (!aside)
			return;

		const tagAllUrl = "https://www.pixiv.net/member_tag_all.php?id=" + authorId;

		if (this._currenAuthorId !== authorId) {
			this._currenAuthorId = authorId;
			try {
				let tags = await this.fetchJSON("https://www.pixiv.net/ajax/user/" + authorId + "/illustmanga/tags");
				tags.sort(this.compareTagByCount);	// 多い順にソート

				const dispCnt = this.tagCloudDisplayCount;
				if (tags.length > dispCnt) {
					// とりあえず目安位置以下の値を破棄
					const lastCnt = tags[dispCnt - 1].cnt;
					tags = tags.filter(v => v.cnt >= lastCnt);
					const tags2 = tags.filter(v => v.cnt > lastCnt);
					// 目安位置と同数とそれより多いのがどちらが目安位置に近いか
					if (dispCnt - tags2.length < tags.length - dispCnt && tags2.length > 5) {
						tags = tags2;
					}
				}

				if (tags.length > 0) {
					let lv = 1,
						cur = tags[0].cnt;
					tags.forEach(tag => {
						// レベル付け
						if (lv < 6 && cur !== tag.cnt) {
							cur = tag.cnt;
							lv++;
						}

						// <li class="level1"><a href="/member_illust.php?id=${authorId}&amp;tag=${tag.tag}">${tag.tag}<span class="cnt">(${tag.cnt})</span></a></li>
						tag.dom = this.$C("li", { class: "level" + lv, "data-cnt": tag.cnt, "data-tag": tag.tag });
						const a = this.$C("a", { href: "/member_illust.php?id=" + authorId + "&tag=" + encodeURIComponent(tag.tag) });
						a.textContent = tag.tag;
						const span = this.$C("span", { class: "cnt" });
						span.textContent = "(" + tag.cnt + ")";
						a.appendChild(span);
						tag.dom.appendChild(a);
					});
				}

				if (this.tagCloudSortByName) {
					tags.sort(this.compareTagByName);
				}

				const tagCloud = this.$C("ul", { class: "tagCloud" });
				tags.forEach(tag => tagCloud.appendChild(tag.dom));
				this._tagCloud = tagCloud;

			} catch (e) {
				console.error(e);
				this._tagCloud = null;
			}
		}

		let container = document.getElementById("author-tags");
		if (container) {
			container.parentElement.removeChild(container);
		}
		if (this._tagCloud) {
			container = this.$C("div", {
				id:		"author-tags",
				class:	"_34Uqb-T"
			});
			aside.insertBefore(container, document.querySelector("._3M6FtEB"));

			const header = this.$C("div", { class: "tags-header" });
			header.innerHTML = `<h2><a href="${tagAllUrl}">作品タグ</a></h2>`;

			const sortBtn = this.$C("button", { class: "sort-button" });
			sortBtn.textContent = "▼";
			sortBtn.addEventListener("click", event => {
				const tags = document.querySelector("#author-tags .tagCloud");
				if (tags) {
					const byName = this.tagCloudSortByName = !this.tagCloudSortByName;
					Array.from(tags.querySelectorAll("li"))
					.map(v => { return { dom: v, cnt: v.dataset.cnt, tag: v.dataset.tag }; })
					.sort(byName? this.compareTagByName: this.compareTagByCount)
					.forEach(v => tags.appendChild(v.dom));
				}
			});
			header.appendChild(sortBtn);

			container.appendChild(header);
			container.appendChild(this._tagCloud);
		}
	},
	compareTagByCount: function(a, b) {
		const r = b.cnt - a.cnt;
		return r? r: pixivService.compareTagByName(a, b);
	},
	compareTagByName: function(a, b) {
		return a.tag.localeCompare(b.tag, {}, { numeric: true });
	},

	replaceAuthorIcon: function() {
		if (!this.enableReplaceAuthorIcon)
			return;

		const icon = document.querySelector("article + aside section ._2lyPnMP");
		if (icon) {
			const img = icon.style.backgroundImage.replace("_50.", "_170.").replace("_s.", ".");
			if (icon.style.backgroundImage !== img) {
				icon.style.backgroundImage = img;
				let p = icon.parentElement;
				if (p.nodeName === "A") {
					p = p.parentElement;
				}
				p.classList.add("icon170");
			}
		}
	},

	replaceThumbnail: function(nodes) {
		if (!nodes || nodes.length <= 0)
			return;
		nodes.forEach(elem => {
			let img = elem.style.backgroundImage;
			let img_r = img.replace(/(?:250x250_80_a2|360x360_70)(.+)_square1200/, "240x240$1_master1200");
			if (/240x240.+_master1200/.test(img_r)) {
				elem.classList.add("non-trim-thumb");
				if (img !== img_r) {
					elem.style.backgroundImage = img_r;
				}
			}
		});
	},

	fetchSameOrigin: function (url) {
		return fetch(url, { mode: "same-origin", credentials: "same-origin" });
	},
	fetchJSON: async function(url) {
		const response = await this.fetchSameOrigin(url);
		const data = await response.json();
		if (data.error)
			throw new Error(data.message);
		return data.body;
	},

	$C: function(tag, attrs) {
		const elem = document.createElement(tag);
		if (attrs) Object.keys(attrs).forEach(key => elem.setAttribute(key, attrs[key]));
		return elem;
	},

	_style: `
#n-overlay {
	position: fixed;
	z-index: 10000;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	background-color: rgba(0, 0, 0, .7);
}
#n-tag-list {
	color: #333;
	background-color: #eee;
	border: 1px solid #999;
	margin: 20px 40px;
	padding: 8px 16px;
	line-height: 1.3;
	overflow-y: auto;
	height: calc(100vh - 90px);
}
#n-tag-list dt {
	width: 8ch;
	clear: left;
	float: left;
	margin: 0;
	padding: 2px 0;
	text-align: right;
	font-weight: bold;
}
#n-tag-list dt::after {
	content: " : ";
	margin-inline-end: 1ch;
}
#n-tag-list dd {
	margin: 0;
	padding: 2px 0;
}
.n-inline-list {
	list-style: none;
	padding: 0;
	margin: 0;
}
.n-inline-list li {
	display: inline-block;
	padding: 0;
	margin: 0;
}
.n-inline-list li:nth-last-of-type(n+2)::after {
	content: " /";
	margin-inline-end: 1ch;
}
.n-inline-list li > a {
	text-decoration: none;
}
/* 百科事典 */
.pixpedia-icon {
	display: inline-block;
	margin-left: 2px;
	width: 15px;
	height: 14px;
	vertical-align: -2px;
	text-decoration: none;
	background: url(https://s.pximg.net/www/images/inline/pixpedia.png) no-repeat;
}
.pixpedia-icon-no-item {
	background: url(https://s.pximg.net/www/images/inline/pixpedia-no-item.png) no-repeat;
}
.pixpedia-icon::before {
	display: none;
}

/* 作者タグ */
.author-tag-marker::before {
	content: "*" !important;
	color: #E66;
}
/* "#"を消す */
figcaption footer > ul > li a.gtm-new-work-tag-event-click::before {
	display: none !important;
}

/* tag cloud */
#author-tags {
	padding: 8px;
	background-color: #FFF;
	border-radius: 8px;
}
#author-tags .tags-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	margin-bottom: 8px;
}
#author-tags h2 {
	color: #333;
	font-size: 14px;
	margin: 0;
}
#author-tags h2 a {
	color: inherit;
	text-decoration: none;
}
#author-tags .sort-button {
	padding: 0;
	font-size: 14px;
	background: none;
	border: none;
	color: inherit;
	cursor: pointer;
}
.tagCloud {
	font-size: 12px;
	line-height: 1.6;
	padding: 0;
	margin: 0;
	word-break: break-all;
}
.tagCloud li {
	display: inline;
	font-size: 12px;
	padding: 0px 2px;
	margin: 0px;
}
.tagCloud li a {
	color: inherit;
	text-decoration: none;
}
.tagCloud li.level1 {
	font-size: 20px;
	font-weight: bold;
}
.tagCloud li.level1 a {
	color: #3E5B71;
}
.tagCloud li.level2 {
	font-size: 18px;
	font-weight: bold;
}
.tagCloud li.level2 a {
	color: #3E5B71;
}
.tagCloud li.level3 {
	font-size: 17px;
	font-weight: bold;
}
.tagCloud li.level3 a {
	color: #587C97;
}
.tagCloud li.level4 {
	font-size: 16px;
	font-weight: bold;
}
.tagCloud li.level4 a {
	color: #587C97;
}
.tagCloud li.level5 {
	font-size: 14px;
	font-weight: bold;
}
.tagCloud li.level5 a {
	color: #587C97;
}
.tagCloud li.level6 a {
	color: #5E9ECE;
}
.tagCloud li a:hover {
	background-color: #3E5B71;
	color: #FFF;
}
.tagCloud li .cnt {
	font-size: 11px;
	font-weight: normal;
	color: #999999;
}

/* 作者アイコンを大きく */
article + aside section {
	margin-top: 0;
}
.icon170 {
	display: block !important;
	text-align: center !important;
}
.icon170 ._2lyPnMP {
	width: 170px !important;
	height: 170px !important;
	margin: 0 auto 4px !important;
	border-radius: 4px !important;
	background-position: center !important;
	background-repeat: no-repeat !important;
	background-size: contain !important;
}

/* トリミングなしサムネイル */
.non-trim-thumb {
	background-size: contain;
}
`
};

pixivService.run();
})();