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