diff --git a/package-lock.json b/package-lock.json
index 3b3cda4..9232671 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2970,8 +2970,7 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
- "dev": true
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"define-properties": {
"version": "1.1.3",
@@ -5173,6 +5172,11 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
"dev": true
},
+ "loglevel": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
+ "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po="
+ },
"long": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
@@ -8017,6 +8021,15 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
+ "query-string": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.2.0.tgz",
+ "integrity": "sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA==",
+ "requires": {
+ "decode-uri-component": "^0.2.0",
+ "strict-uri-encode": "^2.0.0"
+ }
+ },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -9158,6 +9171,11 @@
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
+ "strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
+ },
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
diff --git a/package.json b/package.json
index d2f1e19..04a1889 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,8 @@
"version": "0.0.0",
"dependencies": {
"browser-info": "^1.2.0",
+ "loglevel": "^1.6.1",
+ "query-string": "^6.1.0",
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-router": "^4.3.1",
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index d5a2e3c..e9190f9 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -19,8 +19,7 @@
"message": "Target language"
},
"langList": {
- "message":
- "af:Afrikaans, sq:Albanian, am:Amharic, ar:Arabic, hy:Armenian, az:Azerbaijani, eu:Basque, be:Belarusian, bn:Bengali, bs:Bosnian, bg:Bulgarian, ca:Catalan, ceb:Cebuano, ny:Chewa, zh-CN:Chinese (PRC), zh-TW:Chinese (Taiwan), co:Corsican, hr:Croatian, cs:Czech, da:Danish, nl:Dutch, en:English, eo:Esperanto, et:Estonian, fi:Finnish, fr:French, fy:Frisian, gl:Galician, ka:Georgian, de:German, el:Greek, gu:Gujarati, ht:Haitian, ha:Hausa, haw:Hawaiian, he:Hebrew, hi:Hindi, hu:Hungarian, is:Icelandic, ig:Igbo, id:Indonesian, ga:Irish, it:Italian, ja:Japanese, jv:Javanese, kn:Kannada, kk:Kazakh, km:Khmer, ky:Kirghiz, ko:Korean, ku:Kurdish, lo:Laotian, la:Latin, lv:Latvian, lt:Lithuanian, lb:Luxembourgish, mk:Macedonian, mg:Malagasy, ms:Malay, ml:Malayalam, mt:Maltese, mi:Maori, mr:Marathi, mn:Mongolian, hmn:Monk, my:Myanmar, ne:Nepali, no:Norwegian, fa:Persian, pl:Polish, pt:Portuguese, pa:Punjabi, ps:Pushto, ro:Romanian, ru:Russian, sm:Samoan, gd:Scottish Gaelic, sr:Serbian, sn:Shona, sd:Sindhi, si:Sinhala, sk:Slovak, sl:Slovenian, so:Somali, sx:Sotho, es:Spanish, su:Sundanese, sw:Swahili, sv:Swedish, tl:Tagalog, tg:Tajiki, ta:Tamil, te:Telugu, th:Thai, tr:Turkish, uk:Ukrainian, ur:Urdu, uz:Uzbek, vi:Vietnamese, cy:Welsh, xh:Xosa, yi:Yiddish, yo:Yoruba, zu:Zulu"
+ "message": "af:Afrikaans, sq:Albanian, am:Amharic, ar:Arabic, hy:Armenian, az:Azerbaijani, eu:Basque, be:Belarusian, bn:Bengali, bs:Bosnian, bg:Bulgarian, ca:Catalan, ceb:Cebuano, ny:Chewa, zh-CN:Chinese (PRC), zh-TW:Chinese (Taiwan), co:Corsican, hr:Croatian, cs:Czech, da:Danish, nl:Dutch, en:English, eo:Esperanto, et:Estonian, fi:Finnish, fr:French, fy:Frisian, gl:Galician, ka:Georgian, de:German, el:Greek, gu:Gujarati, ht:Haitian, ha:Hausa, haw:Hawaiian, he:Hebrew, hi:Hindi, hu:Hungarian, is:Icelandic, ig:Igbo, id:Indonesian, ga:Irish, it:Italian, ja:Japanese, jv:Javanese, kn:Kannada, kk:Kazakh, km:Khmer, ky:Kirghiz, ko:Korean, ku:Kurdish, lo:Laotian, la:Latin, lv:Latvian, lt:Lithuanian, lb:Luxembourgish, mk:Macedonian, mg:Malagasy, ms:Malay, ml:Malayalam, mt:Maltese, mi:Maori, mr:Marathi, mn:Mongolian, hmn:Monk, my:Myanmar, ne:Nepali, no:Norwegian, fa:Persian, pl:Polish, pt:Portuguese, pa:Punjabi, ps:Pushto, ro:Romanian, ru:Russian, sm:Samoan, gd:Scottish Gaelic, sr:Serbian, sn:Shona, sd:Sindhi, si:Sinhala, sk:Slovak, sl:Slovenian, so:Somali, sx:Sotho, es:Spanish, su:Sundanese, sw:Swahili, sv:Swedish, tl:Tagalog, tg:Tajiki, ta:Tamil, te:Telugu, th:Thai, tr:Turkish, uk:Ukrainian, ur:Urdu, uz:Uzbek, vi:Vietnamese, cy:Welsh, xh:Xosa, yi:Yiddish, yo:Yoruba, zu:Zulu"
},
"settingsLabel": {
@@ -66,8 +65,7 @@
"message": "Do not display the button if translation is not required"
},
"ifCheckLangCaptionLabel": {
- "message":
- "Detects the language of the selected text, and if it is the same as the target language, the button is not displayed."
+ "message": "Detects the language of the selected text, and if it is the same as the target language, the button is not displayed."
},
"toolbarLabel": {
@@ -77,8 +75,7 @@
"message": "Automatically switch to the second language"
},
"ifChangeSecondLangCaptionLabel": {
- "message":
- "Detects the language of the input text, and if it is the same as the default target language, translate it into the second language."
+ "message": "Detects the language of the input text, and if it is the same as the default target language, translate it into the second language."
},
"secondTargetLangLabel": {
"message": "Second language"
@@ -90,8 +87,7 @@
"message": "Waiting time to translate"
},
"waitTimeCaptionLabel": {
- "message":
- "Specify the waiting time from the input of a character to the start of translation. (millisecond)"
+ "message": "Specify the waiting time from the input of a character to the start of translation. (millisecond)"
},
"waitTime2CaptionLabel": {
"message": "If you translate it many times in a short time, it may become unusable for a while."
@@ -104,8 +100,7 @@
"message": "Display the context menu"
},
"ifShowMenuCaptionLabel": {
- "message":
- "Add items to the context menu displayed when right clicking on the web page or the tab."
+ "message": "Add items to the context menu displayed when right clicking on the web page or the tab."
},
"styleLabel": {
@@ -160,6 +155,59 @@
"message": "Background color"
},
+ "otherLabel": {
+ "message": "Other"
+ },
+ "isShowOptionsPageWhenUpdatedLabel": {
+ "message": "Display option page when updating"
+ },
+ "isShowOptionsPageWhenUpdatedCaptionLabel": {
+ "message": "Display the options page when Simple Translate is updated. You can know the update contents quickly."
+ },
+ "isDebugModeLabel": {
+ "message": "Enable debug mode"
+ },
+ "isDebugModeCaptionLabel": {
+ "message": "When debug mode is enabled, the log is output to the debugger."
+ },
+ "resetSettingsLabel": {
+ "message": "Reset settings"
+ },
+ "resetSettingsCaptionLabel": {
+ "message": "Restore all settings to default."
+ },
+ "resetSettingsButtonLabel": {
+ "message": "Reset"
+ },
+
+ "shortcutsLabel": {
+ "message": "Shortcuts"
+ },
+ "keyboardShortcutsLabel": {
+ "message": "Keyboard shortcuts"
+ },
+ "setKeyboardShortCutsMessage": {
+ "message": "Set keyboard shortcuts."
+ },
+ "typeShortcutMessage": {
+ "message": "Type a shortcut"
+ },
+ "typeLetterMessage": {
+ "message": "Type a letter"
+ },
+ "includeModifierKeysMessage": {
+ "message": "Include either Ctrl or Alt"
+ },
+ "includeMacModifierKeysMessage": {
+ "message": "Include either Command, Ctrl or Alt"
+ },
+ "invalidLetterMessage": {
+ "message": "The letter can not be used"
+ },
+ "invalidShortcutMessage": {
+ "message": "The shortcut can not be used"
+ },
+
"informationLabel": {
"message": "Information"
},
@@ -170,8 +218,7 @@
"message": "Please make a donation"
},
"donationCaptionLabel": {
- "message":
- "Thank you for using Simple Translate.
Your support will be a big encouragement, as I continue to develop the add-on.
If you like Simple Translate, I would be pleased if you could consider donating."
+ "message": "Thank you for using Simple Translate.
Your support will be a big encouragement, as I continue to develop the add-on.
If you like Simple Translate, I would be pleased if you could consider donating."
},
"amazonTitleLabel": {
"message": "amazon.co.jp eGift Cards"
diff --git a/src/common/Settings.js b/src/common/Settings.js
deleted file mode 100644
index e95e5e2..0000000
--- a/src/common/Settings.js
+++ /dev/null
@@ -1,291 +0,0 @@
-/* Copyright (c) 2017-2018 Sienori All rights reserved.
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-//初回起動時にオプションページを表示して設定を初期化
-//Display option page at initial startup and initialize settings
-/*
-browser.runtime.onInstalled.addListener(function(){
- browser.runtime.openOptionsPage();
-});
-*/
-function settingsObj() {}
-(function() {
- //オプションページを書き換え,設定の初期化
- //Rewrite option page, initialize setting
- settingsObj.prototype.initOptionsPage = function() {
- return new Promise(function(resolve, reject) {
- labelSet();
- getSettingsByHtml();
- overRideSettingsByStorage().then(function() {
- overRideHtml();
- saveSettings();
- resolve();
- });
- });
- };
- //オプションページから設定を保存
- //Save settings from options page
- settingsObj.prototype.saveOptionsPage = function() {
- return new Promise(function(resolve, reject) {
- getSettingsByHtml();
- saveSettings().then(function() {
- resolve();
- });
- });
- };
- //設定を初期化
- //Initialize setting
- settingsObj.prototype.init = function() {
- return new Promise(function(resolve, reject) {
- getSettings().then(function() {
- resolve(Settings);
- });
- });
- };
- //設定を返す
- //return settings
- settingsObj.prototype.get = function() {
- return Settings;
- };
- //受け取ったオブジェクトを保存
- //Save the received object
- settingsObj.prototype.save = function(settings) {
- return new Promise(function(resolve, reject) {
- for (let i in settings) {
- Settings[i] = settings[i];
- }
- saveSettings().then(function() {
- resolve();
- });
- });
- };
- //設定を削除
- //Delete settings
- settingsObj.prototype.clear = function(setting) {
- return new Promise(function(resolve, reject) {
- delete Settings[setting];
- saveSettings().then(function() {
- resolve();
- });
- });
- };
- //全ての設定を削除
- //Delete all settings
- settingsObj.prototype.clearAll = function() {
- return new Promise(function(resolve, reject) {
- Settings = new settingsObj();
- saveSettings().then(function() {
- resolve();
- });
- });
- };
- settingsObj.prototype.labelSet = function() {
- labelSet();
- };
-
- //let Settings = new settingsObj();
- let Settings = {};
- //S = new settingsObj(); //外部から呼び出し Call from outside
-
- //spanやoptionのid,buttonのclassに"Label"が含まれるときi18nから値を取得して書き換え
- //When "label" is included in span and option id, button class Retrieve the value from i18n and rewrite it
- function labelSet() {
- textLabelSet("p");
- textLabelSet("span");
- textLabelSet("option");
- textLabelSet("input");
-
- function textLabelSet(tagName) {
- const items = document.getElementsByTagName(tagName);
- for (let i of items) {
- let label;
- if (i.id != undefined && i.id.includes("Label")) {
- label = browser.i18n.getMessage(i.id);
- } else if (i.className != undefined && i.className.includes("Label")) {
- const labelList = i.className.split(" ").filter((element, index, array) => {
- return element.includes("Label");
- });
- label = browser.i18n.getMessage(labelList[0]);
- } else {
- continue;
- }
-
- if (!label == "") {
- if (tagName == "input") {
- switch (i.type) {
- case "button":
- case "submit":
- i.value = label;
- break;
- case "text":
- i.placeholder = label;
- break;
- }
- } else {
- i.innerHTML = label;
- }
- }
- }
- }
- }
-
- //storageからSettingsの項目を取得して存在しない物をSettingsに上書き
- //Retrieve the Settings item from storage and overwrite Settings that do not exist
- function overRideSettingsByStorage() {
- return new Promise(function(resolve, reject) {
- browser.storage.local.get("Settings", function(value) {
- for (let i in Settings) {
- if (value.Settings != undefined && value.Settings[i] != undefined) {
- Settings[i] = value.Settings[i];
- }
- }
- for (let i in value.Settings) {
- if (Settings[i] == undefined) Settings[i] = value.Settings[i];
- }
- resolve();
- });
- });
- }
-
- //オプションページにSettingsを反映
- //Reflect Settings on option page
- function overRideHtml() {
- let inputs = document.getElementsByTagName("input");
- for (let i in inputs) {
- if (inputs[i].id == undefined) continue;
- if (inputs[i].className != undefined && inputs[i].className.indexOf("noSetting") != -1)
- continue;
-
- switch (inputs[i].type) {
- case "text":
- case "number":
- case "search":
- case "tel":
- case "url":
- case "email":
- case "password":
- case "datetime":
- case "month":
- case "week":
- case "time":
- case "datetime-local":
- case "range":
- case "color":
- inputs[i].value = Settings[inputs[i].id];
- break;
- case "checkbox":
- inputs[i].checked = Settings[inputs[i].id];
- break;
- case "radio":
- if (Settings[inputs[i].name] == inputs[i].value) {
- inputs[i].checked = true;
- }
- break;
- }
- }
- let textareas = document.getElementsByTagName("textarea");
- for (let i in textareas) {
- if (textareas[i].id == undefined) continue;
- if (textareas[i].className != undefined && textareas[i].className.indexOf("noSetting") != -1)
- continue;
- textareas[i].value = Settings[textareas[i].id];
- }
-
- let selects = document.getElementsByTagName("select");
- for (let i in selects) {
- if (selects[i].id == undefined) continue;
- if (selects[i].className != undefined && inputs[i].className.indexOf("noSetting") != -1)
- continue;
-
- selects[i].value = Settings[selects[i].id];
- }
- }
-
- //オプションページから設定の値を取得
- //Get setting value from option page
- function getSettingsByHtml() {
- let inputs = document.getElementsByTagName("input");
-
- for (let i in inputs) {
- if (inputs[i].id == undefined) continue;
- if (inputs[i].className != undefined && inputs[i].className.indexOf("noSetting") != -1)
- continue;
-
- switch (inputs[i].type) {
- case "text":
- case "number":
- case "search":
- case "tel":
- case "url":
- case "email":
- case "password":
- case "datetime":
- case "month":
- case "week":
- case "time":
- case "datetime-local":
- case "range":
- case "color":
- Settings[inputs[i].id] = inputs[i].value;
- break;
- case "checkbox":
- Settings[inputs[i].id] = inputs[i].checked;
- break;
- case "radio":
- if (inputs[i].checked == true) {
- Settings[inputs[i].name] = inputs[i].value;
- }
- break;
- }
- }
-
- let textareas = document.getElementsByTagName("textarea");
- for (let i in textareas) {
- if (textareas[i].id == undefined) continue;
- if (textareas[i].className != undefined && textareas[i].className.indexOf("noSetting") != -1)
- continue;
- Settings[textareas[i].id] = textareas[i].value;
- }
-
- let selects = document.getElementsByTagName("select");
- for (let i in selects) {
- if (selects[i].id == undefined) continue;
- if (selects[i].className != undefined && selects[i].className.indexOf("noSetting") != -1)
- continue;
-
- Settings[selects[i].id] = selects[i].value;
- }
- }
-
- //ストレージが変更されたらget
- browser.storage.onChanged.addListener(changedSettings);
-
- function changedSettings(changes, area) {
- if (Object.keys(changes).includes("Settings")) {
- Settings = changes.Settings.newValue;
- }
- }
-
- function getSettings() {
- return new Promise(function(resolve, reject) {
- browser.storage.local.get("Settings", function(value) {
- Settings = value.Settings;
- resolve(Settings);
- });
- });
- }
-
- function saveSettings() {
- return new Promise(function(resolve, reject) {
- browser.storage.local
- .set({
- Settings: Settings
- })
- .then(function() {
- resolve(Settings);
- });
- });
- }
-})();
diff --git a/src/common/genelateLangOptions.js b/src/common/genelateLangOptions.js
new file mode 100644
index 0000000..3fba170
--- /dev/null
+++ b/src/common/genelateLangOptions.js
@@ -0,0 +1,12 @@
+const alphabeticallySort = (a, b) => a.name.localeCompare(b.name);
+
+export default () => {
+ const langListText = browser.i18n.getMessage("langList");
+ const langList = langListText.split(", ");
+ const langOptions = langList.map(lang => ({
+ value: lang.split(":")[0],
+ name: lang.split(":")[1]
+ }));
+ langOptions.sort(alphabeticallySort);
+ return langOptions;
+};
diff --git a/src/common/getShortcut.js b/src/common/getShortcut.js
new file mode 100644
index 0000000..2c4ac77
--- /dev/null
+++ b/src/common/getShortcut.js
@@ -0,0 +1,23 @@
+import browserInfo from "browser-info";
+import manifest from "src/manifest-firefox.json";
+
+export default commandId => {
+ const suggestedKeys = manifest.commands[commandId].suggested_key || null;
+ if (!suggestedKeys) return null;
+
+ const os = browserInfo().os;
+ switch (os) {
+ case "Windows":
+ return suggestedKeys.windows || suggestedKeys.default;
+ case "OS X":
+ return suggestedKeys.mac || suggestedKeys.default;
+ case "Linux":
+ return suggestedKeys.linux || suggestedKeys.default;
+ case "Android":
+ return suggestedKeys.android || suggestedKeys.default;
+ case "iOS":
+ return suggestedKeys.ios || suggestedKeys.default;
+ default:
+ return suggestedKeys.default || null;
+ }
+};
diff --git a/src/common/log.js b/src/common/log.js
new file mode 100644
index 0000000..1fc26eb
--- /dev/null
+++ b/src/common/log.js
@@ -0,0 +1,19 @@
+import log from "loglevel";
+import { getSettings } from "src/settings/settings";
+
+export const overWriteLogLevel = () => {
+ const originalFactory = log.methodFactory;
+ log.methodFactory = (methodName, logLevel, loggerName) => {
+ const rawMethod = originalFactory(methodName, logLevel, loggerName);
+
+ return (logDir, ...args) => {
+ rawMethod(`[${methodName}]`, `${logDir}:`, ...args);
+ };
+ };
+};
+
+export const updateLogLevel = () => {
+ const isDebugMode = getSettings("isDebugMode");
+ if (isDebugMode) log.enableAll();
+ else log.disableAll();
+};
diff --git a/src/options/components/CategoryContainer.js b/src/options/components/CategoryContainer.js
new file mode 100644
index 0000000..ab45268
--- /dev/null
+++ b/src/options/components/CategoryContainer.js
@@ -0,0 +1,35 @@
+import React from "react";
+import browser from "webextension-polyfill";
+import OptionContainer from "./OptionContainer";
+import "../styles/CategoryContainer.scss";
+
+export default props => {
+ const { category, elements } = props;
+ return (
+
+
+
+ );
+};
diff --git a/src/options/components/ContentsArea.js b/src/options/components/ContentsArea.js
new file mode 100644
index 0000000..1b7d238
--- /dev/null
+++ b/src/options/components/ContentsArea.js
@@ -0,0 +1,20 @@
+import React from "react";
+import { Route, Switch } from "react-router-dom";
+import browserInfo from "browser-info";
+import SettingsPage from "./SettingsPage";
+import KeyboardShortcutsPage from "./KeyboardShortcutsPage";
+import InformationPage from "./InformationPage";
+import "../styles/ContentsArea.scss";
+
+const isValidShortcuts = browserInfo().name == "Firefox" && browserInfo().version >= 60;
+
+export default () => (
+
+
+
+ {isValidShortcuts && }
+
+
+
+
+);
diff --git a/src/options/components/InformationPage.js b/src/options/components/InformationPage.js
new file mode 100644
index 0000000..6bd3320
--- /dev/null
+++ b/src/options/components/InformationPage.js
@@ -0,0 +1,99 @@
+import React from "react";
+import browser from "webextension-polyfill";
+import browserInfo from "browser-info";
+import queryString from "query-string";
+import OptionsContainer from "./OptionContainer";
+import manifest from "src/manifest-chrome.json";
+
+export default props => {
+ const query = queryString.parse(props.location.search);
+
+ const extensionVersion = manifest.version;
+ const isChrome = browserInfo().name == "Chrome";
+ const paypalLink = `https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&no_shipping=1&business=sienori.firefox@gmail.com&item_name=Simple Translate ${
+ isChrome ? "for Chrome " : ""
+ }- Donation`;
+ const email = `sienori.firefox+st${isChrome ? "fc" : ""}@gmail.com`;
+
+ return (
+
+ }
+ />
+
+
+
+
+ {browserInfo().name === "Firefox"
+ ? browser.i18n.getMessage("addonPageLabel")
+ : browser.i18n.getMessage("extensionPageLabel")}
+
+
+
+ GitHub
+
+
+
+ }
+ />
+
+ );
+};
diff --git a/src/options/components/KeyboardShortcutForm.js b/src/options/components/KeyboardShortcutForm.js
new file mode 100644
index 0000000..9dc824f
--- /dev/null
+++ b/src/options/components/KeyboardShortcutForm.js
@@ -0,0 +1,154 @@
+import React, { Component } from "react";
+import browserInfo from "browser-info";
+import browser from "webextension-polyfill";
+import ClearIcon from "../icons/clear.svg";
+import RestetIcon from "../icons/reset.svg";
+
+const normalizeKey = (key, keyCode) => {
+ const alphabet = /^([a-z]|[A-Z])$/;
+ if (alphabet.test(key)) return key.toUpperCase();
+
+ const digit = /^[0-9]$/;
+ const func = /^F([0-9]|1[0-2])$/;
+ const homes = /^(Home|End|PageUp|PageDown|Insert|Delete)$/;
+ if (digit.test(key) || func.test(key) || homes.test(key)) return key;
+
+ const space = /^\s$/;
+ if (space.test(key)) return "Space";
+
+ const arrows = /^(ArrowUp|ArrowDown|ArrowLeft|ArrowRight)$/;
+ if (arrows.test(key)) return key.split("Arrow")[1];
+
+ const medias = /^(MediaPlayPause|MediaStop)$/;
+ if (medias.test(key)) return key;
+ if (key == "MediaTrackNext") return "MediaNextTrack";
+ if (key == "MediaTrackPrevious") return "MediaPrevTrack";
+
+ const keyCode0 = 48;
+ if (keyCode0 <= keyCode && keyCode <= keyCode0 + 9) return keyCode - keyCode0;
+
+ if (keyCode == 188) return "Comma";
+ if (keyCode == 190) return "Period";
+
+ return "";
+};
+
+export default class KeyboardShortcutForm extends Component {
+ constructor(props) {
+ super(props);
+ this.isMac = browserInfo().os == "OS X";
+ this.state = {
+ shortcut: props.shortcut,
+ value: props.shortcut,
+ defaultValue: props.defaultValue,
+ error: ""
+ };
+ }
+
+ handleFocus(e) {
+ e.target.select();
+ window.document.onkeydown = () => false;
+ window.document.onkeypress = () => false;
+ }
+
+ handleBlur(e) {
+ const shortcut = this.state.shortcut;
+ this.setState({ error: "", value: shortcut });
+ window.document.onkeydown = () => true;
+ window.document.onkeypress = () => true;
+ }
+
+ handleChange(e) {}
+
+ handleKeyDown(e) {
+ if (e.repeat) return;
+ if (e.key == "Tab") {
+ window.document.activeElement.blur();
+ return;
+ }
+ const normalizedKey = normalizeKey(e.key, e.keyCode);
+ let error = "";
+
+ const mediaKeys = /^(MediaPlayPause|MediaStop|MediaNextTrack|MediaPrevTrack)$/;
+ const funcKeys = /^F([0-9]|1[0-2])$/;
+ const modifierKeys = /^(Control|Alt|Shift|Meta)$/;
+
+ if (mediaKeys.test(normalizedKey) || funcKeys.test(normalizedKey)) error = "";
+ else if (modifierKeys.test(e.key)) error = browser.i18n.getMessage("typeLetterMessage");
+ else if (!e.ctrlKey && !e.altKey && !e.metaKey)
+ error = this.isMac
+ ? browser.i18n.getMessage("includeMacModifierKeysMessage")
+ : browser.i18n.getMessage("includeModifierKeysMessage");
+ else if (normalizedKey == "") error = browser.i18n.getMessage("invalidLetterMessage");
+
+ const value = `${e.ctrlKey ? (this.isMac ? "MacCtrl+" : "Ctrl+") : ""}${
+ e.metaKey && this.isMac ? "Command+" : ""
+ }${e.altKey ? "Alt+" : ""}${e.shiftKey ? "Shift+" : ""}${normalizedKey}`;
+
+ this.setState({ error: error, value: value || "" });
+ const isValidShortcut = value != "" && error == "";
+ if (isValidShortcut) this.updateShortcut(value);
+ }
+
+ handleKeyUp(e) {
+ if (this.state.error != "") {
+ this.setState({ value: "" });
+ }
+ }
+
+ async updateShortcut(shortcut) {
+ try {
+ await browser.commands.update({ name: this.props.id, shortcut: shortcut });
+ this.setState({ shortcut: shortcut || "" });
+ } catch (e) {
+ this.setState({ error: browser.i18n.getMessage("invalidShortcutMessage") });
+ }
+ }
+
+ async clearShortcut() {
+ await browser.commands.reset(this.props.id).catch(() => {});
+ this.setState({ shortcut: "", value: "" });
+ }
+
+ async resetShortcut() {
+ const defaultValue = this.state.defaultValue;
+ this.updateShortcut(defaultValue);
+ this.setState({ value: defaultValue || "" });
+ }
+
+ render() {
+ return (
+
+
+ this.handleKeyDown(e)}
+ onKeyUp={e => this.handleKeyUp(e)}
+ onChange={e => this.handleChange(e)}
+ onFocus={e => this.handleFocus(e)}
+ onBlur={e => this.handleBlur(e)}
+ style={{ imeMode: "disabled" }}
+ />
+
+
+
+
{this.state.error}
+
+ );
+ }
+}
diff --git a/src/options/components/KeyboardShortcutsPage.js b/src/options/components/KeyboardShortcutsPage.js
new file mode 100644
index 0000000..1a065ef
--- /dev/null
+++ b/src/options/components/KeyboardShortcutsPage.js
@@ -0,0 +1,62 @@
+import React, { Component } from "react";
+import browser from "webextension-polyfill";
+import getShortcut from "src/common/getShortcut";
+import CategoryContainer from "./CategoryContainer";
+
+export default class KeyboardShortcutPage extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ commands: [],
+ isInit: false
+ };
+ this.initCommands();
+ }
+
+ async initCommands() {
+ const commands = await browser.commands.getAll();
+ const rawDescription = /^__MSG_(.*)__$/;
+ const convertedCommands = commands.map(command => {
+ const isRawDescription = rawDescription.test(command.description);
+ if (isRawDescription)
+ command.description = browser.i18n.getMessage(command.description.match(rawDescription)[1]);
+ return command;
+ });
+
+ this.setState({ commands: convertedCommands, isInit: true });
+ }
+
+ render() {
+ const commandElements = this.state.commands.map(command => ({
+ id: command.name,
+ title: command.description,
+ useRawTitle: true,
+ captions: [],
+ type: "keyboard-shortcut",
+ shortcut: command.shortcut || "",
+ defaultValue: getShortcut(command.name)
+ }));
+
+ const shortcutCategory = {
+ category: "",
+ elements: [
+ {
+ id: "keyboard",
+ title: "keyboardShortcutsLabel",
+ captions: ["setKeyboardShortCutsMessage"],
+ type: "none",
+ new: true,
+ childElements: commandElements
+ }
+ ]
+ };
+
+ return (
+
+
{browser.i18n.getMessage("keyboardShortcutsLabel")}
+
+ {this.state.isInit &&
}
+
+ );
+ }
+}
diff --git a/src/options/components/OptionContainer.js b/src/options/components/OptionContainer.js
new file mode 100644
index 0000000..8ae8811
--- /dev/null
+++ b/src/options/components/OptionContainer.js
@@ -0,0 +1,182 @@
+import React from "react";
+import browser from "webextension-polyfill";
+import { setSettings, getSettings } from "src/settings/settings";
+import KeyboardShortcutForm from "./KeyboardShortcutForm";
+import "../styles/OptionContainer.scss";
+
+export default props => {
+ const { title, captions, type, id, children } = props;
+
+ const handleValueChange = e => {
+ let value = e.target.value;
+
+ if (type == "number") {
+ const validity = e.target.validity;
+ if (validity.rangeOverflow) value = props.max;
+ else if (validity.rangeUnderflow) value = props.min;
+ else if (validity.badInput || value == "" || !validity.valid) value = props.default;
+ }
+
+ setSettings(id, value);
+ };
+
+ const handleCheckedChange = e => {
+ setSettings(id, e.target.checked);
+ };
+
+ let formId;
+ let optionForm;
+ switch (type) {
+ case "checkbox":
+ formId = id;
+ optionForm = (
+
+ );
+ break;
+ case "number":
+ formId = id;
+ optionForm = (
+
+ );
+ break;
+ case "text":
+ formId = id;
+ optionForm = (
+
+ );
+ break;
+ case "radio":
+ formId = `${id}_${props.value}`;
+ optionForm = (
+
+ );
+ break;
+ case "color":
+ formId = id;
+ optionForm = (
+
+ );
+ break;
+ case "select":
+ formId = id;
+ optionForm = (
+
+
+
+ );
+ break;
+ case "button":
+ formId = "";
+ optionForm = (
+
+ );
+ break;
+ case "file":
+ formId = "";
+ optionForm = (
+
+ );
+ break;
+ case "keyboard-shortcut":
+ formId = id;
+ optionForm = (
+
+ );
+ break;
+ case "none":
+ formId = "";
+ optionForm = "";
+ break;
+ }
+
+ const shouldShow = props.shouldShow == undefined || props.shouldShow;
+
+ return (
+ shouldShow && (
+
+ {props.hr &&
}
+
+
+
+ {captions.map((caption, index) => (
+
+ {caption
+ ? props.useRawCaptions
+ ? caption
+ : browser.i18n.getMessage(caption).replace(/
/g, "\n")
+ : ""}
+
+ ))}
+ {props.extraCaption ? props.extraCaption : ""}
+
+
{optionForm}
+
+ {children && (
+
+ )}
+
+ )
+ );
+};
diff --git a/src/options/components/OptionsPage.js b/src/options/components/OptionsPage.js
new file mode 100644
index 0000000..ee79333
--- /dev/null
+++ b/src/options/components/OptionsPage.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { HashRouter } from "react-router-dom";
+import SideBar from "./SideBar";
+import ContentsArea from "./ContentsArea";
+import ScrollToTop from "./ScrollToTop";
+import "../styles/OptionsPage.scss";
+
+export default () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/options/components/ScrollToTop.js b/src/options/components/ScrollToTop.js
new file mode 100644
index 0000000..6383105
--- /dev/null
+++ b/src/options/components/ScrollToTop.js
@@ -0,0 +1,16 @@
+import { Component } from "react";
+import { withRouter } from "react-router-dom";
+
+class ScrollToTop extends Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.location !== prevProps.location) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+export default withRouter(ScrollToTop);
diff --git a/src/options/components/SettingsPage.js b/src/options/components/SettingsPage.js
new file mode 100644
index 0000000..e910305
--- /dev/null
+++ b/src/options/components/SettingsPage.js
@@ -0,0 +1,59 @@
+import React, { Component } from "react";
+import browser from "webextension-polyfill";
+import { updateLogLevel, overWriteLogLevel } from "src/common/log";
+import { initSettings, resetAllSettings } from "src/settings/settings";
+import defaultSettings from "src/settings/defaultSettings";
+import CategoryContainer from "./CategoryContainer";
+
+export default class SettingsPage extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isInit: false
+ };
+ this.init();
+ }
+
+ async init() {
+ await initSettings();
+ overWriteLogLevel();
+ updateLogLevel();
+ this.setState({ isInit: true });
+ }
+
+ render() {
+ const settingsContent = (
+
+ {defaultSettings.map((category, index) => (
+
+ ))}
+
+
+ );
+
+ return (
+
+
{browser.i18n.getMessage("settingsLabel")}
+
+ {this.state.isInit ? settingsContent : ""}
+
+ );
+ }
+}
+
+const additionalCategory = {
+ category: "",
+ elements: [
+ {
+ id: "resetSettings",
+ title: "resetSettingsLabel",
+ captions: ["resetSettingsCaptionLabel"],
+ type: "button",
+ value: "resetSettingsButtonLabel",
+ onClick: async () => {
+ await resetAllSettings();
+ location.reload(true);
+ }
+ }
+ ]
+};
diff --git a/src/options/components/SideBar.js b/src/options/components/SideBar.js
new file mode 100644
index 0000000..35ef5dd
--- /dev/null
+++ b/src/options/components/SideBar.js
@@ -0,0 +1,37 @@
+import React from "react";
+import browser from "webextension-polyfill";
+import { Link, withRouter } from "react-router-dom";
+import browserInfo from "browser-info";
+import "../styles/SideBar.scss";
+
+const isValidShortcuts = browserInfo().name == "Firefox" && browserInfo().version >= 60;
+
+const SideBar = props => (
+
+
+
+
Simple Translate
+
+
+ - path != props.location.pathname)
+ ? "selected"
+ : ""
+ }`}
+ >
+ {browser.i18n.getMessage("settingsLabel")}
+
+ {isValidShortcuts && (
+ -
+ {browser.i18n.getMessage("shortcutsLabel")}
+
+ )}
+ -
+ {browser.i18n.getMessage("informationLabel")}
+
+
+
+);
+
+export default withRouter(SideBar);
diff --git a/src/options/icons/clear.svg b/src/options/icons/clear.svg
new file mode 100644
index 0000000..2b99687
--- /dev/null
+++ b/src/options/icons/clear.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/options/icons/reset.svg b/src/options/icons/reset.svg
new file mode 100644
index 0000000..e6a27fc
--- /dev/null
+++ b/src/options/icons/reset.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/src/options/index.html b/src/options/index.html
index 1633849..2396515 100644
--- a/src/options/index.html
+++ b/src/options/index.html
@@ -2,426 +2,14 @@
-
-
- Simple Translate
-
-
-
+
+ Simple Translate
+
+
-
-
-
-
設定
-
-
- -
-
-
- -
-
-
翻訳先の言語
-
デフォルトの翻訳先の言語を選択します。
-
-
-
-
- -
-
-
翻訳候補を表示する
-
単一の語句が翻訳された時に複数の候補を表示します。
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
- -
-
-
翻訳ボタンを表示する
-
クリックすると翻訳パネルが開くボタンを表示します。
-
-
-
-
-
- -
-
-
翻訳パネルを表示する
-
ボタンを表示せずに直接翻訳パネルを表示します。
-
-
-
-
-
- -
-
-
ボタンやパネルを表示しない
-
翻訳ボタンや翻訳パネルを表示しません。
-
-
-
-
-
-
- -
-
-
翻訳の必要がなければボタンを表示しない
-
選択したテキストの言語を判定し,翻訳先言語と同じ場合はボタンを表示しません。
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
自動的に第2言語に切り替える
-
入力されたテキストの言語を判定し,デフォルトの翻訳先言語と同じ場合には第2言語に翻訳します。
-
-
-
-
-
-
-
-
-
- -
-
-
翻訳までの待ち時間
-
文字が入力されてから翻訳を開始するまでの待ち時間を指定します。(ミリ秒)
-
短時間に何回も翻訳すると,しばらくの間利用できなくなることがあります。
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
翻訳ボタン
-
webページ上で表示される翻訳ボタンのスタイルを指定します。
-
-
-
-
-
-
- -
-
-
翻訳パネル
-
webページ上で表示される翻訳パネルのスタイルを指定します。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/options/index.js b/src/options/index.js
index 89b123c..f662e32 100644
--- a/src/options/index.js
+++ b/src/options/index.js
@@ -1,69 +1,5 @@
-/* Copyright (c) 2017-2018 Sienori All rights reserved.
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const S = new settingsObj();
-let targetLang = document.getElementById("targetLang");
-let secondTargetLang = document.getElementById("secondTargetLang");
+import React from "react";
+import ReactDOM from "react-dom";
+import OptionsPage from "./components/OptionsPage";
-setLangList();
-
-function setLangList() {
- let langList = browser.i18n.getMessage("langList");
- langList = langList.split(", ");
-
- for (let i in langList) {
- langList[i] = langList[i].split(":");
- }
- langList = langList.sort(alphabeticallySort);
-
- let langListHtml = "";
- for (let i of langList) {
- langListHtml += ``;
- }
- targetLang.innerHTML = langListHtml;
- secondTargetLang.innerHTML = langListHtml;
-
- initialSetting();
-}
-
-function alphabeticallySort(a, b) {
- if (a[1].toString() > b[1].toString()) {
- return 1;
- } else {
- return -1;
- }
-}
-
-function initialSetting() {
- switch (
- browser.i18n.getUILanguage() //一部の言語はブラウザの設定に合わせる
- ) {
- case "ja":
- case "zh-CN":
- case "zh-TW":
- case "ko":
- case "ru":
- case "de":
- case "fr":
- case "it":
- targetLang.value = browser.i18n.getUILanguage();
- secondTargetLang.value = "en";
- break;
- default:
- targetLang.value = "en";
- secondTargetLang.value = "ja";
- break;
- }
-}
-
-S.initOptionsPage().then(function() {
- const saveByChangeItems = document.getElementsByClassName("saveByChange");
- for (let item of saveByChangeItems) {
- item.addEventListener("change", save);
- }
-});
-
-function save() {
- S.saveOptionsPage();
-}
+ReactDOM.render(, document.getElementById("root"));
diff --git a/src/options/options.css b/src/options/options.css
deleted file mode 100644
index fe3205f..0000000
--- a/src/options/options.css
+++ /dev/null
@@ -1,415 +0,0 @@
-:root {
- --main-text: #0c0c0d;
- --sub-text: #737373;
- --line: #ededf0;
- --button: #d7d7db;
- --highlight: #5595ff;
- --main-bg: #ffffff;
- --new: #ff4f4f;
-}
-
-body {
- font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica",
- sans-serif;
- font-size: 15px;
- font-weight: 400;
- color: var(--main-text);
- background-color: var(--main-bg);
- line-height: 1.5;
- display: flex;
- flex-direction: row;
-}
-
-p {
- margin: 0px;
-}
-
-ul {
- padding: 0px;
-}
-
-li {
- list-style-type: none;
-}
-
-hr {
- width: 100%;
- background-color: var(--line);
- height: 1px;
- border: none;
- margin-top: 20px;
- margin-bottom: 20px;
-}
-
-/*----sidebar----*/
-
-#sidebar {
- font-size: 17px;
- font-weight: 400;
- text-align: right;
- flex-shrink: 0;
- -moz-user-select: none;
-}
-
-.titleContainer {
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-.logo {
- height: 64px;
- width: 64px;
-}
-
-.logotitle {
- text-align: left;
- font-size: 14px;
- font-weight: 300;
- color: var(--sub-text);
- /*margin: auto;*/
-}
-
-.sidebarItem:hover {
- text-decoration-line: underline;
-}
-
-#sidebar > ul {
- padding-left: 40px;
-}
-
-#sidebar > ul > li {
- padding: 10px 15px;
-}
-
-#sidebar .selected {
- color: var(--highlight);
-}
-
-/*----contents----*/
-
-#contents {
- padding-top: 20px;
- padding-left: 20px;
- padding-bottom: 50px;
- width: 650px;
-}
-
-.contentTitle {
- font-size: 33px;
- font-weight: 200;
- color: var(--sub-text);
- line-height: 2;
-}
-
-.caption {
- font-size: 13px;
- font-weight: 400;
- color: var(--sub-text);
-}
-
-#contents ul {
- margin: 0px;
-}
-
-.childElements {
- padding-left: 20px;
- margin-bottom: 30px;
- border-left: solid 10px var(--line);
-}
-
-.categoryContainer {
-}
-
-.categoryElements {
- padding-left: 20px;
- margin-bottom: 30px;
-}
-
-.categoryTitle {
- font-size: 16px;
- font-weight: 600;
- color: var(--sub-text);
-}
-
-.optionContainer {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- padding: 10px 0px 10px 0px;
-}
-
-.optionContainer.reverse {
- flex-direction: row-reverse;
-}
-
-.buttonsContainer {
- justify-content: flex-start;
-}
-
-.optionText {
- flex: 1;
-}
-
-.new p:nth-child(1)::after {
- content: "New";
- color: var(--new);
- font-size: 14px;
- border: 1px solid var(--new);
- border-radius: 2px;
- padding: 0px 5px;
- margin-left: 5px;
-}
-
-.updated p:nth-child(1)::after {
- content: "Updated";
- color: var(--new);
- font-size: 14px;
- border: 1px solid var(--new);
- border-radius: 2px;
- padding: 0px 5px;
- margin-left: 5px;
-}
-
-.optionForm {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- margin-left: 10px;
-}
-
-.reverse .optionForm {
- flex-basis: 40px;
- justify-content: flex-start;
-}
-
-#importClear {
- position: relative;
- left: 10px;
-}
-
-.favicon {
- width: 18px;
- height: 18px;
- padding: 1px;
- display: block;
- float: left;
-}
-
-/*----forms----*/
-
-input {
- font-family: inherit;
- font-size: 14px;
-}
-
-input[type="number"],
-input[type="text"],
-input[type="color"] {
- -moz-appearance: textfield;
- width: 50px;
- height: 30px;
- padding-left: 5px;
- padding-right: 5px;
- border: 1px solid var(--button);
- border-radius: 2px;
-}
-
-input[type="number"]:hover,
-input[type="text"]:hover,
-input[type="color"]:hover,
-input[type="number"]:focus,
-input[type="text"]:focus,
-input[type="color"]:focus {
- border-color: var(--highlight);
-}
-
-input[type="text"] {
- width: 200px;
-}
-
-input[type="color"] {
- background-color: var(--main-bg);
- padding: 5px;
-}
-
-.button {
- display: flex;
- flex-direction: column;
- justify-content: center;
- min-width: 100px;
- text-align: center;
- padding: 5px;
- height: 30px;
- font-size: 13px;
- color: var(--main-text);
- border: 1px solid var(--button);
- border-radius: 2px;
- background-color: #fbfbfb;
- cursor: pointer;
-
- white-space: nowrap;
-}
-
-.includeSpan {
- padding: 0px;
- height: 28px;
-}
-
-.button:hover {
- background: #f5f5f5;
- border-color: var(--highlight);
-}
-
-::-moz-selection {
- background: var(--line);
-}
-
-a:link {
- color: var(--sub-text);
- text-decoration-line: none;
-}
-
-a:visited {
- color: var(--sub-text);
-}
-
-.pageLink {
- color: var(--highlight) !important;
- display: inline-block;
- margin-right: 10px;
-}
-
-.pageLink:hover {
- color: var(--highlight);
- text-decoration-line: underline;
-}
-
-input[type="checkbox"] {
- display: none;
-}
-
-.checkbox {
- padding-left: 20px;
- position: relative;
- /*margin-right: 20px;*/
- cursor: pointer;
-}
-
-.checkbox::before {
- content: "";
- display: block;
- position: absolute;
- top: 0;
- left: -2px;
- width: 20px;
- height: 20px;
- border: 1px solid var(--button);
- border-radius: 2px;
-}
-
-.checkbox:hover::before {
- border-color: var(--highlight);
-}
-
-input[type="checkbox"]:checked + .checkbox {
- color: var(--highlight);
-}
-
-input[type="checkbox"]:checked + .checkbox::after {
- content: "";
- display: block;
- position: absolute;
- top: 1px;
- left: 4px;
- width: 6px;
- height: 14px;
- transform: rotate(40deg);
- border-bottom: 3px solid var(--highlight);
- border-right: 3px solid var(--highlight);
-}
-
-input[type="radio"] {
- display: none;
-}
-
-.radio {
- padding-left: 20px;
- position: relative;
- cursor: pointer;
-}
-
-.radio::before {
- content: "";
- display: block;
- position: absolute;
- top: 0;
- left: -2px;
- width: 20px;
- height: 20px;
- border: 1px solid var(--button);
- border-radius: 50%;
-}
-
-.radio:hover::before {
- border-color: var(--highlight);
-}
-
-input[type="radio"]:checked + .radio {
- color: var(--highlight);
-}
-
-input[type="radio"]:checked + .radio::after {
- content: "";
- display: block;
- position: absolute;
- top: 6px;
- left: 4px;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background-color: var(--highlight);
-}
-
-select {
- -moz-appearance: none;
- text-overflow: ellipsis;
- border: var(--button) solid 1px;
- border-radius: 2px;
- padding: 3px 5px;
- padding-right: 20px;
- width: 100%;
-}
-
-select:hover {
- border: var(--highlight) solid 1px;
-}
-
-.selectWrap {
- position: relative;
-}
-
-.selectWrap:before {
- pointer-events: none;
- content: "";
- z-index: 1;
- position: absolute;
- top: 40%;
- right: 7px;
- width: 5px;
- height: 5px;
-
- transform: rotate(45deg);
- border-bottom: 2px solid var(--sub-text);
- border-right: 2px solid var(--sub-text);
-}
-
-.selectWrap:hover::before {
- border-bottom: 2px solid var(--highlight);
- border-right: 2px solid var(--highlight);
-}
-
-option {
- font-family: inherit;
- font-size: 14px;
-}
diff --git a/src/options/styles/CategoryContainer.scss b/src/options/styles/CategoryContainer.scss
new file mode 100644
index 0000000..2433ad2
--- /dev/null
+++ b/src/options/styles/CategoryContainer.scss
@@ -0,0 +1,15 @@
+.categoryContainer {
+ list-style-type: none;
+
+ .categoryTitle {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--sub-text);
+ margin: 0;
+ }
+
+ .categoryElements {
+ padding-left: 20px;
+ margin-bottom: 30px;
+ }
+}
diff --git a/src/options/styles/ContentsArea.scss b/src/options/styles/ContentsArea.scss
new file mode 100644
index 0000000..7cc69d8
--- /dev/null
+++ b/src/options/styles/ContentsArea.scss
@@ -0,0 +1,25 @@
+.contentsArea {
+ margin-left: 150px;
+ width: 650px;
+
+ .contentTitle {
+ font-size: 33px;
+ font-weight: 200;
+ color: var(--sub-text);
+ line-height: 2;
+ margin: 0;
+ }
+
+ hr {
+ width: 100%;
+ background-color: var(--line);
+ height: 1px;
+ border: none;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+ ul {
+ padding: 0;
+ margin: 0;
+ }
+}
diff --git a/src/options/styles/OptionContainer.scss b/src/options/styles/OptionContainer.scss
new file mode 100644
index 0000000..f15249d
--- /dev/null
+++ b/src/options/styles/OptionContainer.scss
@@ -0,0 +1,381 @@
+.optionContainer {
+ list-style-type: none;
+
+ .optionElement {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 10px 0px 10px 0px;
+
+ .optionText {
+ flex: 1;
+ p {
+ margin: 0px;
+ }
+ .caption {
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--sub-text);
+ white-space: pre-wrap;
+ }
+ a {
+ color: var(--highlight);
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+ .optionForm {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin-left: 10px;
+ }
+ &.buttonsContainer {
+ justify-content: flex-start;
+ }
+ }
+ .childElements {
+ padding-left: 20px;
+ border-left: solid 10px var(--line);
+ }
+}
+
+fieldset {
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+.noDisplayLegend {
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+/*new, updated*/
+.new > .optionElement p:nth-child(1)::after,
+.updated > .optionElement p:nth-child(1)::after {
+ color: var(--new);
+ font-size: 14px;
+ border: 1px solid var(--new);
+ border-radius: 2px;
+ padding: 0px 5px;
+ margin-left: 5px;
+}
+.updated > .optionElement p:nth-child(1)::after {
+ content: "Updated";
+}
+.new > .optionElement p:nth-child(1)::after {
+ content: "New";
+}
+
+/*forms*/
+input,
+textarea {
+ font-family: inherit;
+ font-size: 14px;
+}
+
+textarea {
+ -moz-appearance: textfield;
+ border: 1px var(--button) solid;
+ border-radius: 2px;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding: 5px;
+ width: calc(100% - 12px) !important;
+ height: 50px;
+ min-height: 50px;
+ &:hover,
+ &:focus {
+ border-color: var(--highlight);
+ }
+}
+
+input[type="number"],
+input[type="text"],
+input[type="color"] {
+ -moz-appearance: textfield;
+ width: 50px;
+ height: 30px;
+ padding-left: 5px;
+ padding-right: 5px;
+ border: 1px solid var(--button);
+ border-radius: 2px;
+
+ &:hover,
+ &:focus {
+ border-color: var(--highlight);
+ }
+}
+
+input[type="text"] {
+ width: 200px;
+}
+.noHover {
+ pointer-events: none;
+}
+
+/*checkbox*/
+
+input[type="checkbox"] {
+ opacity: 0;
+ position: absolute;
+ &:checked + .checkbox {
+ color: var(--highlight);
+ &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 1px;
+ left: 4px;
+ width: 6px;
+ height: 14px;
+ transform: rotate(40deg);
+ border-bottom: 3px solid var(--highlight);
+ border-right: 3px solid var(--highlight);
+ }
+ }
+ &:focus + .checkbox::before {
+ border-color: var(--highlight);
+ }
+}
+
+.checkbox {
+ padding-left: 20px;
+ position: relative;
+ cursor: pointer;
+ &::before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ left: -2px;
+ width: 20px;
+ height: 20px;
+ border: 1px solid var(--button);
+ border-radius: 2px;
+ }
+ &:hover::before {
+ border-color: var(--highlight);
+ }
+}
+
+/*radio*/
+
+input[type="radio"] {
+ opacity: 0;
+ position: absolute;
+ &:checked + .radio {
+ color: var(--highlight);
+ }
+ &:checked + .radio::after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 6px;
+ left: 4px;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background-color: var(--highlight);
+ }
+ &:focus + .radio::before {
+ border-color: var(--highlight);
+ }
+}
+
+.radio {
+ padding-left: 20px;
+ position: relative;
+ cursor: pointer;
+ &::before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ left: -2px;
+ width: 20px;
+ height: 20px;
+ border: 1px solid var(--button);
+ border-radius: 50%;
+ }
+ &:hover::before {
+ border-color: var(--highlight);
+ }
+}
+
+/*color*/
+
+input[type="color"] {
+ background-color: var(--main-bg);
+ padding: 5px;
+}
+
+/*select*/
+
+select {
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ text-overflow: ellipsis;
+ border: var(--button) solid 1px;
+ border-radius: 2px;
+ padding: 3px 5px;
+ padding-right: 20px;
+ width: 100%;
+ &:hover,
+ &:focus {
+ border: var(--highlight) solid 1px;
+ }
+}
+
+.selectWrap {
+ position: relative;
+ &:before {
+ pointer-events: none;
+ content: "";
+ z-index: 1;
+ position: absolute;
+ top: 40%;
+ right: 7px;
+ width: 5px;
+ height: 5px;
+
+ transform: rotate(45deg);
+ border-bottom: 2px solid var(--sub-text);
+ border-right: 2px solid var(--sub-text);
+ }
+ &:hover::before {
+ border-bottom: 2px solid var(--highlight);
+ border-right: 2px solid var(--highlight);
+ }
+}
+
+option {
+ font-family: inherit;
+ font-size: 14px;
+}
+
+/*button*/
+
+input[type="button"],
+.button {
+ min-width: 100px;
+ text-align: center;
+ padding: 5px;
+ height: 30px;
+ font-size: 13px;
+ color: var(--main-text);
+ border: 1px solid var(--button);
+ border-radius: 2px;
+ background-color: #fbfbfb;
+ cursor: pointer;
+ white-space: nowrap;
+ &:hover,
+ &:focus {
+ background: #f5f5f5;
+ border-color: var(--highlight);
+ }
+ &.includeSpan {
+ padding: 0px;
+ height: 30px;
+ box-sizing: border-box;
+ span {
+ width: fit-content;
+ padding: 0px 5px;
+ }
+ }
+}
+.button {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ &:focus-within {
+ background: #f5f5f5;
+ border-color: var(--highlight);
+ }
+}
+
+input[type="file"] {
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ opacity: 0;
+ width: 1px;
+ height: 0;
+}
+
+.keyboardShortcut {
+ position: relative;
+
+ .row {
+ display: flex;
+ align-items: center;
+ }
+
+ button {
+ position: absolute;
+ border: none;
+ padding: 0;
+ background-color: rgba(0, 0, 0, 0);
+ cursor: pointer;
+ height: 20px;
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ &.clearButton {
+ right: 0px;
+ margin: 3px;
+ svg {
+ fill: var(--sub-text);
+ }
+ &:hover svg {
+ fill: var(--new);
+ }
+ }
+
+ &.resetButton {
+ right: -20px;
+ margin: -3px;
+ svg {
+ fill: var(--sub-text);
+ path {
+ stroke: var(--sub-text);
+ }
+ }
+ &:hover svg {
+ fill: var(--highlight);
+ path {
+ stroke: var(--highlight);
+ }
+ }
+ }
+ }
+
+ input {
+ &::placeholder {
+ color: #fff;
+ }
+ &:focus::placeholder {
+ color: var(--main-text);
+ }
+ }
+
+ &.isError input {
+ border-color: var(--new);
+ }
+
+ .error {
+ position: absolute;
+ margin: 0;
+ width: 100%;
+ font-size: 12px;
+ color: var(--new);
+ text-align: center;
+ white-space: nowrap;
+ }
+}
diff --git a/src/options/styles/OptionsPage.scss b/src/options/styles/OptionsPage.scss
new file mode 100644
index 0000000..bbc1edf
--- /dev/null
+++ b/src/options/styles/OptionsPage.scss
@@ -0,0 +1,27 @@
+:root {
+ --main-text: #0c0c0d;
+ --sub-text: #737373;
+ --line: #ededf0;
+ --button: #d7d7db;
+ --highlight: #5595ff;
+ --main-bg: #ffffff;
+ --new: #ff4f4f;
+}
+
+.optionsPage {
+ font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica",
+ sans-serif;
+ font-size: 15px;
+ font-weight: 400;
+ color: var(--main-text);
+ background-color: var(--main-bg);
+ line-height: 1.5;
+ margin: 20px 40px;
+}
+
+::-moz-selection {
+ background-color: var(--line);
+}
+::selection {
+ background-color: var(--line);
+}
diff --git a/src/options/styles/SideBar.scss b/src/options/styles/SideBar.scss
new file mode 100644
index 0000000..fde5149
--- /dev/null
+++ b/src/options/styles/SideBar.scss
@@ -0,0 +1,53 @@
+.sideBar {
+ position: fixed;
+ font-size: 17px;
+ font-weight: 400;
+ text-align: right;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ flex-shrink: 0;
+
+ .titleContainer {
+ display: flex;
+ flex-direction: column;
+ padding: 10px 15px;
+ align-items: center;
+
+ .logo {
+ height: 64px;
+ width: 64px;
+ margin-right: 5px;
+ }
+ .logoTitle {
+ text-align: left;
+ font-size: 14px;
+ font-weight: 300;
+ color: var(--sub-text);
+ margin: auto;
+ }
+ }
+
+ ul {
+ padding: 0;
+ margin: 0;
+
+ .sideBarItem {
+ list-style-type: none;
+ padding: 10px 15px;
+
+ :hover {
+ text-decoration-line: underline;
+ color: var(--highlight);
+ }
+
+ a {
+ color: var(--sub-text);
+ text-decoration-line: none;
+ }
+
+ &.selected a {
+ color: var(--highlight);
+ }
+ }
+ }
+}
diff --git a/src/options/tm.hash-observer.js b/src/options/tm.hash-observer.js
deleted file mode 100644
index c5210ab..0000000
--- a/src/options/tm.hash-observer.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
-* phi
-*/
-
-var tm = tm || {};
-
-(function(){
-
-tm.HashObserver = {};
-tm.HashObserver.timerID = null;
-tm.HashObserver.FPS = 100;
-
-tm.HashObserver.enable = function()
-{
-var prevHash = window.location.hash;
-tm.HashObserver.timerID = setInterval(function(){
-if (prevHash != window.location.hash) {
-var e = document.createEvent("HTMLEvents");
-e.initEvent("changehash", true, false);
-e.hash = window.location.hash;
-e.prevHash = prevHash;
-document.dispatchEvent(e);
-}
-prevHash = window.location.hash;
-}, tm.HashObserver.FPS);
-};
-
-tm.HashObserver.disable = function()
-{
-clearInterval(tm.HashObserver.timerID);
-};
-
-})();
-
-
-(function(){
-
-tm.FormObserver = {};
-
-tm.FormObserver.observe = function(elm, fps)
-{
-fps = fps || 100;
-
-var prevValue = elm.value;
-var timerID = null;
-
-var observeForm = function() {
-if (elm.value != prevValue) {
-var e = document.createEvent("HTMLEvents");
-e.initEvent("change", true, false);
-elm.dispatchEvent(e);
-}
-prevValue = elm.value;
-};
-
-elm.addEventListener("focus", function() { timerID = setInterval(observeForm, fps); });
-elm.addEventListener("blur", function() { clearInterval(timerID); });
-};
-
-tm.FormObserver.observeAll = function(className)
-{
-className = className || "tm-form-observer";
-var targetList = document.getElementsByClassName(className);
-
-for (var i=0,len=targetList.length; i {
+ const uiLang = browser.i18n.getUILanguage();
+ const langOptions = genelateLangOptions();
+
+ const shouldUseUiLang = langOptions.some(lang => lang.value == uiLang);
+ const targetLang = shouldUseUiLang ? uiLang : "en";
+ const secondTargetLang = targetLang === "en" ? "ja" : "en";
+
+ return { targetLang, secondTargetLang };
+};
+
+const langListOptions = genelateLangOptions();
+const defaultLangs = getDefaultLangs();
+
+export default [
+ {
+ category: "generalLabel",
+ elements: [
+ {
+ id: "targetLang",
+ title: "targetLangLabel",
+ captions: ["targetLangCaptionLabel"],
+ type: "select",
+ default: defaultLangs.targetLang,
+ options: langListOptions,
+ useRawOptionName: true
+ },
+ {
+ id: "ifShowCandidate",
+ title: "ifShowCandidateLabel",
+ captions: ["ifShowCandidateCaptionLabel"],
+ type: "checkbox",
+ default: true
+ }
+ ]
+ },
+ {
+ category: "webPageLabel",
+ elements: [
+ {
+ id: "whenSelectText",
+ title: "whenSelectTextLabel",
+ captions: [],
+ type: "none",
+ default: "showButton",
+ childElements: [
+ {
+ id: "whenSelectText",
+ title: "ifShowButtonLabel",
+ captions: ["ifShowButtonCaptionLabel"],
+ type: "radio",
+ value: "showButton"
+ },
+ {
+ id: "whenSelectText",
+ title: "ifAutoTranslateLabel",
+ captions: ["ifAutoTranslateCaptionLabel"],
+ type: "radio",
+ value: "showPanel"
+ },
+ {
+ id: "whenSelectText",
+ title: "dontShowButtonLabel",
+ captions: ["dontShowButtonCaptionLabel"],
+ type: "radio",
+ value: "dontShowButton"
+ },
+ {
+ id: "ifCheckLang",
+ title: "ifCheckLangLabel",
+ captions: ["ifCheckLangCaptionLabel"],
+ type: "checkbox",
+ default: true,
+ hr: true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ category: "toolbarLabel",
+ elements: [
+ {
+ id: "ifChangeSecondLang",
+ title: "ifChangeSecondLangLabel",
+ captions: ["ifChangeSecondLangCaptionLabel"],
+ type: "checkbox",
+ default: false,
+ childElements: [
+ {
+ id: "secondTargetLang",
+ title: "secondTargetLangLabel",
+ captions: ["secondTargetLangCaptionLabel"],
+ type: "select",
+ default: defaultLangs.secondTargetLang,
+ options: langListOptions,
+ useRawOptionName: true
+ }
+ ]
+ },
+ {
+ id: "waitTime",
+ title: "waitTimeLabel",
+ captions: ["waitTimeCaptionLabel", "waitTime2CaptionLabel"],
+ type: "number",
+ min: 0,
+ placeholder: 500,
+ default: 500
+ }
+ ]
+ },
+ {
+ category: "menuLabel",
+ elements: [
+ {
+ id: "ifShowMenu",
+ title: "ifShowMenuLabel",
+ captions: ["ifShowMenuCaptionLabel"],
+ type: "checkbox",
+ default: true
+ }
+ ]
+ },
+ {
+ category: "styleLabel",
+ elements: [
+ {
+ title: "buttonStyleLabel",
+ captions: ["buttonStyleCaptionLabel"],
+ type: "none",
+ childElements: [
+ {
+ id: "buttonSize",
+ title: "buttonSizeLabel",
+ captions: [],
+ type: "number",
+ min: 1,
+ placeholder: 22,
+ default: 22
+ },
+ {
+ id: "buttonPosition",
+ title: "buttonPositionLabel",
+ captions: [],
+ type: "select",
+ default: "rightDown",
+ options: [
+ {
+ name: "rightUpLabel",
+ value: "rightUp"
+ },
+ {
+ name: "rightDownLabel",
+ value: "rightDown"
+ },
+ {
+ name: "leftUpLabel",
+ value: "leftUp"
+ },
+ {
+ name: "leftDownLabel",
+ value: "leftDown"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: "panelStyleLabel",
+ captions: ["panelStyleCaptionLabel"],
+ type: "none",
+ childElements: [
+ {
+ id: "width",
+ title: "widthLabel",
+ captions: [],
+ type: "number",
+ min: 1,
+ placeholder: 300,
+ default: 300
+ },
+ {
+ id: "height",
+ title: "heightLabel",
+ captions: [],
+ type: "number",
+ min: 1,
+ placeholder: 200,
+ default: 200
+ },
+ {
+ id: "fontSize",
+ title: "fontSizeLabel",
+ captions: [],
+ type: "number",
+ min: 1,
+ placeholder: 13,
+ default: 13
+ },
+ {
+ id: "resultFontColor",
+ title: "resultFontColorLabel",
+ captions: [],
+ type: "color",
+ default: "#000000",
+ new: true
+ },
+ {
+ id: "candidateFontColor",
+ title: "candidateFontColorLabel",
+ captions: [],
+ type: "color",
+ default: "#737373",
+ new: true
+ },
+ {
+ id: "bgColor",
+ title: "bgColorLabel",
+ captions: [],
+ type: "color",
+ default: "#ffffff"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ category: "otherLabel",
+ elements: [
+ {
+ id: "isShowOptionsPageWhenUpdated",
+ title: "isShowOptionsPageWhenUpdatedLabel",
+ captions: ["isShowOptionsPageWhenUpdatedCaptionLabel"],
+ type: "checkbox",
+ default: true,
+ new: true
+ },
+ {
+ id: "isDebugMode",
+ title: "isDebugModeLabel",
+ captions: ["isDebugModeCaptionLabel"],
+ type: "checkbox",
+ default: false,
+ new: true
+ }
+ ]
+ }
+];
diff --git a/src/settings/settings.js b/src/settings/settings.js
new file mode 100644
index 0000000..c4cac9b
--- /dev/null
+++ b/src/settings/settings.js
@@ -0,0 +1,59 @@
+import browser from "webextension-polyfill";
+import log from "loglevel";
+import defaultSettings from "./defaultSettings";
+
+const logDir = "settings/settings";
+let currentSettings = {};
+
+export const initSettings = async () => {
+ const response = await browser.storage.local.get("Settings");
+ currentSettings = response.Settings || {};
+ let shouldSave = false;
+
+ const pushSettings = element => {
+ if (element.id == undefined || element.default == undefined) return;
+ if (currentSettings[element.id] == undefined) {
+ currentSettings[element.id] = element.default;
+ shouldSave = true;
+ }
+ };
+
+ const fetchDefaultSettings = () => {
+ defaultSettings.forEach(category => {
+ category.elements.forEach(optionElement => {
+ pushSettings(optionElement);
+ if (optionElement.childElements) {
+ optionElement.childElements.forEach(childElement => {
+ pushSettings(childElement);
+ });
+ }
+ });
+ });
+ };
+
+ fetchDefaultSettings();
+ if (shouldSave) await browser.storage.local.set({ Settings: currentSettings });
+};
+
+export const setSettings = async (id, value) => {
+ log.info(logDir, "setSettings()", id, value);
+ currentSettings[id] = value;
+ await browser.storage.local.set({ Settings: currentSettings });
+};
+
+export const getSettings = id => {
+ return currentSettings[id];
+};
+
+export const resetAllSettings = async () => {
+ log.info(logDir, "resetAllSettings()");
+ currentSettings = {};
+ await browser.storage.local.set({ Settings: currentSettings });
+ await initSettings();
+};
+
+export const handleSettingsChange = (changes, area) => {
+ if (Object.keys(changes).includes("Settings")) {
+ currentSettings = changes.Settings.newValue;
+ }
+};