mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat(desktop): support clearing hotkey bindings in ShortcutManager (#12727)
* 🔧 chore: update @lobehub/ui dependency to a specific version URL and enhance ShortcutManager functionality - Updated @lobehub/ui dependency in package.json to a specific version URL. - Improved ShortcutManager to handle empty accelerator bindings, allowing users to clear shortcuts. - Updated tests to reflect changes in shortcut handling and added localization for clear binding messages in both Chinese and English. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update @lobehub/ui dependency to version 5.4.0 in package.json - Changed the @lobehub/ui dependency from a specific version URL to version 5.4.0 for improved stability and consistency. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update @lobehub/ui dependency to use caret versioning in package.json - Changed the @lobehub/ui dependency from a fixed version to caret versioning (^5.4.0) to allow for minor updates and improvements. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -84,13 +84,23 @@ export class ShortcutManager {
|
||||
}
|
||||
|
||||
// 2. Basic format validation
|
||||
if (!accelerator || typeof accelerator !== 'string' || accelerator.trim() === '') {
|
||||
if (typeof accelerator !== 'string') {
|
||||
logger.error(`Invalid accelerator format: ${accelerator}`);
|
||||
return { errorType: 'INVALID_FORMAT', success: false };
|
||||
}
|
||||
|
||||
const trimmedAccelerator = accelerator.trim();
|
||||
|
||||
// Empty value means disable this shortcut binding
|
||||
if (trimmedAccelerator === '') {
|
||||
this.shortcutsConfig[id] = '';
|
||||
this.saveShortcutsConfig();
|
||||
this.registerConfiguredShortcuts();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Convert frontend format to Electron format
|
||||
const convertedAccelerator = this.convertAcceleratorFormat(accelerator.trim());
|
||||
const convertedAccelerator = this.convertAcceleratorFormat(trimmedAccelerator);
|
||||
const cleanAccelerator = convertedAccelerator.toLowerCase();
|
||||
|
||||
logger.debug(`Converted accelerator from ${accelerator} to ${convertedAccelerator}`);
|
||||
@@ -221,7 +231,7 @@ export class ShortcutManager {
|
||||
// If no configuration, use default configuration
|
||||
if (!config || Object.keys(config).length === 0) {
|
||||
logger.debug('No shortcuts config found, using defaults');
|
||||
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
|
||||
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
|
||||
this.saveShortcutsConfig();
|
||||
} else {
|
||||
// Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
|
||||
@@ -257,7 +267,7 @@ export class ShortcutManager {
|
||||
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
|
||||
} catch (error) {
|
||||
logger.error('Error loading shortcuts config:', error);
|
||||
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
|
||||
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
|
||||
this.saveShortcutsConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,11 +175,17 @@ describe('ShortcutManager', () => {
|
||||
expect(result.errorType).toBe('INVALID_ID');
|
||||
});
|
||||
|
||||
it('should reject empty accelerator', () => {
|
||||
it('should clear shortcut when accelerator is empty', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', '');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('INVALID_FORMAT');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.errorType).toBeUndefined();
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith(
|
||||
'shortcuts',
|
||||
expect.objectContaining({
|
||||
showApp: '',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject accelerator without modifier keys', () => {
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"header.sessionDesc": "助理档案与会话偏好",
|
||||
"header.sessionWithName": "会话设置 · {{name}}",
|
||||
"header.title": "设置",
|
||||
"hotkey.clearBinding": "取消绑定",
|
||||
"hotkey.conflicts": "与现有快捷键冲突",
|
||||
"hotkey.errors.CONFLICT": "快捷键冲突:该快捷键已被其他功能占用",
|
||||
"hotkey.errors.INVALID_FORMAT": "快捷键格式无效:请使用正确的格式(如 CommandOrControl+E)",
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
"@lobehub/icons": "^5.0.0",
|
||||
"@lobehub/market-sdk": "^0.31.3",
|
||||
"@lobehub/tts": "^5.1.2",
|
||||
"@lobehub/ui": "^5.0.0",
|
||||
"@lobehub/ui": "^5.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@napi-rs/canvas": "^0.1.88",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
|
||||
@@ -218,6 +218,7 @@ export default {
|
||||
'header.sessionDesc': 'Agent Profile and session preferences',
|
||||
'header.sessionWithName': 'Session Settings · {{name}}',
|
||||
'header.title': 'Settings',
|
||||
'hotkey.clearBinding': 'Clear binding',
|
||||
'hotkey.conflicts': 'Conflicts with existing hotkeys',
|
||||
'hotkey.errors.CONFLICT': 'Hotkey conflict: This hotkey is already assigned to another function',
|
||||
'hotkey.errors.INVALID_FORMAT':
|
||||
|
||||
@@ -25,6 +25,19 @@ const HotkeySetting = memo(() => {
|
||||
|
||||
if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
|
||||
|
||||
const clearHotkeyBinding = async (id: HotkeyItem['id']) => {
|
||||
if (!hotkey[id]) return;
|
||||
|
||||
setLoading(true);
|
||||
form.setFieldValue(id, '');
|
||||
|
||||
try {
|
||||
await setSettings({ hotkey: { [id]: '' } });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const mapHotkeyItem = (item: HotkeyItem) => {
|
||||
const hotkeyConflicts = Object.entries(hotkey)
|
||||
.map(([key, value]) => {
|
||||
@@ -36,10 +49,13 @@ const HotkeySetting = memo(() => {
|
||||
return {
|
||||
children: (
|
||||
<HotkeyInput
|
||||
allowClear={!item.nonEditable}
|
||||
disabled={item.nonEditable}
|
||||
hotkeyConflicts={hotkeyConflicts}
|
||||
placeholder={t('hotkey.record')}
|
||||
resetValue={item.keys}
|
||||
texts={{ clear: t('hotkey.clearBinding') }}
|
||||
onClear={() => void clearHotkeyBinding(item.id)}
|
||||
/>
|
||||
),
|
||||
desc: hotkeyMeta[`${item.id}.desc`] ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
|
||||
@@ -66,8 +82,11 @@ const HotkeySetting = memo(() => {
|
||||
variant={'filled'}
|
||||
onValuesChange={async (values) => {
|
||||
setLoading(true);
|
||||
await setSettings({ hotkey: values });
|
||||
setLoading(false);
|
||||
try {
|
||||
await setSettings({ hotkey: values });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
{...FORM_STYLE}
|
||||
/>
|
||||
|
||||
@@ -34,30 +34,34 @@ const HotkeySetting = memo(() => {
|
||||
|
||||
if (!isHotkeysInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
|
||||
|
||||
const updateHotkey = async (id: DesktopHotkeyItem['id'], value: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await updateDesktopHotkey(id, value);
|
||||
if (result.success) {
|
||||
message.success(t('hotkey.updateSuccess', { ns: 'setting' }));
|
||||
} else {
|
||||
// Show the appropriate error message based on error type
|
||||
message.error(t(`hotkey.errors.${result.errorType}` as any, { ns: 'setting' }));
|
||||
}
|
||||
} catch {
|
||||
message.error(t('hotkey.updateError', { ns: 'setting' }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const mapHotkeyItem = (item: DesktopHotkeyItem) => ({
|
||||
children: (
|
||||
<HotkeyInput
|
||||
allowClear={!item.nonEditable}
|
||||
disabled={item.nonEditable}
|
||||
placeholder={t('hotkey.record')}
|
||||
resetValue={item.keys}
|
||||
texts={{ clear: t('hotkey.clearBinding') }}
|
||||
value={hotkeys[item.id]}
|
||||
onChange={async (value) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await updateDesktopHotkey(item.id, value);
|
||||
if (result.success) {
|
||||
message.success(t('hotkey.updateSuccess', { ns: 'setting' }));
|
||||
} else {
|
||||
// Show the appropriate error message based on error type
|
||||
|
||||
message.error(t(`hotkey.errors.${result.errorType}` as any, { ns: 'setting' }));
|
||||
}
|
||||
} catch {
|
||||
message.error(t('hotkey.updateError', { ns: 'setting' }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
onChange={(value) => void updateHotkey(item.id, value)}
|
||||
onClear={() => void updateHotkey(item.id, '')}
|
||||
/>
|
||||
),
|
||||
|
||||
|
||||
@@ -25,6 +25,19 @@ const HotkeySetting = memo(() => {
|
||||
|
||||
if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
|
||||
|
||||
const clearHotkeyBinding = async (id: HotkeyItem['id']) => {
|
||||
if (!hotkey[id]) return;
|
||||
|
||||
setLoading(true);
|
||||
form.setFieldValue(id, '');
|
||||
|
||||
try {
|
||||
await setSettings({ hotkey: { [id]: '' } });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const mapHotkeyItem = (item: HotkeyItem) => {
|
||||
const hotkeyConflicts = Object.entries(hotkey)
|
||||
.map(([key, value]) => {
|
||||
@@ -36,10 +49,13 @@ const HotkeySetting = memo(() => {
|
||||
return {
|
||||
children: (
|
||||
<HotkeyInput
|
||||
allowClear={!item.nonEditable}
|
||||
disabled={item.nonEditable}
|
||||
hotkeyConflicts={hotkeyConflicts}
|
||||
placeholder={t('hotkey.record')}
|
||||
resetValue={item.keys}
|
||||
texts={{ clear: t('hotkey.clearBinding') }}
|
||||
onClear={() => void clearHotkeyBinding(item.id)}
|
||||
/>
|
||||
),
|
||||
desc: hotkeyMeta[`${item.id}.desc`] ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
|
||||
@@ -66,8 +82,11 @@ const HotkeySetting = memo(() => {
|
||||
variant={'filled'}
|
||||
onValuesChange={async (values) => {
|
||||
setLoading(true);
|
||||
await setSettings({ hotkey: values });
|
||||
setLoading(false);
|
||||
try {
|
||||
await setSettings({ hotkey: values });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
{...FORM_STYLE}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user