diff --git a/frontend/src/components/buttons/backup_btn.tsx b/frontend/src/components/buttons/backup_btn.tsx new file mode 100644 index 0000000..db286f4 --- /dev/null +++ b/frontend/src/components/buttons/backup_btn.tsx @@ -0,0 +1,75 @@ +import { FileOpen } from '@mui/icons-material'; +import { Button, type ButtonProps, Tooltip } from '@mui/material'; +import { type ReactNode } from 'react'; + +import { notify } from '@/components/notifications'; +import { useTranslation } from '@/hooks'; +import { backup } from '@/tauri_cmd'; + +type Props = { + buttonName: ReactNode; + tooltipTitle: ReactNode; +} & ButtonProps; + +export const BackupButton = ({ buttonName, tooltipTitle, ...props }: Readonly) => ( + + + +); + +export const ImportBackupButton = () => { + const { t } = useTranslation(); + + const handleClick = async () => { + try { + await backup.import(); + } catch (e) { + notify.error(`${e}`); + } + }; + + return ( + + ); +}; + +export const ExportBackupButton = () => { + const { t } = useTranslation(); + + const handleClick = async () => { + try { + if (await backup.export()) { + notify.success(t('backup-export-success')); + } + } catch (e) { + notify.error(`${e}`); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/components/buttons/index.ts b/frontend/src/components/buttons/index.ts index e1e1122..7b59583 100644 --- a/frontend/src/components/buttons/index.ts +++ b/frontend/src/components/buttons/index.ts @@ -1,8 +1,8 @@ // @index('./*', f => `export * from '${f.path}'`) +export * from './backup_btn'; export * from './convert_btn'; export * from './import_lang_btn'; -export * from './log_dir_btn'; -export * from './log_file_btn'; +export * from './log_btn'; export * from './path_selector'; export * from './remove_oar_btn'; export * from './unhide_dar_btn'; diff --git a/frontend/src/components/buttons/log_btn.tsx b/frontend/src/components/buttons/log_btn.tsx new file mode 100644 index 0000000..64e9bc3 --- /dev/null +++ b/frontend/src/components/buttons/log_btn.tsx @@ -0,0 +1,70 @@ +import { FileOpen } from '@mui/icons-material'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import { Button, type ButtonProps, Tooltip } from '@mui/material'; +import { type ReactNode } from 'react'; + +import { notify } from '@/components/notifications'; +import { useTranslation } from '@/hooks'; +import { openLogDir, openLogFile } from '@/tauri_cmd'; + +type Props = { + buttonName: ReactNode; + tooltipTitle: ReactNode; +} & ButtonProps; + +export const LogButton = ({ buttonName, tooltipTitle, ...props }: Props) => ( + + + +); + +export const LogFileButton = () => { + const { t } = useTranslation(); + + const handleClick = async () => { + try { + await openLogFile(); + } catch (error) { + if (error instanceof Error) { + notify.error(error.message); + } + } + }; + + return ; +}; + +export const LogDirButton = () => { + const { t } = useTranslation(); + + const handleClick = async () => { + try { + await openLogDir(); + } catch (error) { + if (error instanceof Error) { + notify.error(error.message); + } + } + }; + + return ( + } + tooltipTitle={t('open-log-dir-tooltip')} + /> + ); +}; diff --git a/frontend/src/components/buttons/log_dir_btn.tsx b/frontend/src/components/buttons/log_dir_btn.tsx deleted file mode 100644 index 84d679c..0000000 --- a/frontend/src/components/buttons/log_dir_btn.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import FolderOpenIcon from '@mui/icons-material/FolderOpen'; -import { Button, Tooltip } from '@mui/material'; - -import { notify } from '@/components/notifications'; -import { useTranslation } from '@/hooks'; -import { openLogDir } from '@/tauri_cmd'; - -export const LogDirButton = () => { - const { t } = useTranslation(); - const handleClick = async () => { - try { - await openLogDir(); - } catch (error) { - if (error instanceof Error) { - notify.error(error.message); - } - } - }; - - return ( - - - - ); -}; diff --git a/frontend/src/components/buttons/log_file_btn.tsx b/frontend/src/components/buttons/log_file_btn.tsx deleted file mode 100644 index 4faf0c8..0000000 --- a/frontend/src/components/buttons/log_file_btn.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FileOpen } from '@mui/icons-material'; -import { Button, Tooltip } from '@mui/material'; - -import { notify } from '@/components/notifications'; -import { useTranslation } from '@/hooks'; -import { openLogFile } from '@/tauri_cmd'; - -export const LogFileButton = () => { - const { t } = useTranslation(); - - const handleClick = async () => { - try { - await openLogFile(); - } catch (error) { - if (error instanceof Error) { - notify.error(error.message); - } - } - }; - - return ( - - - - ); -}; diff --git a/frontend/src/components/notifications/notify.tsx b/frontend/src/components/notifications/notify.tsx index a4bd6e3..e0958df 100644 --- a/frontend/src/components/notifications/notify.tsx +++ b/frontend/src/components/notifications/notify.tsx @@ -26,7 +26,13 @@ export const getPosition = (): SnackbarOrigin => { horizontal: 'right', vertical: 'bottom', } as const; - const posJson = JSON.parse(localStorage.getItem('snackbar-position') ?? '{}') as Partial; + + let posJson: Partial = defaultPosition; + try { + posJson = JSON.parse(localStorage.getItem('snackbar-position') ?? '{}'); + } catch (error) { + console.error(error); + } return { horizontal: posJson?.horizontal ?? defaultPosition.horizontal, diff --git a/frontend/src/components/pages/settings.tsx b/frontend/src/components/pages/settings.tsx index 643e4ce..723dc33 100644 --- a/frontend/src/components/pages/settings.tsx +++ b/frontend/src/components/pages/settings.tsx @@ -8,14 +8,14 @@ import InputLabel from '@mui/material/InputLabel'; import Tab from '@mui/material/Tab'; import AceEditor from 'react-ace'; -import { ImportLangButton } from '@/components/buttons'; +import { ImportBackupButton, ExportBackupButton, ImportLangButton } from '@/components/buttons'; import { NoticePositionList, SelectEditorMode, - type SelectEditorProps, StyleList, - type StyleListProps, TranslationList, + type SelectEditorProps, + type StyleListProps, } from '@/components/lists'; import { useDynStyle, useInjectScript, useLocale, useStorageState, useTranslation } from '@/hooks'; import { start } from '@/tauri_cmd'; @@ -184,6 +184,7 @@ const Tabs = ({ editorMode, setEditorMode, preset, setPreset, setStyle }: TabsPr + @@ -197,6 +198,10 @@ const Tabs = ({ editorMode, setEditorMode, preset, setPreset, setStyle }: TabsPr + + + + ); diff --git a/frontend/src/tauri_cmd/backup.ts b/frontend/src/tauri_cmd/backup.ts new file mode 100644 index 0000000..cb655d0 --- /dev/null +++ b/frontend/src/tauri_cmd/backup.ts @@ -0,0 +1,46 @@ +import { save } from '@tauri-apps/api/dialog'; +import { writeTextFile } from '@tauri-apps/api/fs'; + +import { readFile } from '.'; + +export const backup = { + /** @throws Error */ + async import() { + const pathKey = 'import-backup-path'; + const settings = await readFile(pathKey, 'g_dar2oar_settings'); + if (settings) { + // TODO: This is unsafe because the key is not validated. + const obj = JSON.parse(settings); + Object.keys(obj).forEach((key) => { + // The import path does not need to be overwritten. + if (key === pathKey) { + return; + } + localStorage.setItem(key, obj[key]); + }); + window.location.reload(); // To enable + } + }, + + /** @throws Error */ + async export() { + const pathKey = 'export-settings-path'; + const path = await save({ + defaultPath: localStorage.getItem(pathKey) ?? '', + filters: [ + { + name: 'g_dar2oar_settings', + extensions: ['json'], + }, + ], + }); + + if (typeof path === 'string') { + localStorage.setItem(pathKey, path); + await writeTextFile(path, JSON.stringify(localStorage)); + return path; + } else { + return null; + } + }, +}; diff --git a/frontend/src/tauri_cmd/converter.ts b/frontend/src/tauri_cmd/converter.ts new file mode 100644 index 0000000..1b57148 --- /dev/null +++ b/frontend/src/tauri_cmd/converter.ts @@ -0,0 +1,91 @@ +import { invoke } from '@tauri-apps/api'; + +import { selectLogLevel } from '@/utils/selector'; + +type ConverterOptions = { + src: string; + dst?: string; + modName?: string; + modAuthor?: string; + mappingPath?: string; + mapping1personPath?: string; + runParallel?: boolean; + hideDar?: boolean; + showProgress?: boolean; +}; + +/** + * Converts a DAR (DynamicAnimationReplacer) to an OAR (OpenAnimationReplacer). + * @param {ConverterOptions} props - Converter Options. + * @returns {Promise} A promise that resolves when converted. + * @throws + * - `props.src` is '' or non-exist as path + * - Convert is failed. + */ +export async function convertDar2oar(props: ConverterOptions): Promise { + if (props.src === '') { + throw new Error('src must be specified.'); + } + + const args = { + options: { + darDir: props.src, + oarDir: props.dst === '' ? undefined : props.dst, + modName: props.modName === '' ? undefined : props.modName, + modAuthor: props.modAuthor === '' ? undefined : props.modAuthor, + mappingPath: props.mappingPath === '' ? undefined : props.mappingPath, + mapping1personPath: props.mapping1personPath === '' ? undefined : props.mapping1personPath, + runParallel: props.runParallel ?? false, + hideDar: props.hideDar ?? false, + }, + }; + + let logLevel = selectLogLevel(localStorage.getItem('logLevel') ?? ''); + changeLogLevel(logLevel); + + const showProgress = props.showProgress ?? false; + //! Warning! If there is no `return` or `await` in invoke, the progress bar will not work. + if (showProgress) { + await invoke('convert_dar2oar_with_progress', args); + } else { + await invoke('convert_dar2oar', args); + } +} + +export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'; + +/** + * Invokes the `change_log_level` command with the specified log level. + * @param {LogLevel} [logLevel] - The log level to set. If not provided, the default log level will be used. + * @returns {Promise} A promise that resolves when the log level is changed. + */ +export async function changeLogLevel(logLevel?: LogLevel): Promise { + return invoke('change_log_level', { logLevel }); +} + +/** + * Add `.mohidden` to DAR's files. + * @param {string} darDir - A string representing the directory path of the DAR directory that needs to be + * unhidden. + * @throws + */ +export async function unhideDarDir(darDir: string) { + if (darDir === '') { + throw new Error('darDir is empty string.'); + } + await invoke('unhide_dar_dir', { darDir }); +} + +/** + * The function `removeOarDir` is an asynchronous function that removes a specified directory and throws an error if the + * path is empty. + * @param {string} path - The `path` parameter is a string that specifies the directory path of the DAR or OAR directory + * that needs to be removed. + * @throws + */ +export async function removeOarDir(path: string) { + if (path === '') { + throw new Error('Specified path is empty string.'); + } + await invoke('remove_oar_dir', { path }); +} diff --git a/frontend/src/tauri_cmd/default_api_wrapper.ts b/frontend/src/tauri_cmd/default_api_wrapper.ts new file mode 100644 index 0000000..92a5850 --- /dev/null +++ b/frontend/src/tauri_cmd/default_api_wrapper.ts @@ -0,0 +1,78 @@ +import { type OpenDialogOptions, open } from '@tauri-apps/api/dialog'; +import { readTextFile } from '@tauri-apps/api/fs'; +import { open as openShell } from '@tauri-apps/api/shell'; + +import { notify } from '@/components/notifications'; + +/** + * Read the entire contents of a file into a string. + * @param {string} pathKey - target path cache key + * @return [isCancelled, contents] + * @throws + */ +export async function readFile(pathKey: string, filterName: string) { + let path = localStorage.getItem(pathKey) ?? ''; + + const setPath = (newPath: string) => { + path = newPath; + localStorage.setItem(pathKey, path); + }; + + if ( + await openPath(path, { + setPath, + filters: [ + { + name: filterName, + extensions: ['json'], + }, + ], + }) + ) { + return await readTextFile(path); + } + return null; +} + +type OpenOptions = { + /** + * path setter. + * - If we don't get the result within this function, somehow the previous value comes in.(React component) + * @param path + * @returns + */ + setPath?: (path: string) => void; +} & OpenDialogOptions; + +/** + * Open a file or Dir + * @returns selected path or cancelled null + * @throws + */ +export async function openPath(path: string, options: OpenOptions) { + const res = await open({ + defaultPath: path, + ...options, + }); + + if (typeof res === 'string' && options.setPath) { + options.setPath(res); + } + return res; +} + +/** + * Wrapper tauri's `open` with `notify.error` + * @export + * @param {string} path + * @param {string} [openWith] + */ +export async function start(path: string, openWith?: string) { + try { + await openShell(path, openWith); + } catch (error) { + if (error instanceof Error) { + notify.error(error.message); + } + } +} diff --git a/frontend/src/tauri_cmd/index.ts b/frontend/src/tauri_cmd/index.ts index 6e9ffa2..87af465 100644 --- a/frontend/src/tauri_cmd/index.ts +++ b/frontend/src/tauri_cmd/index.ts @@ -1,197 +1,7 @@ -import { invoke } from '@tauri-apps/api'; -import { type OpenDialogOptions, open } from '@tauri-apps/api/dialog'; -import { appLogDir } from '@tauri-apps/api/path'; -import { open as openShell } from '@tauri-apps/api/shell'; - -import { notify } from '@/components/notifications'; -import { selectLogLevel } from '@/utils/selector'; - -export { progressListener } from '@/tauri_cmd/progress_listener'; - -type ConverterOptions = { - src: string; - dst?: string; - modName?: string; - modAuthor?: string; - mappingPath?: string; - mapping1personPath?: string; - runParallel?: boolean; - hideDar?: boolean; - showProgress?: boolean; -}; - -/** - * Converts a DAR (DynamicAnimationReplacer) to an OAR (OpenAnimationReplacer). - * @param {ConverterOptions} props - Converter Options. - * @returns {Promise} A promise that resolves when converted. - * @throws - * - `props.src` is '' or non-exist as path - * - Convert is failed. - */ -export async function convertDar2oar(props: ConverterOptions): Promise { - if (props.src === '') { - throw new Error('src must be specified.'); - } - - const args = { - options: { - darDir: props.src, - oarDir: props.dst === '' ? undefined : props.dst, - modName: props.modName === '' ? undefined : props.modName, - modAuthor: props.modAuthor === '' ? undefined : props.modAuthor, - mappingPath: props.mappingPath === '' ? undefined : props.mappingPath, - mapping1personPath: props.mapping1personPath === '' ? undefined : props.mapping1personPath, - runParallel: props.runParallel ?? false, - hideDar: props.hideDar ?? false, - }, - }; - - let logLevel = selectLogLevel(localStorage.getItem('logLevel') ?? ''); - changeLogLevel(logLevel); - - const showProgress = props.showProgress ?? false; - //! Warning! If there is no `return` or `await` in invoke, the progress bar will not work. - if (showProgress) { - await invoke('convert_dar2oar_with_progress', args); - } else { - await invoke('convert_dar2oar', args); - } -} - -export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'; - -/** - * Invokes the `change_log_level` command with the specified log level. - * @param {LogLevel} [logLevel] - The log level to set. If not provided, the default log level will be used. - * @returns {Promise} A promise that resolves when the log level is changed. - */ -export async function changeLogLevel(logLevel?: LogLevel): Promise { - return invoke('change_log_level', { logLevel }); -} - -/** - * @param darPath - * @throws - */ -/** - * Add `.mohidden` to DAR's files. - * @param {string} darDir - A string representing the directory path of the DAR directory that needs to be - * unhidden. - * @throws - */ -export async function unhideDarDir(darDir: string) { - if (darDir === '') { - throw new Error('darDir is empty string.'); - } - await invoke('unhide_dar_dir', { darDir }); -} - -/** - * The function `removeOarDir` is an asynchronous function that removes a specified directory and throws an error if the - * path is empty. - * @param {string} path - The `path` parameter is a string that specifies the directory path of the DAR or OAR directory - * that needs to be removed. - * @throws - */ -export async function removeOarDir(path: string) { - if (path === '') { - throw new Error('Specified path is empty string.'); - } - await invoke('remove_oar_dir', { path }); -} - -/** - * Read the entire contents of a file into a string. - * @param {string} path - target path - * @return [isCancelled, contents] - * @throws - */ -export async function importLang() { - const langPathKey = 'lang-file-path'; - let path = localStorage.getItem(langPathKey) ?? ''; - - const setPath = (newPath: string) => { - path = newPath; - localStorage.setItem(langPathKey, path); - }; - - if ( - await openPath(path, { - setPath, - filters: [ - { - name: 'Custom Language', - extensions: ['json'], - }, - ], - }) - ) { - return await invoke('read_to_string', { path }); - } - return null; -} - -type OpenOptions = { - /** - * path setter. - * - If we don't get the result within this function, somehow the previous value comes in.(React component) - * @param path - * @returns - */ - setPath?: (path: string) => void; -} & OpenDialogOptions; - -/** - * Open a file or Dir - * @returns selected path or cancelled null - * @throws - */ -export async function openPath(path: string, options: OpenOptions) { - const res = await open({ - defaultPath: path, - ...options, - }); - - if (typeof res === 'string' && options.setPath) { - options.setPath(res); - } - - return res; -} - -/** - * Opens the log file. - * @throws - if not found path - */ -export async function openLogFile() { - const logDir = await appLogDir(); - const logFile = `${logDir}g_dar2oar.log`; - // NOTE: Using notify wrapper (`start`) here had no effect. - // (If there is an error in the `appLogDir` in front of us, we cannot try to catch it.) - await openShell(logFile); -} - -/** - * Opens the log directory. - * @throws - if not found path - */ -export async function openLogDir() { - // NOTE: Using notify wrapper (`start`) here had no effect. - // (If there is an error in the `appLogDir` in front of us, we cannot try to catch it.) - await openShell(await appLogDir()); -} -/** - * Wrapper tauri's `open` with `notify.error` - * @export - * @param {string} path - * @param {string} [openWith] - */ -export async function start(path: string, openWith?: string) { - try { - await openShell(path, openWith); - } catch (error) { - if (error instanceof Error) { - notify.error(error.message); - } - } -} +// @index('./*', f => `export * from '${f.path}'`) +export * from './backup'; +export * from './converter'; +export * from './default_api_wrapper'; +export * from './lang'; +export * from './log'; +export * from './progress_listener'; diff --git a/frontend/src/tauri_cmd/lang.ts b/frontend/src/tauri_cmd/lang.ts new file mode 100644 index 0000000..64a31eb --- /dev/null +++ b/frontend/src/tauri_cmd/lang.ts @@ -0,0 +1,11 @@ +import { readFile } from '.'; + +/** + * Read the entire contents of a file into a string. + * @param {string} path - target path + * @return [isCancelled, contents] + * @throws + */ +export async function importLang() { + return await readFile('lang-file-path', 'Custom Language'); +} diff --git a/frontend/src/tauri_cmd/log.ts b/frontend/src/tauri_cmd/log.ts new file mode 100644 index 0000000..34a574e --- /dev/null +++ b/frontend/src/tauri_cmd/log.ts @@ -0,0 +1,21 @@ +import { appLogDir } from '@tauri-apps/api/path'; +import { open as openShell } from '@tauri-apps/api/shell'; + + + +/** + * Opens the log file. + * @throws - if not found path + */ +export async function openLogFile() { + const logFile = `${await appLogDir()}/g_dar2oar.log`; + await openShell(logFile); +} + +/** + * Opens the log directory. + * @throws - if not found path + */ +export async function openLogDir() { + await openShell(await appLogDir()); +} diff --git a/locales/en-US.json b/locales/en-US.json index b5dfdcf..7ca2ab1 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,5 +1,10 @@ { "all-clear-btn": "All Clear", + "backup-export-btn-name": "Export", + "backup-export-tooltip": "Export current settings", + "backup-export-success": "Settings exported.", + "backup-import-btn-name": "Import", + "backup-import-tooltip": "Import settings from Json file.", "conversion-complete": "Conversion Complete.", "convert-btn": "Convert", "convert-form-author-name": "Mod Author Name", @@ -69,6 +74,7 @@ "run-parallel-btn-tooltip2": "Note: More than twice the processing speed can be expected, but the concurrent processing results in thread termination timings being out of order, so log writes will be out of order as well, greatly reducing readability of the logs.", "run-parallel-label": "Run Parallel", "select-btn": "Select", + "tab-label-backup": "Backup", "tab-label-editor": "Editor / Preset", "tab-label-lang": "Language", "tab-label-notice": "Notice", diff --git a/locales/ja-JP.json b/locales/ja-JP.json index aed46ff..89210f6 100644 --- a/locales/ja-JP.json +++ b/locales/ja-JP.json @@ -1,5 +1,10 @@ { "all-clear-btn": "全入力をクリア", + "backup-export-btn-name": "エクスポート", + "backup-export-tooltip": "現在の設定をエクスポートします", + "backup-export-success": "設定をエクスポートしました", + "backup-import-btn-name": "インポート", + "backup-import-tooltip": "Jsonファイルから設定をインポートします", "conversion-complete": "変換が完了しました", "convert-btn": "変換", "convert-form-author-name": "Mod作者名", @@ -69,6 +74,7 @@ "run-parallel-btn-tooltip2": "注意: 2倍以上の処理速度が期待できますが、並行処理によりスレッドの終了タイミングが順不同になるため、ログの書き込みも同様に順不同になりログの可読性が大幅に低下します。", "run-parallel-label": "並列実行", "select-btn": "選択", + "tab-label-backup": "バックアップ", "tab-label-editor": "エディタ・プリセット", "tab-label-lang": "言語", "tab-label-notice": "通知", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 78d3b51..ae88cff 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1.0", features = ["derive"] } # Implement (De)Serializer tauri = { version = "1.4.0", features = [ "devtools", "dialog-open", + "dialog-save", "fs-all", "path-all", "shell-all", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 30541be..6bb5d78 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,7 +19,7 @@ "confirm": false, "message": false, "open": true, - "save": false + "save": true }, "fs": { "all": true diff --git a/test/sample_scripts/custom_translation.js b/test/sample_scripts/custom_translation.js index 98a348e..3c81f81 100644 --- a/test/sample_scripts/custom_translation.js +++ b/test/sample_scripts/custom_translation.js @@ -2,6 +2,11 @@ // Origin: https://github.com/SARDONYX-sard/dar-to-oar/blob/main/locales/en-US.json const i18n = { 'all-clear-btn': 'All Clear', + 'backup-export-btn-name': 'Export', + 'backup-export-tooltip': 'Export current settings', + 'backup-export-success': 'Settings exported.', + 'backup-import-btn-name': 'Import', + 'backup-import-tooltip': 'Import settings from Json file.', 'conversion-complete': 'Conversion Complete.', 'convert-btn': 'Convert', 'convert-form-author-name': 'Mod Author Name', @@ -49,6 +54,13 @@ 'log-level-list-tooltip3': ' Info: Log the conversion time.', 'log-level-list-tooltip4': 'Error: Logs nothing but errors.', 'mapping-wiki-url-leaf': 'wiki#what-is-the-mapping-file', + 'notice-position-bottom-center': 'Bottom Center', + 'notice-position-bottom-left': 'Bottom Left', + 'notice-position-bottom-right': 'Bottom Right', + 'notice-position-list-label': 'Notice Position', + 'notice-position-top-center': 'Top Center', + 'notice-position-top-left': 'Top Left', + 'notice-position-top-right': 'Top Right', 'open-log-btn': 'Open log', 'open-log-dir-btn': 'Log(dir)', 'open-log-dir-tooltip': 'Open the log storage location.', @@ -67,7 +79,8 @@ 'Note: More than twice the processing speed can be expected, but the concurrent processing results in thread termination timings being out of order, so log writes will be out of order as well, greatly reducing readability of the logs.', 'run-parallel-label': 'Run Parallel', 'select-btn': 'Select', - 'tab-label-edior': 'Editor / Preset', + 'tab-label-backup': 'Backup', + 'tab-label-editor': 'Editor / Preset', 'tab-label-lang': 'Language', 'tab-label-notice': 'Notice', 'unhide-dar-btn': 'Unhide DAR',