diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 826e4a2f..6781dfaf 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -19,7 +19,9 @@ export const en: typeof zhCN = { show: 'Show', hide: 'Hide', saved: 'Saved', + saving: 'Saving…', copied: 'Copied', + operationFailed: 'Operation failed', add: 'Add', }, capsule: { @@ -189,6 +191,7 @@ export const en: typeof zhCN = { asrWhisper: 'OpenAI Whisper (compatible)', }, fillDefault: 'Fill default value', + readFailed: 'Read failed', }, shortcuts: { title: 'Shortcut reference', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index a8b92296..fad775b9 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -17,7 +17,9 @@ export const zhCN = { show: '显示', hide: '隐藏', saved: '已保存', + saving: '保存中', copied: '已复制', + operationFailed: '操作失败', add: '添加', }, capsule: { @@ -187,6 +189,7 @@ export const zhCN = { asrWhisper: 'OpenAI Whisper(兼容)', }, fillDefault: '填入默认值', + readFailed: '读取失败', }, shortcuts: { title: '快捷键速查', diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 666319f6..fd71926d 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -336,6 +336,8 @@ function ProvidersSection() { ); } +type CredentialFieldStatus = 'idle' | 'saving' | 'saved' | 'readError' | 'saveError' | 'copied' | 'copyError'; + interface CredentialFieldProps { label: string; account: string; @@ -351,14 +353,16 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue const [revealed, setRevealed] = useState(false); const [loaded, setLoaded] = useState(false); const [dirty, setDirty] = useState(false); - const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'saveError' | 'copied' | 'copyError'>('idle'); + const [status, setStatus] = useState('idle'); const debounceRef = useRef | null>(null); + const statusRef = useRef | null>(null); useEffect(() => { let cancelled = false; setLoaded(false); setDirty(false); setStatus('idle'); + setValue(''); if (debounceRef.current) { clearTimeout(debounceRef.current); debounceRef.current = null; @@ -373,7 +377,7 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue if (cancelled) return; console.error('[settings] failed to read credential', account, error); setLoaded(true); - setStatus('saveError'); + setStatus('readError'); }); return () => { cancelled = true; @@ -383,26 +387,33 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); + if (statusRef.current) clearTimeout(statusRef.current); }; }, []); + const showTemporaryStatus = (next: CredentialFieldStatus) => { + setStatus(next); + if (statusRef.current) clearTimeout(statusRef.current); + statusRef.current = window.setTimeout(() => setStatus('idle'), 1600); + }; + const save = async (v: string) => { - if (!loaded) return; + if (!loaded || !dirty) return; setStatus('saving'); try { await setCredential(account, v); setDirty(false); - setStatus('saved'); + showTemporaryStatus('saved'); } catch (error) { console.error('[settings] failed to save credential', account, error); - setStatus('saveError'); + showTemporaryStatus('saveError'); } - window.setTimeout(() => setStatus('idle'), 1600); }; const handleChange = (e: React.ChangeEvent) => { const v = e.target.value; setValue(v); + if (!loaded) return; setDirty(true); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => save(v), 300); @@ -425,21 +436,21 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue }; const onCopy = async () => { - if (!value) return; + if (!value || !loaded) return; try { if (!navigator.clipboard?.writeText) { throw new Error('Clipboard API unavailable'); } await navigator.clipboard.writeText(value); - setStatus('copied'); + showTemporaryStatus('copied'); } catch (error) { console.error('[settings] failed to copy credential', account, error); - setStatus('copyError'); + showTemporaryStatus('copyError'); } - window.setTimeout(() => setStatus('idle'), 1600); }; const inputType = mask && !revealed ? 'password' : 'text'; + const disabled = !loaded; return ( @@ -447,13 +458,13 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue - {defaultValue && !value && ( + {defaultValue && !value && loaded && ( @@ -463,6 +474,7 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue onClick={() => setRevealed(r => !r)} title={revealed ? t('common.hide') : t('common.show')} style={iconBtnStyle} + disabled={disabled} > @@ -471,7 +483,7 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue onClick={onCopy} title={t('common.copy')} style={iconBtnStyle} - disabled={!value} + disabled={!value || disabled} > @@ -484,12 +496,14 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue }} > {status === 'saving' - ? '保存中' + ? t('common.saving') : status === 'saved' ? t('common.saved') : status === 'copied' - ? '已复制' - : '操作失败'} + ? t('common.copied') + : status === 'readError' + ? t('settings.providers.readFailed') + : t('common.operationFailed')} )}