From fcf20ab5307d9032838875b34b02ee0ec99482d6 Mon Sep 17 00:00:00 2001 From: sienori Date: Wed, 20 Feb 2019 20:36:57 +0900 Subject: [PATCH] Replace option page with React --- package-lock.json | 22 +- package.json | 2 + src/_locales/en/messages.json | 71 ++- src/common/Settings.js | 291 ------------ src/common/genelateLangOptions.js | 12 + src/common/getShortcut.js | 23 + src/common/log.js | 19 + src/options/components/CategoryContainer.js | 35 ++ src/options/components/ContentsArea.js | 20 + src/options/components/InformationPage.js | 99 ++++ .../components/KeyboardShortcutForm.js | 154 +++++++ .../components/KeyboardShortcutsPage.js | 62 +++ src/options/components/OptionContainer.js | 182 ++++++++ src/options/components/OptionsPage.js | 19 + src/options/components/ScrollToTop.js | 16 + src/options/components/SettingsPage.js | 59 +++ src/options/components/SideBar.js | 37 ++ src/options/icons/clear.svg | 3 + src/options/icons/reset.svg | 6 + src/options/index.html | 422 +----------------- src/options/index.js | 72 +-- src/options/options.css | 415 ----------------- src/options/styles/CategoryContainer.scss | 15 + src/options/styles/ContentsArea.scss | 25 ++ src/options/styles/OptionContainer.scss | 381 ++++++++++++++++ src/options/styles/OptionsPage.scss | 27 ++ src/options/styles/SideBar.scss | 53 +++ src/options/tm.hash-observer.js | 71 --- src/options/ui.js | 72 --- src/settings/defaultSettings.js | 250 +++++++++++ src/settings/settings.js | 59 +++ 31 files changed, 1646 insertions(+), 1348 deletions(-) delete mode 100644 src/common/Settings.js create mode 100644 src/common/genelateLangOptions.js create mode 100644 src/common/getShortcut.js create mode 100644 src/common/log.js create mode 100644 src/options/components/CategoryContainer.js create mode 100644 src/options/components/ContentsArea.js create mode 100644 src/options/components/InformationPage.js create mode 100644 src/options/components/KeyboardShortcutForm.js create mode 100644 src/options/components/KeyboardShortcutsPage.js create mode 100644 src/options/components/OptionContainer.js create mode 100644 src/options/components/OptionsPage.js create mode 100644 src/options/components/ScrollToTop.js create mode 100644 src/options/components/SettingsPage.js create mode 100644 src/options/components/SideBar.js create mode 100644 src/options/icons/clear.svg create mode 100644 src/options/icons/reset.svg delete mode 100644 src/options/options.css create mode 100644 src/options/styles/CategoryContainer.scss create mode 100644 src/options/styles/ContentsArea.scss create mode 100644 src/options/styles/OptionContainer.scss create mode 100644 src/options/styles/OptionsPage.scss create mode 100644 src/options/styles/SideBar.scss delete mode 100644 src/options/tm.hash-observer.js delete mode 100644 src/options/ui.js create mode 100644 src/settings/defaultSettings.js create mode 100644 src/settings/settings.js 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 ( +
  • +
    + +

    + {category !== "" ? browser.i18n.getMessage(category) : ""} +

    +
    +
      + {elements.map((option, index) => ( +
      + + {option.hasOwnProperty("childElements") && ( +
        + {option.childElements.map((option, index) => ( + + ))} +
      + )} +
      +
      +
      + ))} +
    +
    +
  • + ); +}; 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 ( +
    +

    {browser.i18n.getMessage("informationLabel")}

    +
    + + + Version {extensionVersion} + +

    + } + /> + + +
    + + + Donate + + } + /> + +

    + + {browser.i18n.getMessage("amazonTitleLabel")} + +

    +

    email: {email}

    +
    + } + /> +
    + +

    + + {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 && ( +
    + + {title ? (props.useRawTitle ? title : browser.i18n.getMessage(title)) : ""} + + {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 + + - -
    - - - -
    - - - - - - +
    \ 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; + } +};