From 91d1b47359c8ac561351d8a14c8ffdc04b3babd0 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:22:09 +0900 Subject: [PATCH] feat(front): impl `infer` & change design --- gui/backend/tauri.conf.json | 4 +- .../ButtonWithToolTip/ButtonWithTooltip.tsx | 63 +- .../atoms/ConvertButton/ConvertButton.tsx | 61 +- .../molecules/LogDirButton/LogDirButton.tsx | 2 +- .../molecules/LogFileButton/LogFileButton.tsx | 2 +- .../organisms/ConvertForm/CheckboxField.tsx | 54 ++ .../organisms/ConvertForm/ConvertForm.tsx | 536 ++++-------------- .../ConvertForm/InputModInfoField.tsx | 42 ++ .../organisms/ConvertForm/InputPathField.tsx | 74 +++ .../ConvertForm/MappingHelpButton.tsx | 24 + .../organisms/ConvertForm/index.tsx | 1 + .../organisms/ConvertForm/useCheckField.tsx | 46 ++ .../ConvertForm/useInputPathField.tsx | 49 ++ .../organisms/ConvertForm/useModInfoField.tsx | 23 + .../organisms/ConvertNav/ConvertNav.tsx | 47 ++ .../components/organisms/ConvertNav/index.tsx | 1 + .../organisms/LogLevelList/LogLevelList.tsx | 2 +- .../RemoveOarButton/RemoveOarButton.tsx | 42 +- .../UnhideDarButton/UnhideDarButton.tsx | 37 +- gui/frontend/src/lib/css/index.ts | 12 +- .../src/lib/path/parseDarPath.test.ts | 45 ++ gui/frontend/src/lib/path/parseDarPath.ts | 50 ++ gui/frontend/src/lib/storage/cacheKeys.ts | 3 + gui/frontend/src/lib/storage/storage.ts | 8 + locales/en-US.json | 16 +- locales/ja-JP.json | 24 +- 26 files changed, 734 insertions(+), 534 deletions(-) create mode 100644 gui/frontend/src/components/organisms/ConvertForm/CheckboxField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/InputModInfoField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/MappingHelpButton.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/useCheckField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/useInputPathField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertForm/useModInfoField.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertNav/ConvertNav.tsx create mode 100644 gui/frontend/src/components/organisms/ConvertNav/index.tsx create mode 100644 gui/frontend/src/lib/path/parseDarPath.test.ts create mode 100644 gui/frontend/src/lib/path/parseDarPath.ts diff --git a/gui/backend/tauri.conf.json b/gui/backend/tauri.conf.json index 57ca930..e36fb43 100644 --- a/gui/backend/tauri.conf.json +++ b/gui/backend/tauri.conf.json @@ -10,13 +10,13 @@ "windows": [ { "fullscreen": false, - "height": 920, + "height": 790, "resizable": true, "theme": "Dark", "title": "DAR to OAR Converter", "transparent": true, "visible": false, - "width": 825, + "width": 980, "windowEffects": { "effects": ["micaDark"] } diff --git a/gui/frontend/src/components/atoms/ButtonWithToolTip/ButtonWithTooltip.tsx b/gui/frontend/src/components/atoms/ButtonWithToolTip/ButtonWithTooltip.tsx index d004c40..3faecf4 100644 --- a/gui/frontend/src/components/atoms/ButtonWithToolTip/ButtonWithTooltip.tsx +++ b/gui/frontend/src/components/atoms/ButtonWithToolTip/ButtonWithTooltip.tsx @@ -1,25 +1,56 @@ import { Button, type ButtonProps, Tooltip } from '@mui/material'; +import { useEffect, useRef, useState } from 'react'; import type { ReactNode } from 'react'; type Props = { buttonName: ReactNode; tooltipTitle?: ReactNode; + icon?: ReactNode; + minWidth?: number; + minScreenWidth?: number; } & ButtonProps; -export const ButtonWithToolTip = ({ buttonName, sx, tooltipTitle, ...props }: Props) => ( - - - -); +export const ButtonWithToolTip = ({ + buttonName, + tooltipTitle, + icon, + minWidth = 97, + minScreenWidth = 890, // Set default value for minimum screen width + ...props +}: Props) => { + const buttonRef = useRef(null); + const [canShowText, setCanShowText] = useState(true); // Default to showing icon + + useEffect(() => { + const updateShowText = () => { + if (buttonRef.current) { + setCanShowText(window.innerWidth > minScreenWidth); + } + }; + + updateShowText(); + window.addEventListener('resize', updateShowText); + + return () => window.removeEventListener('resize', updateShowText); + }, [minScreenWidth]); + + return ( + + + + ); +}; diff --git a/gui/frontend/src/components/atoms/ConvertButton/ConvertButton.tsx b/gui/frontend/src/components/atoms/ConvertButton/ConvertButton.tsx index d4e35b5..9ae4433 100644 --- a/gui/frontend/src/components/atoms/ConvertButton/ConvertButton.tsx +++ b/gui/frontend/src/components/atoms/ConvertButton/ConvertButton.tsx @@ -1,32 +1,79 @@ import ConvertIcon from '@mui/icons-material/Transform'; import LoadingButton, { type LoadingButtonProps } from '@mui/lab/LoadingButton'; +import { useEffect, useState } from 'react'; +import { CircularProgressWithLabel } from '@/components/atoms/CircularProgressWithLabel'; import { useTranslation } from '@/components/hooks/useTranslation'; -type Props = LoadingButtonProps; +type Props = LoadingButtonProps & { progress: number }; /** * * Icon ref * - https://mui.com/material-ui/material-icons/ */ -export function ConvertButton({ loading, ...props }: Props) { +export function ConvertButton({ loading, progress, ...props }: Props) { const { t } = useTranslation(); + // State to track when loading is complete + const [isComplete, setIsComplete] = useState(false); + + useEffect(() => { + if (!loading && progress === 100) { + setIsComplete(true); + + // Keep the "Converted 100%" text visible for 1 second + const completeTimer = setTimeout(() => { + setIsComplete(false); // Reset the background + }, 500); + + return () => { + clearTimeout(completeTimer); + }; + } + }, [loading, progress]); + return ( } + disabled={loading || isComplete} + endIcon={loading || isComplete ? : } loading={loading} loadingPosition='end' sx={{ - height: '40px', - minWidth: '100%', + height: '55px', + minWidth: '40%', + position: 'relative', + overflow: 'hidden', + transition: 'all 0.8s ease', // Smooth transition for button state change + ...(loading || isComplete + ? { + '&:before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + width: `${progress}%`, // Progress-based width + height: '100%', + backgroundColor: '#1e1f1e57', + zIndex: 0, + transition: 'width 0.5s ease-in-out', + transform: `scaleX(${progress / 100})`, + transformOrigin: 'left', + }, + '& .MuiButton-label': { + position: 'relative', + zIndex: 1, // Keep label above background + opacity: loading ? 0.8 : 1, + transition: 'opacity 0.5s ease', + }, + } + : {}), }} - type='button' + type='submit' variant='contained' {...props} > - {loading ? t('converting-btn') : t('convert-btn')} + {loading ? t('converting-btn') : isComplete ? 'OK' : t('convert-btn')} ); } diff --git a/gui/frontend/src/components/molecules/LogDirButton/LogDirButton.tsx b/gui/frontend/src/components/molecules/LogDirButton/LogDirButton.tsx index 63ba37c..feda459 100644 --- a/gui/frontend/src/components/molecules/LogDirButton/LogDirButton.tsx +++ b/gui/frontend/src/components/molecules/LogDirButton/LogDirButton.tsx @@ -17,8 +17,8 @@ export const LogDirButton = ({ ...props }: Props) => { } onClick={handleClick} - startIcon={} tooltipTitle={t('open-log-dir-tooltip')} /> ); diff --git a/gui/frontend/src/components/molecules/LogFileButton/LogFileButton.tsx b/gui/frontend/src/components/molecules/LogFileButton/LogFileButton.tsx index 4ae51f6..f933da1 100644 --- a/gui/frontend/src/components/molecules/LogFileButton/LogFileButton.tsx +++ b/gui/frontend/src/components/molecules/LogFileButton/LogFileButton.tsx @@ -12,8 +12,8 @@ export const LogFileButton = () => { return ( } onClick={handleClick} - startIcon={} tooltipTitle={t('open-log-tooltip')} /> ); diff --git a/gui/frontend/src/components/organisms/ConvertForm/CheckboxField.tsx b/gui/frontend/src/components/organisms/ConvertForm/CheckboxField.tsx new file mode 100644 index 0000000..c1d8b14 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/CheckboxField.tsx @@ -0,0 +1,54 @@ +import { Checkbox, FormControlLabel, Box, Tooltip } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { STORAGE } from '@/lib/storage'; + +import type { FormProps } from './'; +import type { ReactNode } from 'react'; + +type PickBooleans = { + [K in keyof T as T[K] extends boolean ? K : never]: T[K]; +}; + +type BoolFormProps = PickBooleans; + +type CheckboxFieldProps = { + name: Exclude; + label: string; + icon?: ReactNode; + tooltipText: ReactNode; +}; + +export const CheckboxField = ({ name, label, tooltipText, icon }: CheckboxFieldProps) => { + const { control } = useFormContext(); + + return ( + ( + + { + const newValue = !value; + STORAGE.set(name, `${newValue}`); + onChange(newValue); + }} + /> + } + label={ + + {icon} + {label} + + } + /> + + )} + /> + ); +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx b/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx index 2f13035..6fb2c1f 100644 --- a/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx +++ b/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx @@ -1,30 +1,28 @@ import ClearAllIcon from '@mui/icons-material/ClearAll'; -import SlideshowIcon from '@mui/icons-material/Slideshow'; -import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; -import { Box, Button, FormControlLabel, FormGroup, TextField, Tooltip } from '@mui/material'; -import Checkbox from '@mui/material/Checkbox'; +import { Button, FormGroup } from '@mui/material'; import Grid from '@mui/material/Grid2'; -import { Controller, type SubmitHandler, useForm } from 'react-hook-form'; +import { FormProvider, type SubmitHandler, useForm } from 'react-hook-form'; -import { ConvertButton } from '@/components/atoms/ConvertButton'; -import { LinearWithValueLabel } from '@/components/atoms/LinearWithValueLabel'; import { useTranslation } from '@/components/hooks/useTranslation'; -import { LogDirButton } from '@/components/molecules/LogDirButton'; -import { LogFileButton } from '@/components/molecules/LogFileButton'; -import { SelectPathButton } from '@/components/molecules/SelectPathButton'; -import { LogLevelList } from '@/components/organisms/LogLevelList'; -import { RemoveOarButton } from '@/components/organisms/RemoveOarButton'; -import { UnhideDarButton } from '@/components/organisms/UnhideDarButton'; -import { getParent } from '@/lib/path'; +import { ConvertNav, ConvertNavPadding } from '@/components/organisms/ConvertNav'; import { STORAGE } from '@/lib/storage'; +import { PRIVATE_CACHE_OBJ, PUB_CACHE_OBJ } from '@/lib/storage/cacheKeys'; import { convertDar2oar } from '@/services/api/convert'; import { progressListener } from '@/services/api/event'; import { LOG, type LogLevel } from '@/services/api/log'; -import { start } from '@/services/api/shell'; -import type { MouseEventHandler } from 'react'; +import { parseDarPath } from '../../../lib/path/parseDarPath'; -type FormProps = { +import { CheckboxField } from './CheckboxField'; +import { InputModInfoField } from './InputModInfoField'; +import { InputPathField } from './InputPathField'; +import { useCheckFields } from './useCheckField'; +import { useInputPathFields } from './useInputPathField'; +import { useModInfoFields } from './useModInfoField'; + +import type { ComponentPropsWithRef } from 'react'; + +export type FormProps = { src: string; dst: string; modName: string; @@ -36,451 +34,123 @@ type FormProps = { runParallel: boolean; hideDar: boolean; showProgress: boolean; + inferPath: boolean; progress: number; }; const getInitialFormValues = (): FormProps => ({ - src: STORAGE.get('src') ?? '', - dst: STORAGE.get('dst') ?? '', - modName: STORAGE.get('modName') ?? '', - modAuthor: STORAGE.get('modAuthor') ?? '', - mappingPath: STORAGE.get('mappingPath') ?? '', - mapping1personPath: STORAGE.get('mapping1personPath') ?? '', - loading: false as boolean, + src: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.src), + dst: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.dst), + modName: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.modName), + modAuthor: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.modAuthor), + mappingPath: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.mappingPath), + mapping1personPath: STORAGE.getOrDefault(PRIVATE_CACHE_OBJ.mapping1personPath), + loading: false, logLevel: LOG.get(), - runParallel: STORAGE.get('runParallel') === 'true', - hideDar: STORAGE.get('hideDar') === 'true', - showProgress: STORAGE.get('showProgress') === 'true', + runParallel: STORAGE.get(PUB_CACHE_OBJ.runParallel) === 'true', + hideDar: STORAGE.get(PUB_CACHE_OBJ.hideDar) === 'true', + showProgress: STORAGE.get(PUB_CACHE_OBJ.showProgress) === 'true', + inferPath: STORAGE.get(PUB_CACHE_OBJ.inferPath) === 'true', progress: 0, }); +const PATH_FORM_VALUES = ['src', 'dst', 'mapping1personPath', 'mappingPath', 'modAuthor', 'modName'] as const; + +export type PathFormKeys = (typeof PATH_FORM_VALUES)[number]; + +export const setPathToStorage = (name: PathFormKeys, path: string) => { + STORAGE.set(name, path); + if (path !== '') { + STORAGE.set(`cached-${name}`, path); + return; + } + STORAGE.remove(name); +}; + export function ConvertForm() { const { t } = useTranslation(); - const { handleSubmit, control, setValue, watch } = useForm({ + + const methods = useForm({ mode: 'onBlur', criteriaMode: 'all', shouldFocusError: false, defaultValues: getInitialFormValues(), }); + const { setValue, getValues } = methods; - /** Use `getValues` to get the old values and use `watch` to monitor `src` and `dst`. */ - const watchFields = watch(['src', 'dst']); - - const setStorage = (key: keyof FormProps) => { - return (value: string) => { - if (!(key === 'loading' || key === 'progress')) { - STORAGE.set(key, value); - } - - if (value === '') { - localStorage.removeItem(key); - } else { - localStorage.setItem(`cached-${key}`, value); - } - - setValue(key, value); - }; - }; - - const setLoading = (loading: boolean) => setValue('loading', loading); const handleAllClear = () => { - const formValues = ['src', 'dst', 'mapping1personPath', 'mappingPath', 'modAuthor', 'modName'] as const; - - for (const key of formValues) { - setStorage(key)(''); + for (const key of PATH_FORM_VALUES) { + setValue(key, ''); + setPathToStorage(key, ''); } }; const onSubmit: SubmitHandler = async (formProps) => { - await progressListener('/dar2oar/progress/converter', async () => await convertDar2oar(formProps), { + const setLoading = (loading: boolean) => setValue('loading', loading); + const task = async () => await convertDar2oar(formProps); + + await progressListener('/dar2oar/progress/converter', task, { setLoading, - setProgress(percentage: number) { - setValue('progress', percentage); - }, + setProgress: (percentage: number) => setValue('progress', percentage), success: t('conversion-complete'), }); }; + const pathFields = useInputPathFields(); + const modInfoFields = useModInfoFields(); + const checkFields = useCheckFields(); + return ( - - - - ( - - - - {t('convert-form-dar-helper')}
- {t('convert-form-dar-helper2')}
- {t('convert-form-dar-helper3')} - - } - label={t('convert-form-dar-label')} - margin='dense' - onBlur={onBlur} - onChange={(e) => { - onChange(e); - const path = e.target.value; - STORAGE.set('src', path); // For reload cache - if (path !== '') { - STORAGE.set('cached-src', path); // For empty string - } - }} - placeholder='[...]/' - required={true} - sx={{ width: '100%' }} - value={value} - variant='outlined' - /> -
- - - - -
- )} - rules={{ - required: 'Need Path', - }} - /> - - ( - - - - {t('convert-form-oar-helper')}
- {t('convert-form-oar-helper2')} - - } - label={t('convert-form-oar-label')} - margin='dense' - onBlur={onBlur} - onChange={(e) => { - onChange(e); - const path = e.target.value; - STORAGE.set('dst', path); - if (path !== '') { - STORAGE.set('cached-dst', path); - } - }} - placeholder='' - sx={{ width: '100%' }} - value={value} - variant='outlined' - /> -
- - - -
- )} - /> - - ( - - - } - label={t('convert-form-mapping-label')} - margin='dense' - onBlur={onBlur} - onChange={(e) => { - const path = e.target.value; - STORAGE.set('mappingPath', path); - if (path !== '') { - STORAGE.set('cached-mappingPath', path); - } - onChange(e); - }} - placeholder='./mapping_table.txt' - sx={{ width: '100%' }} - value={value} - variant='outlined' - /> - - - - { - STORAGE.set('cached-mappingPath', value); - setStorage('mappingPath')(value); - }} - /> - - - )} - /> - - ( - - - { - const path = e.target.value; - STORAGE.set('mapping1personPath', path); - if (path !== '') { - STORAGE.set('cached-mapping1personPath', path); - } - onChange(e); - }} - placeholder='./mapping_table_for_1st_person.txt' - sx={{ minWidth: '100%' }} - value={value} - variant='outlined' - /> - - - - - - - )} - /> - - - - ( - { - STORAGE.set('modName', e.target.value); - onChange(e); - }} - placeholder={t('convert-form-mod-name')} - value={value} - variant='outlined' - /> - )} - /> - - - - ( - { - STORAGE.set('modAuthor', e.target.value); - onChange(e); - }} - placeholder={t('convert-form-author-placeholder')} - value={value} - variant='outlined' - /> - )} - /> + + + + {pathFields.map((props) => { + let onChange: ComponentPropsWithRef['onChange'] | undefined; + + if (props.name === 'src') { + onChange = (e) => { + if (getValues('inferPath')) { + const parsedPath = parseDarPath(e.target.value); + setValue('dst', parsedPath.oarRoot); + setValue('modName', parsedPath.modName ?? ''); + } + }; + } + return ; + })} + + + {modInfoFields.map((props) => { + return ; + })} + + {checkFields.map((props) => { + return ( + + + + ); + })} + + - - - - - - - - - - - - - - - - ( - - {t('hide-dar-btn-tooltip')}
- {t('hide-dar-btn-tooltip2')} -

- } - > - { - STORAGE.set('hideDar', `${!value}`); - setValue('hideDar', !value); - }} - /> - } - label={ - - - {t('hide-dar-btn')} - - } - /> -
- )} - /> -
- - - ( - - { - setValue('showProgress', !value); - STORAGE.set('showProgress', `${!value}`); - }} - /> - } - label={ - - - {t('progress-btn')} - - } - /> - - )} - /> - - - - ( - - {t('run-parallel-btn-tooltip')}
- {t('run-parallel-btn-tooltip2')} -

- } - > - { - STORAGE.set('runParallel', `${!value}`); - setValue('runParallel', !value); - }} - /> - } - label={t('run-parallel-label')} - /> -
- )} - /> -
-
- - - - - - - - - - - ( - - - - )} - /> - - } - /> -
-
- ); -} - -function MappingHelpBtn() { - const { t } = useTranslation(); - const href = `https://github.com/SARDONYX-sard/dar-to-oar/${t('mapping-wiki-url-leaf')}`; - const handleMappingClick: MouseEventHandler = (_e) => { - start(href); - }; - - return ( - <> - {t('convert-form-mapping-helper')} -
- {t('convert-form-mapping-helper2')} - - + + + ); } diff --git a/gui/frontend/src/components/organisms/ConvertForm/InputModInfoField.tsx b/gui/frontend/src/components/organisms/ConvertForm/InputModInfoField.tsx new file mode 100644 index 0000000..c77cc9f --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/InputModInfoField.tsx @@ -0,0 +1,42 @@ +import { TextField, Grid2 as Grid } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { setPathToStorage } from './ConvertForm'; + +import type { FormProps } from './'; +import type { ReactNode } from 'react'; + +type Props = { + name: 'modName' | 'modAuthor'; + label: string; + placeholder: string; + helperText: ReactNode; +}; + +export const InputModInfoField = ({ name, ...props }: Props) => { + const { control } = useFormContext(); + + return ( + + ( + { + onChange(e); + setPathToStorage(name, e.target.value); + }} + sx={{ width: '100%' }} + value={value} + variant='outlined' + {...props} + /> + )} + /> + + ); +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx b/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx new file mode 100644 index 0000000..b5ad90a --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx @@ -0,0 +1,74 @@ +import { TextField, Grid2 as Grid, type TextFieldProps } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { SelectPathButton } from '@/components/molecules/SelectPathButton'; +import { getParent } from '@/lib/path'; +import { STORAGE } from '@/lib/storage'; + +import { type PathFormKeys, setPathToStorage, type FormProps } from './ConvertForm'; + +import type { ReactNode } from 'react'; + +type Props = { + name: PathFormKeys; + label: string; + placeholder: string; + helperText: string | ReactNode; + onChange?: TextFieldProps['onChange']; +}; + +export const InputPathField = ({ name, label, placeholder, helperText, onChange: onChangeOuter }: Props) => { + const { control, getValues, setValue } = useFormContext(); + + const path = (() => { + const path = getParent(getValues(name)); + + if (path === '') { + return STORAGE.get(`cached-${name}`) ?? path; + } + + return path; + })(); + + const handleSetPath = (path: string) => { + setValue(name, path); + setPathToStorage(name, path); + }; + + return ( + { + const handleChange: TextFieldProps['onChange'] = (e) => { + onChange(e); + onChangeOuter?.(e); + setPathToStorage(name, e.target.value); + }; + + return ( + + + + + + + + + + ); + }} + /> + ); +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/MappingHelpButton.tsx b/gui/frontend/src/components/organisms/ConvertForm/MappingHelpButton.tsx new file mode 100644 index 0000000..acea40a --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/MappingHelpButton.tsx @@ -0,0 +1,24 @@ +import { Button } from '@mui/material'; + +import { useTranslation } from '@/components/hooks/useTranslation'; +import { start } from '@/services/api/shell'; + +import type { MouseEventHandler } from 'react'; +export const MappingHelpButton = () => { + const { t } = useTranslation(); + const href = `https://github.com/SARDONYX-sard/dar-to-oar/${t('mapping-wiki-url-leaf')}`; + const handleMappingClick: MouseEventHandler = (_e) => { + start(href); + }; + + return ( + <> + {t('convert-form-mapping-helper')} +
+ {t('convert-form-mapping-helper2')} + + + ); +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/index.tsx b/gui/frontend/src/components/organisms/ConvertForm/index.tsx index 0b15153..cc8cf98 100644 --- a/gui/frontend/src/components/organisms/ConvertForm/index.tsx +++ b/gui/frontend/src/components/organisms/ConvertForm/index.tsx @@ -1 +1,2 @@ export { ConvertForm } from './ConvertForm'; +export type { FormProps } from './ConvertForm'; diff --git a/gui/frontend/src/components/organisms/ConvertForm/useCheckField.tsx b/gui/frontend/src/components/organisms/ConvertForm/useCheckField.tsx new file mode 100644 index 0000000..a05c932 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/useCheckField.tsx @@ -0,0 +1,46 @@ +import AutoFixNormalIcon from '@mui/icons-material/AutoFixNormal'; +import DynamicFeedIcon from '@mui/icons-material/DynamicFeed'; +import SlideshowIcon from '@mui/icons-material/Slideshow'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; + +import { useTranslation } from '@/components/hooks/useTranslation'; + +import type { CheckboxField } from './CheckboxField'; +import type { ComponentPropsWithRef } from 'react'; + +export const useCheckFields = () => { + const { t } = useTranslation(); + + return [ + { + icon: , + label: t('infer-btn'), + name: 'inferPath', + tooltipText: t('infer-btn-tooltip'), + }, + { + icon: , + label: t('hide-dar-btn'), + name: 'hideDar', + tooltipText: t('hide-dar-btn-tooltip'), + }, + + { + icon: , + label: t('progress-btn'), + name: 'showProgress', + tooltipText: t('progress-btn-tooltip'), + }, + { + icon: , + label: t('run-parallel-label'), + name: 'runParallel', + tooltipText: ( +

+ {t('run-parallel-btn-tooltip')}
+ {t('run-parallel-btn-tooltip2')} +

+ ), + }, + ] satisfies ComponentPropsWithRef[]; +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/useInputPathField.tsx b/gui/frontend/src/components/organisms/ConvertForm/useInputPathField.tsx new file mode 100644 index 0000000..ccca831 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/useInputPathField.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from '@/components/hooks/useTranslation'; + +import { MappingHelpButton } from './MappingHelpButton'; + +import type { InputPathField } from './InputPathField'; +import type { ComponentPropsWithRef } from 'react'; + +export const useInputPathFields = () => { + const { t } = useTranslation(); + + return [ + { + helperText: ( + <> + {t('convert-form-dar-helper')}
+ {t('convert-form-dar-helper2')}
+ {t('convert-form-dar-helper3')} + + ), + label: t('convert-form-dar-label'), + name: 'src', + placeholder: '[...]/', + }, + { + helperText: ( + <> + {t('convert-form-oar-helper')}
+ {t('convert-form-oar-helper2')} + + ), + label: t('convert-form-oar-label'), + name: 'dst', + placeholder: '[...]/', + }, + + { + helperText: , + label: t('convert-form-mapping-label'), + name: 'mapping1personPath', + placeholder: './mapping_table.txt', + }, + { + helperText: t('convert-form-mapping-helper'), + label: t('convert-form-mapping-1st-label'), + name: 'mappingPath', + placeholder: './mapping_table_for_1st_person.txt', + }, + ] satisfies ComponentPropsWithRef[]; +}; diff --git a/gui/frontend/src/components/organisms/ConvertForm/useModInfoField.tsx b/gui/frontend/src/components/organisms/ConvertForm/useModInfoField.tsx new file mode 100644 index 0000000..e43e947 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertForm/useModInfoField.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from '@/components/hooks/useTranslation'; + +import type { InputPathField } from './InputPathField'; +import type { ComponentPropsWithRef } from 'react'; + +export const useModInfoFields = () => { + const { t } = useTranslation(); + + return [ + { + name: 'modName', + helperText: t('convert-form-mod-name-helper'), + label: t('convert-form-mod-name'), + placeholder: t('convert-form-mod-name'), + }, + { + name: 'modAuthor', + helperText: t('convert-form-author-name-helper'), + label: t('convert-form-author-name'), + placeholder: t('convert-form-author-placeholder'), + }, + ] satisfies ComponentPropsWithRef[]; +}; diff --git a/gui/frontend/src/components/organisms/ConvertNav/ConvertNav.tsx b/gui/frontend/src/components/organisms/ConvertNav/ConvertNav.tsx new file mode 100644 index 0000000..4f0af84 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertNav/ConvertNav.tsx @@ -0,0 +1,47 @@ +'use client'; +import { Box, type SxProps, type Theme } from '@mui/material'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; + +import { ConvertButton } from '@/components/atoms/ConvertButton'; +import { LogDirButton } from '@/components/molecules/LogDirButton'; +import { LogFileButton } from '@/components/molecules/LogFileButton'; +import { LogLevelList } from '@/components/organisms/LogLevelList'; +import { RemoveOarButton } from '@/components/organisms/RemoveOarButton/RemoveOarButton'; +import { UnhideDarButton } from '@/components/organisms/UnhideDarButton'; + +import type { FormProps } from '../ConvertForm/ConvertForm'; + +const sx: SxProps = { + position: 'fixed', + bottom: 50, + width: '100%', + display: 'flex', + alignItems: 'center', + padding: '10px', + justifyContent: 'space-between', + backgroundColor: '#252525d8', +}; + +/** A transparent element that prevents a component in a fixed position from rising up and hiding other components. */ +export const ConvertNavPadding = () =>
; +export const ConvertNav = () => { + const { control } = useFormContext(); + const { progress } = useWatch(); + + return ( + ( + + + + + + + + + )} + /> + ); +}; diff --git a/gui/frontend/src/components/organisms/ConvertNav/index.tsx b/gui/frontend/src/components/organisms/ConvertNav/index.tsx new file mode 100644 index 0000000..4d40c03 --- /dev/null +++ b/gui/frontend/src/components/organisms/ConvertNav/index.tsx @@ -0,0 +1 @@ +export { ConvertNav, ConvertNavPadding } from './ConvertNav'; diff --git a/gui/frontend/src/components/organisms/LogLevelList/LogLevelList.tsx b/gui/frontend/src/components/organisms/LogLevelList/LogLevelList.tsx index 8c338a0..2a9ca23 100644 --- a/gui/frontend/src/components/organisms/LogLevelList/LogLevelList.tsx +++ b/gui/frontend/src/components/organisms/LogLevelList/LogLevelList.tsx @@ -39,7 +39,7 @@ export const LogLevelList = () => { ); return ( - + { +export const RemoveOarButton = () => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); + const { src: darPath, dst: oarPath } = useWatch(); const handleClick = useCallback(async () => { - if (oarPath === '' && darPath === '') { + if ((oarPath === undefined || oarPath === '') && (darPath === undefined || darPath === '')) { NOTIFY.error(t('remove-oar-specify-error')); return; } + const path = oarPath === '' ? darPath : oarPath; await progressListener( '/dar2oar/progress/remove-oar', async () => { - const path = oarPath === '' ? darPath : oarPath; - await removeOarDir(path); + await removeOarDir(path ?? ''); }, { setLoading, setProgress, success: t('remove-oar-success'), - error: t('remove-oar-failed'), + error: `${path}:\n${t('remove-oar-failed')}`, }, ); }, [darPath, oarPath, t]); return ( - {t('remove-oar-tooltip')}

}> - -
+ : } + tooltipTitle={

{t('remove-oar-tooltip')}

} + variant='contained' + /> ); }; diff --git a/gui/frontend/src/components/organisms/UnhideDarButton/UnhideDarButton.tsx b/gui/frontend/src/components/organisms/UnhideDarButton/UnhideDarButton.tsx index 1c10250..d62de3b 100644 --- a/gui/frontend/src/components/organisms/UnhideDarButton/UnhideDarButton.tsx +++ b/gui/frontend/src/components/organisms/UnhideDarButton/UnhideDarButton.tsx @@ -1,25 +1,24 @@ import VisibilityIcon from '@mui/icons-material/Visibility'; -import { Tooltip } from '@mui/material'; -import Button from '@mui/material/Button'; import { useCallback, useState } from 'react'; +import { useWatch } from 'react-hook-form'; +import { ButtonWithToolTip } from '@/components/atoms/ButtonWithToolTip'; import { CircularProgressWithLabel } from '@/components/atoms/CircularProgressWithLabel'; import { useTranslation } from '@/components/hooks/useTranslation'; import { NOTIFY } from '@/lib/notify'; import { unhideDarDir } from '@/services/api/convert'; import { progressListener } from '@/services/api/event'; -type Props = { - path: string; -}; +import type { FormProps } from '../ConvertForm/ConvertForm'; -export const UnhideDarButton = ({ path }: Props) => { +export const UnhideDarButton = () => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); + const { src: path } = useWatch(); const handleClick = useCallback(async () => { - if (path === '') { + if (path === undefined || path === '') { NOTIFY.error(t('unhide-dar-specify-error')); return; } @@ -33,26 +32,18 @@ export const UnhideDarButton = ({ path }: Props) => { setLoading, setProgress, success: t('unhide-dar-success'), - error: t('unhide-dar-failed'), + error: `${path}:\n${t('unhide-dar-failed')}`, }, ); }, [path, t]); return ( - {t('unhide-dar-btn-tooltip')}

}> - -
+ : } + onClick={handleClick} + tooltipTitle={

{t('unhide-dar-btn-tooltip')}

} + variant='contained' + /> ); }; diff --git a/gui/frontend/src/lib/css/index.ts b/gui/frontend/src/lib/css/index.ts index 1c14009..373eeb6 100644 --- a/gui/frontend/src/lib/css/index.ts +++ b/gui/frontend/src/lib/css/index.ts @@ -76,13 +76,13 @@ label.Mui-focused, background-color: var(--hover-btn-color); } -#x-data-grid-selected, -.MuiLoadingButton-root { +.MuiButton-containedPrimary, +#x-data-grid-selected { color: #fff; background-color: var(--convert-btn-color); } -.MuiLoadingButton-root:hover { +.MuiButton-containedPrimary:hover { background-color: var(--hover-convert-btn-color); } @@ -118,13 +118,13 @@ const preset2 = createPreset( const preset3 = createPreset( `--autofill-color: #eb37ff1c; - --convert-btn-color: #ab2b7e6e; + --convert-btn-color: #7c00c932; --hover-btn-color: #8b51fb8b; - --hover-convert-btn-color: #7d00c9a3; + --hover-convert-btn-color: #8737b884; --image-size: cover; --image-url: url("https://images.pexels.com/photos/6162265/pexels-photo-6162265.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"); --main-bg-color: #2223; - --theme-color: #9644f1;`, + --theme-color: #9644f1`, ); const preset4 = createPreset( diff --git a/gui/frontend/src/lib/path/parseDarPath.test.ts b/gui/frontend/src/lib/path/parseDarPath.test.ts new file mode 100644 index 0000000..b22fd7b --- /dev/null +++ b/gui/frontend/src/lib/path/parseDarPath.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; + +import { parseDarPath } from './parseDarPath'; + +describe('parseDarPath', () => { + it('should parse DAR path correctly when "meshes" is present', () => { + const path = '../ModName/meshes'; + const result = parseDarPath(path); + + expect(result).toHaveProperty('oarRoot', '../ModName/meshes'); + expect(result).toHaveProperty('modName', 'ModName'); + }); + + it('should return the full path as oarRoot when "meshes" is not present', () => { + const path = '../OtherMod/otherDir'; + const result = parseDarPath(path); + + expect(result).toHaveProperty('oarRoot', '../OtherMod/otherDir'); + expect(result).toHaveProperty('modName', 'otherDir'); + }); + + it('should handle paths with mixed slashes', () => { + const path = '../ModName\\meshes'; + const result = parseDarPath(path); + + expect(result).toHaveProperty('oarRoot', '../ModName/meshes'); + expect(result).toHaveProperty('modName', 'ModName'); + }); + + it('should return only ASCII parts as modName when no "meshes" is found', () => { + const path = '../ModName/NonASCII_ディレクトリ_With_English'; + const result = parseDarPath(path); + + expect(result).toHaveProperty('oarRoot', '../ModName/NonASCII_ディレクトリ_With_English'); + expect(result).toHaveProperty('modName', 'NonASCII__With_English'); // ASCII parts should be extracted + }); + + it('should return modName as the last ASCII directory when no "meshes" is found and it is ASCII', () => { + const path = '../ModName/lastDir'; + const result = parseDarPath(path); + + expect(result).toHaveProperty('oarRoot', '../ModName/lastDir'); + expect(result).toHaveProperty('modName', 'lastDir'); // ASCII should yield 'lastDir' + }); +}); diff --git a/gui/frontend/src/lib/path/parseDarPath.ts b/gui/frontend/src/lib/path/parseDarPath.ts new file mode 100644 index 0000000..a4d037a --- /dev/null +++ b/gui/frontend/src/lib/path/parseDarPath.ts @@ -0,0 +1,50 @@ +// The information necessary for the conversion +export type ParsedPath = { + oarRoot: string; + modName?: string; +}; + +const PATH_SEP = /\\|\//g; +// biome-ignore lint/suspicious/noControlCharactersInRegex: +const ASCII0 = /^[\x00-\x7F]*$/; +// biome-ignore lint/suspicious/noControlCharactersInRegex: +const NON_ASCII = /[^\x00-\x7F]+/; + +// Check if a string is ASCII +function isAscii(str: string): boolean { + return ASCII0.test(str); +} + +// Extract ASCII parts from a string +function extractAsciiParts(str: string): string { + return str + .split(NON_ASCII) // Split by non-alphabetical characters + .filter(isAscii) + .join(''); +} + +// Function to parse the DAR path +export function parseDarPath(path: string): ParsedPath { + const paths = path.split(PATH_SEP).filter(Boolean); + + const meshIndex = paths.findIndex((part) => part.toLowerCase() === 'meshes'); + + // Correctly derive oarRoot based on the meshIndex + const oarRoot = meshIndex !== -1 ? paths.slice(0, meshIndex + 1).join('/') : path; + + // Determine modName based on meshIndex + let modName: string | undefined; + if (meshIndex === -1) { + const lastPart = paths.at(-1); // Using at method + if (lastPart) { + modName = extractAsciiParts(lastPart); // Extract ASCII parts from last directory + } + } else { + modName = extractAsciiParts(paths.at(meshIndex - 1) || ''); // Extract ASCII parts from the preceding directory + } + + return { + oarRoot, + modName, + }; +} diff --git a/gui/frontend/src/lib/storage/cacheKeys.ts b/gui/frontend/src/lib/storage/cacheKeys.ts index 4c77a2d..8785829 100644 --- a/gui/frontend/src/lib/storage/cacheKeys.ts +++ b/gui/frontend/src/lib/storage/cacheKeys.ts @@ -4,12 +4,15 @@ const FORM_PUB_CACHE_KEYS_OBJ = { hideDar: 'hideDar', runParallel: 'runParallel', showProgress: 'showProgress', + inferPath: 'inferPath', } as const; const FORM_PRIVATE_CACHE_KEYS_OBJ = { cachedDst: 'cached-dst', cachedMapping1PersonPath: 'cached-mapping1personPath', cachedMappingPath: 'cached-mappingPath', + cachedModAuthor: 'cached-modName', + cachedModName: 'cached-modAuthor', cachedSrc: 'cached-src', dst: 'dst', mapping1personPath: 'mapping1personPath', diff --git a/gui/frontend/src/lib/storage/storage.ts b/gui/frontend/src/lib/storage/storage.ts index cf5c3a2..09587da 100644 --- a/gui/frontend/src/lib/storage/storage.ts +++ b/gui/frontend/src/lib/storage/storage.ts @@ -25,6 +25,14 @@ export const createStorage = < */ get: (key: CacheKey) => localStorage.getItem(key), + /** + * Retrieves the value from localStorage for the given cache key. + * @param key - The cache key to retrieve. + * @example + * assert(storage.getOrDefault('snackbar-limit') === ''); + */ + getOrDefault: (key: CacheKey) => localStorage.getItem(key) ?? '', + /** * Retrieves the values for multiple cache keys from localStorage. * @param keys - Array of cache keys to retrieve. diff --git a/locales/en-US.json b/locales/en-US.json index 2dd7666..d1e0351 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -16,7 +16,7 @@ "convert-form-author-name-helper": "[Optional]", "convert-form-author-placeholder": "Name", "convert-form-dar-helper": "[Required] Path of dir containing \"DynamicAnimationReplacer\".", - "convert-form-dar-helper2": "\"C:\\[...]/Mod Name/\" -> Convert 1st & 3rd person", + "convert-form-dar-helper2": "\"C:/[...]/Mod Name/\" -> Convert 1st & 3rd person", "convert-form-dar-helper3": "\"[...]/animations/DynamicAnimationReplacer\" -> Convert 3rd person", "convert-form-dar-label": "DAR(source) Directory", "convert-form-mapping-1st-label": "Mapping Table Path(For _1st_person)", @@ -50,6 +50,8 @@ "import-lang-btn": "Import Language", "import-lang-tooltip": "Import any language from a Json file. (automatically reloads for validation).", "import-lang-tooltip2": "Note: For invalid Json, fall back to English. (See Wiki for how to write Json)", + "infer-btn": "Infer", + "infer-btn-tooltip": "Infer OAR and ModName from DAR (input). (Even if each item is not entered without this function, it is inferred to some extent on the back-end side.)", "lang-preset-auto": "Auto", "lang-preset-custom": "Custom", "lang-preset-label": "Language", @@ -71,17 +73,17 @@ "open-log-dir-btn": "Log(dir)", "open-log-dir-tooltip": "Open the log storage location.", "open-log-tooltip": "Open current log file.(Rotate to a new log file each time the application is launched.)", - "progress-btn": "ProgressBar", - "progress-btn-tooltip": "Display detail progress", - "progress-btn-tooltip2": "", + "progress-btn": "Progress", + "progress-btn-tooltip": "Let the back-end report detailed progress.", + "progress-btn-tooltip2": "(conversion may be slightly slower)", "remove-oar-btn": "Remove OAR", "remove-oar-failed": "Not found \"OpenAnimationReplacer\" directory", "remove-oar-specify-error": "DAR or OAR dir must be specified.", "remove-oar-success": "Removed OAR directory.", "remove-oar-tooltip": "Find and delete OAR dir from \"OAR(destination) Directory\"(or \"DAR(source) Directory*\" if not specified).", - "run-parallel-btn-tooltip": "Use multi-threading.", - "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", + "run-parallel-btn-tooltip": "Attempt file-by-file parallel conversion.", + "run-parallel-btn-tooltip2": "Pros: extremely fast conversion / Cons: entries in logs are out of order and difficult to read", + "run-parallel-label": "Parallel", "select-btn": "Select", "tab-label-backup": "Backup", "tab-label-editor": "Editor / Preset", diff --git a/locales/ja-JP.json b/locales/ja-JP.json index ab07aa3..dfb55c7 100644 --- a/locales/ja-JP.json +++ b/locales/ja-JP.json @@ -16,16 +16,16 @@ "convert-form-author-name-helper": "[任意]", "convert-form-author-placeholder": "作者名", "convert-form-dar-helper": "[必須] \"DynamicAnimationReplacer\"が含まれているディレクトリ", - "convert-form-dar-helper2": "\"C:\\[...]/Mod Name/\" -> 1人称と3人称を変換", + "convert-form-dar-helper2": "\"C:/[...]/Mod Name/\" -> 1人称と3人称を変換", "convert-form-dar-helper3": "\"[...]/animations/DynamicAnimationReplacer\" -> 3人称のみ変換", "convert-form-dar-label": "DAR(入力)ディレクトリ", "convert-form-mapping-1st-label": "マッピングテーブルのパス(1人称用)", - "convert-form-mapping-help-link-name": "マッピングファイルとは?", + "convert-form-mapping-help-link-name": "マッピングテーブルについて", "convert-form-mapping-helper": "[任意] 優先番号とセクション名の対応付けが書かれたファイルを指定", "convert-form-mapping-helper2": "ヘルプ: ", "convert-form-mapping-label": "マッピングテーブルのパス", "convert-form-mod-name": "Mod名", - "convert-form-mod-name-helper": "[任意] ASCII(英語)推奨", + "convert-form-mod-name-helper": "[任意] (英数字推奨)", "convert-form-oar-helper": "[任意] OARの出力先を指定(例: \"NewMod\" -> \"NewMod/meshes/[...]\")", "convert-form-oar-helper2": "指定されない場合、DARと同階層の場所にOARが作られます。", "convert-form-oar-label": "OAR(出力先)ディレクトリ", @@ -44,12 +44,14 @@ "custom-js-auto-run-tooltip2": "この設定項目はユーザーが手動で選択しない限り有効化されることはありません", "custom-js-label": "(実行許可時のみ)ページ移動ごとに実行されるJavaScript", "editor-mode-list-label": "エディタモード", - "hide-dar-btn": "DARを非表示", + "hide-dar-btn": "DAR隠蔽", "hide-dar-btn-tooltip": "変換後、DARの全ファイルに「.mohidden」を追加して非表示化します(MO2ユーザ向け)", "hide-dar-btn-tooltip2": "INFO: OARの出力先を指定しない場合、特に便利です。", "import-lang-btn": "言語インポート", "import-lang-tooltip": "Jsonファイルから任意の言語をインポートします。(有効化のため自動で再読み込みします)", "import-lang-tooltip2": "注: 無効なJsonの場合、英語にフォールバックします。(Jsonの書き方はWikiを参照してください)", + "infer-btn": "パス推論", + "infer-btn-tooltip": "DAR(入力)からOARとMod名(ASCIIのみを自動抽出)を推論します(この機能を使わず各項目が未入力でもバックエンド側である程度推論します)", "lang-preset-auto": "自動", "lang-preset-custom": "カスタム", "lang-preset-label": "言語", @@ -71,17 +73,17 @@ "open-log-dir-btn": "ログ(dir)", "open-log-dir-tooltip": "ログの格納場所を開きます。", "open-log-tooltip": "現在のログファイルを開きます。(アプリを起動するたびに新しいログファイルにローテーションします)", - "progress-btn": "進捗バー", - "progress-btn-tooltip": "詳細な進捗状況を表示します", - "progress-btn-tooltip2": "", + "progress-btn": "進捗報告", + "progress-btn-tooltip": "バックエンドから詳細な進捗状況を報告させます", + "progress-btn-tooltip2": "(変換が遅くなる可能性があります)", "remove-oar-btn": "OARを削除", "remove-oar-failed": "「OpenAnimationReplacer」ディレクトリが見つかりません", "remove-oar-specify-error": "DARまたはOARが入力されていません", "remove-oar-success": "OARディレクトリを削除しました", "remove-oar-tooltip": "「OAR(出力先)」、未指定なら「DAR(入力)」からOARディレクトリを探して削除します", - "run-parallel-btn-tooltip": "マルチスレッドを使用します", - "run-parallel-btn-tooltip2": "注意: 2倍以上の処理速度が期待できますが、並行処理によりスレッドの終了タイミングが順不同になるため、ログの書き込みも同様に順不同になりログの可読性が大幅に低下します。", - "run-parallel-label": "並列実行", + "run-parallel-btn-tooltip": "ファイル別並列変換を試みます", + "run-parallel-btn-tooltip2": "長所: 変換の高速化 / 短所: ログへの記述が順不同になり読みづらくなります", + "run-parallel-label": "並列", "select-btn": "選択", "tab-label-backup": "バックアップ", "tab-label-editor": "エディタ・プリセット", @@ -94,6 +96,6 @@ "unhide-dar-btn": "DAR再表示", "unhide-dar-btn-tooltip": "「DARを非表示」による非表示化の解除(MO2ユーザ向け)", "unhide-dar-failed": "拡張子「.mohidden」のついたファイルが見つかりません", - "unhide-dar-specify-error": "DAR(src)を指定してください", + "unhide-dar-specify-error": "DAR(入力)を指定してください", "unhide-dar-success": "DARの非表示化が解除されました" }