Greasy Fork is available in English.

futaba_catalog_NG

カタログのスレをNGで非表示

// ==UserScript==
// @name        futaba_catalog_NG
// @namespace   https://github.com/akoya-tomo
// @description カタログのスレをNGで非表示
// @author      akoya_tomo
// @match       http://*.2chan.net/*/futaba.php?mode=cat*
// @match       https://*.2chan.net/*/futaba.php?mode=cat*
// @version     1.10.0
// @require     http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js
// @require     https://cdn.jsdelivr.net/npm/js-md5@0.7.3/src/md5.min.js
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @license     MIT
// @run-at      document-start
// @icon        data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAABXElEQVQ4T8VSTUvDQBRcEcGL0H+QQ3c3KSVtmhRMe5aC4rX+Fb15F/PVUqFSqwcV8SxSPYknUcSDKCqICNZ/YBEv62xcS0CQ5iBOeMm+92beDrsh/w8WMIuFrEdD2kdc8ZAvacvapGoPIWs84IsxJ+J96I6yftYmKLywiInCekFUdirCbJsCzfNiUMwoLdECLQPe2TfH6lhCarDxK8HrKU5UlLfKwu7agvnsUOkJDehBaaMknE1nyIsjYM8ExBkMGSQbkiid0FU6D9uzcv1DHLEB9Wkt3kH3dBNDTpIEd9uVFvdA2o3XiR4cnfIVXojFScDSNAghnnu9qUuLD1jf5Vo5gTN4RETUo66i/w7msTmI3yH64D5fQGnsq5MCcHCNa71VaXpgwAXiUqXpgd1vEOkH5Fv5mt7Qj+WJG01jTZVHh9Ewuvhd3+S32qlOqfLoqO/Xx522M6HSvwQhn8ChorvE+0t7AAAAAElFTkSuQmCC
// ==/UserScript==
this.$ = this.jQuery = jQuery.noConflict(true);

(function ($) {
	/**
	 *	設定
	 */
	var USE_NG_IMAGES = true;				// スレ画像のNGを有効にする
	var MAX_NG_THREADS = 512;				// NGスレの最大保持数(板毎)
	var MAX_REGISTERED_NG_IMAGES = 2048;	// NG画像の最大登録数
	var MAX_OK_IMAGES = 1024;				// 非NG画像名の最大保持数(板毎)
	var HIDE_CATALOG_BEFORE_LOAD = false;	// ページの読み込みが完了するまでカタログを隠す
	var USE_NG_THREAD_CLEAR_BUTTON = true;	// スレNGのクリアボタンを使用する
	var USE_NG_DISABLE_BUTTON = true;		// NG機能一時無効ボタンを使用する
	var USE_DHASH = false;					// 近似画像NGを使用する
	var DISTANCE_THRESHOLD = 3;				// 近似画像判定閾値(デフォルト:3)
	var ENABLE_DHASH_TEST = false;			// 近似画像NGのテストモードを有効にする
	var CHECK_NG_LIST = true;				// NGリストが正常かチェックする

	var serverName = document.domain.match(/^[^.]+/);
	var pathName = location.pathname.match(/[^/]+/);
	var serverFullPath = serverName + "_" + pathName;
	var selectIndex = -1;
	var imageList = GM_getValue("_futaba_catalog_NG_images", []);
	var commentList = GM_getValue("_futaba_catalog_NG_comment", []);
	var dateList = GM_getValue("_futaba_catalog_NG_date", []);
	var dHashList = GM_getValue("_futaba_catalog_NG_dHashes", []);
	var images, ngDate, okImages, dHashes;
	var item_md5_text = " md5";
	var item_comment_text = " コメント";
	var item_date_text = " 最終検出日";
	var item_dHash_text = " dHash";
	var isNgEnable = true;

	// dHashリスト初期化
	if (dHashList.length == 0 && imageList.length > 0) {
		dHashList = new Array(imageList.length).fill(null);
		GM_setValue("_futaba_catalog_NG_dHashes", dHashList);
	}

	if (CHECK_NG_LIST && USE_NG_IMAGES && !isNgListNormal()) {
		alert("NGリストが壊れています");
	}

	if (HIDE_CATALOG_BEFORE_LOAD) {
		hideCatalog();
	} else {
		$(init);
	}

	function isNgListNormal() {
		var imageNum = imageList.length;
		var commentNum = commentList.length;
		var dateNum = dateList.length;
		var dHashNum = dHashList.length;
		//console.log("futaba catalog NG - imageNum: " + imageNum);
		//console.log("futaba catalog NG - commentNum: " + commentNum);
		//console.log("futaba catalog NG - dateNum: " + dateNum);
		//console.log("futaba catalog NG - dHashNum: " + dHashNum);
		return (imageNum == commentNum) && (imageNum == dateNum) && (imageNum == dHashNum);
	}

	function init() {
		//clearNgNumber();
		//console.log("futaba_catalog_NG - commmon: " +
		//	GM_getValue("_futaba_catalog_NG_words", ""));
		//console.log("futaba_catalog_NG - indivisual: " +
		//	getCurrentIndivValue("NG_words_indiv", ""));
		GM_registerMenuCommand("NGワード編集", editNgWords);
		if (USE_NG_IMAGES) {
			GM_registerMenuCommand("NGリスト編集", editNgList);
		}
		GM_registerMenuCommand("スレNGクリア", confirmClearNgNumber);
		setStyle();
		makeNgMenubar();
		makeConfigUI();
		makeNgListUI();
		makeNgButton();
		hideNgThreads();
		checkAkahukuReload();
		listenKoshianDelEvent();
		listenFthPickupEvent();
	}

	/**
	 * NG番号クリア
	 * @param {boolean} forced 強制的にクリアするか
	 */
	function clearNgNumber(forced) {
		if (!forced && window.name) {
			return;
		}
		window.name = location.href;

		var ngNumberObj = getIndivObj("NG_numbers_indiv");
		if (ngNumberObj === "") {
			ngNumberObj = {};
		}
		ngNumberObj[serverFullPath] = [];
		var jsonString = JSON.stringify(ngNumberObj);
		GM_setValue("NG_numbers_indiv", jsonString);
	}

	/**
	 * 各板個別NGデータを保存
	 * @param {string} target 保存するNG名
	 * @param {string|Array} val 保存するNGデータ
	 */
	function setIndivValue(target, val) {
		var indivObj = getIndivObj(target);
		if (indivObj === "") {
			indivObj = {};
		}
		indivObj[serverFullPath] = val;
		var jsonString = JSON.stringify(indivObj);
		GM_setValue(target, jsonString);
		//console.log("futaba_catalog_NG: " + target + " updated@" + serverFullPath + " - " + val);
	}

	/**
	 * 各板個別NGデータのオブジェクトを取得
	 * @param {string} target 取得するNG名
	 * @return {object} 取得したNGデータ
	 */
	function getIndivObj(target) {
		var indivVal = GM_getValue(target, "");
		var indivObj = "";
		if (indivVal !== "") {
			indivObj = JSON.parse(indivVal);
		}
		return indivObj;
	}

	/**
	 * NGワード編集メニュー表示
	 */
	function editNgWords(){
		var wordsCommon = GM_getValue("_futaba_catalog_NG_words", "");
		var wordsIndiv = getCurrentIndivValue("NG_words_indiv", "");
		$("#GM_fcn_ng_words_common").val(wordsCommon);
		$("#GM_fcn_ng_words_individual").val(wordsIndiv);
		var $configContainer = $("#GM_fcn_config_container");
		$configContainer.fadeIn(100);
	}

	/**
	 * 現在の板の個別NGデータを取得
	 * @param {string} target NGデータ名
	 * @param {string|Array} defaultVal NGデータが未定義のときの既定値
	 * @return {string|Array} 現在の板の個別NGデータ
	 */
	function getCurrentIndivValue(target, defaultVal) {
		var indivObj = getIndivObj(target);
		var currentIndivVal;
		if (indivObj !== "") {
			currentIndivVal = indivObj[serverFullPath];
		}
		if (!currentIndivVal) {
			currentIndivVal = defaultVal;
		}
		return currentIndivVal;
	}

	/**
	 * NGリスト編集メニュー表示
	 */
	function editNgList() {
		// マウスホイールリロード対策
		$("<div>", {
			id: "GM_fcn_catalog_space"
		}).appendTo("body");
		if ($(window).scrollTop() < 2) {
			$("html, body").scrollTop(2);
		}
		$("html, body").css("overflow", "hidden");

		refreshNgList(true);
		resetNgListItemText();
		var $ngListContainer = $("#GM_fcn_ng_list_container");
		$ngListContainer.fadeIn(100);
	}

	/**
	 * NGリスト項目名リセット
	 */
	function resetNgListItemText() {
		$("#GM_fcn_ng_list_item_md5").text(item_md5_text + " ");
		$("#GM_fcn_ng_list_item_comment").text(item_comment_text + " ");
		$("#GM_fcn_ng_list_item_date").text(item_date_text + " ");
		$("#GM_fcn_ng_list_item_dHash").text(item_dHash_text + " ");
	}

	/**
	 * スレNGクリア確認
	 */
	function confirmClearNgNumber() {
		if (confirm("この板のスレNGを全てクリアします。\nよろしいですか?")) {
			$(".GM_fcn_ng_numbers").each(function() {
				$(this).removeClass("GM_fcn_ng_numbers");
				$(this).css("display", "");
			});
			clearNgNumber(true);
		}
	}

	/**
	 * NG機能無効切り替え
	 */
	function toggleNgDisable() {
		var $ngDisableButton = $("#GM_fcn_ng_disable_button");
		if (isNgEnable) {
			$ngDisableButton.css("background", "#a9d8ff");
			isNgEnable = false;
			$(".GM_fcn_ng_words").css("display", "");
			$(".GM_fcn_ng_words").removeClass("GM_fcn_ng_words");
			$(".GM_fcn_ng_numbers").css("display", "");
			$(".GM_fcn_ng_numbers").removeClass("GM_fcn_ng_numbers");
			$(".GM_fcn_ng_images").css("display", "");
			$(".GM_fcn_ng_images").removeClass("GM_fcn_ng_images");
			$(".GM_fcn_ng_dhash_td").removeClass("GM_fcn_ng_dhash_td");
			$(".GM_fcn_ng_dhash_img").removeClass("GM_fcn_ng_dhash_img");
		} else {
			$ngDisableButton.css("background", "none");
			isNgEnable = true;
			hideNgThreads();
		}
	}

	/**
	 * NGリスト表示更新
	 * @param {boolean} loadNgList NGリストデータを読み込みするか
	 */
	function refreshNgList(loadNgList) {
		if (loadNgList) {
			imageList = GM_getValue("_futaba_catalog_NG_images", []);
			commentList = GM_getValue("_futaba_catalog_NG_comment", []);
			dateList = GM_getValue("_futaba_catalog_NG_date", []);
			dHashList = GM_getValue("_futaba_catalog_NG_dHashes", []);
		}
		var listCount = imageList.length;
		$(".GM_fcn_ng_list_row").remove();

		for (var i = 0; i < listCount; ++i) {
			var row = $("<div>", {
				class: "GM_fcn_ng_list_row",
				click: function() {	// eslint-disable-line no-loop-func
					selectIndex = $(this).index();
					selectNgList();
				}
			}).appendTo("#GM_fcn_ng_list_content");
			row.append(
				$("<div>", {
					class: "GM_fcn_ng_list_image",
					text: imageList[i],
				}),
				$("<div>", {
					class: "GM_fcn_ng_list_dHash",
					text: dHashList[i] ? ("000000000000" + dHashList[i].toString(16)).slice(-13) : "-",
				}),
				$("<div>", {
					class: "GM_fcn_ng_list_comment",
					text: commentList[i],
				}),
				$("<div>", {
					class: "GM_fcn_ng_list_date",
					text: dateList[i],
				}),
				$("<div>", {
					class: "GM_fcn_ng_list_scrl",
				})
			);
		}
		$(".GM_fcn_ng_list_row").css("background-color", "#ffffff");
	}

	/**
	 * NGリスト選択
	 */
	function selectNgList() {
		$(".GM_fcn_ng_list_row").css("background-color", "#ffffff")
			.eq(selectIndex).css("background-color", "#ffecfd");
		$("#GM_fcn_md5").val(imageList[selectIndex]);
		$("#GM_fcn_comment").val(commentList[selectIndex]);
	}

	/**
	 * NGメニューバー作成
	 */
	function makeNgMenubar() {
		var $ngMenubarArea = $("<div>", {
			id: "GM_fcn_ng_menubar",
			css: {
				"background-color": "#F0E0D6"
			}
		});
		var $ngWordsHeader = $("<span>", {
			id: "GM_fcn_ng_words_header",
			text: "NGワード",
			css: {
				"background-color": "#F0E0D6",
				fontWeight: "bolder",
				"padding-right": "16px"
			}
		});
		$("#cattable").before($ngMenubarArea);
		$ngMenubarArea.append($ngWordsHeader);
		// 設定ボタン
		var $ngWordsButton = $("<span>", {
			id: "GM_fcn_config_ng_words",
			text: "[設定]",
			css: {
				cursor: "pointer",
			},
			click: editNgWords
		});
		$ngWordsButton.hover(function() {
			$(this).css("background-color", "#EEAA88");
		}, function() {
			$(this).css("background-color", "#F0E0D6");
		});
		$ngWordsHeader.append($ngWordsButton);

		if (USE_NG_DISABLE_BUTTON) {
			// NG機能一時無効
			var $ngDisableHeader = $("<span>", {
				id: "GM_fcn_ng_disalbe_header",
				text: "NG機能",
				css: {
					"background-color": "#F0E0D6",
					fontWeight: "bolder",
					"padding-right": "16px"
				}
			});
			$ngWordsHeader.after($ngDisableHeader);
			// NG機能一時無効ボタン
			var $ngDisableButton = $("<a>", {
				id: "GM_fcn_ng_disable_button",
				text: "[一時無効]",
				css: {
					cursor: "pointer",
				},
				click: toggleNgDisable
			});
			$ngDisableHeader.append($ngDisableButton);
		}

		if (USE_NG_THREAD_CLEAR_BUTTON) {
			// スレNG
			var $ngThreadHeader = $("<span>", {
				id: "GM_fcn_ng_thread_header",
				text: "スレNG",
				css: {
					"background-color": "#F0E0D6",
					fontWeight: "bolder",
					"padding-right": "16px"
				}
			});
			$ngWordsHeader.after($ngThreadHeader);
			// スレNGクリアボタン
			var $ngThreadClearButton = $("<span>", {
				id: "GM_fcn_ng_thread_clear_button",
				text: "[クリア]",
				css: {
					cursor: "pointer",
				},
				click: confirmClearNgNumber
			});
			$ngThreadClearButton.hover(function() {
				$(this).css("background-color", "#EEAA88");
			}, function() {
				$(this).css("background-color", "#F0E0D6");
			});
			$ngThreadHeader.append($ngThreadClearButton);
		}

		if (USE_NG_IMAGES) {
			// NGリスト
			var $ngListHeader = $("<span>", {
				id: "GM_fcn_ng_list_header",
				text: "NGリスト",
				css: {
					"background-color": "#F0E0D6",
					fontWeight: "bolder",
					"padding-right": "16px"
				}
			});
			$ngWordsHeader.after($ngListHeader);
			// NGリスト編集ボタン
			var $ngListButton = $("<span>", {
				id: "GM_fcn_edit_ng_list",
				text: "[編集]",
				css: {
					cursor: "pointer",
				},
				click: editNgList
			});
			$ngListButton.hover(function () {
				$(this).css("background-color", "#EEAA88");
			}, function () {
				$(this).css("background-color", "#F0E0D6" );
			});
			$ngListHeader.append($ngListButton);
		}
	}

	/**
	 * NGワード編集メニュー作成
	 */
	function makeConfigUI() {
		var $configContainer = $("<div>", {
			id: "GM_fcn_config_container",
			css: {
				position: "fixed",
				"z-index": "1001",
				left: "50%",
				top: "50%",
				"text-align": "center",
				"margin-left": "-475px",
				"margin-top": "-50px",
				"background-color": "rgba(240, 192, 214, 0.95)",
				width: "950px",
				//height: "100px",
				display: "none",
				fontSize: "16px",
				fontWeight: "normal",
				"box-shadow": "3px 3px 5px #853e52",
				"border": "1px outset",
				"border-radius": "10px",
				"padding": "5px",
			}
		});
		$("#GM_fcn_ng_words_header").append($configContainer);
		$configContainer.append(
			$("<div>").append(
				$("<div>").text("NGワード編集").css({
					"background-color": "#ffeeee",
					"padding": "2px",
					"font-weight": "bold"
				}),
				$("<div>").text("スレ本文に含まれる語句を入力してください。 | を挟むと複数指定できます。正規表現使用可。")
			),
			$("<div>").css("margin-top", "1em").append(
				$("<div>").append(
					$("<label>").text("全板共通").attr("for", "GM_fcn_ng_words_common"),
					$("<input>", {
						id: "GM_fcn_ng_words_common",
						class: "GM_fcn_input",
						css: {
							width: "54em",
							fontSize: "13.33px"
						}
					}),
					$("<span>").append(
						$("<input>", {
							class: "GM_fcn_config_button",
							type: "button",
							val: "区切り文字挿入",
							click: function() {
								insertDelimiter("GM_fcn_ng_words_common");
							},
						})
					)
				),
				$("<div>").append(
					$("<label>").text("各板個別").attr("for", "GM_fcn_ng_words_individual"),
					$("<input>", {
						"id": "GM_fcn_ng_words_individual",
						"class": "GM_fcn_input",
						css: {
							width: "54em",
							fontSize: "13.33px"
						}
					}),
					$("<span>").append(
						$("<input>", {
							class: "GM_fcn_config_button",
							type: "button",
							val: "区切り文字挿入",
							click: function() {
								insertDelimiter("GM_fcn_ng_words_individual");
							},
						})
					)
				)
			),
			$("<div>").css({
				"margin-top": "1em",
			}).append(
				$("<span>").css("margin", "0 1em").append(
					$("<input>", {
						class: "GM_fcn_config_button",
						type: "button",
						val: "更新",
						click: setNgWords
					})
				),
				$("<span>").css("margin", "0 1em").append(
					$("<input>", {
						class: "GM_fcn_config_button",
						type: "button",
						val: "キャンセル",
						click: function() {
							setCursor();
							$configContainer.fadeOut(100);
						},
					})
				)
			)
		);
		$(".GM_fcn_config_button").css({
			"cursor": "pointer",
			"background-color": "#FFECFD",
			"border": "2px outset #96ABFF",
			"border-radius": "5px",
			"font-size": "13.33px",
		}).hover(function() {
			$(this).css("background-color", "#CCE9FF");
		}, function() {
			$(this).css("background-color", "#FFECFD");
		});

		/**
		 * カーソル位置にデリミタ挿入
		 * @param {string} id デリミタを挿入する入力欄のID名
		 */
		function insertDelimiter(id){
			var $input = $("#" + id);
			var val = $input.val();
			var position = $input[0].selectionStart;
			var newval = val.substr(0, position) + "|" + val.substr(position);
			$input.val(newval);
			$input[0].setSelectionRange(position + 1 ,position + 1);
		}
	}

	/**
	 * NGワードをセット
	 */
	function setNgWords() {
		var inputCommon = $("#GM_fcn_ng_words_common").val();
		var inputIndiv = $("#GM_fcn_ng_words_individual").val();
		GM_setValue("_futaba_catalog_NG_words", inputCommon);
		console.log("futaba_catalog_NG: common NGword updated - " + inputCommon);	// eslint-disable-line no-console
		setIndivValue("NG_words_indiv", inputIndiv);
		setCursor();
		$("#GM_fcn_config_container").fadeOut(100);
		hideNgThreads(true);
	}

	/**
	 * NGワードのカーソルを先頭に移動
	 */
	function setCursor() {
		var inputCommonElm = $("#GM_fcn_ng_words_common").get(0);
		if (inputCommonElm) {
			inputCommonElm.focus();
			inputCommonElm.setSelectionRange(0, 0);	// カーソルを先頭に移動
			inputCommonElm.blur();
		}
		var inputIndivElm = $("#GM_fcn_ng_words_individual").get(0);
		if (inputIndivElm) {
			inputIndivElm.focus();
			inputIndivElm.setSelectionRange(0, 0);	// カーソルを先頭に移動
			inputIndivElm.blur();
		}
	}

	/**
	 * NGリスト編集メニュー作成
	 */
	function makeNgListUI() {
		if (!USE_NG_IMAGES) {
			GM_setValue("OK_images_indiv", "");
			return;
		}
		imageList = GM_getValue("_futaba_catalog_NG_images", []);
		commentList = GM_getValue("_futaba_catalog_NG_comment", []);
		dateList = GM_getValue("_futaba_catalog_NG_date", []);
		dHashList = GM_getValue("_futaba_catalog_NG_dHashes", []);

		var $ngListContainer = $("<div>", {
			id: "GM_fcn_ng_list_container",
			css: {
				position: "fixed",
				"z-index": "1001",
				left: "50%",
				top: "50%",
				"text-align": "center",
				"margin-left": "-475px",
				"margin-top": "-250px",
				"background-color": "rgba(240, 192, 214, 0.95)",
				width: "950px",
				//height: "500px",
				display: "none",
				fontSize: "16px",
				fontWeight: "normal",
				"box-shadow": "3px 3px 5px #853e52",
				"border": "1px outset",
				"border-radius": "10px",
				"padding": "5px",
			}
		});
		$("#GM_fcn_ng_list_header").append($ngListContainer);
		$ngListContainer.append(
			$("<div>").append(
				$("<div>").text("NGリスト編集").css({
					"background-color": "#ffeeee",
					"padding": "2px",
					"font-size": "16px",
					"font-weight": "bold"
				}),
				$("<div>").css("margin-top", "1em").append(
					$("<div>").append(
						$("<label>").text("md5:").attr("for", "GM_fcn_md5"),
						$("<input>", {
							id: "GM_fcn_md5",
							class: "GM_fcn_ng_list_input",
							readonly: "readonly",
						}),
						$("<label>").text("コメント:").attr("for", "GM_fcn_comment"),
						$("<input>", {
							id: "GM_fcn_comment",
							class: "GM_fcn_ng_list_input",
							keypress: function(e) {
								if (e.key == "Enter") {
									editSelectedRow();
								}
							}
						})
					)
				),
				$("<div>").css("margin-top", "1em").append(
					$("<div>").css("margin-left", "475px").append(
						$("<span>").append(
							$("<input>", {
								class: "GM_fcn_ng_list_button GM_fcn_ng_list_edit_button",
								type: "button",
								val: "修正",
								click: editSelectedRow
							})
						),
						$("<span>").append(
							$("<input>", {
								class: "GM_fcn_ng_list_button GM_fcn_ng_list_edit_button",
								type: "button",
								val: "削除",
								click: deleteSelectedRow
							})
						),
						$("<span>").css("margin", "0 0 0 1em").append(
							$("<input>", {
								class: "GM_fcn_ng_list_button GM_fcn_ng_list_move_button",
								type: "button",
								val: "上",
								click: function() {
									swapRow(selectIndex - 1);
								},
							})
						),
						$("<span>").append(
							$("<input>", {
								class: "GM_fcn_ng_list_button GM_fcn_ng_list_move_button",
								type: "button",
								val: "下",
								click: function() {
									swapRow(selectIndex);
								},
							})
						)
					)
				)
			),
			$("<div>").css("margin-top", "1em").append(
				$("<div>", {
					id: "GM_fcn_ng_list_pane",
				}).append(
					$("<div>", {
						id: "GM_fcn_ng_list_item_row"
					}).append(
						$("<div>", {
							id: "GM_fcn_ng_list_item_md5",
							class: "GM_fcn_ng_list_item",
							text: item_md5_text + " ",
							click: function() {
								if ($(this).text() == item_md5_text + "▲") {
									resetNgListItemText();
									$(this).text(item_md5_text + "▼");
									sortNgList(0, -1);
								} else {
									resetNgListItemText();
									$(this).text(item_md5_text + "▲");
									sortNgList(0);
								}
							},
						}),
						$("<div>", {
							id: "GM_fcn_ng_list_item_dHash",
							class: "GM_fcn_ng_list_item",
							text: item_dHash_text + " ",
							click: function() {
								if ($(this).text() == item_dHash_text + "▲") {
									resetNgListItemText();
									$(this).text(item_dHash_text + "▼");
									sortNgList(3, -1);
								} else {
									resetNgListItemText();
									$(this).text(item_dHash_text + "▲");
									sortNgList(3);
								}
							},
						}),
						$("<div>", {
							id: "GM_fcn_ng_list_item_comment",
							class: "GM_fcn_ng_list_item",
							text: item_comment_text + " ",
							click: function() {
								if ($(this).text() == item_comment_text + "▲") {
									resetNgListItemText();
									$(this).text(item_comment_text + "▼");
									sortNgList(1, -1);
								} else {
									resetNgListItemText();
									$(this).text(item_comment_text + "▲");
									sortNgList(1);
								}
							},
						}),
						$("<div>", {
							id: "GM_fcn_ng_list_item_date",
							class: "GM_fcn_ng_list_item",
							text: item_date_text + " ",
							click: function() {
								if ($(this).text() == item_date_text + "▼") {
									resetNgListItemText();
									$(this).text(item_date_text + "▲");
									sortNgList(2);
								} else {
									resetNgListItemText();
									$(this).text(item_date_text + "▼");
									sortNgList(2, -1);
								}
							},
						}),
						$("<div>", {
							id: "GM_fcn_ng_list_item_scrl",
							class: "GM_fcn_ng_list_item",
						})
					),
					$("<div>", {
						id: "GM_fcn_ng_list_content"
					})
				),
				$("<div>").css("margin-top", "1em").append(
					$("<span>").css("margin", "0 1em").append(
						$("<input>", {
							class: "GM_fcn_ng_list_button",
							type: "button",
							val: "更新",
							click: function() {
								if (!CHECK_NG_LIST || isNgListNormal()) {
									GM_setValue("_futaba_catalog_NG_images", imageList);
									GM_setValue("_futaba_catalog_NG_comment", commentList);
									GM_setValue("_futaba_catalog_NG_date", dateList);
									GM_setValue("_futaba_catalog_NG_dHashes", dHashList);
									$(".GM_fcn_ng_list_row").css("background-color", "#ffffff");
									$("#GM_fcn_md5").val("");
									$("#GM_fcn_comment").val("");
									$("#GM_fcn_ng_list_content").scrollTop(0);
									$("#GM_fcn_catalog_space").remove();
									$("html, body").css("overflow", "");
									selectIndex = -1;
									$ngListContainer.fadeOut(100);
									$(".GM_fcn_ng_images").css("display", "");
									$(".GM_fcn_ng_images").removeClass("GM_fcn_ng_images");
									$(".GM_fcn_ng_dhash_td").removeClass("GM_fcn_ng_dhash_td");
									$(".GM_fcn_ng_dhash_img").removeClass("GM_fcn_ng_dhash_img");
									hideNgThreads();
								} else {
									alert("NGリストが壊れています\n一度キャンセルしてからやり直してください");
								}
							},
						})
					),
					$("<span>").css("margin", "0 1em").append(
						$("<input>", {
							class: "GM_fcn_ng_list_button",
							type: "button",
							val: "キャンセル",
							click: function(){
								$(".GM_fcn_ng_list_row").css("background-color", "#ffffff");
								$("#GM_fcn_md5").val("");
								$("#GM_fcn_comment").val("");
								$("#GM_fcn_ng_list_content").scrollTop(0);
								$("#GM_fcn_catalog_space").remove();
								$("html, body").css("overflow", "");
								selectIndex = -1;
								$ngListContainer.fadeOut(100);
							},
						})
					)
				)
			)
		);
		$(".GM_fcn_ng_list_button").css({
			"cursor": "pointer",
			"background-color": "#FFECFD",
			"border": "2px outset #96ABFF",
			"border-radius": "5px",
			"font-size": "13.33px"
		}).hover(function() {
			$(this).css("background-color", "#CCE9FF");
		}, function() {
			$(this).css("background-color", "#FFECFD");
		});

		$(document).keydown((e) => {
			if (e.key == "ArrowUp" && selectIndex > 0) {
				$(".GM_fcn_ng_list_comment").eq(selectIndex - 1).click();
				e.preventDefault();
				scrollToSelectedRow();
				$("#GM_fcn_comment").blur();
			} else if (e.key == "ArrowDown" && selectIndex > -1 && selectIndex < imageList.length) {
				$(".GM_fcn_ng_list_comment").eq(selectIndex + 1).click();
				e.preventDefault();
				scrollToSelectedRow();
				$("#GM_fcn_comment").blur();
			} else if (e.key == "F2" && selectIndex > -1) {
				$("#GM_fcn_comment").focus();
				e.preventDefault();
			}
		});

		/**
		 * NGリスト選択行コメント修正
		 */
		function editSelectedRow() {
			if (selectIndex > -1) {
				commentList[selectIndex] = $("#GM_fcn_comment").val();
				$(".GM_fcn_ng_list_comment").eq(selectIndex).text(commentList[selectIndex]);
			}
		}

		/**
		 * NGリスト選択行削除
		 */
		function deleteSelectedRow() {
			if (selectIndex > -1) {
				imageList.splice(selectIndex, 1);
				commentList.splice(selectIndex, 1);
				dateList.splice(selectIndex, 1);
				dHashList.splice(selectIndex, 1);
			}
			$(".GM_fcn_ng_list_row").css("background-color", "#ffffff");
			$("#GM_fcn_md5").val("");
			$("#GM_fcn_comment").val("");
			selectIndex = -1;
			refreshNgList();
		}

		/**
		 * NGリスト行入替
		 *     indexの行と一つ後の行を入れ替える。
		 * @param {number} index 入替する行番号(先頭行が0)
		 */
		function swapRow(index) {
			if (index <= -1 || index + 1 >= imageList.length) {
				return;
			}

			imageList.splice(index, 2, imageList[index + 1], imageList[index]);
			commentList.splice(index, 2, commentList[index + 1], commentList[index]);
			dateList.splice(index, 2, dateList[index + 1], dateList[index]);
			dHashList.splice(index, 2, dHashList[index + 1], dHashList[index]);
			selectIndex = selectIndex == index ? index + 1 : index;

			scrollToSelectedRow();
			refreshNgList();
			selectNgList();
			resetNgListItemText();
		}

		/**
		 * 選択行までスクロール
		 */
		function scrollToSelectedRow() {
			var rowHeight = 22;	// NGリストの1行当たりの高さ(px)
			var listLines = 13;	// NGリストの表示行数
			var selectPos = selectIndex * rowHeight;
			var scrollTop = $("#GM_fcn_ng_list_content").scrollTop();
			var scrollBottom = scrollTop + (rowHeight * (listLines - 2));
			if (selectPos < scrollTop) {
				$("#GM_fcn_ng_list_content").scrollTop(selectPos);
			}
			if (selectPos > scrollBottom) {
				$("#GM_fcn_ng_list_content").scrollTop(selectPos - (rowHeight * (listLines - 2)));
			}
		}

		/**
		 * NGリストソート
		 * @param {number} index ソート対象の項目(md5:0, コメント:1, 最終検出日:2, dHash:3)
		 * @param {number} order ソート方向(昇順:1, 降順:-1)未指定は昇順
		 */
		function sortNgList(index, order = 1) {
			var array = new Array();
			for (var i = 0; i < imageList.length; ++i) {
				array[i] = new Array();
				array[i][0] = imageList[i];
				array[i][1] = commentList[i];
				array[i][2] = dateList[i];
				array[i][3] = dHashList[i];
			}
			array.sort(function(a, b){
				if (a[index] > b[index]) {
					return order;
				}
				if (a[index] < b[index]) {
					return -order;
				}
				return 0;
			});
			for (var j = 0; j < imageList.length; ++j) {
				imageList[j] = array[j][0];
				commentList[j] = array[j][1];
				dateList[j] = array[j][2];
				dHashList[j] = array[j][3];
			}

			$(".GM_fcn_ng_list_row").css("background-color", "#ffffff");
			$("#GM_fcn_md5").val("");
			$("#GM_fcn_comment").val("");
			selectIndex = -1;
			refreshNgList();
		}
	}

	/**
	 * 動的リロードの状態を取得
	 */
	function checkAkahukuReload() {
		// 赤福
		var target = $("#akahuku_catalog_reload_status").get(0);
		if (target) {
			checkAkahukuReloadStatus(target);
		} else {
			$(document).on("AkahukuContentApplied", () => {
				target = $("#akahuku_catalog_reload_status").get(0);
				if (target) {
					checkAkahukuReloadStatus(target);
				} else {
					console.error ("futaba_catalog_NG - #akahuku_catalog_reload_status not found");
				}
			});
		}

		function checkAkahukuReloadStatus(target) {
			var status = "";
			var config = { childList: true };
			var observer = new MutationObserver(function() {
				if (target.textContent == status) {
					return;
				}
				status = target.textContent;
				if (status == "完了しました" || status == "アンドゥしました" || status == "リドゥしました") {
					if (!isNgEnable) {
						$("#GM_fcn_ng_disable_button").css("background", "none");
						isNgEnable = true;
					}
					makeNgButton();
					hideNgThreads();
					$("body").attr("__fcn_catalog_visibility", "visible");
					$("#cattable > tbody").css("opacity", "1");
					$("#GM_fth_highlighted_threads").css("visibility", "visible");
				} else if (HIDE_CATALOG_BEFORE_LOAD && status !== "") {
					$("body").attr("__fcn_catalog_visibility", "hidden");
					$("#cattable > tbody").css("opacity", "0");
					$("#GM_fth_highlighted_threads").css("visibility", "hidden");
				}
			});
			observer.observe(target, config);
		}

		// KOSHIAN
		$(document).on("KOSHIAN_cat_reload", () => {
			if (HIDE_CATALOG_BEFORE_LOAD) {
				$("body").attr("__fcn_catalog_visibility", "hidden");
				$("#cattable > tbody").css("opacity", "0");
				$("#GM_fth_highlighted_threads").css("visibility", "hidden");
			}
			if (!isNgEnable) {
				$("#GM_fcn_ng_disable_button").css("background", "none");
				isNgEnable = true;
			}
			makeNgButton();
			hideNgThreads();
			$("body").attr("__fcn_catalog_visibility", "visible");
			$("#cattable > tbody").css("opacity", "1");
			$("#GM_fth_highlighted_threads").css("visibility", "visible");
		});

		// ふたクロ
		checkFutakuroReloadStatus();

		function checkFutakuroReloadStatus() {
			var opacityZero = false;
			var target = $("#cattable").get(0);
			var config = { attributes: true , attributeFilter: ["style"] };
			var observer = new MutationObserver(function(mutations) {
				if ($("#cat_search").length) {
					mutations.forEach(function(mutation) {
						if (mutation.target.attributes.style.nodeValue == "opacity: 0;") {
							opacityZero = true;
							if (HIDE_CATALOG_BEFORE_LOAD) {
								$("body").attr("__fcn_catalog_visibility", "hidden");
								$("#cattable > tbody").css("opacity", "0");
								$("#GM_fth_highlighted_threads").css("visibility", "hidden");
							}
						} else if (opacityZero && mutation.target.attributes.style.nodeValue != "opacity: 0;") {
							opacityZero = false;
							if (!isNgEnable) {
								$("#GM_fcn_ng_disable_button").css("background", "none");
								isNgEnable = true;
							}
							makeNgButton();
							hideNgThreads();
							$("body").attr("__fcn_catalog_visibility", "visible");
							$("#cattable > tbody").css("opacity", "1");
							$("#GM_fth_highlighted_threads").css("visibility", "visible");
						}
					});
				}
			});
			observer.observe(target, config);
		}
	}

	/**
	 * カタログのスレにNGボタン作成
	 */
	function makeNgButton() {
		// カタログソートが「設定」なら作成しない
		if (location.search.match(/mode=catset/)) {
			return;
		}
		// NGボタン
		var $ngButton = $("<span>", {
			class: "GM_fcn_ng_button",
			text: "[NG]",
			css: {
				color: "blue",
				display: "none",
			},
		});
		// NGボタンメニュー
		var $ngButtonMenu = $("<div>", {
			class: "GM_fcn_ng_menu",
			css: {
				display: "none",
			}
		});

		var $catTd = $("#cattable td");
		$catTd.each(function() {
			var $oldNgButtons = $(this).children(".GM_fcn_ng_button");
			if ($oldNgButtons.length) {
				$oldNgButtons.remove();
			}

			var $cloneNgButton = $ngButton.clone();
			var $cloneNgButtonMenu = $ngButtonMenu.clone();

			$cloneNgButton.hover(function () {
				$(this).css("color", "red");
			}, function () {
				$(this).css("color", "blue");
			});
			$cloneNgButton.on("click",function(){
				makeNgButtonMenu($(this));
			});
			$(this).hover(function () {
				$cloneNgButton.css("display", "inline");
				$cloneNgButton.siblings(".KOSHIAN_response_increase").css("display", "none");
				$cloneNgButton.siblings(".fvw_num").css("display", "none");
			}, function () {
				$cloneNgButton.css("display", "none");
				$cloneNgButtonMenu.css("display", "none");
				$cloneNgButton.siblings(".KOSHIAN_response_increase").css("display", "inline");
				$cloneNgButton.siblings(".fvw_num").css("display", "");
			});

			$cloneNgButton.append($cloneNgButtonMenu);
			$(this).append($cloneNgButton);
		});
	}

	/**
	 * NGボタンメニュー作成
	 * @param {jQuery} $button メニューを作成するNGボタンのjQueryオブジェクト
	 */
	function makeNgButtonMenu($button) {
		if (!isNgEnable) {
			return;
		}
		var $menu = $button.children(".GM_fcn_ng_menu");
		if (!$button.find(".GM_fcn_ng_menu_item").length) {
			// スレNG
			var $ngNumber = $("<div>", {
				class: "GM_fcn_ng_menu_item",
				text: "スレNG",
				css: {
					color: "blue",
					"background-color": "rgba(240, 224, 214, 0.95)",
				}
			});
			// 本文NG
			var $ngWordCommon = $("<div>", {
				class: "GM_fcn_ng_menu_item",
				text: "本文NG(共通)",
				css: {
					color: "blue",
					"background-color": "rgba(240, 224, 214, 0.95)",
				}
			});
			var $ngWordIndiv = $("<div>", {
				class: "GM_fcn_ng_menu_item",
				text: "本文NG(板別)",
				css: {
					color: "blue",
					"background-color": "rgba(240, 224, 214, 0.95)",
				}
			});
			// 画像NG
			var $ngImage = $("<div>", {
				class: "GM_fcn_ng_menu_item",
				text: "画像NG",
				css: {
					color: "blue",
					"background-color": "rgba(240, 224, 214, 0.95)",
				}
			});

			var $td = $button.parent();
			var threadNumber = $td.find("a:first").length ? $td.find("a:first").attr("href").slice(4,-4) : "";
			var threadImgObj = $td.find("img:first").length ? $td.find("img:first")[0] : "";
			var threadComment = $td.find("small:first, .GM_fth_pickuped_caption, .GM_fth_opened_caption").length ? $td.find("small:first, .GM_fth_pickuped_caption, .GM_fth_opened_caption").text() : "";

			var $cloneNgNumber = $ngNumber.clone();
			var $cloneNgWordCommon = $ngWordCommon.clone();
			var $cloneNgWordIndiv = $ngWordIndiv.clone();
			var $cloneNgImage = $ngImage.clone();

			$cloneNgNumber.hover(function () {
				$(this).css("color", "red");
				$(this).css("background-color", "rgba(204, 233, 255, 0.95)");
			}, function () {
				$(this).css("color", "blue");
				$(this).css("background-color", "rgba(240, 224, 214, 0.95)");
			});
			$cloneNgNumber.click(function () {
				addNgNumber(threadNumber);
				$td.addClass("GM_fcn_ng_numbers");
				$td.css("display","none");
				if ($td.hasClass("GM_fth_pickuped") || $td.hasClass("GM_fth_opened")) {
					hideNgThreads();
				}
			});

			$cloneNgWordCommon.hover(function () {
				$(this).css("color", "red");
				$(this).css("background-color", "rgba(204, 233, 255, 0.95)");
			}, function () {
				$(this).css("color", "blue");
				$(this).css("background-color", "rgba(240, 224, 214, 0.95)");
			});
			$cloneNgWordCommon.click(function () {
				var words = GM_getValue("_futaba_catalog_NG_words", "");
				words = addNgWord(words, threadComment);
				GM_setValue("_futaba_catalog_NG_words", words);
				$td.addClass("GM_fcn_ng_words");
				$td.css("display","none");
				if ($td.hasClass("GM_fth_pickuped") || $td.hasClass("GM_fth_opened")) {
					hideNgThreads();
				}
			});

			$cloneNgWordIndiv.hover(function () {
				$(this).css("color", "red");
				$(this).css("background-color", "rgba(204, 233, 255, 0.95)");
			}, function () {
				$(this).css("color", "blue");
				$(this).css("background-color", "rgba(240, 224, 214, 0.95)");
			});
			$cloneNgWordIndiv.click(function () {
				var words = getCurrentIndivValue("NG_words_indiv", "");
				words = addNgWord(words, threadComment);
				setIndivValue("NG_words_indiv", words);
				$td.addClass("GM_fcn_ng_words");
				$td.css("display","none");
				if ($td.hasClass("GM_fth_pickuped") || $td.hasClass("GM_fth_opened")) {
					hideNgThreads();
				}
			});

			$cloneNgImage.hover(function () {
				$(this).css("color", "red");
				$(this).css("background-color", "rgba(204, 233, 255, 0.95)");
			}, function () {
				$(this).css("color", "blue");
				$(this).css("background-color", "rgba(240, 224, 214, 0.95)");
			});
			$cloneNgImage.click(function () {
				hideNgImageThread(threadImgObj, threadComment, $td);
				if ($td.hasClass("GM_fth_pickuped") || $td.hasClass("GM_fth_opened") || USE_DHASH) {
					hideNgThreads();
				}
			});

			if (threadNumber) {
				$menu.append($cloneNgNumber);
			}
			if (threadComment) {
				$menu.append($cloneNgWordCommon);
				$menu.append($cloneNgWordIndiv);
			}
			if (threadImgObj && USE_NG_IMAGES) {
				$menu.append($cloneNgImage);
			}
		}

		var menuLeft = 0;
		var menuTop = $button.height();
		$menu.css("left", `${menuLeft}px`);
		$menu.css("top", `${menuTop}px`);
		$menu.css("display", "block");

		/**
		 * NGワード追加
		 * @param {string} ngWords 追加前のNGワード
		 * @param {string} newNgWord 追加するNGワード
		 * @return {string} 追加後のNGワード
		 */
		function addNgWord(ngWords, newNgWord) {
			newNgWord = newNgWord.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
			if (newNgWord && ngWords) {
				ngWords = newNgWord + "|" + ngWords;
			} else {
				ngWords += newNgWord;
			}
			return ngWords;
		}

		/**
		 * スレ画像NG
		 * @param {HTMLImageElement} imgObj NGにするスレ画像のimg要素
		 * @param {string} comment NG画像のコメント
		 * @param {jQuery} $td NGにするスレ画像の親td要素のjQueryオブジェクト
		 */
		function hideNgImageThread(imgObj, comment, $td) {
			var data = convertDataURI(imgObj);
			//console.log("futaba_catalog_NG - data: " + data);
			if (!data) {
				alert("スレ画像の取得に失敗しました");
				return;
			}
			var hexHash = md5(data);
			var dHash = convertDHash(imgObj, true);
			//console.log("futaba_catalog_NG - hexHash: " + hexHash);
			//console.log("futaba_catalog_NG - dHash: " + dHash.toString(16));
			addNgListObj("_futaba_catalog_NG_images", hexHash);
			addNgListObj("_futaba_catalog_NG_comment", comment);
			addNgListObj("_futaba_catalog_NG_date", getDate());
			addNgListObj("_futaba_catalog_NG_dHashes", dHash);
			if (isNgEnable) {
				$td.addClass("GM_fcn_ng_images");
				$td.css("display","none");
			}
			// 非NG画像リストからNG画像を削除
			var okImages = getCurrentIndivValue("OK_images_indiv", []);
			var imgNumber = parseInt($td.find("img").attr("src").match(/(\d+)s\.jpg$/)[1], 10);
			if (USE_DHASH) {
				// 近似画像NG使用時は非NG画像リストを全削除
				GM_setValue("OK_images_indiv", "{}");
			} else {
				var index = okImages.indexOf(imgNumber);
				if (index > -1) {
					okImages.splice(index, 1);
					setIndivValue("OK_images_indiv", okImages);
				}
			}

			/**
			 * NGリストにNGデータを追加
			 * @param {string} target NGデータを追加するNGリスト名
			 * @param {string} val NGリストへ追加するNGデータ
			 */
			function addNgListObj(target, val) {
				var ngListObj = GM_getValue(target, "");
				if (ngListObj === ""){
					ngListObj = [];
				}
				ngListObj.unshift(val);
				if (ngListObj.length > MAX_REGISTERED_NG_IMAGES) {
					ngListObj.splice(MAX_REGISTERED_NG_IMAGES);
				}
				GM_setValue(target, ngListObj);
			}
		}
	}

	/**
	 * NG番号追加
	 * @param {string} number 追加するNG番号
	 */
	function addNgNumber(number) {
		var ngNumberObj = getIndivObj("NG_numbers_indiv");
		if (ngNumberObj === ""){
			ngNumberObj = {};
		}
		if (!ngNumberObj[serverFullPath]) {
			ngNumberObj[serverFullPath] = [];
		}
		if (ngNumberObj[serverFullPath].indexOf(number) > -1) {
			return;
		}
		ngNumberObj[serverFullPath].push(number);
		var deleteCount = ngNumberObj[serverFullPath].length - MAX_NG_THREADS;
		if (deleteCount > 0) {
			ngNumberObj[serverFullPath].splice(0, deleteCount);
		}
		var jsonString = JSON.stringify(ngNumberObj);
		GM_setValue("NG_numbers_indiv", jsonString);
	}

	/**
	 * dataURI変換
	 * @param {HTMLImageElement} imgObj dataURIに変換する画像のimg要素
	 * @param {number} width 変換する画像の幅 未指定時は画像の本来の幅
	 * @param {number} height 変換する画像の高さ 未指定時は画像の本来の高さ
	 * @return {string} 変換したdataURI文字列
	 */
	function convertDataURI(imgObj, width = imgObj.naturalWidth, height = imgObj.naturalHeight){
		if (!imgObj || !imgObj.complete || !width || !height) {
			return;
		}
		// canvasを生成してimg要素を反映
		var cvs = document.createElement("canvas");
		cvs.width = width;
		cvs.height = height;
		var ctx = cvs.getContext("2d", {
			alpha: false	// 背景不透明で高速化
		});
		try {
			ctx.drawImage(imgObj, 0, 0);
		} catch (e) {
			console.error("futaba_catalog_NG - drawImage error: src= " + imgObj.src + ", " + e.name + ": " + e.message);
			console.dir(e);
			return;
		}
		// canvasをdataURI化
		var data;
		try {
			data = cvs.toDataURL("image/jpeg");
		} catch (e) {
			console.error("futaba_catalog_NG - dataURI convert error: src= " + imgObj.src + ", " + e.name + ": " + e.message);
			console.dir(e);
			return;
		}
		if (data.substr(0,23) !== "data:image/jpeg;base64,") {
			console.error("futaba_catalog_NG - dataURI abnormal: src= " + imgObj.src + ", dataURI= " + data);
			return;
		}
		return data;
	}

	/**
	 * dHash変換
	 * @param {HTMLImageElement} imgObj dHashに変換する画像のimg要素
	 * @param {boolean} force 近似画像NGが無効でも強制的にdHash変換する
	 * @return {number} 変換したdHash 0 ~ 2^49 - 1
	 *     変換できないときはnullを返す
	 *     オリジナルは64bitのHash値だがJavaScriptの最大整数値2^53に収める為49bitのHash値を返す
	 */
	function convertDHash(imgObj, force = false) {
		if ((!USE_DHASH && !force) || !imgObj || !imgObj.complete || !imgObj.naturalWidth || !imgObj.naturalHeight) {
			return null;
		}
		// 8x7のcanvasを生成してimg要素を反映
		var cvs = document.createElement("canvas");
		cvs.width = 8;
		cvs.height = 7;
		var ctx = cvs.getContext("2d", {
			alpha: false	// 背景不透明で高速化
		});
		try {
			ctx.drawImage(imgObj, 0, 0, imgObj.naturalWidth, imgObj.naturalHeight, 0, 0, cvs.width, cvs.height);
		} catch (e) {
			console.error("futaba_catalog_NG - drawImage error: src= " + imgObj.src + ", " + e.name + ": " + e.message);
			console.dir(e);
			return null;
		}
		var pixels = ctx.getImageData(0, 0, cvs.width, cvs.height);
		var grayScale = [];
		for (var y = 0; y < pixels.height; ++y) {
			for (var x = 0; x < pixels.width; ++x) {
				var i = (y * 4) * pixels.width + x * 4;
				var rgb = pixels.data[i] + pixels.data[i + 1] + pixels.data[i + 2];	// 画素をグレイスケール化
				grayScale.push(rgb);
			}
		}
		var dHash = 0;
		var exponent = 0;	// べき指数
		grayScale.forEach((v, i) => {
			if ((i + 1) % cvs.width !== 0) {	// 右端のピクセルは右隣が無いのでスキップ
				// 右隣の輝度と比較した結果を1bitとしてセット
				dHash += (v < grayScale[i + 1] ? 1 : 0) * 2 ** exponent;
				++exponent;
			}
		});
		//console.log("futaba_catalog_NG - imgObj.src: " + imgObj.src + ", dHash: 0x" + dHash.toString(16).toUpperCase());
		return dHash;
	}

	/**
	 * 日付取得
	 * @return {string} 現在の日付の文字列 yy/mm/dd
	 */
	function getDate() {
		var now = new Date();
		var date = ("" + (now.getFullYear())).slice(-2) + "/" +
			("0" + (now.getMonth() + 1)).slice(-2) + "/" +
			("0" + now.getDate()).slice(-2);
		return date;
	}

	/**
	 * カタログを検索してNGスレを非表示
	 * @param {boolean} isWordsChanged NGワードを変更したか
	 */
	function hideNgThreads(isWordsChanged) {
		if (!isNgEnable) {
			return;
		}
		var Start = new Date().getTime();//count parsing time
		var words = "";
		var wordsCommon = GM_getValue("_futaba_catalog_NG_words", "");
		var wordsIndiv = getCurrentIndivValue("NG_words_indiv", "");
		var numbers = getCurrentIndivValue("NG_numbers_indiv", []);
		images = GM_getValue("_futaba_catalog_NG_images", []);
		ngDate = GM_getValue("_futaba_catalog_NG_date", []);
		okImages = getCurrentIndivValue("OK_images_indiv", []);
		dHashes = GM_getValue("_futaba_catalog_NG_dHashes", []);

		// NGワード
		if( wordsCommon !== "" ) {
			words += wordsCommon;
			if( wordsIndiv !== "" ) {
				words += "|" + wordsIndiv;
			}
		}
		else {
			words += wordsIndiv;
		}
		//console.log(words);
		//console.dir(numbers);
		//console.dir("futaba_catalog_NG - images.length: " + images.length);
		try {
			var re = new RegExp(words, "i");
		}
		catch (e) {
			alert("NGワードのパターンが無効です\n\n" + e);
			editNgWords();
			return;
		}
		if (isWordsChanged) {
			$(".GM_fcn_ng_words").css("display", "");
			$(".GM_fcn_ng_words").removeClass("GM_fcn_ng_words");
		}
		if (words !== "") {
			$("#cattable td small").each(function() {
				if (re.test($(this).text())) {
					if ($(this).parent("a").length) {		//文字スレ
						$(this).parent().parent("td").addClass("GM_fcn_ng_words");
						$(this).parent().parent("td").css("display", "none");
					} else {
						$(this).parent("td").addClass("GM_fcn_ng_words");
						$(this).parent("td").css("display", "none");
					}
					//console.log("futaba catalog NG - caption: " + $(this).text() + " NG word: " + $(this).text().match(re)[0]);
				}
			});
		}
		if (isWordsChanged) {
			console.log("futaba_catalog_NG - Parsing@" + serverFullPath + ": "+((new Date()).getTime()-Start) +"msec");	// eslint-disable-line no-console
			return;
		}

		// NG番号
		if (numbers.length) {
			var $catAnchors = $("#cattable td[class!='GM_fcn_ng_words'] > a:first-of-type");
			$catAnchors.each(function() {
				var hrefNum = $(this).attr("href").slice(4,-4);
				if (numbers.indexOf(hrefNum) > -1){
					$(this).closest("td, .cs").addClass("GM_fcn_ng_numbers");
					$(this).closest("td, .cs").css("display", "none");
				}
			});
		}

		// NG画像
		if (USE_NG_IMAGES) {
			if (images.length) {
				var $catImages = $("#cattable td:not([class^='GM_fcn_ng_']) > a:first-of-type > img");
				$catImages.each(function() {
					var $td = $(this).closest("td");
					var imgSrc = this.src.match(/(\d+)s\.jpg$/);
					if (imgSrc) {
						var imgNumber = parseInt(imgSrc[1], 10);
						if (okImages.indexOf(imgNumber) == -1) {
							var data = convertDataURI(this);
							if (data) {
								var hexHash = md5(data);
								var index = images.indexOf(hexHash);
								if (index > -1){
									$td.addClass("GM_fcn_ng_images");
									$td.css("display", "none");
									ngDate[index] = getDate();
									if (dHashes[index] == null) {
										dHashes[index] = convertDHash(this, true);
									}
								} else {
									var isError = false;	// md5変換エラーフラグ
									if (hexHash.length !== 32) {
										isError = true;
										console.error("futaba_catalog_NG - hexHash abnormal: image No." + imgNumber + ", hexHash: " + hexHash);	// eslint-disable-line no-console
									}
									// dHash判定
									var distance, dHash = convertDHash(this);
									[index, distance] = findIndexOfDHashes(dHash);
									if (index > -1) {
										if (ENABLE_DHASH_TEST) {
											$(this).addClass("GM_fcn_ng_dhash_img");
											$td.children(".GM_fcn_ng_button").attr("title", "distance: " + distance + ", dHash: " + ("000000000000" + dHash.toString(16)).slice(-13));
											$td.addClass("GM_fcn_ng_dhash_td");
										} else {
											$td.addClass("GM_fcn_ng_images");
											$td.css("display", "none");
										}
										ngDate[index] = getDate();
									} else if (!isError) {
										okImages.unshift(imgNumber);
									}
								}
							} else {
								// スレ画像読込完了確認
								this.onload = () => {
									this.onload = null;
									var imgNumber = parseInt(this.src.match(/(\d+)s\.jpg$/)[1], 10);
									var data = convertDataURI(this);
									if (data) {
										var hexHash = md5(data);
										var index = images.indexOf(hexHash);
										if (index > -1){
											$td.addClass("GM_fcn_ng_images");
											$td.css("display", "none");
											ngDate[index] = getDate();
											GM_setValue("_futaba_catalog_NG_date", ngDate);
											if (dHashes[index] == null) {
												dHashes[index] = convertDHash(this, true);
												GM_setValue("_futaba_catalog_NG_dHashes", dHashes);
											}
										} else {
											var isError = false;	// md5変換エラーフラグ
											if (hexHash.length !== 32) {
												isError = true;
												console.error("futaba_catalog_NG - hexHash abnormal: image No." + imgNumber + ", hexHash: " + hexHash);
											}
											// dHash判定
											var distance, dHash = convertDHash(this);
											[index, distance] = findIndexOfDHashes(dHash);
											if (index > -1) {
												if (ENABLE_DHASH_TEST) {
													$(this).addClass("GM_fcn_ng_dhash_img");
													$td.children(".GM_fcn_ng_button").attr("title", "distance: " + distance + ", dHash: " + ("000000000000" + dHash.toString(16)).slice(-13));
													$td.addClass("GM_fcn_ng_dhash_td");
												} else {
													$td.addClass("GM_fcn_ng_images");
													$td.css("display", "none");
												}
												ngDate[index] = getDate();
												GM_setValue("_futaba_catalog_NG_date", ngDate);
											} else if (!isError) {
												okImages.unshift(imgNumber);
											}
										}
									} else {
										console.error("futaba_catalog_NG - image data abnormal: image No." + imgNumber);
									}
								};
								if (this.complete && this.width && this.height && this.onload) {
									// onloadセット中に画像読込完了していたらloadをトリガーする
									$(this).trigger("load");
								}
							}
						}
					}
				});
				GM_setValue("_futaba_catalog_NG_date", ngDate);
				GM_setValue("_futaba_catalog_NG_dHashes", dHashes);
				if (okImages.length > MAX_OK_IMAGES) {
					okImages.splice(MAX_OK_IMAGES);
				}
				setIndivValue("OK_images_indiv", okImages);
			} else {
				var $catImg = $("#cattable td a img");
				$catImg.each(function() {
					var imgSrc = this.src.match(/(\d+)s\.jpg$/);
					if (imgSrc) {
						var imgNumber = parseInt(imgSrc[1], 10);
						if (okImages.indexOf(imgNumber) == -1) {
							okImages.unshift(imgNumber);
						}
					}
				});
				if (okImages.length > MAX_OK_IMAGES) {
					okImages.splice(MAX_OK_IMAGES);
				}
				setIndivValue("OK_images_indiv", okImages);
			}
		}
		//console.log("futaba_catalog_NG - okImages.length: " + okImages.length);
		console.log("futaba_catalog_NG - Parsing@" + serverFullPath + ": " + ((new Date()).getTime() - Start) + "msec");	// eslint-disable-line no-console

		/**
		 * dHashリストのインデックス探索
		 * @param {number} dHash 近似画像NG判定する画像のdHash値
		 * @return {Array.<number>} [i, distance]
		 *     {number} i 近似度が閾値以下のdHashリスト(dHashes)配列内のインデックス
		 *     {number} distance ハミング距離
		 *     dHashリストに該当が無ければi, distance共に-1を返す
		 */
		function findIndexOfDHashes(dHash) {
			if (dHash != null) {
				for (var i = 0, num = dHashes.length; i < num; ++i) {
					if (dHashes[i]) {
						var distance = getHammingDistance(dHash, dHashes[i]);
						if (distance <= DISTANCE_THRESHOLD) {
							if (ENABLE_DHASH_TEST) {
								console.debug("futaba_catalog_NG - catalog dHash: " + ("000000000000" + dHash.toString(16)).slice(-13) + ", NG list dHash: " + ("000000000000" + dHashes[i].toString(16)).slice(-13));
								console.debug("futaba_catalog_NG - hamming distance: " + distance);
							}
							return [i, distance];
						}
					}
				}
			}
			return [-1, -1];
		}

		/**
		 * ハミング距離取得
		 * @param {number} hash1 測定するHash(49bit)
		 * @param {number} hash2 測定するHash(49bit)
		 * @return {number} ハミング距離(0~49)
		 */
		function getHammingDistance(hash1, hash2) {
			// 2つのHashを上位17bitと下位32bitに分割して32ビット演算する
			var hash1L = hash1 >>> 0;
			var hash1H = (hash1 - hash1L) / 0x100000000;
			var hash2L = hash2 >>> 0;
			var hash2H = (hash2 - hash2L) / 0x100000000;

			// 下位32bitのビットの異なる位置を抽出
			var xorL = hash1L ^ hash2L;
			var count = 0;
			// 立っている最下位ビットを消してカウント
			while (xorL) {
				xorL &= xorL - 1;
				++count;
			}

			// 上位17bitのビットの異なる位置を抽出
			var xorH = hash1H ^ hash2H;
			// 立っている最下位ビットを消してカウント
			while (xorH) {
				xorH &= xorH - 1;
				++count;
			}

			return count;
		}
	}

	/**
	 * KOSHIAN del イベント監視
	 */
	function listenKoshianDelEvent() {
		$(document).on("KOSHIAN_del", () => {
			// delされたスレをNG登録して非表示
			$(".KOSHIAN_del").each(function() {
				var threadNumber = $(this).find("a:first").length ? $(this).find("a:first").attr("href").slice(4,-4) : "";
				if (threadNumber) {
					addNgNumber(threadNumber);
				}
				if (isNgEnable) {
					$(this).addClass("GM_fcn_ng_numbers");
					$(this).css("display", "none");
				}
				$(this).removeClass("KOSHIAN_del");
			});
			hideNgThreads();
		});
	}

	/**
	 * futaba thread highlighter K ピックアップイベント監視
	 */
	function listenFthPickupEvent() {
		$(document).on("FutabaTH_pickup", () => {
			// ピックアップされたスレにNGボタンをセット
			$(".GM_fth_pickuped, .GM_fth_opened").each(function() {
				var $ngButton = $(this).find(".GM_fcn_ng_button");
				if ($ngButton.length) {
					var $ngButtonMenu = $ngButton.children(".GM_fcn_ng_menu");
					$ngButton.hover(function() {
						$(this).css("color", "red");
					}, function () {
						$(this).css("color", "blue");
					});
					$ngButton.on("click",function() {
						makeNgButtonMenu($ngButton);
					});
					$(this).hover(function () {
						$ngButton.css("display", "inline");
						$ngButton.siblings(".KOSHIAN_response_increase").css("display", "none");
						$ngButton.siblings(".fvw_num").css("display", "none");
					}, function () {
						$ngButton.css("display", "none");
						$ngButtonMenu.css("display", "none");
						$ngButton.siblings(".KOSHIAN_response_increase").css("display", "inline");
						$ngButton.siblings(".fvw_num").css("display", "");
					});
				}
			});
		});
	}

	/**
	 * カタログ非表示
	 */
	function hideCatalog() {
		setCatalogHiddenStyle();
		$(function() {
			$("body").attr("__fcn_catalog_visibility", "hidden");
			$("#GM_fth_highlighted_threads").css("visibility", "hidden");
			init();
		});
		$(window).on("load", function() {
			$("body").attr("__fcn_catalog_visibility", "visible");
			setCatalogShownStyle();
			$("#GM_fth_highlighted_threads").css("visibility", "visible");
		});
	}

	/**
	 * カタログ非表示スタイル設定
	 */
	function setCatalogHiddenStyle() {
		var css =
			"#cattable {" +
			"  opacity: 0;" +
			"}";
		GM_addStyle(css);
	}

	/**
	 * カタログ表示スタイル設定
	 */
	function setCatalogShownStyle() {
		var css =
			"#cattable {" +
			"  opacity: 1;" +
			"}";
		GM_addStyle(css);
	}

	/**
	 * スタイル設定
	 */
	function setStyle() {
		var css =
			// NGワード
			".GM_fcn_ng_words {" +
			"  display: none;" +
			"}" +
			// NG番号
			".GM_fcn_ng_numbers {" +
			"  display: none;" +
			"}" +
			// NG画像
			".GM_fcn_ng_images {" +
			"  display: none;" +
			"}" +
			// 近似画像NGスレ
			".GM_fcn_ng_dhash_td {" +
			"  background-color: #e1b2ec;" +
			"  opacity: 0.2;" +
			"}" +
			".GM_fcn_ng_dhash_td:hover {" +
			"  opacity: 1;" +
			"}" +
			// 近似NG画像
			".GM_fcn_ng_dhash_img {" +
			"  opacity: 0.2;" +
			"}" +
			".GM_fcn_ng_dhash_img:hover {" +
			"  opacity: 1;" +
			"}" +
			// NGボタン
			".GM_fcn_ng_button {" +
			"  position: relative;" +
			"  font-size: 12px;" +
			"  cursor: pointer;" +
			"}" +
			// NGメニュー
			".GM_fcn_ng_menu {" +
			"  font-size: medium;" +
			"  background-color: rgba(240, 192, 214, 0.95);" +
			"  z-index: 203;" +
			"  position: absolute;" +
			"  min-width: 140px;" +
			"  width: auto;" +
			"  border: 1px outset;" +
			"  border-radius: 5px;" +
			"  padding: 5px;" +
			"}" +
			// NGメニュー項目
			".GM_fcn_ng_menu_item {" +
			"  padding: 5px;" +
			"  z-index: 203;" +
			"  cursor: pointer;" +
			"}" +
			// NGリストラベル
			".GM_fcn_ng_list_label {" +
			"  display: inline-block;" +
			"  width: 100px;" +
			"}" +
			// NGリスト入力
			".GM_fcn_ng_list_input {" +
			"  width: 360px;" +
			"  margin-right: 16px;" +
			"  font-size: 16px;" +
			"}" +
			// NGリスト編集ボタン
			".GM_fcn_ng_list_edit_button {" +
			"  width: 70px;" +
			"  margin-right: 16px;" +
			"}" +
			// NGリスト移動ボタン
			".GM_fcn_ng_list_move_button {" +
			"  margin-right: 16px;" +
			"}" +
			// NGリスト枠
			"#GM_fcn_ng_list_pane {" +
			"  width: 928px;" +
			"  height: 308px;" +
			"  margin-left: 11px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  background-color: #eee;" +
			"}" +
			// NGリスト項目行
			"#GM_fcn_ng_list_item_row {" +
			"  display: inline-block;" +
			"  height: 22px;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"}" +
			// NGリスト項目md5
			"#GM_fcn_ng_list_item_md5 {" +
			"  width: 360px;" +
			"}" +
			// NGリスト項目dHash
			"#GM_fcn_ng_list_item_dHash {" +
			"  width: 170px;" +
			"}" +
			// NGリスト項目コメント
			"#GM_fcn_ng_list_item_comment {" +
			"  width: 250px;" +
			"}" +
			// NGリスト項目最終検出日
			"#GM_fcn_ng_list_item_date {" +
			"  width: 130px;" +
			"}" +
			// NGリスト項目スクロールバースペース
			"#GM_fcn_ng_list_item_scrl {" +
			"  width: 18px;" +
			"}" +
			// NGリスト項目
			".GM_fcn_ng_list_item {" +
			"  display: inline-block;" +
			"  height: 22px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"  text-overflow: clip;" +
			"  cursor: pointer;" +
			"}" +
			// NGリストコンテンツ
			"#GM_fcn_ng_list_content {" +
			"  width: 928px;" +
			"  height: 286px;" +
			"  overflow-x: hidden;" +
			"  overflow-y: auto;" +
			"}" +
			// NGリスト行
			".GM_fcn_ng_list_row {" +
			"  width: 928px;" +
			"  height: 22px;" +
			"  cursor: pointer;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"}" +
			// NGリスト画像
			".GM_fcn_ng_list_image {" +
			"  display: inline-block;" +
			"  width: 360px;" +
			"  height: 22px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"  text-overflow: ellipsis;" +
			"  font-family: Consolas, 'Courier New', monospace;" +
			"}" +
			// NGリストdHash
			".GM_fcn_ng_list_dHash {" +
			"  display: inline-block;" +
			"  width: 170px;" +
			"  height: 22px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"  text-overflow: ellipsis;" +
			"  font-family: Consolas, 'Courier New', monospace;" +
			"}" +
			// NGリストコメント
			".GM_fcn_ng_list_comment {" +
			"  display: inline-block;" +
			"  width: 250px;" +
			"  height: 22px;" +
			"  padding-left: 10px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  text-align: left;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"  text-overflow: ellipsis;" +
			"}" +
			// NGリスト日時
			".GM_fcn_ng_list_date {" +
			"  display: inline-block;" +
			"  width: 130px;" +
			"  height: 22px;" +
			"  border-width: 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"  text-overflow: ellipsis;" +
			"  font-family: Consolas, 'Courier New', monospace;" +
			"}" +
			// NGリストスクロールバー
			".GM_fcn_ng_list_scrl {" +
			"  display: inline-block;" +
			"  width: 18px;" +
			"  height: 22px;" +
			"  border-width: 0px 1px;" +
			"  border-style: solid;" +
			"  box-sizing: border-box;" +
			"  overflow: hidden;" +
			"  white-space: nowrap;" +
			"}" +
			// カタログ下スペース
			"#GM_fcn_catalog_space {" +
			"  min-height: 2000px;" +
			"}" +
			// futaba thread highlighter ピックアップ
			".GM_fth_pickuped {" +
			"  overflow: visible !important;" +
			"}" +
			// futaba thread highlighter 既読
			".GM_fth_opened {" +
			"  overflow: visible !important;" +
			"}" +
			// ふたクロNGボタン
			".fvw_ng {" +
			"  display: none !important;" +
			"}" +
			// スレのプルダウンメニューボタン用スペース
			"#cattable > tbody > tr > td {" +
			"  padding-bottom: 12px !important;" +
			"}";
		GM_addStyle(css);
	}

})(jQuery);