futaba_catalog_NG

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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/[email protected]/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);