Greasy Fork is available in English.
Restores the tag cloud (illustration or novel tags column), and if there are tags attached to a work, this script brings those tags to the top of the tag cloud (illustration tags column).
当前为
// ==UserScript==
// @name pixiv タグクラウドからピックアップ
// @name:ja pixiv タグクラウドからピックアップ
// @name:en pixiv Tag Cloud Prioritizer
// @description Restores the tag cloud (illustration or novel tags column), and if there are tags attached to a work, this script brings those tags to the top of the tag cloud (illustration tags column).
// @description:ja 作品ページへタグクラウド (作品タグ・小説タグ) を復活させ、閲覧中の作品についているタグと同じものをピックアップします。
// @namespace https://userscripts.org/users/347021
// @version 2.9.0
// @match https://www.pixiv.net/*
// @exclude https://www.pixiv.net/apps.php*
// @require https://gitcdn.xyz/cdn/greasemonkey/gm4-polyfill/a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.js
// @require http://greasyfork.icu/scripts/17895/code/polyfill.js?version=625392
// @require http://greasyfork.icu/scripts/19616/code/utilities.js?version=230651
// @require http://greasyfork.icu/scripts/17896/code/start-script.js?version=112958
// @license MPL-2.0
// @compatible Edge 非推奨 / Deprecated
// @compatible Firefox
// @compatible Opera
// @compatible Chrome
// @grant GM.setValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM_getValue
// @grant GM.deleteValue
// @grant GM_deleteValue
// @grant GM.listValues
// @grant GM_listValues
// @noframes
// @run-at document-start
// @icon data:image/vnd.microsoft.icon;base64,AAABAAEAMDAAAAEAIADnDAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAwAAAAMAgGAAAAVwL5hwAADK5JREFUaIHFmXuwVdV9xz9rrf0459z3hXsFUUAhIBQBUTQVJT5iCKbR2Gga22qatnl1apM2M4ZmGitpp7F5TB7TmiY6JpOZmDEmxhjUpEZrjVoeopjyEigI8r4P7vOcvfd69Y997rn3wgUutsDvzDp7zzlrr/V7/77rtwXAih9t4L7bF/Klxzfytx+YB7w3+uZj904wVk/YuKd7nhRCc4ZIgHfeq/cuPG/dL9fvqjx0962HAOb//fPityuv8UJIvHfD8z/14Bq+/edXsPwLj0ZP/8Nt2S0rH33XjYsvuLu9teXGaZNbQSqEEGeKf/D5V5qmbN6130mrv37n577zIK8/sPX3vvgzteqeW+xRAsN1K34UPnff7foP73v8nz66bMHd9U3NCiG8ybQXQuRrnlFyCCRhHMusUmbfwY4D//z9Jz7+2vc/vypuniSmLvsLv/2Re3IBPv3Qy3zzT6/kPSt++NnPfXjJV01QtNJbhcngLLA+mgREJQSWXbv39a782neX7332u+sBjZAe73IL8K7Pz/jJP96xNigUm1RYUCS9gDyrrA+TwxdbKZHw5AuvPPf1u2758Owb7ux745kfpAABwMqPLfujJDOtYakIvZ3knmVPtOqZJXOESlRnFs59x3WFqZddMj04vG5b25zMd2zxAZd9sqFvMLnZq4BypQJJBlKBP9vuUyUhAINzoayPI+Zff9MnnvnePRumLHp3uLdjSxYsmDOpBH5mWZSw5UHQjtz3T0UAAXi8B+89rvqor64hqp4qZX53akktn+wVAhxTJk+ZuxbqfdJXBrKgXvnYexrLmcVWEtB63DtIIXDOkRqLFILGUkxDKaalLkZKQRwGCAGDSYb30NFbppIa+pOMQEqiQGG9z3V1ki19kAlrtSuWSq1Ag0kGewCCtFKWqTaUU41NNFgzLsZzxjRtjSUWzWyjpb7A9PYm4lARBYowkIRK4YFKqvFAqg1JZti+/wg7D/awfV839cUI5/3JPVYZjNNoYwVQ1GkSAARZlohUWyqpwWkD5sQCeO9RUtJUF7P8qhnMmNxCMQrwgLEO5zzGOXYf6KW3nBErxfRJTUSBohiH1BciJjaVWDzrXPYc7uXpV3ZyqGew5l7H3VcZjDdk2gAoZ60ARACgrSMzFptZcMdmHwFY5/HeM629iWsXTGPBBe1Y57HO09lXZtu+bjbv6eTNQ72kxuL9kF/k1ziUzJvWziUzzmHWlFaUFMyY3MKn3reIZ17bxW82vYVzHiGOE33a4LzJXQ6ExwvAB8Y6Mm1JM51bwI4WQElBaiwTG4osnT+VK+dMoRAFJNqQZpbHX97GzkM97OvsJwwUgZIjQsjXrpXU8vLmvazeuo+FM87h96+cTX0xBODGxRcy49xmHnh6A8a4MaGLDzTOGjJtPSB81ecC53OTp9rgjB0VA0IIstQy5/wJfGL5JQRSIoUg05YoUPx605u8sGkPgZIoJXDekRl3zOYjyTrP+u0H2LG/m7+6aTFtTSW0ccyc3MJHb5jPvz31GtaOUYMyi/MWa/P1h2JGGpNbINOaTJvq/ZBVDO+7dCaf+cDlRIFCW4d1wwxKKTDW1+aPd2jj6OpL+NdV60kyXc1mnllTWlk0YxJJls8Z5iVXcJoZtM2T9JAFpPcebRxJqkkyS6Itmcmvf7ZsAR+8ejbWOg4eGeTbT75Kd39SS3nGOJLM5NlF23GPSmaopJqdB3p46pWdOYauKuSmd76DUhzkCtSWRJuqu2qSzGCd9SOdMzDOkVnr00wLrw0CS+9gyhfvWMqimZMoZ4au3jIrH/4NUaBQqoqRBBjnyIxBSsnbAX7GelZv2ce186fRWBejraO9ucS86W088+ougkDWXMVLQ+BzDxnTApk2ZCY310Al48JzmskyC87ztcfW0tVbwVqPdx58rjLnPKkebepTGc55Nuw6xOu7DiM8CA8DFc3tS+dSzgx6pGtmmjSzODdaUdJVU6E2llRrUm2JwoCH/3MzDaWIJ9buYOPujty1rK3q2dcCMjM2F9zYtzEc1noOHhmopU/vPfWliJmTW+hPMlJj0dbhnENbKwaTzAE1QYLuvrJXnX22XZaChtCBd4RK8sgLm7l4WhvFMCDVFikgMw7vPUMf6xyptih54sxzMnpx015uXXIRzjuEFBjruGb+VNZtP0BTKaa3nNJnUt/Z3U9vR5cFRJoXNIJykvndh3rt7l4flKShsRjSWCrQVBfx5Z+upqW+gDYWIUAbW4NnILA2t0Ag3/6pzXs40N2PdW4UBBtMDFt2dyKEYCDJICiAM2BGZ6EAked7vKOSGsqVlINd/YCgEIfEkaIUh8SBoqEY56arPmycy/0yyANbiGHkWWNwCKVWNx1Cq955nM/d1zmHdR4hBAKPl3C4Z4DBjj5EU6la2FwN3Y6kgCF+nAU8CIGQuUMm2pBkmt7+Crj88czYnAE83f0J2/d3E4WqBpPliDO0gBpQ8x6sczjvyeMuZ9qmmisunoaSMq8xVaDY2VeBIM9uOX9uzDNKMFJXI81aIyGqYxin5OhGoLWlXE4pK3kKByAx6kJiuO2q2bnwUlTxkGTrW10QqBHLjh1nwZi/jpfyUMgh29ttvcQB505oqDEqBHT0DLLq+U0EDUWMPXGCOLsnd+tYcskFXLdgOpC7VnNdgZUPvwTF6KicP7aC5OjbM9TA8hAFCqTgrvcvorkuRghBMQ5Zv/0Aj720FaUUbqRbCjnmSVHWVhRnzhhKCbJM841P3sCHls4l1XmaTjLDvT98kb7BDOeOcp3juOgw1/LUBbDOQTnDO48UonrUFNV0Wt2X4ewkBJDk6PP+u5bz6ZsX09VXAQGlOOT+VetZtXpbDdwdy+qxQgwHsZD58OOrqpmxLLv0Qrr++Goefn4zh97sACUgDvP0J+TwcsbhKxmEktuXLeRjyxdy9bzz6eyrUFcIqaSGzz7wLN/4yWoI1NgbSpljsGMEEOQpUKpTCgFjHRedP5GVdyzlM7dcTpIZVm/dz1PrdrCno48jAwnlRFOMQ6a2NfKRd1/M4lmTaW8uESjJYKKpK4T8dtdh7vzqL9i2r/v4zAMIBeLY8/oIC1Rz4jgpUJL12w+w80AP1yyYRl0h5NarZnPn9fPGnJ9VQdkQzn9l20GeWLON+3+6BlmI8hpzolJyHP6qAniQp1YS4lDx1LqdfOl7/8HFc8+jvbmOyy+azKIZk1BS0lAMkTI/H1vnKaeaTFt+9eou3tjbxeot+9ADCWF9AWPdybGUVGMGcpBXIjE84UQFacR/3nsa62IoRfz3zkPgPc9u2EUUhUgpiJTMMYzI4YQxDmMdOtXgPIQKUYrRzo+vkSZVNU6PEkDkuwivgnzSCRtbRwO1/DcRqJoGtfVg84POseQhCI56fpykcgGEUMKDHxI6EFLkEa7CXAA5diB55yhndoSfClLjIHPIuhA3Tm5OGXEMzVchqAAh5SjBg1AFqCiUvhhDFoFyo1VThdtxoPjL919KUynOzwXes/R3zuODN8zn52t2UArVMce9/xcakjiOgQwqIRWqfVkgiEJFFEfOxwGUYkjcKDVJIahkhr+++TK+8AdXMFDJgFzGJXOmMOvcVv5r68Fqy+U0tuTrIiDCB4oK1JpfQeQzUyxEfU6YiaKuDoQ9KlghLoR85eevce8jawnUcMUewiptTSVCpYhOG5QSUIjARsKKrB8QwufaClTf3qQUssMrP1E0NoOtHBMHHk9DqYCSY7T8PCeFvP9X3kHgowCRSQYHDu4EhHCZBQgOb36pp+13P/RcLMw7aW2FSvdxA/nskMgzV33JSz0odq/72YthXYs35V4NeSFzAxv//enWaz/ycan7JjJlKnQfBnUG4fWJyFloPw850Cu79254PRvo7lRhodc7lwEEE6bPY/eaJ19tnjr3X6Ze9p57fX1sfHyOEoMD4mTvCk47FQr4Qp1X0otKf8fg9l89+Avv3GGTlo8AuQW63tzoJs5cxOuPfuUHQpcLc66/bUU4oQ3d0miQUoqTvXk4DeSHWhk6c5GQQc+OrYOrH1rxnXLX/jeAt4Aeqq9Rh1iTCNGI9xc0T5q+eOmf/N3fTJk9f/Z43l2dLhJCkKap2/jsj19c8+Nv/VoqdcBZuxnYAXQDpiaACiI8PhBCNMX1zVPLRw5PBGYtWH7Hkrrmia31Le1Ntd7I6SYpfMfuHZ0DXft7tq/+5QagEhXre7LKwP8Ae6rMa6rl9mj9KqmCUrGhpS0tD0w0WaUeKAEhecCfCXsM2V1HxTrjrOk2WXoYRCf4fnLXGQFojiVBDvIKCFkECkAEfigtnU4zVF844wDtvUuBSnUYxmgO/S9jF1Zlift4fQAAAABJRU5ErkJggg==
// @author 100の人
// @homepageURL http://greasyfork.icu/scripts/262
// ==/UserScript==
'use strict';
/**
* タグ一覧ページをキャッシュしておく期間 (秒数)。
* @constant {number}
*/
const CACHE_LIFETIME = 24 * 60 * 60;
/**
* @typedef {Object} TagsData
* @property {HTMLDivElement} tagCloudSection - タグクラウド。
* @property {Object.<number>} tagsAndCounts - タグをキー、タグの出現数を値に持つ連想配列。
*/
if (typeof content !== 'undefined') {
// For Greasemonkey 4
XMLHttpRequest = content.XMLHttpRequest.bind(content); //eslint-disable-line no-global-assign, no-native-reassign, no-undef, max-len
}
/**
* 小説ページなら真。
* @type {boolean}
*/
const novel = location.pathname.startsWith('/novel/');
/** @type {Promise.<TagsData>} */
let tagsDataPromise;
getUserId().then(async function (userId) {
new MutationObserver(function (mutations) {
for (const mutation of mutations) {
const addedNode = mutation.addedNodes[0];
if (!addedNode) {
continue;
}
const target = mutation.target;
switch (target.localName) {
case 'div':
if (novel && addedNode.id === 'chapter_0_0') {
// 作品ページから作品ページへの移動 (小説)
pickup();
return;
}
if (addedNode.localName !== 'div') {
continue;
}
// 作品ページ外から作品ページへの移動
insertTagCloud();
return;
case 'section':
if (addedNode.localName !== 'div' || addedNode !== target.firstElementChild) {
continue;
}
// 作品ページ外から作品ページへの移動
pickup();
return;
case 'a': {
if (novel || !target.closest('figure') || addedNode.localName !== 'img') {
continue;
}
// 作品ページから作品ページへの移動 (イラスト)
const newUserId
= new URLSearchParams(document.querySelector('[href*="/member.php?id="]').search).get('id');
if (newUserId !== userId) {
// 別ユーザーの関連作品への移動
userId = newUserId;
tagsDataPromise = getTagsData(userId);
insertTagCloud();
}
pickup();
return;
}
}
}
}).observe(document, { childList: true, subtree: true });
tagsDataPromise = async function () {
let nextCleaningDate = await GM.getValue('next-cleaning-date');
if (nextCleaningDate) {
if (new Date(nextCleaningDate).getTime() < Date.now()) {
// 予定時刻を過ぎていれば、古いキャッシュを削除
for (const name of await GM.listValues()) {
if (/-(?:tags|expire)$/.test(name)) {
// バージョン2.2.0以前で生成されたデータの削除
await GM.deleteValue(name);
continue;
}
if (!/^[0-9]+(?:-novel)?$/.test(name)) {
continue;
}
const data = await GM.getValue(name);
if (new Date(data.expire).getTime() < Date.now()) {
// キャッシュの有効期限が切れていれば
await GM.deleteValue(name);
}
}
nextCleaningDate = null;
}
} else {
// バージョン1.0.0で生成されたデータの削除
await Promise.all((await GM.listValues()).map(GM.deleteValue));
}
if (!nextCleaningDate) {
await GM.setValue(
'next-cleaning-date',
new Date(Date.now() + CACHE_LIFETIME * DateUtils.MINUTES_TO_MILISECONDS).toISOString()
);
}
return getTagsData(userId);
}();
addStyleSheet();
});
async function addStyleSheet()
{
let tagCloudStyles = await GM.getValue('tag-cloud-styles');
if (!tagCloudStyles) {
tagCloudStyles = (await (await fetch('https://s.pximg.net/www/css/global.css')).text())
.match(/^(?:\.area_(?:new|title|inside)|\.view_mypixiv|ul\.tagCloud) .+?$/umg).join('\n');
GM.setValue('tag-cloud-styles', tagCloudStyles);
}
document.head.insertAdjacentHTML('beforeend', h`<style>
${tagCloudStyles}
.area_new {
width: unset;
margin: 16px;
}
.area_new + section {
/* 小説ページにもともと含まれる「作者の作品タグ」 */
display: none;
}
.tagCloud {
padding: 0;
}
.tagCloud .last-current-tag::after {
content: "";
display: inline-block;
height: 18px;
border-right: solid 1px #999;
width: 10px;
margin-bottom: -3px;
-webkit-transform: rotate(0.3rad);
transform: rotate(0.3rad);
}
`);
}
async function insertTagCloud()
{
const tagCloudSection = (await tagsDataPromise).tagCloudSection.cloneNode(true);
const currentTagCloudSection = document.getElementsByClassName('area_new')[0];
if (currentTagCloudSection) {
if (currentTagCloudSection.getElementsByTagName('a')[0].getAttribute('href')
!== tagCloudSection.getElementsByTagName('a')[0].getAttribute('href')) {
currentTagCloudSection.replaceWith(tagCloudSection);
}
return;
}
if (novel) {
document.querySelector('main + aside > div ~ section header > h2').closest('section').before(tagCloudSection);
} else {
document.querySelector('main + aside > div').after(tagCloudSection);
}
}
async function pickup()
{
/** @type {TagsData} */
const tagsData = await tagsDataPromise;
if (!document.getElementsByClassName('area_new')[0]) {
await insertTagCloud();
}
/** @type {HTMLUListElement} */
const tagCloud = tagsData.tagCloudSection.getElementsByClassName('tagCloud')[0].cloneNode(true);
let tagCloudItemTemplate;
let tagCloudItemTemplateAnchor;
const currentTags = [];
// 表示している作品のタグを取得する
for (const tagItem
of document.getElementsByClassName('gtm-new-work-tag-event-click')[0].closest('ul').getElementsByTagName('a')) {
/**
* RFC 3986にもとづいてパーセント符号化されたタグ。
* @type {string}
*/
const urlencodedTag = /[^=]+$/.exec(tagItem.search)[0];
let tagCloudItem;
const anchor = tagCloud.querySelector('[href$="tag=' + urlencodedTag + '"]');
if (anchor) {
// タグクラウドに同じタグが存在すれば、抜き出す
tagCloudItem = anchor.parentElement;
} else {
// 存在しなければ、もっとも出現度の低いタグとして追加
if (!tagCloudItemTemplate) {
tagCloudItemTemplate = tagCloud.firstElementChild.cloneNode(true);
tagCloudItemTemplate.className = 'level6';
tagCloudItemTemplateAnchor = tagCloudItemTemplate.firstElementChild;
}
tagCloudItemTemplateAnchor.href = tagCloudItemTemplateAnchor.href.replace(/[^=]+$/, urlencodedTag);
const tag = tagItem.textContent;
tagCloudItemTemplateAnchor.text = tag;
if (tag in tagsData.tagsAndCounts) {
// タグの数を表示
tagCloudItemTemplateAnchor
.insertAdjacentHTML('beforeend', `<span class="cnt">(${tagsData.tagsAndCounts[tag]})</span>`);
}
tagCloudItem = tagCloudItemTemplate.cloneNode(true);
}
currentTags.push(' ', tagCloudItem);
}
// 表示している作品のタグとそれ以外のタグとの区切りを示すクラスを設定
currentTags[currentTags.length - 1].classList.add('last-current-tag');
// タグクラウドの先頭に挿入
tagCloud.prepend(...currentTags);
// 更新
document.getElementsByClassName('tagCloud')[0].replaceWith(tagCloud);
}
/**
* 表示している作品の作者のユーザーIDを取得します。
* @returns {Promise.<string>}
*/
function getUserId()
{
const pattern = /,\s*user\s*:\s*{\s*([0-9]+)\s*:\s*{\s*"userId"\s*:\s*"\1"/;
for (const script of document.querySelectorAll('head script:not([src])')) {
const result = pattern.exec(script.text);
if (result) {
return Promise.resolve(result[1]);
}
}
return new Promise(function (resolve) {
new MutationObserver(function (mutations, observer) {
for (const mutation of mutations) {
if (mutation.target.localName !== 'head') {
continue;
}
for (const node of mutation.addedNodes) {
if (node.localName !== 'script' || node.src) {
continue;
}
const result = pattern.exec(node.text);
if (!result) {
continue;
}
observer.disconnect();
resolve(result[1]);
return;
}
}
}).observe(document, {childList: true, subtree: true});
});
}
/**
* 指定したユーザーのタグクラウド、および出現数が2回以上のタグ一覧を取得します。
* @param {string} userId
* @returns {Promise.<TagsData>}
*/
async function getTagsData(userId)
{
const serializedTagsData = await GM.getValue(userId);
if (serializedTagsData && new Date(serializedTagsData.expire).getTime() > Date.now()) {
const body = document.implementation.createHTMLDocument().body;
body.innerHTML = serializedTagsData.tagCloudSection;
return { tagCloudSection: body.firstElementChild, tagsAndCounts: serializedTagsData.tagsAndCounts };
}
return getTagsDataFromPage(userId);
}
/**
* 指定したユーザーのタグクラウド、および出現数が2回以上のタグ一覧をページから取得し、キャッシュとして保存します。
* @param {string} userId
* @returns {Promise.<TagsData>}
*/
function getTagsDataFromPage(userId)
{
return new Promise(function (resolve) {
const client = new XMLHttpRequest();
client.open('GET', new URL((novel ? '/novel' : '') + '/member_tag_all.php?id=' + userId, location));
client.responseType = 'document';
client.addEventListener('load', function (event) {
const doc = event.target.response;
const tagCloudSection = doc.getElementsByClassName(novel
? 'area_new'
: /* class="area_new promotion-comic" の回避 */'user-tags')[0];
const counts = doc.querySelectorAll('.tag-list > dt');
const tagsAndCounts = {};
for (const dt of counts) {
const count = Number.parseInt(dt.textContent);
if (count === 1) {
break;
}
for (const anchor of dt.nextElementSibling.getElementsByTagName('a')) {
tagsAndCounts[anchor.text] = count;
}
}
GM.setValue(userId + (novel ? '-novel' : ''), {
expire: new Date(Date.now() + CACHE_LIFETIME * DateUtils.MINUTES_TO_MILISECONDS).toISOString(),
tagCloudSection: tagCloudSection.outerHTML,
tagsAndCounts,
});
resolve({ tagCloudSection, tagsAndCounts });
});
client.send();
});
}