Skip to content
Merged
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
85 changes: 75 additions & 10 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,35 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
const { t } = useTranslation();
const [value, setValue] = useState('');
const [revealed, setRevealed] = useState(false);
const [saved, setSaved] = useState(false);
const [loaded, setLoaded] = useState(false);
const [dirty, setDirty] = useState(false);
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'saveError' | 'copied' | 'copyError'>('idle');
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
readCredential(account).then(v => setValue(v ?? ''));
let cancelled = false;
setLoaded(false);
setDirty(false);
setStatus('idle');
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
}
readCredential(account)
.then(v => {
if (cancelled) return;
setValue(v ?? '');
setLoaded(true);
})
.catch(error => {
if (cancelled) return;
console.error('[settings] failed to read credential', account, error);
setLoaded(true);
setStatus('saveError');
});
return () => {
cancelled = true;
};
}, [account]);

useEffect(() => {
Expand All @@ -367,19 +391,29 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
}, []);

const save = async (v: string) => {
await setCredential(account, v);
setSaved(true);
setTimeout(() => setSaved(false), 1200);
if (!loaded) return;
setStatus('saving');
try {
await setCredential(account, v);
setDirty(false);
setStatus('saved');
} catch (error) {
console.error('[settings] failed to save credential', account, error);
setStatus('saveError');
}
window.setTimeout(() => setStatus('idle'), 1600);
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
setDirty(true);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => save(v), 300);
};

const onBlur = () => {
if (!loaded || !dirty) return;
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
Expand All @@ -388,11 +422,27 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
};

const fillDefault = async () => {
if (!defaultValue) return;
if (!loaded || !defaultValue) return;
setValue(defaultValue);
setDirty(true);
await save(defaultValue);
};

const onCopy = async () => {
if (!value) return;
try {
if (!navigator.clipboard?.writeText) {
throw new Error('Clipboard API unavailable');
}
await navigator.clipboard.writeText(value);
setStatus('copied');
} catch (error) {
console.error('[settings] failed to copy credential', account, error);
setStatus('copyError');
}
window.setTimeout(() => setStatus('idle'), 1600);
};

const inputType = mask && !revealed ? 'password' : 'text';

return (
Expand All @@ -404,10 +454,11 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
placeholder={placeholder}
onChange={handleChange}
onBlur={onBlur}
disabled={!loaded}
style={{ ...inputStyle, fontFamily: mono ? 'var(--ol-font-mono)' : 'inherit' }}
/>
{defaultValue && !value && (
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle}>
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle} disabled={!loaded}>
<Icon name="check" size={13} />
</button>
)}
Expand All @@ -421,15 +472,29 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
</button>
)}
<button
onClick={() => navigator.clipboard?.writeText(value)}
onClick={onCopy}
title={t('common.copy')}
style={iconBtnStyle}
disabled={!value}
>
<Icon name="copy" size={14} />
</button>
{saved && (
<span style={{ fontSize: 11, color: 'var(--ol-ok)', whiteSpace: 'nowrap' }}>{t('common.saved')}</span>
{status !== 'idle' && (
<span
style={{
fontSize: 11,
color: status.endsWith('Error') ? 'var(--ol-warn)' : 'var(--ol-ok)',
whiteSpace: 'nowrap',
}}
>
{status === 'saving'
? '保存中'
: status === 'saved'
? t('common.saved')
: status === 'copied'
? '已复制'
Comment on lines +490 to +495
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Hardcoded Chinese status messages break consistency with the i18n pattern used elsewhere.

These status texts ('保存中', '已复制', '操作失败') should also use the i18n helper instead of being hardcoded. Please define appropriate keys (e.g. under settings.credential.*) and call t(...) so status messages are localized consistently with the rest of the component.

Suggested implementation:

        {status !== 'idle' && (
          <span
            style={{
              fontSize: 11,
              color: status.endsWith('Error') ? 'var(--ol-warn)' : 'var(--ol-ok)',
              whiteSpace: 'nowrap',
            }}
          >
            {status === 'saving'
              ? t('settings.credential.saving')
              : status === 'saved'
                ? t('common.saved')
                : status === 'copied'
                  ? t('settings.credential.copied')
                  : t('settings.credential.actionFailed')}
          </span>
        )}
      </div>
    </SettingRow>

To fully implement this, add the corresponding i18n keys in your locale files, for example:

  • In locales/zh/settings.json (or equivalent):

    • "credential.saving": "保存中"
    • "credential.copied": "已复制"
    • "credential.actionFailed": "操作失败"
  • In other locale files (e.g. en), provide appropriate translations for:

    • settings.credential.saving
    • settings.credential.copied
    • settings.credential.actionFailed

Also ensure that the settings namespace is loaded for this page if your i18n setup requires explicit namespace loading.

: '操作失败'}
</span>
)}
</div>
</SettingRow>
Expand Down
Loading