js/options.js

const { shim } = require('regexp-match-indices'); // eslint-disable-line

var options = {};
var win = nw.Window.get();
win.restore(); // restore if minimized
win.show(); // show if hidden

win.setResizable(true);

var $DV 	= window.opener.$DV;
var trans 	= window.opener.trans;
var sys 	= window.opener.sys;
var addonLoader = window.opener.addonLoader;
var AddonInstaller = window.opener.AddonInstaller;
var fs 		= require('fs');
var lt 		= lt || window.opener.lt;
var info 	= window.opener.info;
var updater	= window.opener.updater;
var Updater	= window.opener.Updater;
var langTools = window.opener.langTools;
var TranslatorEngine = window.opener.TranslatorEngine;

/**
 * Triggered when option window is opened
 * @event ui#optionsWindowOpened
 * @since 4.3.20
 */
window.opener.ui.trigger("optionsWindowOpened");



trans.project = trans.project||{};
trans.project.options = trans.project.options||{};

options.__onBeforeClose = [];
options.onBeforeCloseRunOnce = function(fn) {
	if (typeof fn !== 'function') return console.info("Argument is not a function", fn)
	options.__onBeforeClose.push(fn);
}

options.localStorage = new (require("better-localstorage"))("options");


options.initLanguageList = function() {
	var langList = common.getLanguageCode();
	if ($("#languageList").length > 0) return;
	var $languageList = $(`<datalist id="languageList"></datalist>`);
	for (var i in langList) {
		$languageList.append(`<option value="${i}">${langList[i]}</option>`);
	}
	$("#template").append($languageList);
}

options.needRestart = false;
options.requestRestart = function() {
	this.needRestart = true;	
}

options.isNeedRestart = function() {
	if (options.needRestart) {
		alert(t("Some configuration require you to restart the application to take effect.\nPlease save your work, and restart Translator++."));
	}
}

options.updateLanguageSelectors = function() {
	options.drawLanguageSelector($("#sl"),'sl', sys.config.default.sl);
	options.drawLanguageSelector($("#tl"),'tl', sys.config.default.tl);

	if ($("#language .languagePair").hasClass("initialized")) {
		$("#sl").trigger("input")
		$("#tl").trigger("input")
		return;
	}
	$("#sl").on("input", function() {
		var $this = $(this)
		$this.closest(".fieldLine").find("label .attention").remove();

		if (!$this.val()) {
			$this.addClass("error");
			$this.closest(".fieldLine").find("label").append(`<i class="attention icon-attention red" title="Please select a language"></i>`);

		} else {
			$this.removeClass("error");

		}

		/**
		 * Triggered when the default translator is changed
		 * @event Trans#languageChange
		 * @param  {String} sl - Source language
		 * @param  {String} tl - Target language
		 * @since 4.12.10
		 */
		trans.trigger("languageChange", [$("#sl").val(), $("#tl").val()])
	})
	$("#tl").on("input", function() {
		var $this = $(this)
		$this.closest(".fieldLine").find("label .attention").remove();

		if (!$this.val()) {
			$this.addClass("error");
			$this.closest(".fieldLine").find("label").append(`<i class="attention icon-attention red" title="Please select a language"></i>`);
		} else {
			$this.removeClass("error");
		}
		trans.trigger("languageChange", [$("#sl").val(), $("#tl").val()])

	})
	$("#sl").trigger("input")
	$("#tl").trigger("input")
	$("#language .languagePair").addClass("initialized")
}


options.drawTranslatorSelector = function($obj, defaultVal) {
	defaultVal = defaultVal || "google";
	$obj.data("previousValue", defaultVal);
	if (!trans.getTranslatorEngine) return;
	if (!trans.translator) return;
	if (Array.isArray(trans.translator)) trans.translator.sort();
	for (var i=0; i<trans.translator.length; i++) {
		var thisTranslator = trans.getTranslatorEngine(trans.translator[i]);
		if (!thisTranslator) {
			console.warn("Can not process option for:", trans.translator[i]);
			continue;
		}
		$obj.append("<option value='"+trans.translator[i]+"'>"+thisTranslator.name+"</option>");
	}
	$obj.val(defaultVal);
	
	if ($obj.hasClass("eventApplied") == false) {
		$obj.on("change.updater", function(e) {
			trans.project.options = trans.project.options||{};
			trans.project.options.translator = $(this).val();
			options.updateLanguageSelectors();
			// registering into sys
			try {
				sys.config.translator = $(this).val();
				sys.saveConfig();
			} catch (e) {
				console.info("error writing sys.config", e);
			}
		})
		$obj.addClass("eventApplied")
	}
	
	// initializing;
	$obj.trigger("change");

	$obj.on("change", function() {
		var $this = $(this);
		// store old value
		var oldValue = $(this).data("previousValue");
		// set old value to new value;
		$(this).data("previousValue", $this.val());
		options.updateLanguageSelectors();
		// trigger event
		
		/**
		 * Triggered when the default translator is changed
		 * @event Trans#translatorIsChanged
		 * @param  {String} oldTranslator - The id of the old translator
		 * @param  {String} newTranslator - The id of the new Translator
		 * @since 4.3.20
		 */
		options.trigger("translatorIsChanged", [$this.val(), oldValue])
		trans.trigger("translatorIsChanged", [$this.val(), oldValue]);
	})

	return $obj;
}

options.drawLanguageSelector = function($obj, id, defaultVal, options) {
	var languages;
	try {
		var thisTranslator = sys.config.translator||trans.project.options.translator;
		var keyName = "sourceLanguages"
		if (id == "tl") {
			keyName = "targetLanguages"
		}
		languages = trans[thisTranslator][keyName]||trans[thisTranslator].languages||consts.defaultLanguages||{};
	} catch (e) {
		languages = consts.defaultLanguages||{};
	}
	
	$obj.empty();
	options = options||{};
	options.disableChoice = options.disableChoice||[];
	if (typeof(defaultVal) == 'undefined') {
		defaultVal = 'en';
		if (id == 'sl') defaultVal = 'ja'; 
	}
	
	for (var langCode in languages) {
		var $opt;
		//var langDBData = langTools.lookupDB(langCode, languages[langCode])
		var langDBData = langTools.getLanguage(langCode, languages[langCode])
		if (langDBData) {
			$opt = $(`<option value="${langDBData.id}">${langTools.getFullName(langDBData)}</option>`);
		} else {
			$opt = $("<option value='"+langCode+"'>"+languages[langCode]+"</option>");
		}

		if (options.disableChoice.includes(langCode)) $opt.prop("disabled", true);
		$obj.append($opt);
	}
	$obj.val(defaultVal);

	if ($obj.hasClass("eventApplied") == false) {
		$obj.on("change.updater", function(e) {
			sys.config.default[id] = $(this).val();
			$DV.config[id] = $(this).val();		
		})
		$obj.addClass("eventApplied")
	}

	
	return $obj;
}

options.applyAll =function($obj) {
	$obj = $obj||$("body");
	
	$obj.find("input").trigger("change");
	$obj.find("select").trigger("change");
	$obj.find("textarea").trigger("change");
}




// ===================================================
// Handling tab
// ===================================================
options.tab = {};
options.tab.select = function($obj) {
	$obj.closest(".tabMenu").find("li.selectable").removeClass("selected");
	$obj.addClass("selected");
	let $panelContents = $(".panel-right .panelContent");
	$panelContents.removeClass("activeTab");
	$panelContents.addClass("hidden");
	
	let thisRef = $obj.data("for");
	let $targetObj = $("#"+thisRef);
	if ($targetObj.length > 0) {
		$targetObj.removeClass("hidden");
		$targetObj.addClass("activeTab");
	}
	if (typeof $obj.data("onOptionSelected") == "function") {
		$obj.data("onOptionSelected")($targetObj, $obj);
	}
}

options.tab.insertBefore = function(id, title, $insertBefore, options) {
	options = options||{};
	options.icon = options.icon||"circle";
	options.tabClass = options.tabClass||"";
	var $tab = $('<li data-for="'+id+'" class="selectable '+options.tabClass+'"><a><i class="icon-'+options.icon+'"></i><span class="menuTitle">'+title+'</span></a></li>');
	$tab.insertBefore($insertBefore);
	$tab.attr("data-keyword", title);
	if (typeof options.onSelect == "function") $tab.data("onOptionSelected", options.onSelect);
	
	var $tabContent = $('<div class="panelContent '+id+'" id="'+id+'"></div>');
	$(".panel-right").append($tabContent);
	
	return {tab:$tab, content:$tabContent};
}

options.tab.insertAfter = function(id, title, $insertAfter, options) {
	options = options||{};
	options.icon = options.icon||"circle";
	options.tabClass = options.tabClass||"";
	options.tabContent = options.tabContent||"";

	var $tab = $('<li data-for="'+id+'" class="selectable '+options.tabClass+'"><a><i class="icon-'+options.icon+'"></i><span class="menuTitle">'+title+'</span></a></li>');
	$tab.insertAfter($insertAfter);
	$tab.attr("data-keyword", title);
	if (typeof options.onSelect == "function") $tab.data("onOptionSelected", options.onSelect);

	var $tabContent = $('<div class="panelContent '+id+'" id="'+id+'"></div>');
	$tabContent.append(options.tabContent);
	$(".panel-right").append($tabContent);
	return {tab:$tab, content:$tabContent};
}

options.tab.init = function() {
	var $selectableMenu = $(".tabMenu li.selectable");
	$selectableMenu.each(function() {
		var $thisLi = $(this);
		$thisLi.off("click.selectTab");
		$thisLi.on("click.selectTab", function(e){
			options.tab.select($(this));
		})
	});
}

// ===================================================
// Handling translator options
// ===================================================
options.translatorMenu = {};

options.translatorMenu.assignDefault = function(thisTranslator) {
	if (typeof thisTranslator.optionsForm == 'undefined') return thisTranslator;
	if (typeof thisTranslator.optionsForm.schema == 'undefined') return thisTranslator;
	
	for (var key in thisTranslator.optionsForm.schema) {
		if (typeof thisTranslator[key] == 'undefined') continue;
		thisTranslator.optionsForm.schema[key]['default'] = thisTranslator[key];
	}
	console.log("after assigning default :");
	console.log(thisTranslator);
	return thisTranslator;
	
}
	
options.translatorMenu.linkify = function(text) {
    var urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig;
    return text.replace(urlRegex, function(url) {
        return '<a href="' + url + '", target="_system">' + url + '</a>';
    });
}

options.translatorMenu.renderOptions = function(thisTranslator, $elm) {
	console.log("%cRendering options", "color:yellow;", thisTranslator.id)
	options.translatorMenu.assignDefault(thisTranslator);
	$elm ||= $("#"+thisTranslator.id+" .pluginOptions")
	$elm.empty();
	var jsonForm = $elm.jsonForm(thisTranslator.optionsForm);
	$elm.data("jsonForm", jsonForm);
	$elm.closest(".panelContent").data("translatorInfo", thisTranslator);

	console.log("rendering custom attr for translator engine:", thisTranslator.id, thisTranslator.optionsForm);
	for (var key in thisTranslator.optionsForm.schema) {
		var thisForm = thisTranslator.optionsForm.schema[key];
		if (!thisForm.attr) continue;
		console.log("rendering attr for:", key);
		var $thisField = $elm.find(`[name="${key}"]`);
		for (var attrKey in thisForm.attr) {
			$thisField.attr(attrKey, thisForm.attr[attrKey]);
		}
	}

	$elm.find(".help-block").each(function() {
		$(this).html(options.translatorMenu.linkify($(this).html()));
		$(this).find("a[target=_system]").on("click", function(e) {
			e.preventDefault();
			nw.Shell.openExternal($(this).attr("href"));
		})
	});
	console.log(`Translator option: ${thisTranslator.id} JSON form:`, jsonForm);
	// to get the values:
	// jsonForm.root.getFormValues()
	// to set the values
	// let's create a function on translatorEngine to batch apply value	
}

/**
 * Set the value of a <select> element. If the value does not exist in the options,
 * a new option is added.
 *
 * @param {jQuery} $select - The jQuery object representing the <select> element.
 * @param {string} valueToSet - The value to set in the <select>.
 */
options.setSelectValue = function($select, valueToSet) {
	// Check if the value already exists in the options
	if ($select.find('option[value="' + valueToSet + '"]').length > 0) {
		// If it exists, simply set the value
		$select.val(valueToSet);
	} else {
		// If it doesn't exist, add a new option and then set the value
		$select.append($('<option>', {
			value: valueToSet,
			text: valueToSet
		}));
		$select.val(valueToSet);
	}
}

options.translatorMenu.configLoad = async function($panelContent, configName="") {
	console.log("Changing configuration to:", configName);
	const translatorInfo = $panelContent.data("translatorInfo");
	const $pluginOptions = $panelContent.find(".pluginOptions");
	//const jsonForm = $pluginOptions.data("jsonForm");

	const oldConfigName = 	translatorInfo.getOptions("activeConfiguration") || "";

	console.log("Save current configuration which is", oldConfigName);
	await options.translatorMenu.configSave($panelContent, oldConfigName);

	console.log("Old options:", JSON.stringify(translatorInfo.getOptions(), undefined, 2))
	console.log("Reset options");
	await translatorInfo.resetOptions();
	console.log("After reset:", JSON.stringify(translatorInfo.getOptions(), undefined, 2))

	var storageKey = `tlEngineConfig/${translatorInfo.id}`
	var existingConfig = await options.localStorage.get(storageKey) || {}
	var thisConfig = existingConfig[configName] || {}
	if (!thisConfig) {
		console.warn("No configuration found");
	}

	for (let i in thisConfig) {
		translatorInfo.update(i, thisConfig[i])
	}
	console.log("After setting value:", JSON.stringify(translatorInfo.getOptions(), undefined, 2))

	options.translatorMenu.renderOptions(translatorInfo, $pluginOptions);
	translatorInfo.update("activeConfiguration", configName);

	console.log("After everything:", JSON.stringify(translatorInfo.getOptions(), undefined, 2))

	return thisConfig;
}

options.translatorMenu.configSave = async function($panelContent, configName="") {
	if (!$panelContent?.length) return;
	const translatorInfo = $panelContent.data("translatorInfo");
	const $pluginOptions = $panelContent.find(".pluginOptions");
	const jsonForm = $pluginOptions.data("jsonForm");
	console.log("Saving config saveAs", translatorInfo, jsonForm);

	var storageKey = `tlEngineConfig/${translatorInfo.id}`
	console.log("Set: ", storageKey, "value:", jsonForm.root.getFormValues())
	var existingConfig = await options.localStorage.get(storageKey) || {}
	existingConfig[configName] = jsonForm.root.getFormValues()
	await options.localStorage.set(storageKey, existingConfig);
	return existingConfig;
}

options.translatorMenu.configSaveAs = async function($panelContent, configName="") {
	if (!$panelContent?.length) return;
	const translatorInfo = $panelContent.data("translatorInfo");
	const $pluginOptions = $panelContent.find(".pluginOptions");
	const jsonForm = $pluginOptions.data("jsonForm");
	console.log("Saving config saveAs", translatorInfo, jsonForm);
	if (!configName) {
		configName = prompt(t("Pleae select the name for your configuration."));
	}
	if (!configName) return;

	const result = await options.translatorMenu.configSave($panelContent, configName);
	options.setSelectValue($panelContent.find(".configurationControl .configSelector"), configName);
	return result;
}

/**
 * Renders select box of configuration selection
 * @param {JQuery} $panelContent - Jquery instance of panelContent
 * @param {string} defaultSelection - Selected configuration as default
 */
options.translatorMenu.renderConfigSelection = async function($panelContent, defaultSelection="") {
	const translatorInfo = $panelContent.data("translatorInfo");
	const $configSelector = $panelContent.find(".configurationControl .configSelector");
	$configSelector.empty();

	var storageKey = `tlEngineConfig/${translatorInfo.id}`
	var existingConfig = await options.localStorage.get(storageKey) || {}
	$configSelector.append(`<option value="">Default</option>`)
	for (let confName in existingConfig) {
		$configSelector.append(`<option value="${common.htmlEntities(confName)}">${common.htmlEntities(confName)}</option>`)
	}

	$configSelector.off("change");
	$configSelector.on("change", function() {
		console.log("Selection change, changing to configuration ", $(this).val());
		options.translatorMenu.configLoad($panelContent, $(this).val());
	})

	defaultSelection ||= translatorInfo.getOptions("activeConfiguration") || "";

	if (defaultSelection) options.setSelectValue( $configSelector, defaultSelection);

}

options.translatorMenu.init = function() {
	if (Array.isArray(trans.translator) == false) return false;
	console.log("Initializing translator engine's options");

	var translators = common.sort(TranslatorEngine.translators, "name", true);
	for (var i=0; i<translators.length; i++) {
		(()=> {
			var thisTranslator = translators[i];
			thisTranslator.description = thisTranslator.description||"";
			thisTranslator.author = thisTranslator.author||"Anonymous";
			thisTranslator.version = thisTranslator.version||"1.0";
			let thisIcon = thisTranslator.icon || 'mtl'
			var thisOption = {
				icon: thisIcon,
				tabClass: 'submenu translatorSetting hidden',
				tabContent:$("<h1 class='pluginTitle'><i class='icon-"+thisIcon+"'></i> "+thisTranslator.name+"</h1>\
							<div class='subtitle'>Translator Engine configuration</div>\
							<div class='authorBlock'>"+t("Translator engine version.")+" <span class='author'>"+thisTranslator.version+"</span></div>\
							<div class='versionBlock'>"+t("By.")+" <span class='engineVersion'>"+thisTranslator.author+"</span></div>\
							<p class='description'>"+thisTranslator.description+"</p>\
							<div class='configurationControl'></div>\
							<div class='pluginOptions'></div>")
			}
			
			if (typeof thisTranslator.onOptionSelected == "function") thisOption.onSelect = thisTranslator.onOptionSelected
			const tab = options.tab.insertAfter(thisTranslator.id, thisTranslator.name, $(".langSetting"), thisOption);
			let $formContainer = tab.content.find(".pluginOptions");

			if (typeof thisTranslator.optionsForm == 'undefined') {
				$formContainer.html(t("No option for this engine"));
				return;
			}
			
			options.translatorMenu.renderOptions(thisTranslator, $formContainer);

			// ================================================================
			// Option manager section only available if enableOptionManager is TRUE
			if (!thisTranslator.getOptions("enableOptionManager")) return;
			if (thisOption.tabContent.parent().find(".configurationControl").html(`<span class='configurationLabel'>Manage configuration</span><span class='configSelectorFieldWrapper'><select type='search' class='configSelector'><option value=''>Default</option></select><button class='configSave'>Save</button><button class='configSaveAs'>Save As</button></span>`))

			thisOption.tabContent.parent().find(".configurationControl .configSave").on("click", function() {
				options.translatorMenu.configSave($(this).closest(".panelContent"));

			});
			thisOption.tabContent.parent().find(".configurationControl .configSaveAs").on("click", function() {
				const $panelContent = $(this).closest(".panelContent");
				const currentConfigName = $panelContent.find(".configurationControl .configSelector").val();
				options.translatorMenu.configSaveAs($panelContent, currentConfigName);
				alert(`Configuration ${currentConfigName} saved!`);

			});

			options.translatorMenu.renderConfigSelection(tab.content);
	
		})()
	}
}


// ===================================================
// Handling addons options
// ===================================================
var AddonsOption = function(addons) {
	this.addons = addons || addonLoader.addons;
}
// Draw the list of addons
AddonsOption.prototype.initInstallFilter = function(defaultFilter) {
	const doFilter = (keyword)=> {
        $("#addons [data-id=online] .addonListWrapper > .listMember").filter(function() {
			$(this).toggle($(this).text().toLowerCase().indexOf(keyword) > -1)
		});
	}
    $("#addonInstallerSearch").off("input")
    $("#addonInstallerSearch").on("input", function() {
        var value = $(this).val().toLowerCase();
		doFilter(value);
    });

	doFilter(defaultFilter);
}

AddonsOption.prototype.initManagerFilter = function(defaultFilter) {
	const doFilter = (keyword)=> {
        $("#addons [data-id=installed] .addonListWrapper > .listMember").filter(function() {
			$(this).toggle($(this).text().toLowerCase().indexOf(keyword) > -1)
		});
	}
    $("#addonManagerSearch").off("input")
    $("#addonManagerSearch").on("input", function() {
        var value = $(this).val().toLowerCase();
		doFilter(value);
    });

	doFilter(defaultFilter);
}

AddonsOption.prototype.drawList = function() {
	var $container = $("#addons .installedAddons .addonListWrapper");
	var that = this;
	for (var id in this.addons) {
		var thisAddon = this.addons[id];
		var label 	= thisAddon.package.title || thisAddon.package.name;
		var author	 = thisAddon.package.author || "Unknown";
		if (typeof thisAddon.package.author?.name == "string") {
			author = author.name;
		}
		var $template = $(`<div class="addonsOption addonList listMember">
			<div class="addonsIconBlock"></div>
			<div class="addonsInfoBlock">
				<div class='titleBlock'><span class="name">`+label+`</span><span class="ver">`+thisAddon.package.version+`</span></div>
				<div class="authorBlock">By. ${author}</div>
				<div class='descBlock'>`+thisAddon.package.description+`</div>
			</div>
			<div class="addonsAction"></div>
		</div>`)	
		$template.data('id', id)
		if (thisAddon.package.icon) {
			$template.find(".addonsIconBlock").append('<img class="addonsIcon" src="'+thisAddon.getWebLocation()+'/'+thisAddon.package.icon+'" alt="icon" />');
		} else {
			$template.find(".addonsIconBlock").append('<img class="addonsIcon" src="img/icon.png" alt="icon" />');
		}

		if (thisAddon.config.isMandatory) {
			var $lock = $("<i class='icon-lock' title='"+t("Can not disable this addon")+"'></i>")
			$template.find(".addonsAction").append($lock);
		} else {
		
			var $flipSwitch = $("<i class='iconToggle icon-toggle-on'></i>")
			if (thisAddon.config.isDisabled) {
				$flipSwitch.removeClass('icon-toggle-on');
				$flipSwitch.addClass('icon-toggle-off');
			}
			
			$template.find(".addonsAction").append($flipSwitch);
			
			$flipSwitch.on("click", function(e) {
				var addonId = $(this).closest(".addonList").data("id");
				if ($(this).hasClass('icon-toggle-on')) {
					$(this).removeClass('icon-toggle-on');
					$(this).addClass('icon-toggle-off');
					that.addons[addonId].setConfig("isDisabled", true)
				} else {
					$(this).removeClass('icon-toggle-off');
					$(this).addClass('icon-toggle-on');
					that.addons[addonId].setConfig("isDisabled", false)
					
				}
				options.needRestart = true;
			})
		}
		
		$container.append($template);
	}
	
	this.initManagerFilter($("#addonManagerSearch").val());
}


AddonsOption.prototype.drawOnlineList = async function(onlineList) {
	var $container = $("#addons [data-id=online] .addonListWrapper");
	$container.empty();
	options.busy();
	var fetchError = false;
	try{
		onlineList = onlineList || this.onlineList || await common.fetch('http://dreamsavior.net/rest/addons/list/');
	} catch (e) {
		alert("Unable to fetch online list");
		onlineList = onlineList || {};
		fetchError = true;
	}
	if (!fetchError) this.onlineList = onlineList;
	console.log("online list is :", onlineList);
	var that = this;
	onlineList.list = onlineList.list || [];
	for (var id in onlineList.list) {
		var thisAddon = onlineList.list[id];
		var cost = thisAddon.patron_level || 0;
		if (thisAddon.purchase_type == "points") {
			cost = thisAddon.patron_points;
		}
		var iconPath = "<img src='/www/img/icon.png' class='addonsIcon' alt='' />";

		if (thisAddon.icon) {
			iconPath = "<img src='"+thisAddon.icon+"' class='addonsIcon' alt='' />";
		}
		
		var $template = $(`<div class="addonsOption addonList listMember">
			<div class="addonsIconBlock">${iconPath}</div>
			<div class="addonsInfoBlock">
				<div class='titleBlock'>
					<span class="name">${thisAddon.title}</span>
					<span class="ver">${thisAddon.version}</span>
				</div>
				<div class="authorBlock">by. ${thisAddon.author}</div>
				<div class='descBlock'>${thisAddon.description}</div>
				<div class="addonReq"><i class="icon-info-circled-1"></i><span>`+t('Requirements')+`: <span>  <span class="reqCost ${thisAddon.purchase_type}">`+cost+`</span> <span class="reqType noInvert" data-reqtype="${thisAddon.purchase_type}">${thisAddon.purchase_type}</span><a href="https://dreamsavior.net/docs/translator/faq/what-is-points-and-level/" class="noInvert" title="${t("What is this?")}" external><i class="icon-help-circled-1"></i></a></div>					

			</div>
			<div class="addonsAction">
				<div class="addonsActionBtn"></div>
			</div>
		</div>`);

		var isUnsupported = false;
		if (thisAddon.min_ver) {
			var $minver = $(`<div class="minVer"><i class="icon-info-circled-1"></i>Minimum version: <span class="version">${thisAddon.min_ver}</span></div>`)
			$template.find(".addonReq").append($minver);

			if (common.versionToFloat(nw.App.manifest.version) < common.versionToFloat(thisAddon.min_ver)) isUnsupported = true;
		}

		$template.find("a[external]").on("click", function(e) {
			e.preventDefault();
			nw.Shell.openExternal($(this).attr("href"));
		});
		
		$template.attr('data-id', thisAddon.id)
		$template.data('id', thisAddon.id)
		$template.attr('data-name', thisAddon.name)
		$template.data('name', thisAddon.name)
		$template.data('addonInfo', thisAddon)
		var $installButtons;
		
		if (addonLoader.getByName(thisAddon.name)) {
		//if (AddonInstaller.isInstalled(thisAddon.id)) {
			var installedVersion = addonLoader.getByName(thisAddon.name).package.version;
			$installButtons = [$(`<div class='addon-installedVer'>Installed ver. ${installedVersion}</div>`)]
			var $installBtn =$("<button class='addon-uninstall'><i class='icon-cancel-circled'></i> Uninstall</button>")
			$installButtons.push($installBtn);
		
			$installBtn.on("click", async function() {
				var thisName = $(this).closest('.addonsOption').data("name");
				var conf = confirm(t("Uninstall")+` ${thisName}?`);
				if (!conf) return;
				
				options.busy();
				var thisId = $(this).closest('.addonsOption').data("id");
				//await addonLoader.uninstall(AddonInstaller.getRootAddonDir(thisId));
				await addonLoader.uninstall(addonLoader.getByName(thisName));
				AddonInstaller.configUninstall(thisId);
				await that.drawOnlineList();
			});


			try {
				
				console.log("Addon:", thisAddon.name);
				console.log("current version:", common.versionToFloat(thisAddon.version) );
				console.log("installed version:", common.versionToFloat(installedVersion));
				if (common.versionToFloat(thisAddon.version) > common.versionToFloat(installedVersion)) {
					// update
					console.log("show update flag");
					var $updateBtn = $("<button class='addon-update noInvert'><i class='icon-arrows-cw'></i> Update</button>");
					$installButtons.push($updateBtn)
		
					$updateBtn.on("click", async function() {
						var thisName = $(this).closest('.addonsOption').data("name");
						var conf = confirm(t("Update")+` ${thisName}?`);
						if (!conf) return;
						
						options.busy();
						// uninstall
						var thisId = $(this).closest('.addonsOption').data("id");
						//await addonLoader.uninstall(AddonInstaller.getRootAddonDir(thisId));
						await addonLoader.uninstall(addonLoader.getByName(thisName));
						AddonInstaller.configUninstall(thisId);

						// install
						thisId = $(this).closest('.addonsOption').data("id");
						var addonInstaller = new AddonInstaller(parseInt(thisId));
						var installOption = {
							onError: async function(message, result) {
								window.opener.ui.alert(message, "options");
							},
							onSuccess: async function() {
								window.opener.ui.alert(t("Addon successfully installed.\r\nTranslator++ may need to be restarted for some addon to take effect."), "options");
								console.log("reloading addon");
								await addonLoader.loadAll();
							}
						}
						await addonInstaller.install(undefined, installOption);
						console.log("refreshing online list")
						await common.wait(500);
						await that.drawOnlineList();
					});					
				}
			} catch(e) {
				console.warn("Unable to get package version of "+thisAddon.name, e)
			}
		} else if (isUnsupported) {
			// unsupported
			$installButtons = $("<button class='addon-unsupported' disabled><i class='icon-cancel-circled'></i> Unsupported</button>")
		} else {
			// install
			$installButtons = $("<button class='addon-install noInvert'><i class='icon-download-cloud-1'></i> Install</button>")
		
			//console.log("drawing install button", thisAddon);
			if (thisAddon.purchase_type == "points") {
				updater.user.points = updater.user.points || 0;
				thisAddon.patron_points = thisAddon.patron_points || 0;
				//console.log(`User points: ${updater.user.points} addonPoints = ${thisAddon.patron_points}`);
				if (updater.user.points < thisAddon.patron_points) {
					console.log("User points less then required");
					$installButtons.prop("disabled", true);
				}
			}

			$installButtons.on("click", async function() {
				console.log("installing addon", $(this).closest('.addonsOption').data());
				const addonInfo = $(this).closest('.addonsOption').data("addonInfo");
				const thisId 		= addonInfo.id;
				const addonName 	=  addonInfo.name;
				const addonInstaller = new AddonInstaller(parseInt(thisId));
				//var thisAddon = onlineList.list[thisId];
				if (addonInfo?.is_parser_change == "yes") {
					let conf = confirm(t(`WARNING!\n`)+t(`This add-on has marked as "parser change". Your old project related to this add-on may no longer worked as expected. It is advised to backup your project before installing this add-on.\nYou can import your existing translation into the newly created project.\n\nDo you wish to install `)+addonName+`?`);
					if (!conf) return;
				} else {
					let conf = confirm(t(`Do you want to install `) +addonName);
					if (!conf) return;
				}

				options.busy();

				var installOption = {
					onError: async function(message, result) {
						window.opener.ui.alert(message, "options");
					},
					onSuccess: async function() {
						window.opener.ui.alert(t(`Add-on ${addonName} is installed successfully.\r\nTranslator++ may need to be restarted for some addon to take effect.`), "options");
						console.log("reloading addon");
						await addonLoader.loadAll();
					}
				}
				await addonInstaller.install(undefined, installOption);
				console.log("refreshing online list")
				await common.wait(500);
				await that.drawOnlineList();
			});
		}
		
		$template.find(".addonsActionBtn").append($installButtons);	


		$container.append($template);
		
	}
	this.initInstallFilter($("#addonInstallerSearch").val());
	options.busyNot();
}

// Drawing the form
AddonsOption.prototype.findForm = function(optionsForm, key) {
	optionsForm.form = optionsForm.form || [];
	for (var i=0; i<optionsForm.form.length; i++) {
		optionsForm.form[i].key = optionsForm.form[i].key|| ""
		if (optionsForm.form[i].key == key) return i;
	}
}
AddonsOption.prototype.camelCaseToWords = function(text) {
	var result = text.replace( /([A-Z])/g, " $1" );
	var finalResult = result.charAt(0).toUpperCase() + result.slice(1);
	return 	finalResult;
}

AddonsOption.prototype.generateFromMini = function(optionsForm) {
	// generates minified json form to full json form
	console.info("generate from mini", optionsForm);
	if (typeof optionsForm.schema !== 'undefined') return optionsForm
	console.log("pass here");
	var form = [];
	for (var key in optionsForm) {
		var thisData = {};
		thisData.key = key;
		
		// enum
		if (Array.isArray(optionsForm[key].enum)) {
			thisData.titleMap = {};
			if (optionsForm[key].preventAutoCamelCase) {
				for (let i=0; i<optionsForm[key].enum.length; i++) {
					thisData.titleMap[optionsForm[key].enum[i]] = optionsForm[key].enum[i]
				}
			} else {
				for (let i=0; i<optionsForm[key].enum.length; i++) {
					thisData.titleMap[optionsForm[key].enum[i]] = this.camelCaseToWords(optionsForm[key].enum[i])
				}
			}

		}
		// array
		if (optionsForm[key].type == "array") {
			thisData.type = "checkboxes";
			if (optionsForm[key].enum && Boolean(optionsForm[key].items)==false) {
				optionsForm[key].items = {
					enum : optionsForm[key].enum
				};
				
			}
		}
		// radio
		if (optionsForm[key].type == "radio" || optionsForm[key].type == "radios") {
			optionsForm[key].type = "string"
			thisData.type = "radios";
		}
		
		// checkbox
		if (optionsForm[key].inlinetitle) thisData.inlinetitle = optionsForm[key].inlinetitle
		
		form.push(thisData);
	}	
	var result = {
		schema:optionsForm,
		form:form
	}
	
	console.info("result : ", result);
	return result;
}

AddonsOption.prototype.renderCustomSchema = function(optionsForm, thisAddon) {
	if (typeof optionsForm == 'undefined') return optionsForm;
	if (typeof optionsForm.schema == 'undefined') return optionsForm;
	console.log("before assigning default :", optionsForm);
	for (var key in optionsForm.schema) {
		var formKey = this.findForm(optionsForm, key);
		var thisForm = optionsForm.form[formKey]
		var onChangeAdd = [];
		var onClickAdd = [];
		
		if (typeof optionsForm.schema[key] == 'undefined') continue;
		//addon.optionsForm.schema[key]['default'] = addon[key];
		
		if (typeof optionsForm.schema[key]['default'] == "function") {
			optionsForm.schema[key]['default'] = optionsForm.schema[key]['default'].call(thisAddon);
		}


		if (typeof thisForm.onInput == 'function') {
			onChangeAdd.push('thisForm.onInput.call(thisAddon, $(evt.target))')	
		}
		
		
		if (typeof optionsForm.schema[key]['onChange'] == "function") {
			console.warn("Rendering on change");
			onChangeAdd.push("("+optionsForm.schema[key]['onChange'].toString()+").apply($(evt.target), arguments)");
		}
		
		if (typeof optionsForm.schema[key]['HOOK'] == "undefined") {
			optionsForm.schema[key]['HOOK'] = "thisAddon.config['"+key+"']";
		}

		if (typeof optionsForm.schema[key]['HOOK'] == "function") {
			optionsForm.schema[key]['default'] = optionsForm.schema[key]['HOOK'].call(thisAddon);
		} else if (typeof optionsForm.schema[key]['HOOK'] == "string") {
			optionsForm.schema[key]['default'] = eval(optionsForm.schema[key]['HOOK']);
			
			console.log("assigning hook for", key);
			if (optionsForm.schema[key].type == 'boolean') {
				onChangeAdd.push('console.log("onChange boolean type")');
				
				onChangeAdd.push('var value = $(evt.target).prop("checked");')	
				onChangeAdd.push(optionsForm.schema[key]['HOOK']+` = value;`)	

			} else if (thisForm.type == 'radios') {
				onChangeAdd.push('console.log("onChange radios type")');
				onChangeAdd.push('var value = $(evt.target).closest(".controls").find("input[type=radio]:checked").val();')	
				onChangeAdd.push(optionsForm.schema[key]['HOOK']+` = value;`)	
				
			} else if (optionsForm.schema[key].type == 'array') {
				onChangeAdd.push('console.log("onChange array type")');
				onChangeAdd.push('var value = $(evt.target).val();')	
				onChangeAdd.push(optionsForm.schema[key]['HOOK']+` = value;`)
				
			} else {
				onChangeAdd.push('console.log("onChange string type")');
				
				onChangeAdd.push(optionsForm.schema[key]['HOOK']+` = $(evt.target).val();`)	
			}
		} 
	
	
		if (onChangeAdd.length > 0) {
			thisForm.onChange = eval (` (evt) => {
				var field = $(evt.target);
				`+onChangeAdd.join("\n")+`}`);	
			//console.log("generated onChange :", key, thisForm.onChange);
		}
		if (onClickAdd.length > 0) {
			thisForm.onChange = eval (` (evt) => {
				var field = $(evt.target);
				`+onClickAdd.join("\n")+`}`);	
			//console.log("generated onChange :", key, thisForm.onChange);
		}
		
	}
	console.info("%cafter assigning default :", "color:green", thisAddon.id, optionsForm);
	return optionsForm;
	
}

/**
 * Assign default value of the JSONform
 * @param {*} optionsForm 
 * @param {*} thisAddon 
 */
AddonsOption.prototype.assignDefault = function(optionsForm, thisAddon) {
	if (!optionsForm?.schema) return;
	for (let key in optionsForm.schema) {
		let thisConfig = thisAddon.getConfig(key);
		if (typeof thisConfig == "undefined") continue;
		optionsForm.schema[key].default = thisConfig;
	}
}
	
AddonsOption.prototype.linkify = function(text) {
    var urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig;
    return text.replace(urlRegex, function(url) {
        return '<a href="' + url + '", target="_system">' + url + '</a>';
    });
}
	
AddonsOption.prototype.init = function() {
	if (Array.isArray(trans.translator) == false) return false;
	
	addonLoader = addonLoader || {};
	addonLoader.addons = addonLoader.addons || {};
	var that = this;


	for (var addonId in addonLoader.addons) {
		var addon = addonLoader.addons[addonId];
		addon.package 			= addon.package || {};
		addon.package.name 		= addon.package.name||"";
		addon.package.title 	= addon.package.title||addon.package.name||"";
		addon.package.description = addon.package.description||"";
		addon.package.author 	= addon.package.author||{name:"Anonymous", email:""}
		addon.package.version 	= addon.package.version||"1.0";
		addon.optionsForm 		= addon.optionsForm||{}

		var thisOptionForm = addon.optionsForm
		if (typeof addon.optionsForm == "function") thisOptionForm = addon.optionsForm.call(addon, options)
		addon.config = addon.config || {}
		
		if (addon.config.isDisabled) continue;

		console.log("drawing option menu for addon : ", addon);
		options.tab.insertAfter(addonId, addon.package.title, $(".addonSetting"), {
			icon:'box',
			tabClass:'submenu addonOptionMember hidden',
			tabContent:$("<h1 class='pluginTitle'>"+addon.package.title+"</h1>\
						<div class='subtitle'>Add-on configuration</div>\
						<div class='authorBlock'>"+t("Add-on version.")+" <span class='engineVersion'>"+addon.package.version+"</span></div>\
						<div class='versionBlock'>"+t("By.")+" <span class='author'>"+addon.package.author.name+"</span></div>\
						<p class='description'>"+addon.package.description+"</p>\
						<div class='pluginOptions'></div>")
		});
		
		const $pluginOptions = $("#"+CSS.escape(addonId)+" .pluginOptions");
		
		console.log("addon", addonId, thisOptionForm, !Object.keys(thisOptionForm).length);
		if (!Object.keys(thisOptionForm).length == false) {
			console.log($pluginOptions);
			if ($pluginOptions.length > 0) {
				if (!thisOptionForm.form) {
					thisOptionForm = this.generateFromMini(thisOptionForm);
					this.renderCustomSchema(thisOptionForm, addon);
				} else {
					this.assignDefault(thisOptionForm, addon);
				}

				console.info("Creating form with option", thisOptionForm);
				$pluginOptions.jsonForm(thisOptionForm);
				
				// open url to external browser
				$pluginOptions.find(".help-block").each(function() {
					$(this).html(that.linkify($(this).html()));
					$(this).find("a[target=_system]").on("click", function(e) {
						e.preventDefault();
						nw.Shell.openExternal($(this).attr("href"));
					})
				});

				// rendering custom attrribute : attr
				console.log("rendering custom attr for:", thisOptionForm);
				for (var key in thisOptionForm.schema) {
					var thisForm = thisOptionForm.schema[key];
					if (!thisForm.attr) continue;
					console.log("rendering attr for:", key);
					//var $thisField = $thisOptionSet.find(`[name="${key}"]`);
					var $thisField = $pluginOptions.find(`[name="${key}"]`);
					for (var attrKey in thisForm.attr) {
						$thisField.attr(attrKey, thisForm.attr[attrKey]);
					}
				}

				$pluginOptions.find("input[type=range]").each(function() {
					var $this 		= $(this);
					var $thisParent = $this.parent();
					var $wrapper 	= $(`<div class='fullWidth flex rangeWrapper'><output></output></div>`);
					$wrapper.find("output").text($this.val());
					$wrapper.prepend($this);
					$thisParent.prepend($wrapper);
					$this.on("input", function() {
						console.log("on input listener");
						$(this).next().html($(this).val());
					})
				});
			}

		}
		
		if (addon.optionsFormHtml) {
			$pluginOptions.append(addon.optionsFormHtml);
		}

		if ($pluginOptions.children().length == 0) $pluginOptions.html(t("No option for this add-on"));
	
		
	}
	
	this.drawList();
}


options.addonsOption = new AddonsOption();







// ===================================================
// Handling ABOUT
// ===================================================
options.about = {};
options.about.init = function() {
	let config = sys.app||nw.App.manifest
	const version = config.version	
	$("#about .version").text(version);
	
	$("#about .viewChangelog").on("click", async ()=> {
		let changelog = fs.readFileSync('changelog.txt', 'utf8');
		$("#about .changelog").text(changelog);
	});
	
	$("#about .nwVersion").text(process.versions.nw);
	$("#about .nodeVersion").text(process.versions.node);
	$("#about .phpVersion").text(sys.phpVersion);
	$("#about .archVersion").text(process.arch);
	
	//var rbVersion = "";
	if (typeof window.spawn  == "undefined") {
		window.spawn = require('child_process').spawn;
	}
	
	var child = window.spawn(nw.App.manifest.localConfig.ruby, ['www\\rb\\getVersion.rb']);
	//var child = spawn("ruby\\bin\\ruby.exe", ['www\\rb\\getVersion.rb']);
	var outputBuffer = "";
	child.stdout.on('data', function (data) {
		console.log("data : ", data);
		outputBuffer += data;
	});	
	child.on('close', function (code) {
		console.log("closed");
		$("#about .rubyVersion").text(outputBuffer);
	});		
	
	$(".licenseLink").attr("href", "file://"+__dirname+"/LICENSE.txt")
	
}


var Association = require("associate-ext");
var thisOptions = {
	iconIndex: nw.App.manifest.localConfig.iconIndex
	,defaultIconIndex : nw.App.manifest.localConfig.defaultIconIndex
}
console.log("thisOptions", thisOptions);
options.association = new Association(nw.App.manifest.localConfig.extensions,thisOptions);
options.association.init = function() {
	var thisAssoc = options.association.getAssociation();
	$(".associateBtn.submit").off("click");
	$(".associateBtn.setAll").off("click");
	$(".associateBtn.unsetAll").off("click");
	const $table = $(".extensionTable");
	$table.empty();
	$table.append("<tr><th>Extension</th><th>Associate</th></tr>");
	for (var ext in thisAssoc) {
		var $thisRow = $('<tr><th>'+ext+'</th><td class="formFld"></td></tr>');
		var $checkBox = $('<input type="checkbox" data-ext="'+ext+'" value="'+ext+'" class="flipSwitch associate ext_'+ext+'" name="associate['+ext+']" />');

		$checkBox.prop("checked", thisAssoc[ext]);

		$thisRow.find(".formFld").append($checkBox);
		$table.append($thisRow);
	}

	$(".associateBtn.unsetAll").on("click", function() {
		$(".extensionTable input.associate").prop("checked", false);
	});
	$(".associateBtn.setAll").on("click", function() {
		$(".extensionTable input.associate").prop("checked", true)
	});
	
	$(".associateBtn.submit").on("click", function() {
		var $checkBox = $(".extensionTable input.associate");
		var ext = [];
		for (var i=0; i<$checkBox.length; i++) {
			if ($checkBox.eq(i).prop("checked")) {
				ext.push($checkBox.eq(i).data("ext"));
			}
		}
		
		var conf = confirm(t("Translator++ will write Window's registry.\r\nAre you sure?"));
		if (conf) options.association.setExtension(ext);
	});

	return $table;
}


options.initUiLanguageSelector = function() {
	var docLocation = "www/lang/options.json";
	var $wrapper = $("#uiLanguage");
	$wrapper.off("input");
	var langData = {};
	fs.readFile(docLocation, (err, data)=> {
		if (err) return console.info('unable to read ', docLocation);
		
		try {
			langData = JSON.parse(data);
		} catch (e) {
			console.info("unable to parse lang options");
		}
		
		for (var code in langData) {
			let $option = $("<option value='"+code+"'>"+langData[code].name+"</option>")
			$wrapper.append($option);
		}
		
		$wrapper.val(lt.getConfig("lang"))
	
	});	
	$wrapper.on("input", function() {
		var lang = $(this).val();
		console.log("language selected", lang);
		console.log("translation data : ", langData);
		lt.save("lang", lang);
		lt.from(langData[lang].location);
		options.requestRestart();
	})

}


options.initSpellCheckLanguage = async function() {
	options.initLanguageList();
	var $spellCheckLanguage = $("#spellCheckLanguage");
	$spellCheckLanguage.val(sys.getConfig("spellCheckLang"));
	$spellCheckLanguage.off("change");
	$spellCheckLanguage.on("change", function() {
		console.log("spellCheck changed", $(this).val());
		window.opener.ui.changeEditorLanguage($(this).val());
		sys.setConfig("spellCheckLang", $(this).val());
	});
}

var EscaperPattern = function() {
	sys.config.escaperPatterns = sys.config.escaperPatterns || [];
	this.$elm = $("#customEscaper");
	this.init();
}

EscaperPattern.prototype.reset = function() {
	window.opener.HexPlaceholder.initDefaultPattern(true);
	sys.config.escaperString = window.opener.HexPlaceholder.joinRenderedFormula(window.opener.HexPlaceholder.getActiveFormulas());
	this.editor.setValue(sys.config.escaperString);
	this.commit();
}

EscaperPattern.prototype.clear = function() {
	window.opener.HexPlaceholder.setActiveFormulas([]);
	sys.config.escaperString = "";
	this.editor.setValue(sys.config.escaperString);
	this.commit();
}

EscaperPattern.prototype.setValue = function(val) {
	sys.config.escaperString = val;
	var result = [];
	try {
		var rendered = window.opener.HexPlaceholder.parseStringTemplate(val);
		console.log("rendered", rendered);
		var pattern = window.opener.HexPlaceholder.renderedFormulaToStrings(rendered);
		console.log("rendered to string", pattern);
		for (var i in pattern) {
			result.push({
				value:pattern[i]
			})
		}
		sys.config.escaperPatterns = result;
	
		window.opener.HexPlaceholder.renderedFormulas = []
		// eslint-disable-next-line
		MiniEditor.patterns = window.opener.HexPlaceholder.getActiveFormulas() || [];
		//this.miniEditor.trigger();
	} catch (e) {
		console.warn(e);
		var conf = confirm(t("Error parsing patterns:")+e.toString()+`\nDo you want to revert the pattern into default?`);
		if (conf) {
			this.reset();
		}
	}
	return result;
}

EscaperPattern.prototype.toText = function() {
	if (sys.config.escaperString) return sys.config.escaperString;

	// initialize from sys.config.escaperPatterns
	sys.config.escaperPatterns = sys.config.escaperPatterns || [];
	var texts = [];
	for (var i in sys.config.escaperPatterns) {
		if (!sys.config.escaperPatterns[i].value) continue;
		texts.push(sys.config.escaperPatterns[i].value);
	}
	console.log("pattern : ", texts);
	return texts.join(",\n");
}

EscaperPattern.prototype.commit = function() {
	var value = this.editor.getValue();
	this.setValue(value);
	this.renderHighlight(value, this.editor);
}

EscaperPattern.prototype.renderHighlight = (text, editor)=> {
	editor 	= editor || this.testEditor;
	if (!editor) return;
	text	= text || editor.getValue();
	var clearMarker = (editor) => {
		const prevMarkers = editor.session.getMarkers();
		if (prevMarkers) {
			const prevMarkersArr = Object.keys(prevMarkers);
			for (let item of prevMarkersArr) {
				editor.session.removeMarker(prevMarkers[item].id);
			}
		}
	}
	var rules = window.opener.HexPlaceholder.getActiveFormulas();
	var possitions = [];
	for (var i=0; i<rules.length; i++) {
		if (!rules[i]) continue;
		
		if (typeof rules[i] == 'function') {
			var arrayStrings = rules[i].call(this, text);
			if (typeof arrayStrings == 'string') arrayStrings = [arrayStrings];
			if (Array.isArray(arrayStrings) == false) continue;

			for (var x in arrayStrings) {
				text = text.replaceAll(arrayStrings[x], function(match) {
					if (!match) return match;
					var start 	= arguments[arguments.length-2];
					var end 	= start + match.length;
					possitions.push({
						start:editor.session.doc.indexToPosition(start),
						end  :editor.session.doc.indexToPosition(end)
					});
					return match;
				});				
			}
			continue;            
		}
		
		text = text.replaceAll(rules[i], function(match) {
			if (!match) return match;
			var start 	= arguments[arguments.length-2];
			var end 	= start + match.length;
			console.log("start", start, "end", end);
			possitions.push({
				start:editor.session.doc.indexToPosition(start),
				end  :editor.session.doc.indexToPosition(end)
			});
			return match;
		});

	}
	console.log("possitions:", possitions);
	clearMarker(editor);
	if (possitions.length > 0) {
		for(var posIdx=0; posIdx<possitions.length; posIdx++) {
			var thisPos = possitions[posIdx];
			editor.session.addMarker(
				new window.ace.Range(thisPos.start.row, thisPos.start.column, thisPos.end.row, thisPos.end.column),
				"testHighlight",
				"text",
				true
			);
		}
	}

}

EscaperPattern.prototype.init = function() {
	/*
	this.miniEditor = new MiniEditor($('.dvEditor'));
	MiniEditor.patterns = window.opener.HexPlaceholder.getActiveFormulas()
	*/
	//var that = this;
	/*
	$fld = $(".customEscaperFld");
	$fld.val(this.toText());
	$fld.on("change", function() {
		that.setValue($(this).val());
	})*/
	//var Range = ace.require("ace/range").Range;

	var editor = window.ace.edit($(".customEscaperFld")[0]);
	editor.setTheme("ace/theme/monokai");
	editor.session.setMode("ace/mode/javascript");
	editor.setShowPrintMargin(false);
	editor.session.on('changeMode', function(e, session){
        if ("ace/mode/javascript" === session.getMode().$id) {
            if (session.$worker) {
                session.$worker.send("setOptions", [{
                    "asi":true,
					"expr":true
                }]);
            }
        }
    });
	editor.setOptions({
		fontSize: "12pt"
	});
	editor.on("blur", ()=> {
		this.commit();
	});
	editor.setValue(this.toText() || "");
	this.editor = editor;


	var initTestEditor = ()=> {
		var editor = window.ace.edit($("#patternTester")[0]);
		editor.setTheme("ace/theme/monokai");
		editor.session.setMode("ace/mode/text");
		editor.setShowPrintMargin(false);
		editor.setOptions({
			fontSize: "12pt"
		});
		editor.lastSampleRender = 0;
		editor.on("change", ()=> {
			clearTimeout(editor.timeOutRender);
			editor.timeOutRender = setTimeout(async ()=> {
				this.renderHighlight(editor.getValue(), editor);
			},300);
			/*
			if (Date.now() < editor.lastSampleRender+1000) return;
			renderHighlight(editor.getValue(), editor);
			editor.lastSampleRender = Date.now();
			*/
		});
		editor.on("blur", ()=> {
			this.renderHighlight(editor.getValue(), editor);
			localStorage.setItem("options.escaperPattern.sample", editor.getValue());
		});
		var defaultVal = localStorage.getItem("options.escaperPattern.sample") || ""
		editor.setValue(defaultVal);
		if (defaultVal) this.renderHighlight(defaultVal, editor);
		this.testEditor = editor;
		return editor;
	}
	this.testerEditor = initTestEditor();

	this.$elm.find(".clearEscaper").off("click");
	this.$elm.find(".clearEscaper").on("click", ()=> {
		var conf = confirm(t(`Do you really want to clear the custom escaper?`));
		if (!conf) return;
		this.clear();
	});

	this.$elm.find(".resetEscaper").off("click");
	this.$elm.find(".resetEscaper").on("click", ()=> {
		var conf = confirm(t(`Do you really want to reset the custom escaper?`));
		if (!conf) return;
		this.reset();
	});
}

// ===================================================
// CLASS Autoset
// ===================================================

var Autoset = function() {
	
}
Autoset.prototype.init = function() {
	$(document).ready(function() {	
		$("[data-autoset]").each(function() {
			var $this = $(this);
			//if ($this[0].tagName == "select")
			try {
				if ($this.attr("type") == "checkbox") {
					$this.prop("checked", Boolean(common.varAsStringGet($this.attr('data-autoset'))));
				} else if (["INPUT", "SELECT"].includes($this.prop("tagName")) == false) {
					$this.text(common.varAsStringGet($this.attr('data-autoset')));
				} else {
					$this.val(common.varAsStringGet($this.attr('data-autoset')));
				}
			} catch (e) {
				//do nothing
			}
		});
		
		$("[data-autoset]").on("change.autoset", function(e) {
			console.log("Autoset triggered!");
			var $this = $(this);
			if ($this.data("autoset") == "") return;
			
			var autosetPath = $this.data('autoset').split(".")
			if (autosetPath.length < 1) return;
			
			console.log("autoset path : ", autosetPath);
			var thisObj = window;
			for (var i=0; i<autosetPath.length-1; i++) {
				thisObj = thisObj[autosetPath[i]];
				console.log("accessing "+autosetPath[i], thisObj);
				
			}
			console.log("setting up : "+autosetPath[autosetPath.length-1], $this.val());
			
			if ($this.attr("type") == "checkbox") {
				thisObj[autosetPath[autosetPath.length-1]] = $this.prop("checked");
			} else {
				thisObj[autosetPath[autosetPath.length-1]] = $this.val();
			}

		})
	});
}

options.autoset = new Autoset();
options.autoset.init();


options.busy = function() {
	options.isBusy = true;
	return new Promise((resolve, reject) => {
		$("#busyOverlay").fadeIn( 200, ()=>{
			resolve();
		})
	});
	
}
options.busyNot = function() {
	options.isBusy = false;
	return new Promise((resolve, reject) => {
		$("#busyOverlay").fadeOut( 200, ()=>{
			resolve();
		})
	});
}


options.UpdateUI = function() {
	this.$baseElm = $();
}
options.UpdateUI.prototype.evalUpdateButton = function() {
	this.$baseElm.find(".updateStatus > div").addClass("hidden");
	if (sys.config.autoUpdateReinstall) {
		this.$baseElm.find(".updateStatus .pendingUpdate").removeClass("hidden");
	} else {
		if (info.isUpToDate()) {
			this.$baseElm.find(".updateStatus .upToDate").removeClass("hidden");
		} else {
			this.$baseElm.find(".updateStatus .pendingUpdate").removeClass("hidden");
		}
	}
}

options.UpdateUI.prototype.evalLastChecked = function() {
	this.$baseElm.find(".updateLastChecked").html(common.formatDate(new Date(info.getLastCheckedUpdate())));
}

options.UpdateUI.prototype.onUpdateStart = function() {
	this.$baseElm.find(".updateNow").prop("disabled", true);
	
	this.$baseElm.find(".updateNow > i").addClass("rotating-slow");
	this.$baseElm.find(".updateNow > span").html("updating");
	this.$baseElm.find(".updateProcess").removeClass("hidden");
	this.$baseElm.find(".updateProcess").html("Updating...");
	
}

options.UpdateUI.prototype.onUpdateEnd = function() {
	this.$baseElm.find(".updateNow").prop("disabled", false);
	
	this.$baseElm.find(".updateProcess").html("");
	this.$baseElm.find(".updateNow > span").html("Update now");
	this.$baseElm.find(".updateNow > i").removeClass("rotating-slow");
	this.$baseElm.find(".updateProcess").addClass("hidden");
	this.evalUpdateButton();	
}

options.UpdateUI.prototype.setUserAvatar = function() {
	console.warn("user loaded");
	$("#userinfo .username").html(updater.getUser().display_name || "click to login");
	$("#userinfo .userLevel").html(updater.getUser().level || "0");
	$("#userinfo .userPoints").html(updater.getUser().points || "0");
	
	$("#userinfo .defaultUserPicture").removeClass("hasAvatar");		
	if (updater.getUser().localAvatar) {
		$("#userinfo .defaultUserPicture").addClass("hasAvatar");		
	}
	var imgPath = updater.getUser().localAvatar || "/www/img/transparent.png" ;
	console.warn("setting avatar : ", imgPath);
	$("#userinfo .userPicture").attr("src", imgPath);
}
options.UpdateUI.prototype.setUserName = function() {
	if (!updater.user) return;
	console.log("Set username", updater.getUser().display_name);
	$("#userinfo .username").html(updater.getUser().display_name || "login");	
}

options.UpdateUI.prototype.init = function() {
	this.$baseElm = $("#updaterBlock");
	$("#userinfo .username").html(updater.getUser().display_name || "login");
	$("#userinfo .loadingSymbol").addClass("hidden");
	/*
	updater.onAvatarReady((imgPath)=> {
		imgPath = imgPath || "www/img/blank.png" ;
		$("#userinfo .userPicture").attr("src", imgPath);
	});
	*/
	this.setUserName();
	this.setUserAvatar();
	
	updater.onBeforeLoginSuccessFunctions = updater.onBeforeLoginSuccessFunctions || [];
	updater.onBeforeLoginSuccessFunctions[0] = () => {
		console.log("Trigger on before login");
		$("#userinfo .loadingSymbol").removeClass("hidden");
	}

	updater.onLoginSuccessFunctions = updater.onLoginSuccessFunctions || [];
	updater.onLoginSuccessFunctions[0] = () => {
		this.setUserAvatar();
		this.setUserName();
	}

	
	updater.onUserLoadedFunctions = updater.onUserLoadedFunctions || [];
	updater.onUserLoadedFunctions[0] = () => {
		this.setUserAvatar();
		this.setUserName();
		$("#userinfo .loadingSymbol").addClass("hidden");
	}
	updater.onUserLoadingFunctions = updater.onUserLoadingFunctions || [];
	updater.onUserLoadingFunctions[0] = () => {
		$("#userinfo .loadingSymbol").removeClass("hidden");
	}
	
	if (updater.isUpdating) {
		this.onUpdateStart();
	}
	
	updater.onUpdateStart(()=> {
		this.onUpdateStart();
	})
	updater.onUpdateEnd(()=> {
		this.onUpdateEnd();
	})
	
	this.$baseElm.find(".updateCheckNow").on("click", async () => {
		const $this = this.$baseElm.find(".updateCheckNow");
		$this.find("i").addClass("rotating-slow");
		await window.top.info.updateNotification({force:true});
		this.evalLastChecked();
		this.evalUpdateButton();
		$this.find("i").removeClass("rotating-slow");
	});
	
	this.$baseElm.find(".updateNow").on("click", async () => {
		if (this.$baseElm.find(".updateNow").prop("disabled")) return;
		if (info.isUpToDate()) {
			var conf = confirm(t("You are currently on the latest version.\nDo you still want to update?"));
			if (!conf) return;
		}
		var updateIsSuccess = await updater.update({
			type:"manual",
			onStart : ()=> {
				this.onUpdateStart();
			},
			onFetchUrl : ()=> {
				this.$baseElm.find(".updateProcess").html("Fetching information...");
			},			
			downloadOptions: {
				onProgress : (status) => {
					this.$baseElm.find(".updateProcess").html("Downloading patch "+status.percent+"%");
				},
				onEnd : ()=> {
					this.$baseElm.find(".updateProcess").html("Downlod completed");
				}
			},
			onExtract : ()=> {
				this.$baseElm.find(".updateProcess").html("Patching");
			},
			onEnd : ()=> {
				this.onUpdateEnd();
			},
			onFail : (errorId, message)=> {
				alert(`Update failed!\nReason:${message}\nPlease see console log(F12) for more information.`);
			}
			
		});
		if (updateIsSuccess) alert("Translator++ is successfully updated, please restart your Translator++.");
	});
	//this.$baseElm.find(".updateLastChecked").html(common.formatDate(new Date(info.getLastCheckedUpdate())));
	this.evalLastChecked();
	if (Updater.config.debugMode == false && Boolean(info.debugVersion)==false) this.evalUpdateButton();
}
options.updateUI = new options.UpdateUI();


var ExpanderButton = function(elements) {
	this.elements = elements || $(".panel-left .expanderButton");
	this.init();
}

ExpanderButton.prototype.expandAll = function() {
	if (this.allExpanded) return;
	var that = this;
	this.elements.each(function() {
		that.toggle($(this), true);
	});
	this.allExpanded = true;
}


ExpanderButton.prototype.toggle = function($button, expand) {
	if (typeof expand == 'undefined') {
		// auto toggle
		expand = !$button.hasClass('isExpanded');
	}

	var $targets;
	try {
		$targets = $($button.attr('data-expand'));
	} catch (e) {
		$targets = $();
	}

	if (expand) {
		console.log("expanding child", $button.attr('data-expand'), $targets.length);
		// expand child
		$button.removeClass('icon-right-dir')
		$button.addClass('icon-down-dir')
		$button.addClass('isExpanded');
		$targets.removeClass("hidden");
	} else {
		// collapse child
		console.log("collapsing child", $button.attr('data-expand'),$targets.length);

		$button.removeClass('icon-down-dir')
		$button.addClass('icon-right-dir')
		$button.removeClass('isExpanded');
		$targets.addClass("hidden");
		ExpanderButton.allExpanded = false;
	}
}

ExpanderButton.prototype.init = function() {
	var thisExpanderButton = this;
	this.elements.each(function() {
		const $this = $(this);
		$this.addClass("expanderTogler");
		$this.attr("title", "Click to expand/collapse");
		if ($this.is(".rendered")) return;
		
		$this.on("click", function() {
			console.log("expander button clicked");
			thisExpanderButton.toggle($(this));
		});
		
		$this.closest(".selectable").on("dblclick", function() {
			console.log("expander button clicked");
			thisExpanderButton.toggle($(this).find(".expanderTogler"));			
		})

		$this.addClass("rendered");
	})
}

options.hasError = function() {
	if ($("#sl").hasClass("error")) {
		return "There is an error on Source Language field!\nPlease make sure that the field is not empty.";
	}	
	if ($("#tl").hasClass("error")) {
		return "There is an error on Target Language field!\nPlease make sure that the field is not empty.";
	}
}

options.drawListContent = function() {
	var $lists = $(".tabMenu.list li[data-for]");
	$lists.each(function() {
		var $tabMenu = $(this);
		if ($tabMenu.find(".contains").length > 0) return;
		var target = $tabMenu.attr("data-for");
		var $tabContent = $(`#${CSS.escape(target)}`);
		if (!$tabContent.length) return;

		var $contains = $(`<span class="hidden contains"></span>`);
		$contains.text($tabContent.text().replace(/\s+/g, ' '));

		$tabMenu.append($contains);
	})
}

options.initList = function() {
	if (this.listIsInitialized) return;
	this.busy();
	console.log("Initializing list");
	this.drawListContent();
	window.commandOption = {
		valueNames: [
			'menuTitle',
			'contains',
			{ data: ['keyword'] }
		]
	};	
	this.menus = new window.List('leftMenu', window.commandOption);
	this.menus.reIndex();
	this.menus.on("searchStart", function() {
		console.log("Search start", arguments);
		options.expanderButton.expandAll();
	})

	this.listIsInitialized = true;
	this.busyNot()
}

options.init = async function() {
	$(".logIntoFile").prop("checked", logger.getConfig("replaceConsole"));
	$(".logTruncate").prop("checked", logger.getConfig("truncate"));

	$(".logIntoFile").on("change", function() {
		logger.setConfig("replaceConsole", $(this).prop("checked"))
		options.requestRestart()
	})
	$(".logTruncate").on("change", function() {
		logger.setConfig("truncate", $(this).prop("checked"))
		options.requestRestart()
	})
}

$(document).ready(function() {
	try {
		common.addEventHandler(options);
		options.dvField = new DVField();
		options.dvField.init();
		options.initUiLanguageSelector()	
		options.expanderButton = new ExpanderButton();	
		options.escaperPatterns = new EscaperPattern()
	} catch (e) {
		console.warn("Error on initializing option window:", e);
		alert("Error on initializing option window, please see the console log to view the detailed information.");
		options.busyNot();
	}

	var thisTranslator = sys.config.translator||trans.project.options.translator;
	if (Boolean(thisTranslator) == false) alert(t("There is no default translator selected.\nChange the default translator to Google!"));
	
	$("#commonReferenceFile").val(trans.getTemplatePath());
	$("#stagingPath").val(sys.config.stagingPath);
	$("#stagingPath").on("change", function() {
		var conf = confirm(t("Change Stagging Path to :")+$(this).val());
		if (!conf) {
			$(this).val(sys.config.stagingPath);
			return;
		}
		var oldStaggingPath = sys.config.stagingPath;
		sys.config.stagingPath = $(this).val();
		sys.saveConfig();

		conf = confirm(t("Move the content of current Stagging Path into the new one?"));
		if (!conf) return;
		// moving the data
		options.busy()
		.then(()=>{
			return common.copy(oldStaggingPath, sys.config.stagingPath, 
			function(result){
				console.log(result)
				options.busyNot()
			})	
		});	
		//sys.config.stagingPath = $(this).val();
	})
	
	// render horizontal tabs
	$(".horizTabMenu").each(function(e) {
		var $tabMenu = $(this);
		var $targetTabWrapper = $("[data-id='"+$tabMenu.data("for")+"']");
		$tabMenu.children().each(function(e) {
			var $tabButton = $(this);
			
			$tabButton.on('click', function() {
				$tabMenu.trigger("tabChange");
				$(this).trigger("tabSelected");
				var $targetTabContent = $targetTabWrapper.find("[data-id='"+$(this).data("for")+"']");
				$tabMenu.children().removeClass("selected");
				$(this).addClass("selected");
				$targetTabWrapper.children().addClass("hidden");
				$targetTabContent.removeClass("hidden");
			})
		});
		
		$tabMenu.children().eq(0).trigger("click")
	})
	
	$(".installAddonFrom").on("change", function() {
		var files = $(this).val().split(";")
		addonLoader.install(files)
		.then(() => {
			options.requestRestart();
			alert(t("Addon(s) has been installed.\nTranslator++ is need to be restarted to take effect."));
		});
	})
	
	$("#userinfo").on("click", function() {
		var conf;
		if (updater.getUser().id) {
			conf = confirm("Logout current user?");
			if (!conf) return;
			updater.logout();
			return;
		}
		
		conf = confirm("Do you want to login?");
		if (!conf) return;
		updater.login();
	});
	
	$(".horizTabMenu [data-for=online]").on("tabSelected", async function() {
		console.log("online tab selected");
		await options.addonsOption.drawOnlineList();
	})

	$(".menuPanelSearch").on("focus", function() {
		console.log("field focused");
		options.initList();
	})

	
	try {	
		options.drawTranslatorSelector($("#defaultTransEngine"), thisTranslator);
		options.translatorMenu.init();
		options.addonsOption.init();
		options.tab.init();
		options.tab.select($(".tabMenu li.selectable").eq(0));
		options.about.init();
		options.association.init();
		options.updateUI.init();
		options.initSpellCheckLanguage();
		options.init();
		options.busyNot();
	} catch (e) {
		console.warn("Error on initializing option window:", e);
		alert("Error on initializing option window, please see the console log to view the detailed information.");
		options.busyNot();
	}

	console.log("initializing open external");

	setTimeout(function() {
		$("a.externalLink, a[external]").on("click", function(e) {
			console.warn("clicked")
			e.preventDefault();
			nw.Shell.openExternal($(this).attr("href"));
		})
	}, 50)


});


win.on('close', async function() {
	for (var i=0; i<options.__onBeforeClose.length; i++){
		var funct = options.__onBeforeClose.shift();
		funct.call(options);
	}
	if (options.hasError()) {
		var conf = confirm(options.hasError()+"\n"+t("Translator++ may not work normally if you don't fix the error.\nDo you want to close the options Window?"));
		if (!conf) return;
	}
	options.isNeedRestart();


	// Hide the window to give user the feeling of closing immediately
	this.hide();
	if (nw.process.versions["nw-flavor"] == "sdk") {
		win.closeDevTools();
		await common.wait(200);
	}
	//await sys.saveConfig();
	//await common.wait(200);
	// unregister this window on parent window.
	// sending event to main window
	
	/**
	 * Triggered when option window is closed
	 * @event ui#optionsWindowClosed
	 * @since 4.3.20
	 */
	window.opener.ui.trigger("optionsWindowClosed");
	if (typeof window.opener.ui.windows.options !== 'undefined') window.opener.ui.windows.options = undefined;

	this.close(true);

});