s3si.ts/gui/src/pages/Settings.tsx

201 lines
6.6 KiB
TypeScript
Raw Normal View History

2024-01-30 05:54:51 -05:00
import { ErrorContent, FallbackComponent } from 'components/ErrorContent';
2023-03-06 09:01:18 -05:00
import { Loading } from 'components/Loading';
2024-01-30 05:54:51 -05:00
import React, { Suspense, useState } from 'react'
2023-03-06 07:21:29 -05:00
import { useTranslation } from 'react-i18next';
2024-01-30 05:54:51 -05:00
import { Config, Profile } from 'services/config';
import clsx from 'clsx';
import { useLogin } from 'services/s3si';
import { STAT_INK } from 'constant';
2023-03-08 09:55:23 -05:00
import { Header } from 'components/Header';
2023-03-08 17:38:39 -05:00
import { useSubField } from 'hooks/useSubField';
import { useNavigate } from 'react-router-dom';
2024-01-30 05:54:51 -05:00
import useSWRMutation from 'swr/mutation'
import { useService, useServiceMutation } from 'services/useService';
import { ErrorBoundary } from 'react-error-boundary';
2023-03-08 17:38:39 -05:00
const STAT_INK_KEY_LENGTH = 43;
2023-03-06 07:21:29 -05:00
2023-03-06 14:36:06 -05:00
const Page: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { t } = useTranslation();
2023-03-08 09:55:23 -05:00
return <div className='full-card'>
2023-03-08 17:38:39 -05:00
<Header title={t('设置')} />
2023-03-06 14:36:06 -05:00
{children}
</div>
}
type FormData = {
config: Config,
profile: Profile,
}
2023-03-13 00:00:01 -04:00
const SPLATNET3_LANGS = {
"de-DE": "German",
"en-GB": "English (UK/Australia)",
"en-US": "English (US)",
"es-ES": "Spanish (Spain)",
"es-MX": "Spanish (Latin America)",
"fr-CA": "French (Canada)",
"fr-FR": "French (France)",
"it-IT": "Italian",
"ja-JP": "Japanese",
"ko-KR": "Korean",
"nl-NL": "Dutch",
"ru-RU": "Russian",
"zh-CN": "Chinese (China)",
"zh-TW": "Chinese (Taiwan)"
}
const UI_LANGS = {
"en": "English",
"zh-CN": "简体中文",
"ja": "日本語",
};
2023-03-06 14:36:06 -05:00
const Form: React.FC<{
oldValue: FormData,
onSaved?: () => void,
}> = ({ oldValue, onSaved }) => {
const { login } = useLogin();
2023-03-13 00:00:01 -04:00
const { t, i18n } = useTranslation();
2023-03-06 14:36:06 -05:00
const [value, setValue] = useState(oldValue);
2023-03-08 17:38:39 -05:00
const { subField } = useSubField({ value, onChange: setValue });
2024-01-30 05:54:51 -05:00
const { trigger: setProfile } = useServiceMutation('profile', 0)
const { trigger: setConfig } = useServiceMutation('config')
2023-03-06 07:21:29 -05:00
2023-03-06 14:36:06 -05:00
const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
2023-03-08 17:38:39 -05:00
const sessionToken = subField('profile.state.loginState.sessionToken')
const statInkApiKey = subField('profile.state.statInkApiKey')
2023-03-13 00:00:01 -04:00
const splatnet3Lang = subField('profile.state.userLang')
2024-01-30 05:54:51 -05:00
const { trigger: onSave, isMutating: loading, error } = useSWRMutation('saveSettings', async () => {
await setProfile(value.profile);
2023-03-06 14:36:06 -05:00
await setConfig(value.config);
onSaved?.();
})
2024-01-30 05:54:51 -05:00
const loginState = useSWRMutation('login', async () => {
const result = await login();
if (!result) {
return;
}
2023-03-08 17:38:39 -05:00
sessionToken.onChange(result.sessionToken);
})
2023-03-06 08:32:00 -05:00
2023-03-08 17:38:39 -05:00
const statInkKeyError = (statInkApiKey.value?.length ?? STAT_INK_KEY_LENGTH) !== STAT_INK_KEY_LENGTH;
2023-03-06 07:21:29 -05:00
return <>
2023-03-06 14:36:06 -05:00
<div className='card'>
<div className="form-control w-full max-w-md mb-4">
<label className="label">
<span className="label-text">{t('Nintendo Account 会话令牌')}</span>
<span className="label-text-alt"><button
2024-01-30 05:54:45 -05:00
type='button'
2024-01-30 05:54:51 -05:00
className={clsx('link', {
loading: loginState.isMutating,
})}
2024-01-30 05:54:51 -05:00
onClick={() => loginState.trigger()}
disabled={loginState.isMutating}
>{t('网页登录')}</button></span>
</label>
<input
className="input input-bordered w-full"
type="text"
placeholder={t('请点击右上角的登录填入') ?? undefined}
2023-03-08 17:38:39 -05:00
value={sessionToken.value ?? ''}
onChange={e => sessionToken.onChange(e.target.value)}
/>
</div>
<div className="form-control w-full max-w-md mb-4">
2023-03-06 14:36:06 -05:00
<label className="label">
<span className="label-text">{t('stat.ink API密钥')}</span>
<span className="label-text-alt"><a
className='underline'
target='_blank'
rel='noopener noreferrer'
href={`${STAT_INK}/profile`}
2023-03-06 14:36:06 -05:00
title={t('打开 stat.ink') ?? undefined}
2023-03-08 17:38:39 -05:00
>{t('查看API密钥')}</a></span>
2023-03-06 14:36:06 -05:00
</label>
2023-03-08 17:38:39 -05:00
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
<input
2024-01-30 05:54:51 -05:00
className={clsx("input input-bordered w-full", {
2023-03-08 17:38:39 -05:00
'input-error': statInkKeyError,
})}
type="text"
placeholder={t('请从stat.ink中获取API密钥') ?? undefined}
value={statInkApiKey.value ?? ''}
onChange={e => statInkApiKey.onChange(e.target.value)}
/>
</div>
2023-03-06 07:21:29 -05:00
</div>
2023-03-13 00:00:01 -04:00
<div className="form-control w-full max-w-md mb-4">
<label className="label">
<span className="label-text">{t('鱿鱼圈3语言偏好')}</span>
</label>
<select className="select w-full" value={splatnet3Lang.value} onChange={
e => splatnet3Lang.onChange(e.target.value)
}>
{Object.entries(SPLATNET3_LANGS).map(([key, value]) => <option key={key} value={key}>{value} ({key})</option>)}
</select>
</div>
<div className="form-control w-full max-w-md mb-4">
<label className="label">
<span className="label-text">{t('界面语言')}</span>
</label>
<select className="select w-full" value={i18n.language} onChange={
e => {
i18n.changeLanguage(e.target.value);
}
}>
{Object.entries(UI_LANGS).map(([key, value]) => <option key={key} value={key}>{value} ({key})</option>)}
</select>
</div>
2023-03-06 14:36:06 -05:00
</div>
<ErrorContent error={error} />
<div className='flex gap-4 max-w-md justify-between flex-auto-all'>
2024-01-30 05:54:51 -05:00
<div className='tooltip' data-tip={changed ? undefined : t('没有更改')}>
2024-01-30 05:54:45 -05:00
<button
type='button'
2024-01-30 05:54:51 -05:00
className={clsx('btn btn-primary w-full', {
2024-01-30 05:54:45 -05:00
loading,
})}
2024-01-30 05:54:51 -05:00
onClick={() => onSave()}
2024-01-30 05:54:45 -05:00
disabled={!changed || statInkKeyError}
>{t('保存')}</button>
</div>
2024-01-30 05:54:45 -05:00
<button
type='button'
2024-01-30 05:54:51 -05:00
className={clsx('btn', {
2024-01-30 05:54:45 -05:00
loading,
})}
onClick={() => setValue(oldValue)}
>{t('重置')}</button>
2023-03-06 07:21:29 -05:00
</div>
</>
2023-03-06 14:36:06 -05:00
}
2024-01-30 05:54:51 -05:00
const SettingsLoader: React.FC = () => {
2023-03-08 17:38:39 -05:00
const navigate = useNavigate();
2024-01-30 05:54:51 -05:00
const { data: config } = useService('config')
const { data: profile } = useService('profile', 0)
2023-03-06 14:36:06 -05:00
2024-01-30 05:54:51 -05:00
if (!config || !profile) {
return <>
Error
</>
2023-03-06 14:36:06 -05:00
}
2024-01-30 05:54:51 -05:00
return <>
<Form oldValue={{ config, profile }} onSaved={() => navigate(-1)} />
</>
}
2023-03-06 14:36:06 -05:00
2024-01-30 05:54:51 -05:00
export const Settings: React.FC = () => {
2023-03-06 14:36:06 -05:00
return <Page>
2024-01-30 05:54:51 -05:00
<ErrorBoundary FallbackComponent={FallbackComponent}>
<Suspense fallback={<div className='h-full flex items-center justify-center'><Loading /></div>}>
<SettingsLoader />
</Suspense>
</ErrorBoundary>
2023-03-06 14:36:06 -05:00
</Page>
}