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
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const en: typeof zhCN = {
},
common: {
loading: 'Loading…',
retry: 'Retry',
settingsLoadFailed: 'Settings load failed',
refresh: 'Refresh',
clear: 'Clear',
copy: 'Copy',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const ja: typeof zhCN = {
},
common: {
loading: '読み込み中…',
retry: '再試行',
settingsLoadFailed: '設定の読み込みに失敗しました',
refresh: '更新',
clear: 'クリア',
copy: 'コピー',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const ko: typeof zhCN = {
},
common: {
loading: '로딩 중…',
retry: '다시 시도',
settingsLoadFailed: '설정 로드 실패',
refresh: '새로고침',
clear: '지우기',
copy: '복사',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const zhCN = {
},
common: {
loading: '加载中…',
retry: '重试',
settingsLoadFailed: '设置加载失败',
refresh: '刷新',
clear: '清空',
copy: '复制',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const zhTW: typeof zhCN = {
},
common: {
loading: '加載中…',
retry: '重試',
settingsLoadFailed: '設置加載失敗',
refresh: '刷新',
clear: '清空',
copy: '複製',
Expand Down
71 changes: 69 additions & 2 deletions openless-all/app/src/pages/Translation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'failed';

export function Translation() {
const { t } = useTranslation();
const { prefs, refresh, updatePrefs: savePrefs } = useHotkeySettings();
const { prefs, loading, error, refresh, updatePrefs: savePrefs } = useHotkeySettings();
const [saveState, setSaveState] = useState<SaveState>('idle');
const [saveMessage, setSaveMessage] = useState('');
const statusTimer = useRef<number | null>(null);
Expand Down Expand Up @@ -69,7 +69,34 @@ export function Translation() {
desc={t('translation.desc')}
/>
<Card>
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>
{error ? (
<div role="alert" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ fontSize: 12, color: 'var(--ol-red, #ef4444)', lineHeight: 1.5 }}>
{t('common.settingsLoadFailed')}:{error}
</div>
<button
type="button"
onClick={() => { void refresh(); }}
disabled={loading}
style={{
alignSelf: 'flex-start',
padding: '6px 12px',
borderRadius: 999,
border: 0,
background: 'var(--ol-blue)',
color: '#fff',
fontSize: 12,
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'default',
opacity: loading ? 0.64 : 1,
}}
>
{loading ? t('common.loading') : t('common.retry')}
</button>
</div>
) : (
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>
)}
</Card>
</>
);
Expand Down Expand Up @@ -121,6 +148,46 @@ export function Translation() {
/>

<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{error && (
<div
role="alert"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 10,
padding: '8px 12px',
borderRadius: 10,
border: '0.5px solid rgba(239,68,68,0.22)',
background: 'rgba(239,68,68,0.07)',
color: 'var(--ol-red, #ef4444)',
fontSize: 11.5,
lineHeight: 1.5,
}}
>
<span>{t('common.settingsLoadFailed')}:{error}</span>
<button
type="button"
onClick={() => { void refresh(); }}
disabled={loading}
style={{
flex: '0 0 auto',
border: 0,
borderRadius: 999,
background: 'rgba(239,68,68,0.12)',
color: 'inherit',
padding: '4px 10px',
fontSize: 11,
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'default',
opacity: loading ? 0.64 : 1,
}}
>
{loading ? t('common.loading') : t('common.retry')}
</button>
</div>
)}

{saveState !== 'idle' && (
<div
role={saveState === 'failed' ? 'alert' : 'status'}
Expand Down
39 changes: 34 additions & 5 deletions openless-all/app/src/state/HotkeySettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface HotkeySettingsContextValue {
hotkey: HotkeyBinding | null;
capability: HotkeyCapability | null;
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
updatePrefs: (
next: UserPreferences | ((current: UserPreferences) => UserPreferences),
Expand All @@ -25,18 +26,45 @@ interface HotkeySettingsContextValue {

const HotkeySettingsContext = createContext<HotkeySettingsContextValue | null>(null);

const errorMessage = (error: unknown) =>
String(error instanceof Error ? error.message : error);

export function HotkeySettingsProvider({ children }: { children: ReactNode }) {
const [prefs, setPrefs] = useState<UserPreferences | null>(null);
const [capability, setCapability] = useState<HotkeyCapability | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const persistQueueRef = useRef<Promise<void>>(Promise.resolve());
const latestPrefsRef = useRef<UserPreferences | null>(null);

const refresh = useCallback(async () => {
const [nextPrefs, nextCapability] = await Promise.all([getSettings(), getHotkeyCapability()]);
setPrefs(nextPrefs);
setCapability(nextCapability);
setLoading(false);
setLoading(true);
setError(null);
try {
const [prefsResult, capabilityResult] = await Promise.allSettled([
getSettings(),
getHotkeyCapability(),
]);
let nextError: string | null = null;
if (prefsResult.status === 'fulfilled') {
setPrefs(prefsResult.value);
} else {
console.error('[hotkey-settings] failed to load preferences', prefsResult.reason);
nextError = errorMessage(prefsResult.reason);
}
if (capabilityResult.status === 'fulfilled') {
setCapability(capabilityResult.value);
} else {
console.error('[hotkey-settings] failed to load hotkey capability', capabilityResult.reason);
nextError = errorMessage(capabilityResult.reason);
}
setError(nextError);
} catch (error) {
console.error('[hotkey-settings] failed to refresh hotkey settings', error);
setError(errorMessage(error));
} finally {
setLoading(false);
}
}, []);

const queueSetSettings = useCallback((resolveNext: (current: UserPreferences) => UserPreferences) => {
Expand Down Expand Up @@ -137,10 +165,11 @@ export function HotkeySettingsProvider({ children }: { children: ReactNode }) {
hotkey: prefs?.hotkey ?? null,
capability,
loading,
error,
refresh,
updatePrefs,
}),
[capability, loading, prefs, refresh, updatePrefs],
[capability, error, loading, prefs, refresh, updatePrefs],
);

return <HotkeySettingsContext.Provider value={value}>{children}</HotkeySettingsContext.Provider>;
Expand Down
Loading