diff --git a/aec b/aec index 9e11f4f..f00bb1f 160000 --- a/aec +++ b/aec @@ -1 +1 @@ -Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163 +Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f diff --git a/src/features/common/config/schema.js b/src/features/common/config/schema.js index b1cfcd4..ad5c3b6 100644 --- a/src/features/common/config/schema.js +++ b/src/features/common/config/schema.js @@ -110,6 +110,13 @@ const LATEST_SCHEMA = { { name: 'selected_stt_model', type: 'TEXT' }, { name: 'updated_at', type: 'INTEGER' } ] + }, + shortcuts: { + columns: [ + { name: 'action', type: 'TEXT PRIMARY KEY' }, + { name: 'accelerator', type: 'TEXT NOT NULL' }, + { name: 'created_at', type: 'INTEGER' } + ] } }; diff --git a/src/features/shortcuts/repositories/index.js b/src/features/shortcuts/repositories/index.js new file mode 100644 index 0000000..7685628 --- /dev/null +++ b/src/features/shortcuts/repositories/index.js @@ -0,0 +1 @@ +module.exports = require('./sqlite.repository'); \ No newline at end of file diff --git a/src/features/shortcuts/repositories/sqlite.repository.js b/src/features/shortcuts/repositories/sqlite.repository.js new file mode 100644 index 0000000..ce770ec --- /dev/null +++ b/src/features/shortcuts/repositories/sqlite.repository.js @@ -0,0 +1,48 @@ +const sqliteClient = require('../../common/services/sqliteClient'); +const crypto = require('crypto'); + +function getAllKeybinds() { + const db = sqliteClient.getDb(); + const query = 'SELECT * FROM shortcuts'; + try { + return db.prepare(query).all(); + } catch (error) { + console.error(`[DB] Failed to get keybinds:`, error); + return []; + } +} + +function upsertKeybinds(keybinds) { + if (!keybinds || keybinds.length === 0) return; + + const db = sqliteClient.getDb(); + const upsert = db.transaction((items) => { + const query = ` + INSERT INTO shortcuts (action, accelerator, created_at) + VALUES (@action, @accelerator, @created_at) + ON CONFLICT(action) DO UPDATE SET + accelerator = excluded.accelerator; + `; + const insert = db.prepare(query); + + for (const item of items) { + insert.run({ + action: item.action, + accelerator: item.accelerator, + created_at: Math.floor(Date.now() / 1000) + }); + } + }); + + try { + upsert(keybinds); + } catch (error) { + console.error('[DB] Failed to upsert keybinds:', error); + throw error; + } +} + +module.exports = { + getAllKeybinds, + upsertKeybinds +}; \ No newline at end of file diff --git a/src/features/shortcuts/shortcutsService.js b/src/features/shortcuts/shortcutsService.js new file mode 100644 index 0000000..83bbcd5 --- /dev/null +++ b/src/features/shortcuts/shortcutsService.js @@ -0,0 +1,242 @@ +const { globalShortcut, screen } = require('electron'); +const shortcutsRepository = require('./repositories'); + + +class ShortcutsService { + constructor() { + this.lastVisibleWindows = new Set(['header']); + this.mouseEventsIgnored = false; + } + + getDefaultKeybinds() { + const isMac = process.platform === 'darwin'; + return { + moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up', + moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down', + moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left', + moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right', + toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\', + toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M', + nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter', + manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S', + previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[', + nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]', + scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', + scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', + }; + } + + async loadKeybinds() { + let keybindsArray = await shortcutsRepository.getAllKeybinds(); + + if (!keybindsArray || keybindsArray.length === 0) { + console.log(`[Shortcuts] No keybinds found. Loading defaults.`); + const defaults = this.getDefaultKeybinds(); + await this.saveKeybinds(defaults); + return defaults; + } + + const keybinds = {}; + keybindsArray.forEach(k => { + keybinds[k.action] = k.accelerator; + }); + + const defaults = this.getDefaultKeybinds(); + let needsUpdate = false; + for (const action in defaults) { + if (!keybinds[action]) { + keybinds[action] = defaults[action]; + needsUpdate = true; + } + } + + if (needsUpdate) { + console.log('[Shortcuts] Updating missing keybinds with defaults.'); + await this.saveKeybinds(keybinds); + } + + return keybinds; + } + + async saveKeybinds(newKeybinds) { + const keybindsToSave = []; + for (const action in newKeybinds) { + if (Object.prototype.hasOwnProperty.call(newKeybinds, action)) { + keybindsToSave.push({ + action: action, + accelerator: newKeybinds[action], + }); + } + } + await shortcutsRepository.upsertKeybinds(keybindsToSave); + console.log(`[Shortcuts] Saved keybinds.`); + } + + toggleAllWindowsVisibility(windowPool) { + const header = windowPool.get('header'); + if (!header) return; + + if (header.isVisible()) { + this.lastVisibleWindows.clear(); + + windowPool.forEach((win, name) => { + if (win && !win.isDestroyed() && win.isVisible()) { + this.lastVisibleWindows.add(name); + } + }); + + this.lastVisibleWindows.forEach(name => { + if (name === 'header') return; + const win = windowPool.get(name); + if (win && !win.isDestroyed()) win.hide(); + }); + header.hide(); + + return; + } + + this.lastVisibleWindows.forEach(name => { + const win = windowPool.get(name); + if (win && !win.isDestroyed()) { + win.show(); + } + }); + } + + async registerShortcuts(movementManager, windowPool) { + const keybinds = await this.loadKeybinds(); + globalShortcut.unregisterAll(); + + const header = windowPool.get('header'); + const mainWindow = header; + + const sendToRenderer = (channel, ...args) => { + windowPool.forEach(win => { + if (win && !win.isDestroyed()) { + try { + win.webContents.send(channel, ...args); + } catch (e) { + // Ignore errors for destroyed windows + } + } + }); + }; + + sendToRenderer('shortcuts-updated', keybinds); + + // --- Hardcoded shortcuts --- + const isMac = process.platform === 'darwin'; + const modifier = isMac ? 'Cmd' : 'Ctrl'; + + // Monitor switching + const displays = screen.getAllDisplays(); + if (displays.length > 1) { + displays.forEach((display, index) => { + const key = `${modifier}+Shift+${index + 1}`; + globalShortcut.register(key, () => movementManager.moveToDisplay(display.id)); + }); + } + + // Edge snapping + const edgeDirections = [ + { key: `${modifier}+Shift+Left`, direction: 'left' }, + { key: `${modifier}+Shift+Right`, direction: 'right' }, + ]; + edgeDirections.forEach(({ key, direction }) => { + globalShortcut.register(key, () => { + if (header && header.isVisible()) movementManager.moveToEdge(direction); + }); + }); + + // --- User-configurable shortcuts --- + if (header?.currentHeaderState === 'apikey') { + if (keybinds.toggleVisibility) { + globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility(windowPool)); + } + console.log('[Shortcuts] ApiKeyHeader is active, only toggleVisibility shortcut is registered.'); + return; + } + + for (const action in keybinds) { + const accelerator = keybinds[action]; + if (!accelerator) continue; + + let callback; + switch(action) { + case 'toggleVisibility': + callback = () => this.toggleAllWindowsVisibility(windowPool); + break; + case 'nextStep': + // Late require to prevent circular dependency + callback = () => require('../../window/windowManager').toggleFeature('ask', {ask: { targetVisibility: 'show' }}); + break; + case 'scrollUp': + callback = () => { + const askWindow = windowPool.get('ask'); + if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { + askWindow.webContents.send('scroll-response-up'); + } + }; + break; + case 'scrollDown': + callback = () => { + const askWindow = windowPool.get('ask'); + if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { + askWindow.webContents.send('scroll-response-down'); + } + }; + break; + case 'moveUp': + callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); }; + break; + case 'moveDown': + callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); }; + break; + case 'moveLeft': + callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); }; + break; + case 'moveRight': + callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); }; + break; + case 'toggleClickThrough': + callback = () => { + this.mouseEventsIgnored = !this.mouseEventsIgnored; + if(mainWindow && !mainWindow.isDestroyed()){ + mainWindow.setIgnoreMouseEvents(this.mouseEventsIgnored, { forward: true }); + mainWindow.webContents.send('click-through-toggled', this.mouseEventsIgnored); + } + }; + break; + case 'manualScreenshot': + callback = () => { + if(mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();'); + } + }; + break; + case 'previousResponse': + callback = () => sendToRenderer('navigate-previous-response'); + break; + case 'nextResponse': + callback = () => sendToRenderer('navigate-next-response'); + break; + } + + if (callback) { + try { + globalShortcut.register(accelerator, callback); + } catch(e) { + console.error(`[Shortcuts] Failed to register shortcut for "${action}" (${accelerator}):`, e.message); + } + } + } + console.log('[Shortcuts] All shortcuts have been registered.'); + } + + unregisterAll() { + globalShortcut.unregisterAll(); + console.log('[Shortcuts] All shortcuts have been unregistered.'); + } +} + +module.exports = new ShortcutsService(); \ No newline at end of file diff --git a/src/window/windowManager.js b/src/window/windowManager.js index 5be986e..81cc2bf 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -7,6 +7,7 @@ const os = require('os'); const util = require('util'); const execFile = util.promisify(require('child_process').execFile); const listenService = require('../features/listen/listenService'); +const shortcutsService = require('../features/shortcuts/shortcutsService'); // Try to load sharp, but don't fail if it's not available let sharp; @@ -20,13 +21,6 @@ try { } const authService = require('../features/common/services/authService'); const systemSettingsRepository = require('../features/common/repositories/systemSettings'); -const Store = require('electron-store'); -const shortCutStore = new Store({ - name: 'user-preferences', - defaults: { - customKeybinds: {} - } -}); /* ────────────────[ GLASS BYPASS ]─────────────── */ let liquidGlass; @@ -263,13 +257,11 @@ function createFeatureWindows(header, namesToCreate) { restoreClicks(); windowPool.delete('shortcut-settings'); console.log('[Shortcuts] Re-enabled after editing.'); - loadAndRegisterShortcuts(movementManager); + shortcutsService.registerShortcuts(movementManager, windowPool); }); shortcutEditor.webContents.once('dom-ready', async () => { - const savedKeybinds = shortCutStore.get('customKeybinds', {}); - const defaultKeybinds = getDefaultKeybinds(); - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; + const keybinds = await shortcutsService.loadKeybinds(); shortcutEditor.webContents.send('load-shortcuts', keybinds); }); @@ -418,7 +410,7 @@ function createWindows() { layoutManager = new WindowLayoutManager(windowPool); header.webContents.once('dom-ready', () => { - loadAndRegisterShortcuts(movementManager); + shortcutsService.registerShortcuts(movementManager, windowPool); }); setupIpcHandlers(movementManager); @@ -475,32 +467,6 @@ function createWindows() { return windowPool; } -function loadAndRegisterShortcuts(movementManager) { - if (windowPool.has('shortcut-settings')) { - console.log('[Shortcuts] Editing in progress, skipping registration.'); - return; - } - - const defaultKeybinds = getDefaultKeybinds(); - const savedKeybinds = shortCutStore.get('customKeybinds', {}); - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - - const sendToRenderer = (channel, ...args) => { - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - try { - win.webContents.send(channel, ...args); - } catch (e) { - // 창이 이미 닫혔을 수 있으므로 오류를 무시합니다. - } - } - }); - }; - - updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager); -} - - function setupIpcHandlers(movementManager) { setupApiKeyIPC(); @@ -535,50 +501,37 @@ function setupIpcHandlers(movementManager) { } else { // 'apikey' | 'permission' destroyFeatureWindows(); } - loadAndRegisterShortcuts(movementManager); + shortcutsService.registerShortcuts(movementManager, windowPool); }); - ipcMain.on('update-keybinds', (event, newKeybinds) => { - updateGlobalShortcuts(newKeybinds); + ipcMain.handle('get-current-shortcuts', async () => { + return await shortcutsService.loadKeybinds(); }); - ipcMain.handle('get-current-shortcuts', () => { - const defaultKeybinds = getDefaultKeybinds(); - const savedKeybinds = shortCutStore.get('customKeybinds', {}); - return { ...defaultKeybinds, ...savedKeybinds }; - }); - - // open-shortcut-editor handler moved to windowBridge.js to avoid duplication - - ipcMain.handle('get-default-shortcuts', () => { - shortCutStore.set('customKeybinds', {}); - return getDefaultKeybinds(); + ipcMain.handle('get-default-shortcuts', async () => { + const defaults = shortcutsService.getDefaultKeybinds(); + await shortcutsService.saveKeybinds(defaults); + // Reregister shortcuts with new defaults + await shortcutsService.registerShortcuts(movementManager, windowPool); + return defaults; }); ipcMain.handle('save-shortcuts', async (event, newKeybinds) => { try { - const defaultKeybinds = getDefaultKeybinds(); - const customKeybinds = {}; - for (const key in newKeybinds) { - if (newKeybinds[key] && newKeybinds[key] !== defaultKeybinds[key]) { - customKeybinds[key] = newKeybinds[key]; - } - } + await shortcutsService.saveKeybinds(newKeybinds); - shortCutStore.set('customKeybinds', customKeybinds); - console.log('[Shortcuts] Custom keybinds saved to store:', customKeybinds); - const editor = windowPool.get('shortcut-settings'); if (editor && !editor.isDestroyed()) { - editor.close(); + editor.close(); // This will trigger re-registration on 'closed' event } else { - loadAndRegisterShortcuts(movementManager); + // If editor wasn't open, re-register immediately + await shortcutsService.registerShortcuts(movementManager, windowPool); } - return { success: true }; } catch (error) { console.error("Failed to save shortcuts:", error); - loadAndRegisterShortcuts(movementManager); + // On failure, re-register old shortcuts to be safe + await shortcutsService.registerShortcuts(movementManager, windowPool); return { success: false, error: error.message }; } }); @@ -1093,163 +1046,6 @@ function setupApiKeyIPC() { //////// after_modelStateService //////// -function getDefaultKeybinds() { - const isMac = process.platform === 'darwin'; - return { - moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up', - moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down', - moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left', - moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right', - toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\', - toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M', - nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter', - manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S', - previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[', - nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]', - scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', - scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', - }; -} - -function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { - globalShortcut.unregisterAll(); - - if (sendToRenderer) { - sendToRenderer('shortcuts-updated', keybinds); - console.log('[Shortcuts] Broadcasted updated shortcuts to all windows.'); - } - - // 하드코딩된 단축키 등록을 위해 변수 유지 - const isMac = process.platform === 'darwin'; - const modifier = isMac ? 'Cmd' : 'Ctrl'; - const header = windowPool.get('header'); - const state = header?.currentHeaderState || currentHeaderState; - - // 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지) - const displays = screen.getAllDisplays(); - if (displays.length > 1) { - displays.forEach((display, index) => { - const key = `${modifier}+Shift+${index + 1}`; - try { - globalShortcut.register(key, () => movementManager.moveToDisplay(display.id)); - console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`); - } catch (error) { - console.error(`Failed to register display switch ${key}:`, error); - } - }); - } - - // API 키 입력 상태에서는 필수 단축키(toggleVisibility) 외에는 아무것도 등록하지 않음 - if (state === 'apikey') { - if (keybinds.toggleVisibility) { - try { - globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility()); - } catch (error) { - console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); - } - } - console.log('ApiKeyHeader is active, skipping conditional shortcuts'); - return; - } - - // 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지) - const edgeDirections = [ - { key: `${modifier}+Shift+Left`, direction: 'left' }, - { key: `${modifier}+Shift+Right`, direction: 'right' }, - // { key: `${modifier}+Shift+Up`, direction: 'up' }, - // { key: `${modifier}+Shift+Down`, direction: 'down' }, - ]; - edgeDirections.forEach(({ key, direction }) => { - try { - globalShortcut.register(key, () => { - if (header && header.isVisible()) movementManager.moveToEdge(direction); - }); - } catch (error) { - console.error(`Failed to register edge move for ${key}:`, error); - } - }); - - - // 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용) - for (const action in keybinds) { - const accelerator = keybinds[action]; - if (!accelerator) continue; - - try { - let callback; - switch(action) { - case 'toggleVisibility': - callback = () => toggleAllWindowsVisibility(); - break; - case 'nextStep': - callback = () => toggleFeature('ask', {ask: { targetVisibility: 'show' }}); - break; - case 'scrollUp': - callback = () => { - // 'ask' 창을 명시적으로 가져옵니다. - const askWindow = windowPool.get('ask'); - // 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다. - if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { - askWindow.webContents.send('scroll-response-up'); - } - }; - break; - case 'scrollDown': - callback = () => { - // 'ask' 창을 명시적으로 가져옵니다. - const askWindow = windowPool.get('ask'); - // 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다. - if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { - askWindow.webContents.send('scroll-response-down'); - } - }; - break; - case 'moveUp': - callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); }; - break; - case 'moveDown': - callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); }; - break; - case 'moveLeft': - callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); }; - break; - case 'moveRight': - callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); }; - break; - case 'toggleClickThrough': - callback = () => { - mouseEventsIgnored = !mouseEventsIgnored; - if(mainWindow && !mainWindow.isDestroyed()){ - mainWindow.setIgnoreMouseEvents(mouseEventsIgnored, { forward: true }); - mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); - } - }; - break; - case 'manualScreenshot': - callback = () => { - if(mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();'); - } - }; - break; - case 'previousResponse': - callback = () => sendToRenderer('navigate-previous-response'); - break; - case 'nextResponse': - callback = () => sendToRenderer('navigate-next-response'); - break; - } - - if (callback) { - globalShortcut.register(accelerator, callback); - } - } catch(e) { - console.error(`Failed to register shortcut for "${action}" (${accelerator}):`, e.message); - } - } -} - - async function captureScreenshot(options = {}) { if (process.platform === 'darwin') { try { @@ -1344,4 +1140,5 @@ module.exports = { getStoredProvider, getCurrentModelInfo, captureScreenshot, + toggleFeature, // Export toggleFeature so shortcutsService can use it }; \ No newline at end of file