From 843694daac53e7f11d7c67df0dd947a45f4a4d6f Mon Sep 17 00:00:00 2001 From: sanio Date: Tue, 8 Jul 2025 21:35:20 +0900 Subject: [PATCH] add shortcut editing(beta) --- README.md | 5 +- src/app/MainHeader.js | 76 +- src/app/PickleGlassApp.js | 3 + src/electron/windowManager.js | 806 +++++++++--------- src/features/ask/AskView.js | 20 + src/features/settings/SettingsView.js | 134 ++- src/features/settings/ShortCutSettingsView.js | 235 +++++ 7 files changed, 759 insertions(+), 520 deletions(-) create mode 100644 src/features/settings/ShortCutSettingsView.js diff --git a/README.md b/README.md index 8397aec..e2842ff 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,6 @@ We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is% | Status | Issue | Description | |--------|--------------------------------|---------------------------------------------------| -| 🚧 WIP | Code Refactoring | Refactoring the entire codebase for better maintainability. | | 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows | | 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers | | 🚧 WIP | AEC Improvement | Transcription is not working occasionally | @@ -128,6 +127,10 @@ We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is% ### Changelog - Jul 5: Now support Gemini, Intel Mac supported +- Jul 6: Full code refactoring has done. +- Jul 7: Now support Claude, LLM/STT model selection +- Jul 8: Now support Windows(beta), Improved AEC by Rust(to seperate mic/system audio), shortcut editing(beta) + ## About Pickle diff --git a/src/app/MainHeader.js b/src/app/MainHeader.js index 5212fae..7c92f7c 100644 --- a/src/app/MainHeader.js +++ b/src/app/MainHeader.js @@ -3,11 +3,12 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; export class MainHeader extends LitElement { static properties = { isSessionActive: { type: Boolean, state: true }, + shortcuts: { type: Object, state: true }, }; static styles = css` :host { - display: block; + display: flex; transform: translate3d(0, 0, 0); backface-visibility: hidden; transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out; @@ -99,7 +100,7 @@ export class MainHeader extends LitElement { } .header { - width: 100%; + width: max-content; height: 47px; padding: 2px 10px 2px 13px; background: transparent; @@ -212,16 +213,6 @@ export class MainHeader extends LitElement { } .action-button, - .settings-button { - background: transparent; - color: white; - border: none; - cursor: pointer; - display: flex; - align-items: center; - gap: 6px; - } - .action-text { padding-bottom: 1px; justify-content: center; @@ -275,9 +266,16 @@ export class MainHeader extends LitElement { .settings-button { padding: 5px; border-radius: 50%; + background: transparent; transition: background 0.15s ease; + color: white; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; } - + .settings-button:hover { background: rgba(255, 255, 255, 0.1); } @@ -286,6 +284,7 @@ export class MainHeader extends LitElement { display: flex; align-items: center; justify-content: center; + padding: 3px; } .settings-icon svg { @@ -346,6 +345,7 @@ export class MainHeader extends LitElement { constructor() { super(); + this.shortcuts = {}; this.dragState = null; this.wasJustDragged = false; this.isVisible = true; @@ -501,6 +501,11 @@ export class MainHeader extends LitElement { this.isSessionActive = isActive; }; ipcRenderer.on('session-state-changed', this._sessionStateListener); + this._shortcutListener = (event, keybinds) => { + console.log('[MainHeader] Received updated shortcuts:', keybinds); + this.shortcuts = keybinds; + }; + ipcRenderer.on('shortcuts-updated', this._shortcutListener); } } @@ -518,6 +523,9 @@ export class MainHeader extends LitElement { if (this._sessionStateListener) { ipcRenderer.removeListener('session-state-changed', this._sessionStateListener); } + if (this._shortcutListener) { + ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener); + } } } @@ -567,6 +575,29 @@ export class MainHeader extends LitElement { } + renderShortcut(accelerator) { + if (!accelerator) return html``; + + const keyMap = { + 'Cmd': '⌘', 'Command': '⌘', + 'Ctrl': 'βŒƒ', 'Control': 'βŒƒ', + 'Alt': 'βŒ₯', 'Option': 'βŒ₯', + 'Shift': '⇧', + 'Enter': '↡', + 'Backspace': '⌫', + 'Delete': '⌦', + 'Tab': 'β‡₯', + 'Escape': 'βŽ‹', + 'Up': '↑', 'Down': '↓', 'Left': '←', 'Right': 'β†’', + '\\': html``, + }; + + const keys = accelerator.split('+'); + return html`${keys.map(key => html` +
${keyMap[key] || key}
+ `)}`; + } + render() { return html`
@@ -599,14 +630,8 @@ export class MainHeader extends LitElement {
Ask
-
-
⌘
-
- - - - -
+
+ ${this.renderShortcut(this.shortcuts.nextStep)}
@@ -614,13 +639,8 @@ export class MainHeader extends LitElement {
Show/Hide
-
-
⌘
-
- - - -
+
+ ${this.renderShortcut(this.shortcuts.toggleVisibility)}
diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index 6cf1666..e2e8495 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -2,6 +2,7 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; import { SettingsView } from '../features/settings/SettingsView.js'; import { AssistantView } from '../features/listen/AssistantView.js'; import { AskView } from '../features/ask/AskView.js'; +import { ShortcutSettingsView } from '../features/settings/ShortCutSettingsView.js'; import '../features/listen/renderer/renderer.js'; @@ -268,6 +269,8 @@ export class PickleGlassApp extends LitElement { .onProfileChange=${profile => (this.selectedProfile = profile)} .onLanguageChange=${lang => (this.selectedLanguage = lang)} >`; + case 'shortcut-settings': + return html``; case 'history': return html``; case 'help': diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 32441b7..ff85e16 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -11,7 +11,13 @@ const authService = require('../common/services/authService'); const systemSettingsRepository = require('../common/repositories/systemSettings'); const userRepository = require('../common/repositories/user'); const fetch = require('node-fetch'); - +const Store = require('electron-store'); +const shortCutStore = new Store({ + name: 'user-preferences', + defaults: { + customKeybinds: {} + } +}); /* ────────────────[ GLASS BYPASS ]─────────────── */ let liquidGlass; @@ -51,6 +57,7 @@ let settingsHideTimer = null; let selectedCaptureSourceId = null; +// let shortcutEditorWindow = null; let layoutManager = null; function updateLayout() { if (layoutManager) { @@ -60,16 +67,16 @@ function updateLayout() { let movementManager = null; -let storedProvider = 'openai'; const featureWindows = ['listen','ask','settings']; +// const featureWindows = ['listen','ask','settings','shortcut-settings']; function isAllowed(name) { if (name === 'header') return true; return featureWindows.includes(name) && currentHeaderState === 'main'; } -function createFeatureWindows(header) { - if (windowPool.has('listen')) return; +function createFeatureWindows(header, namesToCreate) { + // if (windowPool.has('listen')) return; const commonChildOptions = { parent: header, @@ -84,106 +91,207 @@ function createFeatureWindows(header) { webPreferences: { nodeIntegration: true, contextIsolation: false }, }; - // listen - const listen = new BrowserWindow({ - ...commonChildOptions, width:400,minWidth:400,maxWidth:400, - maxHeight:700, - }); - listen.setContentProtection(isContentProtectionOn); - listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - if (process.platform === 'darwin') { - listen.setWindowButtonVisibility(false); - } - const listenLoadOptions = { query: { view: 'listen' } }; - if (!shouldUseLiquidGlass) { - listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); - } - else { - listenLoadOptions.query.glass = 'true'; - listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); - listen.webContents.once('did-finish-load', () => { - const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); + const createFeatureWindow = (name) => { + if (windowPool.has(name)) return; + + switch (name) { + case 'listen': { + const listen = new BrowserWindow({ + ...commonChildOptions, width:400,minWidth:400,maxWidth:400, + maxHeight:700, + }); + listen.setContentProtection(isContentProtectionOn); + listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); + if (process.platform === 'darwin') { + listen.setWindowButtonVisibility(false); + } + const listenLoadOptions = { query: { view: 'listen' } }; + if (!shouldUseLiquidGlass) { + listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); + } + else { + listenLoadOptions.query.glass = 'true'; + listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); + listen.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } + windowPool.set('listen', listen); + break; } - }); - } + // ask + case 'ask': { + const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); + ask.setContentProtection(isContentProtectionOn); + ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); + if (process.platform === 'darwin') { + ask.setWindowButtonVisibility(false); + } + const askLoadOptions = { query: { view: 'ask' } }; + if (!shouldUseLiquidGlass) { + ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); + } + else { + askLoadOptions.query.glass = 'true'; + ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); + ask.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(ask.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } - windowPool.set('listen', listen); - - // ask - const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); - ask.setContentProtection(isContentProtectionOn); - ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - if (process.platform === 'darwin') { - ask.setWindowButtonVisibility(false); - } - const askLoadOptions = { query: { view: 'ask' } }; - if (!shouldUseLiquidGlass) { - ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); - } - else { - askLoadOptions.query.glass = 'true'; - ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); - ask.webContents.once('did-finish-load', () => { - const viewId = liquidGlass.addView(ask.getNativeWindowHandle(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); + ask.on('blur',()=>ask.webContents.send('window-blur')); + + // Open DevTools in development + if (!app.isPackaged) { + ask.webContents.openDevTools({ mode: 'detach' }); + } + windowPool.set('ask', ask); + break; } - }); - } - ask.on('blur',()=>ask.webContents.send('window-blur')); - - // Open DevTools in development - if (!app.isPackaged) { - ask.webContents.openDevTools({ mode: 'detach' }); - } - windowPool.set('ask', ask); - - // settings - const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined }); - settings.setContentProtection(isContentProtectionOn); - settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - if (process.platform === 'darwin') { - settings.setWindowButtonVisibility(false); - } - const settingsLoadOptions = { query: { view: 'settings' } }; - if (!shouldUseLiquidGlass) { - settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) - .catch(console.error); - } - else { - settingsLoadOptions.query.glass = 'true'; - settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) - .catch(console.error); - settings.webContents.once('did-finish-load', () => { - const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); + // settings + case 'settings': { + const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined }); + settings.setContentProtection(isContentProtectionOn); + settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); + if (process.platform === 'darwin') { + settings.setWindowButtonVisibility(false); + } + const settingsLoadOptions = { query: { view: 'settings' } }; + if (!shouldUseLiquidGlass) { + settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) + .catch(console.error); + } + else { + settingsLoadOptions.query.glass = 'true'; + settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) + .catch(console.error); + settings.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } + windowPool.set('settings', settings); + break; } - }); + + case 'shortcut-settings': { + const shortcutEditor = new BrowserWindow({ + ...commonChildOptions, + width: 420, + height: 720, + modal: false, + parent: undefined, + alwaysOnTop: true, + titleBarOverlay: false, + }); + + if (process.platform === 'darwin') { + shortcutEditor.setAlwaysOnTop(true, 'screen-saver'); + } else { + shortcutEditor.setAlwaysOnTop(true); + } + + /* ──────────[ β‘  λ‹€λ₯Έ μ°½ 클릭 차단 ]────────── */ + const disableClicks = () => { + for (const [name, win] of windowPool) { + if (win !== shortcutEditor && !win.isDestroyed()) { + win.setIgnoreMouseEvents(true, { forward: true }); + } + } + }; + const restoreClicks = () => { + for (const [, win] of windowPool) { + if (!win.isDestroyed()) win.setIgnoreMouseEvents(false); + } + }; + + const header = windowPool.get('header'); + if (header && !header.isDestroyed()) { + const { x, y, width } = header.getBounds(); + shortcutEditor.setBounds({ x, y, width }); + } + + shortcutEditor.once('ready-to-show', () => { + disableClicks(); + shortcutEditor.show(); + }); + + const loadOptions = { query: { view: 'shortcut-settings' } }; + if (!shouldUseLiquidGlass) { + shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions); + } else { + loadOptions.query.glass = 'true'; + shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions); + shortcutEditor.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(shortcutEditor.getNativeWindowHandle(), { + cornerRadius: 12, tintColor: '#FF00001A', opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + } + }); + } + + shortcutEditor.on('closed', () => { + restoreClicks(); + windowPool.delete('shortcut-settings'); + console.log('[Shortcuts] Re-enabled after editing.'); + loadAndRegisterShortcuts(movementManager); + }); + + shortcutEditor.webContents.once('dom-ready', async () => { + const savedKeybinds = shortCutStore.get('customKeybinds', {}); + const defaultKeybinds = getDefaultKeybinds(); + const keybinds = { ...defaultKeybinds, ...savedKeybinds }; + shortcutEditor.webContents.send('load-shortcuts', keybinds); + }); + + if (!app.isPackaged) { + shortcutEditor.webContents.openDevTools({ mode: 'detach' }); + } + windowPool.set('shortcut-settings', shortcutEditor); + break; + } + } + }; + + if (Array.isArray(namesToCreate)) { + namesToCreate.forEach(name => createFeatureWindow(name)); + } else if (typeof namesToCreate === 'string') { + createFeatureWindow(namesToCreate); + } else { + createFeatureWindow('listen'); + createFeatureWindow('ask'); + createFeatureWindow('settings'); } - windowPool.set('settings', settings); } function destroyFeatureWindows() { @@ -199,6 +307,7 @@ function destroyFeatureWindows() { } + function getCurrentDisplay(window) { if (!window || window.isDestroyed()) return screen.getPrimaryDisplay(); @@ -354,7 +463,7 @@ function createWindows() { setupIpcHandlers(movementManager); if (currentHeaderState === 'main') { - createFeatureWindows(header); + createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']); } header.setContentProtection(isContentProtectionOn); @@ -385,10 +494,6 @@ function createWindows() { header.on('resize', updateLayout); - // header.webContents.once('dom-ready', () => { - // loadAndRegisterShortcuts(); - // }); - ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager)); ipcMain.handle('toggle-feature', async (event, featureName) => { @@ -584,37 +689,32 @@ function createWindows() { } }); - // setupIpcHandlers(); - return windowPool; } function loadAndRegisterShortcuts(movementManager) { + if (windowPool.has('shortcut-settings')) { + console.log('[Shortcuts] Editing in progress, skipping registration.'); + return; + } + const defaultKeybinds = getDefaultKeybinds(); - const header = windowPool.get('header'); + const savedKeybinds = shortCutStore.get('customKeybinds', {}); + const keybinds = { ...defaultKeybinds, ...savedKeybinds }; + const sendToRenderer = (channel, ...args) => { windowPool.forEach(win => { - try { - if (win && !win.isDestroyed()) { + if (win && !win.isDestroyed()) { + try { win.webContents.send(channel, ...args); + } catch (e) { + // 창이 이미 λ‹«ν˜”μ„ 수 μžˆμœΌλ―€λ‘œ 였λ₯˜λ₯Ό λ¬΄μ‹œν•©λ‹ˆλ‹€. } - } catch (e) {} + } }); }; - - if (!header) { - return updateGlobalShortcuts(defaultKeybinds, undefined, sendToRenderer, movementManager); - } - - header.webContents - .executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`) - .then(saved => (saved ? JSON.parse(saved) : {})) - .then(savedKeybinds => { - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); - }) - .catch(() => updateGlobalShortcuts(defaultKeybinds, header, sendToRenderer, movementManager)); + updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager); } @@ -768,6 +868,7 @@ function setupIpcHandlers(movementManager) { } else { // 'apikey' | 'permission' destroyFeatureWindows(); } + loadAndRegisterShortcuts(movementManager); for (const [name, win] of windowPool) { if (!isAllowed(name) && !win.isDestroyed()) { @@ -777,36 +878,69 @@ function setupIpcHandlers(movementManager) { win.show(); } } - - const header = windowPool.get('header'); - if (header && !header.isDestroyed()) { - header.webContents - .executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`) - .then(saved => { - const defaultKeybinds = getDefaultKeybinds(); - const savedKeybinds = saved ? JSON.parse(saved) : {}; - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - - const sendToRenderer = (channel, ...args) => { - windowPool.forEach(win => { - try { - if (win && !win.isDestroyed()) { - win.webContents.send(channel, ...args); - } - } catch (e) {} - }); - }; - - updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); - }) - .catch(console.error); - } }); ipcMain.on('update-keybinds', (event, newKeybinds) => { updateGlobalShortcuts(newKeybinds); }); + ipcMain.handle('get-current-shortcuts', () => { + const defaultKeybinds = getDefaultKeybinds(); + const savedKeybinds = shortCutStore.get('customKeybinds', {}); + return { ...defaultKeybinds, ...savedKeybinds }; + }); + + ipcMain.handle('open-shortcut-editor', () => { + const header = windowPool.get('header'); + if (!header) return; + + // νŽΈμ§‘κΈ° μ—΄κΈ° μ „ λͺ¨λ“  단좕킀 λΉ„ν™œμ„±ν™” + globalShortcut.unregisterAll(); + console.log('[Shortcuts] Disabled for editing.'); + + createFeatureWindows(header, 'shortcut-settings'); + }); + + ipcMain.handle('get-default-shortcuts', () => { + shortCutStore.set('customKeybinds', {}); + return getDefaultKeybinds(); + }); + + 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]; + } + } + + 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(); + } else { + loadAndRegisterShortcuts(movementManager); + } + + return { success: true }; + } catch (error) { + console.error("Failed to save shortcuts:", error); + loadAndRegisterShortcuts(movementManager); + return { success: false, error: error.message }; + } + }); + + ipcMain.on('close-shortcut-editor', () => { + const editor = windowPool.get('shortcut-settings'); + if (editor && !editor.isDestroyed()) { + editor.close(); + } + }); + ipcMain.handle('open-login-page', () => { const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; const personalizeUrl = `${webUrl}/personalize?desktop=true`; @@ -971,15 +1105,6 @@ function setupIpcHandlers(movementManager) { console.log('[WindowManager] Received request to log out.'); await authService.signOut(); - //////// before_modelStateService //////// - // await setApiKey(null); - - // windowPool.forEach(win => { - // if (win && !win.isDestroyed()) { - // win.webContents.send('api-key-removed'); - // } - // }); - //////// before_modelStateService //////// }); ipcMain.handle('check-system-permissions', async () => { @@ -1114,101 +1239,6 @@ function setupIpcHandlers(movementManager) { } -//////// before_modelStateService //////// -// async function setApiKey(apiKey, provider = 'openai') { -// console.log('[WindowManager] Persisting API key and provider to DB'); - -// try { -// await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); -// console.log('[WindowManager] API key and provider saved to SQLite'); - -// // Notify authService that the key status may have changed -// await authService.updateApiKeyStatus(); - -// } catch (err) { -// console.error('[WindowManager] Failed to save API key to SQLite:', err); -// } - -// windowPool.forEach(win => { -// if (win && !win.isDestroyed()) { -// const js = apiKey ? ` -// localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); -// localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); -// ` : ` -// localStorage.removeItem('openai_api_key'); -// localStorage.removeItem('ai_provider'); -// `; -// win.webContents.executeJavaScript(js).catch(() => {}); -// } -// }); -// } - - -// async function getStoredApiKey() { -// const userId = authService.getCurrentUserId(); -// if (!userId) return null; -// const user = await userRepository.getById(userId); -// return user?.api_key || null; -// } - -// async function getStoredProvider() { -// const userId = authService.getCurrentUserId(); -// if (!userId) return 'openai'; -// const user = await userRepository.getById(userId); -// return user?.provider || 'openai'; -// } - -// function setupApiKeyIPC() { -// const { ipcMain } = require('electron'); - -// // Both handlers now do the same thing: fetch the key from the source of truth. -// ipcMain.handle('get-stored-api-key', getStoredApiKey); - -// ipcMain.handle('api-key-validated', async (event, data) => { -// console.log('[WindowManager] API key validation completed, saving...'); - -// // Support both old format (string) and new format (object) -// const apiKey = typeof data === 'string' ? data : data.apiKey; -// const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); - -// await setApiKey(apiKey, provider); - -// windowPool.forEach((win, name) => { -// if (win && !win.isDestroyed()) { -// win.webContents.send('api-key-validated', { apiKey, provider }); -// } -// }); - -// return { success: true }; -// }); - -// ipcMain.handle('remove-api-key', async () => { -// console.log('[WindowManager] API key removal requested'); -// await setApiKey(null); - -// windowPool.forEach((win, name) => { -// if (win && !win.isDestroyed()) { -// win.webContents.send('api-key-removed'); -// } -// }); - -// const settingsWindow = windowPool.get('settings'); -// if (settingsWindow && settingsWindow.isVisible()) { -// settingsWindow.hide(); -// console.log('[WindowManager] Settings window hidden after clearing API key.'); -// } - -// return { success: true }; -// }); - -// ipcMain.handle('get-ai-provider', getStoredProvider); - -// console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); -// } -//////// before_modelStateService //////// - - - //////// after_modelStateService //////// async function getStoredApiKey() { @@ -1227,15 +1257,15 @@ async function getStoredProvider() { } /** - * λ Œλ”λŸ¬μ—μ„œ μš”μ²­ν•œ νƒ€μž…('llm' λ˜λŠ” 'stt')에 λŒ€ν•œ λͺ¨λΈ 정보λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. - * @param {IpcMainInvokeEvent} event - μΌλ ‰νŠΈλ‘  IPC 이벀트 객체 - * @param {{type: 'llm' | 'stt'}} { type } - μš”μ²­ν•  λͺ¨λΈ νƒ€μž… + * + * @param {IpcMainInvokeEvent} event + * @param {{type: 'llm' | 'stt'}} */ async function getCurrentModelInfo(event, { type }) { if (global.modelStateService && (type === 'llm' || type === 'stt')) { return global.modelStateService.getCurrentModelInfo(type); } - return null; // μ„œλΉ„μŠ€κ°€ μ—†κ±°λ‚˜ μœ νš¨ν•˜μ§€ μ•Šμ€ νƒ€μž…μΌ 경우 null λ°˜ν™˜ + return null; } function setupApiKeyIPC() { @@ -1279,33 +1309,26 @@ function getDefaultKeybinds() { } function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { - // console.log('Updating global shortcuts with:', keybinds); - - // Unregister all existing shortcuts globalShortcut.unregisterAll(); - let toggleVisibilityDebounceTimer = null; - + 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; - if (keybinds.toggleVisibility) { - try { - globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager)); - console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`); - } catch (error) { - console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); - } - } - + // ✨ κΈ°λŠ₯ 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); - }); + 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); @@ -1313,171 +1336,122 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan }); } - if (currentHeaderState === 'apikey') { + // API ν‚€ μž…λ ₯ μƒνƒœμ—μ„œλŠ” ν•„μˆ˜ 단좕킀(toggleVisibility) μ™Έμ—λŠ” 아무것도 λ“±λ‘ν•˜μ§€ μ•ŠμŒ + if (state === 'apikey') { + if (keybinds.toggleVisibility) { + try { + globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager)); + } catch (error) { + console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); + } + } console.log('ApiKeyHeader is active, skipping conditional shortcuts'); return; } - const directions = [ - { key: `${modifier}+Left`, direction: 'left' }, - { key: `${modifier}+Right`, direction: 'right' }, - { key: `${modifier}+Up`, direction: 'up' }, - { key: `${modifier}+Down`, direction: 'down' }, - ]; - - directions.forEach(({ key, direction }) => { - try { - globalShortcut.register(key, () => { - const header = windowPool.get('header'); - if (header && header.isVisible()) { - movementManager.moveStep(direction); - } - }); - // console.log(`Registered global shortcut: ${key} -> ${direction}`); - } catch (error) { - console.error(`Failed to register ${key}:`, error); - } - }); - + // ✨ κΈ°λŠ₯ 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' }, + // { key: `${modifier}+Shift+Up`, direction: 'up' }, + // { key: `${modifier}+Shift+Down`, direction: 'down' }, ]; - edgeDirections.forEach(({ key, direction }) => { try { globalShortcut.register(key, () => { - const header = windowPool.get('header'); - if (header && header.isVisible()) { - movementManager.moveToEdge(direction); - } + if (header && header.isVisible()) movementManager.moveToEdge(direction); }); - console.log(`Registered global shortcut: ${key} -> edge ${direction}`); } catch (error) { - console.error(`Failed to register ${key}:`, error); + console.error(`Failed to register edge move for ${key}:`, error); } }); - if (keybinds.toggleClickThrough) { + + // ✨ κΈ°λŠ₯ 3: μ‚¬μš©μžκ°€ μ„€μ • κ°€λŠ₯ν•œ λͺ¨λ“  단좕킀λ₯Ό λ™μ μœΌλ‘œ 등둝 (μƒˆλ‘œμš΄ 방식 적용) + for (const action in keybinds) { + const accelerator = keybinds[action]; + if (!accelerator) continue; + try { - globalShortcut.register(keybinds.toggleClickThrough, () => { - mouseEventsIgnored = !mouseEventsIgnored; - if (mouseEventsIgnored) { - mainWindow.setIgnoreMouseEvents(true, { forward: true }); - console.log('Mouse events ignored'); - } else { - mainWindow.setIgnoreMouseEvents(false); - console.log('Mouse events enabled'); - } - mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); - }); - // console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); - } catch (error) { - console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error); - } - } - - if (keybinds.nextStep) { - try { - globalShortcut.register(keybinds.nextStep, () => { - console.log('⌘/Ctrl+Enter Ask shortcut triggered'); - - const askWindow = windowPool.get('ask'); - if (!askWindow || askWindow.isDestroyed()) { - console.error('Ask window not found or destroyed'); - return; - } - - if (askWindow.isVisible()) { - askWindow.webContents.send('ask-global-send'); - } else { - try { - askWindow.show(); - - const header = windowPool.get('header'); - if (header) { - const currentHeaderPosition = header.getBounds(); + let callback; + switch(action) { + case 'toggleVisibility': + callback = () => toggleAllWindowsVisibility(movementManager); + break; + case 'nextStep': + callback = () => { + const askWindow = windowPool.get('ask'); + if (!askWindow || askWindow.isDestroyed()) return; + if (askWindow.isVisible()) { + askWindow.webContents.send('ask-global-send'); + } else { + askWindow.show(); updateLayout(); - header.setPosition(currentHeaderPosition.x, currentHeaderPosition.y, false); + askWindow.webContents.send('window-show-animation'); } - - askWindow.webContents.send('window-show-animation'); - } catch (e) { - console.error('Error showing Ask window:', e); - } - } - }); - // console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); - } catch (error) { - console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error); - } - } - - if (keybinds.manualScreenshot) { - try { - globalShortcut.register(keybinds.manualScreenshot, () => { - console.log('Manual screenshot shortcut triggered'); - mainWindow.webContents.executeJavaScript(` - if (window.captureManualScreenshot) { - window.captureManualScreenshot(); - } else { - console.log('Manual screenshot function not available'); - } - `); - }); - // console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); - } catch (error) { - console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error); - } - } - - if (keybinds.previousResponse) { - try { - globalShortcut.register(keybinds.previousResponse, () => { - console.log('Previous response shortcut triggered'); - sendToRenderer('navigate-previous-response'); - }); - // console.log(`Registered previousResponse: ${keybinds.previousResponse}`); - } catch (error) { - console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error); - } - } - - if (keybinds.nextResponse) { - try { - globalShortcut.register(keybinds.nextResponse, () => { - console.log('Next response shortcut triggered'); - sendToRenderer('navigate-next-response'); - }); - // console.log(`Registered nextResponse: ${keybinds.nextResponse}`); - } catch (error) { - console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error); - } - } - - if (keybinds.scrollUp) { - try { - globalShortcut.register(keybinds.scrollUp, () => { - console.log('Scroll up shortcut triggered'); - sendToRenderer('scroll-response-up'); - }); - // console.log(`Registered scrollUp: ${keybinds.scrollUp}`); - } catch (error) { - console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error); - } - } - - if (keybinds.scrollDown) { - try { - globalShortcut.register(keybinds.scrollDown, () => { - console.log('Scroll down shortcut triggered'); - sendToRenderer('scroll-response-down'); - }); - // console.log(`Registered scrollDown: ${keybinds.scrollDown}`); - } catch (error) { - console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error); + }; + 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); } } } diff --git a/src/features/ask/AskView.js b/src/features/ask/AskView.js index 33cb43c..ee0c153 100644 --- a/src/features/ask/AskView.js +++ b/src/features/ask/AskView.js @@ -658,6 +658,8 @@ export class AskView extends LitElement { this.handleDocumentClick = this.handleDocumentClick.bind(this); this.handleWindowBlur = this.handleWindowBlur.bind(this); + this.handleScroll = this.handleScroll.bind(this); + this.loadLibraries(); // --- Resize helpers --- @@ -863,6 +865,9 @@ export class AskView extends LitElement { ipcRenderer.on('ask-response-chunk', this.handleStreamChunk); ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd); + + ipcRenderer.on('scroll-response-up', () => this.handleScroll('up')); + ipcRenderer.on('scroll-response-down', () => this.handleScroll('down')); console.log('βœ… AskView: IPC 이벀트 λ¦¬μŠ€λ„ˆ 등둝 μ™„λ£Œ'); } } @@ -901,9 +906,24 @@ export class AskView extends LitElement { ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk); ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd); + + ipcRenderer.removeListener('scroll-response-up', () => this.handleScroll('up')); + ipcRenderer.removeListener('scroll-response-down', () => this.handleScroll('down')); console.log('βœ… AskView: IPC 이벀트 λ¦¬μŠ€λ„ˆ 제거 μ™„λ£Œ'); } } + + handleScroll(direction) { + const scrollableElement = this.shadowRoot.querySelector('#responseContainer'); + if (scrollableElement) { + const scrollAmount = 100; // ν•œ λ²ˆμ— μŠ€ν¬λ‘€ν•  μ–‘ (px) + if (direction === 'up') { + scrollableElement.scrollTop -= scrollAmount; + } else { + scrollableElement.scrollTop += scrollAmount; + } + } + } // --- 슀트리밍 처리 ν•Έλ“€λŸ¬ --- handleStreamChunk(event, { token }) { diff --git a/src/features/settings/SettingsView.js b/src/features/settings/SettingsView.js index d78703c..7dbd9f1 100644 --- a/src/features/settings/SettingsView.js +++ b/src/features/settings/SettingsView.js @@ -437,22 +437,10 @@ export class SettingsView extends LitElement { } `; - //////// before_modelStateService //////// - // static properties = { - // firebaseUser: { type: Object, state: true }, - // apiKey: { type: String, state: true }, - // isLoading: { type: Boolean, state: true }, - // isContentProtectionOn: { type: Boolean, state: true }, - // settings: { type: Object, state: true }, - // presets: { type: Array, state: true }, - // selectedPreset: { type: Object, state: true }, - // showPresets: { type: Boolean, state: true }, - // saving: { type: Boolean, state: true }, - // }; - //////// before_modelStateService //////// //////// after_modelStateService //////// static properties = { + shortcuts: { type: Object, state: true }, firebaseUser: { type: Object, state: true }, isLoading: { type: Boolean, state: true }, isContentProtectionOn: { type: Boolean, state: true }, @@ -473,20 +461,8 @@ export class SettingsView extends LitElement { constructor() { super(); - //////// before_modelStateService //////// - // this.firebaseUser = null; - // this.apiKey = null; - // this.isLoading = false; - // this.isContentProtectionOn = true; - // this.settings = null; - // this.presets = []; - // this.selectedPreset = null; - // this.showPresets = false; - // this.saving = false; - // this.loadInitialData(); - //////// before_modelStateService //////// - //////// after_modelStateService //////// + this.shortcuts = {}; this.firebaseUser = null; this.apiKeys = { openai: '', gemini: '', anthropic: '' }; this.providerConfig = {}; @@ -507,55 +483,13 @@ export class SettingsView extends LitElement { //////// after_modelStateService //////// } - - //////// before_modelStateService //////// - // async loadInitialData() { - // if (!window.require) return; - - // try { - // this.isLoading = true; - // const { ipcRenderer } = window.require('electron'); - - // // Load all data in parallel - // const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ - // ipcRenderer.invoke('settings:getSettings'), - // ipcRenderer.invoke('settings:getPresets'), - // ipcRenderer.invoke('get-stored-api-key'), - // ipcRenderer.invoke('get-content-protection-status'), - // ipcRenderer.invoke('get-current-user') - // ]); - - // this.settings = settings; - // this.presets = presets || []; - // this.apiKey = apiKey; - // this.isContentProtectionOn = contentProtection; - - // // Set first user preset as selected - // if (this.presets.length > 0) { - // const firstUserPreset = this.presets.find(p => p.is_default === 0); - // if (firstUserPreset) { - // this.selectedPreset = firstUserPreset; - // } - // } - - // if (userState && userState.isLoggedIn) { - // this.firebaseUser = userState.user; - // } - // } catch (error) { - // console.error('Error loading initial data:', error); - // } finally { - // this.isLoading = false; - // } - // } - //////// before_modelStateService //////// - //////// after_modelStateService //////// async loadInitialData() { if (!window.require) return; this.isLoading = true; const { ipcRenderer } = window.require('electron'); try { - const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection] = await Promise.all([ + const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection, shortcuts] = await Promise.all([ ipcRenderer.invoke('get-current-user'), ipcRenderer.invoke('model:get-provider-config'), // Provider μ„€μ • λ‘œλ“œ ipcRenderer.invoke('model:get-all-keys'), @@ -563,7 +497,8 @@ export class SettingsView extends LitElement { ipcRenderer.invoke('model:get-available-models', { type: 'stt' }), ipcRenderer.invoke('model:get-selected-models'), ipcRenderer.invoke('settings:getPresets'), - ipcRenderer.invoke('get-content-protection-status') + ipcRenderer.invoke('get-content-protection-status'), + ipcRenderer.invoke('get-current-shortcuts') ]); if (userState && userState.isLoggedIn) this.firebaseUser = userState; @@ -575,6 +510,7 @@ export class SettingsView extends LitElement { this.selectedStt = selectedModels.stt; this.presets = presets || []; this.isContentProtectionOn = contentProtection; + this.shortcuts = shortcuts || {}; if (this.presets.length > 0) { const firstUserPreset = this.presets.find(p => p.is_default === 0); if (firstUserPreset) this.selectedPreset = firstUserPreset; @@ -668,6 +604,13 @@ export class SettingsView extends LitElement { } //////// after_modelStateService //////// + openShortcutEditor() { + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('open-shortcut-editor'); + } + } + connectedCallback() { super.connectedCallback(); @@ -732,10 +675,15 @@ export class SettingsView extends LitElement { console.error('[SettingsView] Failed to refresh presets:', error); } }; + this._shortcutListener = (event, keybinds) => { + console.log('[SettingsView] Received updated shortcuts:', keybinds); + this.shortcuts = keybinds; + }; ipcRenderer.on('user-state-changed', this._userStateListener); ipcRenderer.on('settings-updated', this._settingsUpdatedListener); ipcRenderer.on('presets-updated', this._presetsUpdatedListener); + ipcRenderer.on('shortcuts-updated', this._shortcutListener); } cleanupIpcListeners() { @@ -752,6 +700,9 @@ export class SettingsView extends LitElement { if (this._presetsUpdatedListener) { ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener); } + if (this._shortcutListener) { + ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener); + } } setupWindowResize() { @@ -797,14 +748,41 @@ export class SettingsView extends LitElement { } } + // getMainShortcuts() { + // return [ + // { name: 'Show / Hide', key: '\\' }, + // { name: 'Ask Anything', key: '↡' }, + // { name: 'Scroll AI Response', key: '↕' } + // ]; + // } getMainShortcuts() { return [ - { name: 'Show / Hide', key: '\\' }, - { name: 'Ask Anything', key: '↡' }, - { name: 'Scroll AI Response', key: '↕' } + { name: 'Show / Hide', accelerator: this.shortcuts.toggleVisibility }, + { name: 'Ask Anything', accelerator: this.shortcuts.nextStep }, + { name: 'Scroll Up Response', accelerator: this.shortcuts.scrollUp }, + { name: 'Scroll Down Response', accelerator: this.shortcuts.scrollDown }, ]; } + renderShortcutKeys(accelerator) { + if (!accelerator) return html`N/A`; + + const keyMap = { + 'Cmd': '⌘', 'Command': '⌘', 'Ctrl': 'βŒƒ', 'Alt': 'βŒ₯', 'Shift': '⇧', 'Enter': '↡', + 'Up': '↑', 'Down': '↓', 'Left': '←', 'Right': 'β†’' + }; + + // scrollDown/scrollUp의 특수 처리 + if (accelerator.includes('↕')) { + const keys = accelerator.replace('↕','').split('+'); + keys.push('↕'); + return html`${keys.map(key => html`${keyMap[key] || key}`)}`; + } + + const keys = accelerator.split('+'); + return html`${keys.map(key => html`${keyMap[key] || key}`)}`; + } + togglePresets() { this.showPresets = !this.showPresets; } @@ -1131,14 +1109,20 @@ export class SettingsView extends LitElement { ${apiKeyManagementHTML} ${modelSelectionHTML} + +
+ +
+
${this.getMainShortcuts().map(shortcut => html`
${shortcut.name}
- ⌘ - ${shortcut.key} + ${this.renderShortcutKeys(shortcut.accelerator)}
`)} diff --git a/src/features/settings/ShortCutSettingsView.js b/src/features/settings/ShortCutSettingsView.js new file mode 100644 index 0000000..aac45e4 --- /dev/null +++ b/src/features/settings/ShortCutSettingsView.js @@ -0,0 +1,235 @@ +import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; + +const commonSystemShortcuts = new Set([ + 'Cmd+Q', 'Cmd+W', 'Cmd+A', 'Cmd+S', 'Cmd+Z', 'Cmd+X', 'Cmd+C', 'Cmd+V', 'Cmd+P', 'Cmd+F', 'Cmd+G', 'Cmd+H', 'Cmd+M', 'Cmd+N', 'Cmd+O', 'Cmd+T', + 'Ctrl+Q', 'Ctrl+W', 'Ctrl+A', 'Ctrl+S', 'Ctrl+Z', 'Ctrl+X', 'Ctrl+C', 'Ctrl+V', 'Ctrl+P', 'Ctrl+F', 'Ctrl+G', 'Ctrl+H', 'Ctrl+M', 'Ctrl+N', 'Ctrl+O', 'Ctrl+T' +]); + +const displayNameMap = { + nextStep: 'Ask Anything', + moveUp: 'Move Up Window', + moveDown: 'Move Down Window', + scrollUp: 'Scroll Up Response', + scrollDown: 'Scroll Down Response', + }; + +export class ShortcutSettingsView extends LitElement { + static styles = css` + * { font-family:'Helvetica Neue',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; + cursor:default; user-select:none; box-sizing:border-box; } + + :host { display:flex; width:100%; height:100%; color:white; } + + .container { display:flex; flex-direction:column; height:100%; + background:rgba(20,20,20,.9); border-radius:12px; + outline:.5px rgba(255,255,255,.2) solid; outline-offset:-1px; + position:relative; overflow:hidden; padding:12px; } + + .close-button{position:absolute;top:10px;right:10px;inline-size:14px;block-size:14px; + background:rgba(255,255,255,.1);border:none;border-radius:3px; + color:rgba(255,255,255,.7);display:grid;place-items:center; + font-size:14px;line-height:0;cursor:pointer;transition:.15s;z-index:10;} + .close-button:hover{background:rgba(255,255,255,.2);color:rgba(255,255,255,.9);} + + .title{font-size:14px;font-weight:500;margin:0 0 8px;padding-bottom:8px; + border-bottom:1px solid rgba(255,255,255,.1);text-align:center;} + + .scroll-area{flex:1 1 auto;overflow-y:auto;margin:0 -4px;padding:4px;} + + .shortcut-entry{display:flex;align-items:center;width:100%;gap:8px; + margin-bottom:8px;font-size:12px;padding:4px;} + .shortcut-name{flex:1 1 auto;color:rgba(255,255,255,.9);font-weight:300; + white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} + + .action-btn{background:none;border:none;color:rgba(0,122,255,.8); + font-size:11px;padding:0 4px;cursor:pointer;transition:.15s;} + .action-btn:hover{color:#0a84ff;text-decoration:underline;} + + .shortcut-input{inline-size:120px;background:rgba(0,0,0,.2); + border:1px solid rgba(255,255,255,.2);border-radius:4px; + padding:4px 6px;font:11px 'SF Mono','Menlo',monospace; + color:white;text-align:right;cursor:text;margin-left:auto;} + .shortcut-input:focus,.shortcut-input.capturing{ + outline:none;border-color:rgba(0,122,255,.6); + box-shadow:0 0 0 1px rgba(0,122,255,.3);} + + .feedback{font-size:10px;margin-top:2px;min-height:12px;} + .feedback.error{color:#ef4444;} + .feedback.success{color:#22c55e;} + + .actions{display:flex;gap:4px;padding-top:8px;border-top:1px solid rgba(255,255,255,.1);} + .settings-button{flex:1;background:rgba(255,255,255,.1); + border:1px solid rgba(255,255,255,.2);border-radius:4px; + color:white;padding:5px 10px;font-size:11px;cursor:pointer;transition:.15s;} + .settings-button:hover{background:rgba(255,255,255,.15);} + .settings-button.primary{background:rgba(0,122,255,.25);border-color:rgba(0,122,255,.6);} + .settings-button.primary:hover{background:rgba(0,122,255,.35);} + .settings-button.danger{background:rgba(255,59,48,.1);border-color:rgba(255,59,48,.3); + color:rgba(255,59,48,.9);} + .settings-button.danger:hover{background:rgba(255,59,48,.15);} + `; + + static properties = { + shortcuts: { type: Object, state: true }, + isLoading: { type: Boolean, state: true }, + capturingKey: { type: String, state: true }, + feedback: { type:Object, state:true } + }; + + constructor() { + super(); + this.shortcuts = {}; + this.feedback = {}; + this.isLoading = true; + this.capturingKey = null; + this.ipcRenderer = window.require ? window.require('electron').ipcRenderer : null; + } + + connectedCallback() { + super.connectedCallback(); + if (!this.ipcRenderer) return; + this.loadShortcutsHandler = (event, keybinds) => { + this.shortcuts = keybinds; + this.isLoading = false; + }; + this.ipcRenderer.on('load-shortcuts', this.loadShortcutsHandler); + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.ipcRenderer && this.loadShortcutsHandler) { + this.ipcRenderer.removeListener('load-shortcuts', this.loadShortcutsHandler); + } + } + + handleKeydown(e, shortcutKey){ + e.preventDefault(); e.stopPropagation(); + const result = this._parseAccelerator(e); + if(!result) return; // modifierν‚€λ§Œ λˆ„λ₯Έ μƒνƒœ + + const {accel, error} = result; + if(error){ + this.feedback = {...this.feedback, [shortcutKey]:{type:'error',msg:error}}; + return; + } + // 성곡 + this.shortcuts = {...this.shortcuts, [shortcutKey]:accel}; + this.feedback = {...this.feedback, [shortcutKey]:{type:'success',msg:'Shortcut set'}}; + this.stopCapture(); + } + + _parseAccelerator(e){ + /* returns {accel?, error?} */ + const parts=[]; if(e.metaKey) parts.push('Cmd'); + if(e.ctrlKey) parts.push('Ctrl'); + if(e.altKey) parts.push('Alt'); + if(e.shiftKey) parts.push('Shift'); + + const isModifier=['Meta','Control','Alt','Shift'].includes(e.key); + if(isModifier) return null; + + const map={ArrowUp:'Up',ArrowDown:'Down',ArrowLeft:'Left',ArrowRight:'Right',' ':'Space'}; + parts.push(e.key.length===1? e.key.toUpperCase() : (map[e.key]||e.key)); + const accel=parts.join('+'); + + /* ---- validation ---- */ + if(parts.length===1) return {error:'Invalid shortcut: needs a modifier'}; + if(parts.length>4) return {error:'Invalid shortcut: max 4 keys'}; + if(commonSystemShortcuts.has(accel)) return {error:'Invalid shortcut: system reserved'}; + return {accel}; + } + + startCapture(key){ this.capturingKey = key; this.feedback = {...this.feedback, [key]:undefined}; } + + disableShortcut(key){ + this.shortcuts = {...this.shortcuts, [key]:''}; // 곡백 => μž‘λ™ X + this.feedback = {...this.feedback, [key]:{type:'success',msg:'Shortcut disabled'}}; + } + + stopCapture() { + this.capturingKey = null; + } + + async handleSave() { + if (!this.ipcRenderer) return; + const result = await this.ipcRenderer.invoke('save-shortcuts', this.shortcuts); + if (!result.success) { + alert('Failed to save shortcuts: ' + result.error); + } + } + + handleClose() { + if (!this.ipcRenderer) return; + this.ipcRenderer.send('close-shortcut-editor'); + } + + async handleResetToDefault() { + if (!this.ipcRenderer) return; + const confirmation = confirm("Are you sure you want to reset all shortcuts to their default values?"); + if (!confirmation) return; + + try { + const defaultShortcuts = await this.ipcRenderer.invoke('get-default-shortcuts'); + this.shortcuts = defaultShortcuts; + } catch (error) { + alert('Failed to load default settings.'); + } + } + + formatShortcutName(name) { + if (displayNameMap[name]) { + return displayNameMap[name]; + } + const result = name.replace(/([A-Z])/g, " $1"); + return result.charAt(0).toUpperCase() + result.slice(1); + } + + render(){ + if(this.isLoading){ + return html`
Loading Shortcuts...
`; + } + return html` +
+ +

Edit Shortcuts

+ +
+ ${Object.keys(this.shortcuts).map(key=>html` +
+
+ ${this.formatShortcutName(key)} + + + + + + this.startCapture(key)} + @keydown=${e=>this.handleKeydown(e,key)} + @blur=${()=>this.stopCapture()} + /> +
+ + ${this.feedback[key] ? html` + ` : html`` + } +
+ `)} +
+ +
+ + + +
+
+ `; + } + } + +customElements.define('shortcut-settings-view', ShortcutSettingsView); \ No newline at end of file