pixiv コメントを展開

コメント (+ 返信) を常に全件表示します。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        pixiv コメントを展開
// @name:ja     pixiv コメントを展開
// @name:en     pixiv Comment Expander
// @description Expands pixiv comments. Always shows all comments (and replies).
// @description:ja コメント (+ 返信) を常に全件表示します。
// @namespace   http://userscripts.org/users/347021
// @version     2.8.0
// @match       https://www.pixiv.net/*
// @require     https://greasyfork.org/scripts/19616/code/utilities.js?version=895049
// @require     https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
// @license     MPL-2.0
// @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
// @compatible  Edge
// @compatible  Firefox Firefoxを推奨 / Firefox is recommended
// @compatible  Opera
// @compatible  Chrome
// @grant       dummy
// @noframes
// @run-at      document-start
// @icon        https://s.pximg.net/common/images/apple-touch-icon.png
// @author      100の人
// @homepageURL https://greasyfork.org/scripts/266
// ==/UserScript==

new class CommentExpander
{
	/**
	 * 1回のコメント取得数制限。
	 * @constant {number}
	 */
	static get LIMIT() {return 200;}

	constructor()
	{
		startScript(
			() => GreasemonkeyUtils.executeOnUnsafeContext(this.changeLimit, [CommentExpander.LIMIT]),
			parent => parent.localName === 'html',
			target => target.localName === 'head',
			() => document.head
		);

		document.addEventListener('DOMContentLoaded', () => {
			this.expandReplies();
		}, {once: true});
	}

	/*globals fetch:true */
	/**
	 * ページ側のコンテキストで実行してコメント取得数を変更する関数。
	 * @param {number} limit
	 */
	changeLimit(limit)
	{
		let offset;
		fetch = new Proxy(fetch, {
			apply(fetch, thisArgument, argumentList)
			{
				if (!(argumentList[0] instanceof Request)) {
					argumentList[0] = new URL(argumentList[0], location);
					if (argumentList[0].pathname.endsWith('/comments/roots')) {
						const params = argumentList[0].searchParams;
						if (Number.parseInt(params.get('offset')) === 0) {
							offset = 0;
						}
						params.set('offset', offset);
						params.set('limit', limit);
						offset += limit;
					}
				}
				return Reflect.apply(fetch, thisArgument, argumentList);
			},
		});
	}

	/**
	 * すべての返信を展開します。
	 * @access private
	 */
	async expandReplies()
	{
		const root = document.getElementById('root');
		if (!root) {
			return;
		}

		new MutationObserver(mutations => {
			for (const mutation of mutations) {
				if (mutation.target.parentElement.matches('main h2 ~ div:nth-of-type(2) > ul [role="button"] *')) {
					// 「以前の返信を見る」の展開
					mutation.target.parentElement.closest('[role="button"]').click();
				}
			}
		}).observe(root, {characterData: true, subtree: true});

		new MutationObserver(mutations => {
			for (const mutation of mutations) {
				if (!mutation.target.matches('main h2 ~ div:nth-of-type(2)')) {
					continue;
				}

				const list = Array.from(mutation.addedNodes).find(node => node.localName === 'ul');
				if (list) {
					this.watchAndClickButtons(list);
					return;
				}
			}
		}).observe(root, {childList: true, subtree: true});
	}

	/**
	 * 指定された要素の子の追加を監視し、要素内のすべてのボタンを押します。
	 * @access private
	 * @param {HTMLUListElement} element
	 */
	watchAndClickButtons(element)
	{
		this.clickButtons(element);
		new MutationObserver(mutations => {
			for (const mutation of mutations) {
				for (const element of mutation.addedNodes) {
					this.clickButtons(element);
				}
			}
		}).observe(element, {childList: true});
	}

	/**
	 * 指定された要素内のすべてのボタンを押します。
	 * @access private
	 * @param {(HTMLUListElement|HTMLLIElement)} element
	 */
	clickButtons(element)
	{
		for (const button of element.querySelectorAll('[role="button"]')) {
			button.click();
		}
	}
}();