NUtools

Tools to interact with novelupdates.com site.

2018-08-13 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         NUtools
// @namespace    JDoe_NUtoolsV2
// @version      6
// @description  Tools to interact with novelupdates.com site.
// @author       John Doe
// @match        http*://*.novelupdates.com/
// @match        http*://*.novelupdates.com/?pg=*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery.serializeJSON/2.9.0/jquery.serializejson.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-popup-overlay/1.7.13/jquery.popupoverlay.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/alasql/0.4.5/alasql.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==
((() => {
	const VERSION = 6;
	const DBVERSION = 5;
	const DEBUG = false;
	const DEFAULTCONFIGS = {
		cfg_move_to_list_id: 1,
		cfg_move_req_confirm: 0,
		cfg_cover_show_icon: 1,
		cfg_move_reload: 1
	};
	const LANGS_OPTIONS = [
		{
			isoAlpha3: "CHN",
			isoAlpha2: "CN",
			m49: 156,
			aliases: [156, "cn", "chn", "china", "chinese", "mandarim", "cantonese"]
		},
		{
			isoAlpha3: "JPN",
			isoAlpha2: "JP",
			m49: 392,
			aliases: ["jp", "jpn", "japan", "japanese"]
		},
		{
			isoAlpha3: "PHL",
			isoAlpha2: "PH",
			m49: 608,
			aliases: ["ph", "phl", "philippines", "filipino"]
		},
		{
			isoAlpha3: "IDN",
			isoAlpha2: "ID",
			m49: 360,
			aliases: ["id", "idn","indonesia", "indonesian"]
		},
		{
			isoAlpha3: "KHM",
			isoAlpha2: "KH",
			m49: 116,
			aliases: ["kh", "khm", "cambodia", "cambodian", "khmer"]
		},
		{
			isoAlpha3: "KOR",
			isoAlpha2: "KR",
			m49: 408,
			aliases: ["kr", "kor", "korea", "korean", 410, "prk","kp"]
		},
		{
			isoAlpha3: "MYS",
			isoAlpha2: "MY",
			m49: 458,
			aliases: ["my", "mys", "malaysia", "malaysian"]
		},
		{
			isoAlpha3: "THA",
			isoAlpha2: "TH",
			m49: 764,
			aliases: ["th", "tha", "thailand", "thai"]
		},
		{
			isoAlpha3: "VNM",
			isoAlpha2: "VN",
			m49: 704,
			aliases: ["vn", "vnm", "viet nam", "vietnamese"]
		}
	];
	const cssFiles = [
		"https://fonts.googleapis.com/icon?family=Material+Icons",
		"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css"
	];
	const htmlStyles = `
<style type="text/css">
/* Rules for sizing the icon. */
.material-icons.md-12 { font-size: 12px; }
.material-icons.md-18 { font-size: 18px; }
.material-icons.md-20 { font-size: 20px; }
.material-icons.md-24 { font-size: 24px; }
.material-icons.md-36 { font-size: 36px; }
.material-icons.md-48 { font-size: 48px; }
/* Rules for using icons as black on a light background. */
.material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
.material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
/* Rules for using icons as white on a dark background. */
.material-icons.md-light { color: rgba(255, 255, 255, 1); }
.material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
.material-icons {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	vertical-align: middle;
}
/* ################################  */
.js-nutools-hidden { display:none; }
.js-nutools-show-cover,.js-nutools-move-to-list { cursor: pointer; }
#js-nutools-settings-overlay {
	-webkit-transform: scale(0.8);
	-moz-transform: scale(0.8);
	-ms-transform: scale(0.8);
	transform: scale(0.8);
}
.popup_visible #js-nutools-settings-overlay {
	-webkit-transform: scale(1);
	-moz-transform: scale(1);
	-ms-transform: scale(1);
	transform: scale(1);
}
#js-nutools-settings-overlay fieldset {
	border: 3px solid #1F497D;
	background: #ddd;
	border-radius: 2px;
	padding: 5px;
	margin-top: 30px;
}
#js-nutools-settings-overlay fieldset legend {
	background: #1F497D;
	color: #fff;
	padding: 5px 20px ;
	font-size: 20px;
	border-radius: 5px;
	box-shadow: 0 0 0 1px #ddd;
	margin-left: 20px;
}
.js-nutools-well{
	min-height:20px;
	padding:19px;
	margin-bottom:20px;
	background-color:#f5f5f5;
	border:1px solid #e3e3e3;
	border-radius:4px;
	-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);
	box-shadow:inset 0 1px 1px rgba(0,0,0,.05)
}
.js-nutools-well blockquote{
	border-color:#ddd;
	border-color:rgba(0,0,0,.15)
}
.js-nutools-well-lg{
	padding:24px;
	border-radius:6px
}
.js-nutools-well-sm{
	padding:9px;
	border-radius:3px
}
</style>
`;
	const htmlCPbutton = `
<div class="">
	<p><button id="js-nutools-open-userscript-cp"><i class="material-icons md-18">settings</i>NUtools Settings</button></p>
	<p><button id="js-nutools-get-language-button">Add language label</button></p>
</div>
`;
	const htmlPageAppend = `
<div class="js-nutools-hidden">
<!-- NUtools overlay -->

<div id="js-nutools-move-confirm-overlay" class="js-nutools-well">
<div class="message">
Move '<b class="novel-title"></b>' to reading list [ <b class="reading-list-id"></b> ]?
</div>
<br><br>
<center>
<button type="button" class="js-nutools-move-confirm-overlay_close">Cancel</button>
<button type="button" id="js-nutools-move-confirm-overlay-move-button" data-reading-list-id="" data-novel-id="">Move</button>
</center>
</div>

<div id="js-nutools-get-language-confirm-overlay" class="js-nutools-well">
<div class="message">
<h3>Warning</h3>
<p>This action will create multiple requests to website and it is very intensive and take a long time to complete. Use it with extreme moderation.</p>
</div>
<br><br>
<center>
<button type="button" class="js-nutools-get-language-confirm-overlay_close">Cancel</button>
<button type="button" id="js-nutools-get-language-confirm-button">Get language</button>
</center>
</div>

<div id="js-nutools-cover-overlay" class="js-nutools-well">
</div>

<div id="js-nutools-settings-overlay" class="js-nutools-well">
<form id="settings_form">
<h3>Settings:</h3>
<fieldset>
<legend><i class="material-icons">format_indent_increase</i> Reading List</legend>
	<p> Move to <a href="https://www.novelupdates.com/reading-list/">Reading List ID</a> :
	<select name="cfg_move_to_list_id" id="cfg_move_to_list_id">
	</select>
</p>
	<p>Require confirmation before moving to list? :<br>
<input type="radio" name="cfg_move_req_confirm" value="1"> Yes
<input type="radio" name="cfg_move_req_confirm" value="0" checked="checked"> No
</p>
	<p>Reload page after moving:<br>
<input type="radio" name="cfg_move_reload" value="1"> Yes
<input type="radio" name="cfg_move_reload" value="0" checked="checked"> No
</p>

</fieldset>
<fieldset>
<legend><i class="material-icons">photo</i> Cover </legend>
	</p>Show icon ? :<br>
<input type="radio" name="cfg_cover_show_icon" value="1"> Yes
<input type="radio" name="cfg_cover_show_icon" value="0" checked="checked"> No</li>
	</p>
</fieldset>

<center>
<button type="button" class="js-nutools-settings-overlay_close">Close</button>
<button type="submit" class="js-nutools-settings-overlay_save">Save</button>
</center>
</form>
<style> input:invalid { border-color: #DD2C00; }</style>

</div>
<!-- /NUtools overlay -->
</div>
`;

	// functions
	let storage = {
		options: {
			prefix: ""
		},
		// “Set” means “add if absent, replace if present.”
		set: function(key, value) {
			let storageVals = this.read(key);

			if (typeof storageVals === "undefined" || !storageVals) {
				// add if absent
				return this.add(key, value);
			} else {
				// replace if present
				this.write(key, value);
				return true;
			}
		},
		// “Add” means “add if absent, do nothing if present” (if a uniquing collection).
		add: function(key, value) {
			let storageVals = this.read(key, false);

			if (typeof storageVals === "undefined" || !storageVals) {
				this.write(key, value);
				return true;
			} else {
				if (this._isArray(storageVals)) { // is array
					let index = storageVals.indexOf(value);

					if (index !== -1) {
						// do nothing if present
						return false;
					} else {
						// add if absent
						storageVals.push(value);
						this.write(key, storageVals);
						return true;
					}
				} else if (this._isObject(storageVals)) { // is object
					// merge obj value on obj
					let result;
					let objToMerge = value;

					result = Object.assign(storageVals, objToMerge);
					this.write(key, result);
					return false;
				}
				return false;
			}
		},
		// “Replace” means “replace if present, do nothing if absent.”
		replace: function(key, itemFind, itemReplacement) {
			let storageVals = this.read(key, false);

			if (typeof storageVals === "undefined" || !storageVals) {
				// do nothing if absent
				return false;
			} else {
				if (this._isArray(storageVals)) { // is Array
					let index = storageVals.indexOf(itemFind);

					if (index !== -1) {
						// replace if present
						storageVals[index] = itemReplacement;
						this.write(key, storageVals);
						return true;
					} else {
						// do nothing if absent
						return false;
					}
				} else if (this._isObject(storageVals)) {
					// is Object
					// replace property's value
					storageVals[itemFind] = itemReplacement;
					this.write(key, storageVals);
					return true;
				}
				return false;
			}
		},
		// “Remove” means “remove if present, do nothing if absent.”
		remove: function(key, value) {
			if (typeof value === "undefined") { // remove key
				this.delete(key);
				return true;
			} else { // value present
				let storageVals = this.read(key);

				if (typeof storageVals === "undefined" || !storageVals) {
					return true;
				} else {
					if (this._isArray(storageVals)) { // is Array
						let index = storageVals.indexOf(value);

						if (index !== -1) {
							// remove if present
							storageVals.splice(index, 1);
							this.write(key, storageVals);
							return true;
						} else {
							// do nothing if absent
							return false;
						}
					} else if (this._isObject(storageVals)) { // is Object
						let property = value;

						delete storageVals[property];
						this.write(key, storageVals);
						return true;
					}
					return false;
				}
			}
		},
		get: function(key, defaultValue) {
			return this.read(key, defaultValue);
		},
		// GM storage API
		read: function(key, defaultValue) {
			return this.unserialize(GM_getValue(this._prefix(key), defaultValue));
		},
		write: function(key, value) {
			return GM_setValue(this._prefix(key), this.serialize(value));
		},
		delete: function(key) {
			return GM_deleteValue(this._prefix(key));
		},
		readKeys: function() {
			return GM_listValues();
		},
		// /GM Storage API
		getAll: function() {
			const keys = this._listKeys();
			let obj = {};

			for (let i = 0, len = keys.length; i < len; i++) {
				obj[keys[i]] = this.read(keys[i]);
			}
			return obj;
		},
		getKeys: function() {
			return this._listKeys();
		},
		getPrefix: function() {
			return this.options.prefix;
		},
		empty: function() {
			const keys = this._listKeys();

			for (let i = 0, len = keys.lenght; i < len; i++) {
				this.delete(keys[i]);
			}
		},
		has: function(key) {
			return this.get(key) !== null;
		},
		forEach: function(callbackFunc) {
			const allContent = this.getAll();

			for (let prop in allContent) {
				callbackFunc(prop, allContent[prop]);
			}
		},
		unserialize: function(value) {
			if (this._isJson(value)) {
				return JSON.parse(value);
			}
			return value;
		},
		serialize: function(value) {
			if (this._isJson(value)) {
				return JSON.stringify(value);
			}
			return value;
		},
		_listKeys: function(usePrefix = false) {
			const prefixed = this.readKeys();
			let unprefixed = [];

			if (usePrefix) {
				return prefixed;
			} else {
				for (let i = 0, len = prefixed.length; i < len; i++) {
					unprefixed[i] = this._unprefix(prefixed[i]);
				}
				return unprefixed;
			}
		},
		_prefix: function(key) {
			return this.options.prefix + key;
		},
		_unprefix: function(key) {
			return key.substring(this.options.prefix.length);
		},
		_isJson: function(item) {
			try {
				JSON.parse(item);
			} catch (e) {
				return false;
			}
			return true;
		},
		_isObject: function(a) {
			return (!!a) && (a.constructor === Object);
		},
		_isArray: function(a) {
			return (!!a) && (a.constructor === Array);
		}
	};

	function isObject(val) {
		if (val === null) {
			return false;
		}
		return ((typeof val === "function") || (typeof val === "object"));
	}

	function setDebug(isDebug = false) {
		if (isDebug) {
			window.debug = window.console.log.bind(window.console, "%s: %s");
		} else {
			window.debug = function() {};
			window.console.log = function() {};
		}
	}

	function appendFilesToHead(arr = [], forceExt = false) {

		for (let i = 0; i < arr.length; i++) {
			let urlStr = arr[i];
			let ext = (forceExt) ? forceExt : urlStr.slice((Math.max(0, urlStr.lastIndexOf(".")) || Infinity) + 1);
			let ele = null;

			switch (ext) {
				case "js":
					ele = document.createElement("script");
					ele.type = "text/javascript";
					ele.src = urlStr;
					break;
				case "css":
					ele = document.createElement("link");
					ele.rel = "stylesheet";
					ele.type = "text/css";
					ele.href = urlStr;
					break;
				default:
					ele = document.createElement("script");
					ele.type = "text/javascript";
					ele.src = urlStr;
			}
			document.getElementsByTagName("head")[0].appendChild(ele);
		}
	}

	function onlyUnique(value, index, self) {
		return self.indexOf(value) === index;
	}

	function decodeHtml(html) {
		let txt = document.createElement("textarea");
		txt.innerHTML = html;
		return txt.value;
	}
	function get_lang_code(langStr) {
		let niddle = langStr.toString().trim().toLowerCase();
		for(let i=0,l=LANGS_OPTIONS.length; i<l; i++) {
			let aliases = LANGS_OPTIONS[i].aliases;
			if (aliases.indexOf(niddle) > -1) {
				return LANGS_OPTIONS[i].isoAlpha3;
			}
		}
		return "";
	}

	function readConfig() {
		let configs = storage.get("configs", DEFAULTCONFIGS);

		configs = Object.assign({}, DEFAULTCONFIGS, configs);
		debug("loading", JSON.stringify(configs));
		return configs;
	}

	function saveConfig(args) {
		args.cfg_move_to_list_id = (args.cfg_move_to_list_id=="---") ? "" : args.cfg_move_to_list_id;
		let configs = {
			cfg_cover_show_icon: args.cfg_cover_show_icon,
			cfg_move_to_list_id: args.cfg_move_to_list_id,
			cfg_move_req_confirm: args.cfg_move_req_confirm,
			cfg_move_reload: args.cfg_move_reload
		};
		storage.set("configs", configs);
		debug("saving", JSON.stringify(configs));
		return configs;
	}

	function sys_check_dbversion() {
		let version = storage.get("sys_dbversion", 0);
		if (version < 5) {
			_upgrade_v5();
			storage.set("sys_dbversion", 5)
		}
		return;
	}

	function _upgrade_v5() {
		res = mybase.exec("SELECT * FROM novels WHERE lang='N/A' ");
		res.forEach(function(arr) {
			let id = arr.id;
			mybase.exec("DELETE FROM novels WHERE id='"+ id +"' ");
		});
		mybase.exec("UPDATE novels SET lang='' WHERE lang NOT IN ('(JP)','(CN)','(KR)')");
		mybase.exec("UPDATE novels SET lang='JPN' WHERE lang='(JP)'");
		mybase.exec("UPDATE novels SET lang='CHN' WHERE lang='(CN)'");
		mybase.exec("UPDATE novels SET lang='KOR' WHERE lang='(KR)'");
		debug("saving db to localstorage", alasql.databases.mybase.tables.novels.data);
		storage.set("noveldb", alasql.databases.mybase.tables.novels.data);
	};

	function db_init() {
		storage_novel_db = storage.get("noveldb", []);
		mybase = new alasql.Database("mybase");
		mybase.exec("CREATE TABLE novels (id INT, title STRING, lang STRING)");
		debug("localstorage novel db data .lenght", storage_novel_db.length);
		if (storage_novel_db.length >= 1) {
			//debug("direct assign data to:", "alasql.databases.mybase.tables.novels.data");
			alasql.databases.mybase.tables.novels.data = storage_novel_db;
		}
	}

	// https://stackoverflow.com/questions/7298364/using-jquery-and-json-to-populate-forms
	function populateForm($form, data) {
		$.each(data, (key, value) => {// all json fields ordered by name
			let $ctrls = $form.find("[name='" + key + "']"); //all form elements for a name. Multiple checkboxes can have the same name, but different values
			if ($ctrls.is("select")) { //special form types
				$("option", $ctrls).each(function() {
					if (this.value == value) {
						this.selected = true;
					}
				});
			} else if ($ctrls.is("textarea")) {
				$ctrls.val(value);
			} else {
				switch ($ctrls.attr("type")) { //input type
					case "text":
					case "hidden":
						$ctrls.val(value);
						break;
					case "radio":
						if ($ctrls.length >= 1) {
							$.each($ctrls, function(index) { // every individual element
								let elemValue = $(this).attr("value");
								let singleVal = value;
								let elemValueInData = singleVal;
								if (elemValue === value) {
									$(this).prop("checked", true);
								} else {
									$(this).prop("checked", false);
								}
							});
						}
						break;
					case "checkbox":
						if ($ctrls.length > 1) {
							$.each($ctrls, function(index) { // every individual element
								let elemValue = $(this).attr("value");
								let elemValueInData;
								let singleVal;
								for (let i = 0; i < value.length; i++) {
									singleVal = value[i];
									debug("singleVal", singleVal + " value[i][1]" + value[i][1]);
									if (singleVal === elemValue) {
										elemValueInData = singleVal;
									}
								}
								if (elemValueInData) {
									$(this).prop("checked", true);
									//$(this).prop("value", true);
								} else {
									$(this).prop("checked", false);
									//$(this).prop("value", false);
								}
							});
						} else if ($ctrls.length == 1) {
							$ctrl = $ctrls;
							if (value) {
								$ctrl.prop("checked", true);
							} else {
								$ctrl.prop("checked", false);
							}
						}
						break;
				} //switch input type
			} // if/else
		}); // all json fields
	} // populate form
	// end functions

	setDebug(DEBUG);
	storage.options.prefix = "nutools_";
	toastr.options = {
		closeButton: false,
		debug: false,
		newestOnTop: false,
		progressBar: false,
		positionClass: "toast-top-right",
		preventDuplicates: false,
		onclick: null,
		showDuration: "300",
		hideDuration: "1000",
		timeOut: "5000",
		extendedTimeOut: "1000",
		showEasing: "swing",
		hideEasing: "linear",
		showMethod: "fadeIn",
		hideMethod: "fadeOut"
	};
	let storage_novel_db = null;
	let mybase = null;
	let cfgs = readConfig();

	db_init();
	sys_check_dbversion();

	appendFilesToHead(cssFiles, "css");
	$("head").append(htmlStyles);
	$("body").append(htmlPageAppend);
	$(".l-content").prepend(htmlCPbutton);

	setTimeout(function() {
		let ln_rows = 0;
		let ln_without_lang = {};

		$("td[class^='sid']").each(function() {
			let $this = $(this);
			let str = $this.attr("class");
			let id = parseInt(str.replace("sid", ""));
			let title = $this.find("a").attr("title");
			let surl = $this.find("a").attr("href");
			let tr = $this.parent().closest("tr");
			let html = "";
			let lang = "";
			let html_cover = (cfgs.cfg_cover_show_icon == 1) ? `<span class="js-nutools-show-cover" data-novel-url="${surl}" title="Show Cover"><i class="material-icons md-18">photo</i></span> ` : "";
			let html_moveToList = ` <span class="js-nutools-move-to-list" data-novel-id="${id}" data-novel-title="${title}" title="Move to list"><i class="material-icons md-18">format_indent_increase</i></span> `;
			let html_lang = ` <span class="js-nutools-lang" data-novel-id="${id}" data-novel-lang=""></span>`;

			// alasql database
			let result = mybase.exec("SELECT * FROM novels WHERE id=" + id + " LIMIT 1");
			if (result.length > 0) {
				debug("FOUND novel", result);
				if (result[0].lang == "") {
					ln_without_lang[id] = surl;
				}
				lang = result[0].lang;
				html_lang = ` <span class="js-nutools-lang" data-novel-id="${id}" data-novel-lang="${result[0].lang}">`;
				if (lang !="" ){
					html_lang += `(<b>${lang}</b>)`;
				} else  {
					html_lang += `<b></b>`;
				}
				html_lang += `</span>`;
			} else {
				debug("NOT FOUND novel, INSERT", [id, title, ""]);
				mybase.exec("INSERT INTO novels (?,?,?)", [id, title, ""]);
				ln_without_lang[id] = surl;
			}

			tr.attr("data-novel-id", id);
			$(this).prepend(`<span class="js-nutools-wrap" data-novel-id="${id}" data-novel-title="${title}">${html}${html_cover}${html_moveToList}${html_lang}</span> `);
			ln_rows++;
		});
		debug("ln without lang", ln_without_lang);
		debug("page nl rows", ln_rows);
		debug("saving db to localstorage", alasql.databases.mybase.tables.novels.data);
		storage.set("noveldb", alasql.databases.mybase.tables.novels.data);

		// Event handlers
		$(".js-nutools-show-cover").click(function() {
			let url = $(this).attr("data-novel-url");

			$.ajax({
				url: url,
				success: function(newHTML, textStatus, jqXHR) {
					let img_html = $(newHTML).find(".seriesimg img, .serieseditimg img").first();
					$("#js-nutools-cover-overlay").html(img_html).popup("show");
				},
				error: function(jqXHR, textStatus, errorThrown) {}
			});
		});
		$(".js-nutools-move-to-list").click(function() {
			let id = parseInt($(this).attr("data-novel-id"));
			let title = $(this).attr("data-novel-title");
			let url = "https://www.novelupdates.com/updatelist.php?lid=" + cfgs.cfg_move_to_list_id + "&act=move&sid=" + id;
			// let url = `https://www.novelupdates.com/updatelist.php?lid=${cfgs.cfg_move_to_list_id}&act=move&sid=${id}`;

			if (cfgs.cfg_move_req_confirm == 1) {
				let message = "Move <i>" + title + "</i> to the reading list [ " + cfgs.cfg_move_to_list_id + " ]?";
				$("#js-nutools-move-confirm-overlay .novel-title").html(title);
				$("#js-nutools-move-confirm-overlay .reading-list-id").html(cfgs.cfg_move_to_list_id);
				$("#js-nutools-move-confirm-overlay-move-button").attr("data-novel-id", id);
				$("#js-nutools-move-confirm-overlay").popup({
					color: "white",
					opacity: 1,
					transition: "0.3s",
					scrolllock: true,
					blur: false
				});
				$("#js-nutools-move-confirm-overlay").popup("show");
			} else {
				$.get(url, function(data) {
					debug("ajax get", url);
					toastr.success("Novel moved to list");
				});
				if (cfgs.cfg_move_reload == 1) {
					location.reload();
				}
			}
		});
		$("body").on("click", "#js-nutools-move-confirm-overlay-move-button", function() {
			let id = parseInt($(this).attr("data-novel-id"));
			let title = $(this).attr("data-novel-title");
			let url = "https://www.novelupdates.com/updatelist.php?lid=" + cfgs.cfg_move_to_list_id + "&act=move&sid=" + id;
			// let url = `https://www.novelupdates.com/updatelist.php?lid=${cfgs.cfg_move_to_list_id}&act=move&sid=${id}`;
			$.get(url, function(data) {
				debug("ajax get", url);
				$("#js-nutools-move-confirm-overlay").popup("hide");
				if (cfgs.cfg_move_reload == 1) {
					location.reload();
				}
			});
		});
		$("#js-nutools-get-language-button").click(function() {
			$("#js-nutools-get-language-confirm-overlay").popup({
				color: "white",
				opacity: 0.5,
				transition: "0.3s",
				scrolllock: true,
				blur: false
			});
			$("#js-nutools-get-language-confirm-overlay").popup("show");
		});
		$("#js-nutools-get-language-confirm-button").click(function() {
			let deferreds = [];
			let novels_lang = [];

			$("#js-nutools-get-language-confirm-overlay").popup("hide");
			for (let key in ln_without_lang) {
				let id = key;
				let url = ln_without_lang[key];
				let deferred = $.ajax(url, {
					success: function(html) {
						let lang = "";

						lang = $(html).find("#showlang a").first().text();
						console.log( lang );
						if (lang != "") {
							lang.replace(/()/gi, "");
						} else {
							lang = "N/A";
						}
						novels_lang[id] = lang;
					}
				});
				deferreds.push(deferred);
			}
			$.when.apply($, deferreds).then(function() {
				for (let key in novels_lang) {
					let isoAlpha3Code = get_lang_code(novels_lang[key]);

					$(".js-nutools-lang[data-novel-id='" + key + "']").html("(<b>" + isoAlpha3Code + "</b>)");
					debug("UPDATE novel entry", [key, isoAlpha3Code]);
					mybase.exec("UPDATE novels SET lang='" + isoAlpha3Code + "' WHERE id=" + key + "");
				}
				debug("saving db to localstorage", alasql.databases.mybase.tables.novels.data);
				storage.set("noveldb", alasql.databases.mybase.tables.novels.data);
			});
		});
		$("#js-nutools-open-userscript-cp").click(function() {
            let url = "/reading-list/";
			$.ajax({
				url: url,
				success: function(newHTML, textStatus, jqXHR) {
					let html = $(newHTML).find("SELECT[name='taskOption']").html();
					debug('html', html);
					$("#cfg_move_to_list_id").html(html);
					$("#js-nutools-settings-overlay").popup("show");
					let form = $("#settings_form");
					populateForm(form, cfgs);
				},
				error: function(jqXHR, textStatus, errorThrown) {}
			});
		});
		$(".js-nutools-settings-overlay_save").click(function() {
			event.preventDefault();
			let args = $("#settings_form").serializeJSON();
			cfgs = saveConfig(args);
			$("#js-nutools-settings-overlay").popup("hide");
			location.reload();
		});
		// $("#settings_form").on("focusin", "input", function() {
		// 	//$(this).data("val", $(this).val());
		// }).on("change", "input", function() {''
		// 	let args = $("#settings_form").serializeJSON();
		// 	cfgs = saveConfig(args);
		// });
		$("#js-nutools-settings-overlay").popup({
			color: "white",
			opacity: 1,
			transition: "0.3s",
			scrolllock: true,
			blur: false
		});

	}, 79); // milisec
}))();