Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -189,6 +191,7 @@ export const en: typeof zhCN = {
asrWhisper: 'OpenAI Whisper (compatible)',
},
fillDefault: 'Fill default value',
readFailed: 'Read failed',
},
shortcuts: {
title: 'Shortcut reference',
Expand Down
3 changes: 3 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export const zhCN = {
show: '显示',
hide: '隐藏',
saved: '已保存',
saving: '保存中',
copied: '已复制',
operationFailed: '操作失败',
add: '添加',
},
capsule: {
Expand Down Expand Up @@ -187,6 +189,7 @@ export const zhCN = {
asrWhisper: 'OpenAI Whisper(兼容)',
},
fillDefault: '填入默认值',
readFailed: '读取失败',
},
shortcuts: {
title: '快捷键速查',
Expand Down
48 changes: 31 additions & 17 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ function ProvidersSection() {
);
}

type CredentialFieldStatus = 'idle' | 'saving' | 'saved' | 'readError' | 'saveError' | 'copied' | 'copyError';

interface CredentialFieldProps {
label: string;
account: string;
Expand All @@ -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<CredentialFieldStatus>('idle');
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const statusRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
let cancelled = false;
setLoaded(false);
setDirty(false);
setStatus('idle');
setValue('');
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
Expand All @@ -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;
Expand All @@ -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<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
if (!loaded) return;
setDirty(true);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => save(v), 300);
Expand All @@ -425,35 +436,35 @@ 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 (
<SettingRow label={label}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', width: '100%', maxWidth: 420 }}>
<input
type={inputType}
value={value}
placeholder={placeholder}
placeholder={loaded ? placeholder : t('common.loading')}
onChange={handleChange}
onBlur={onBlur}
disabled={!loaded}
disabled={disabled}
style={{ ...inputStyle, fontFamily: mono ? 'var(--ol-font-mono)' : 'inherit' }}
/>
{defaultValue && !value && (
{defaultValue && !value && loaded && (
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle} disabled={!loaded}>
<Icon name="check" size={13} />
</button>
Expand All @@ -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}
>
<Icon name="eye" size={14} />
</button>
Expand All @@ -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}
>
<Icon name="copy" size={14} />
</button>
Expand All @@ -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')}
</span>
)}
</div>
Expand Down
Loading