Greasy Fork

Greasy Fork is available in English.

pixiv タグクラウドからピックアップ

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).

当前为 2019-09-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.8.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,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
// @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:not(.user-tags) + 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('user-tags')[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('user-tags')[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('area_new')[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();
	});
}