Merge remote-tracking branch 'origin/main' into pr-84
This commit is contained in:
commit
2bb5fcfae7
@ -126,6 +126,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
|
||||
|
@ -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`<svg viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:6px; height:12px;"><path d="M1.5 1.3L5.1 10.6" stroke="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
};
|
||||
|
||||
const keys = accelerator.split('+');
|
||||
return html`${keys.map(key => html`
|
||||
<div class="icon-box">${keyMap[key] || key}</div>
|
||||
`)}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="header" @mousedown=${this.handleMouseDown}>
|
||||
@ -599,14 +630,8 @@ export class MainHeader extends LitElement {
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Ask</div>
|
||||
</div>
|
||||
<div class="icon-container ask-icons">
|
||||
<div class="icon-box">⌘</div>
|
||||
<div class="icon-box">
|
||||
<svg viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.41797 8.16406C2.41797 8.00935 2.47943 7.86098 2.58882 7.75158C2.69822 7.64219 2.84659 7.58073 3.0013 7.58073H10.0013C10.4654 7.58073 10.9106 7.39636 11.2387 7.06817C11.5669 6.73998 11.7513 6.29486 11.7513 5.83073V3.4974C11.7513 3.34269 11.8128 3.19431 11.9222 3.08492C12.0316 2.97552 12.1799 2.91406 12.3346 2.91406C12.4893 2.91406 12.6377 2.97552 12.7471 3.08492C12.8565 3.19431 12.918 3.34269 12.918 3.4974V5.83073C12.918 6.60428 12.6107 7.34614 12.0637 7.89312C11.5167 8.44011 10.7748 8.7474 10.0013 8.7474H3.0013C2.84659 8.7474 2.69822 8.68594 2.58882 8.57654C2.47943 8.46715 2.41797 8.31877 2.41797 8.16406Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.58876 8.57973C2.4794 8.47034 2.41797 8.32199 2.41797 8.16731C2.41797 8.01263 2.4794 7.86429 2.58876 7.75489L4.92209 5.42156C5.03211 5.3153 5.17946 5.25651 5.33241 5.25783C5.48536 5.25916 5.63167 5.32051 5.73982 5.42867C5.84798 5.53682 5.90932 5.68313 5.91065 5.83608C5.91198 5.98903 5.85319 6.13638 5.74693 6.24639L3.82601 8.16731L5.74693 10.0882C5.80264 10.142 5.84708 10.2064 5.87765 10.2776C5.90823 10.3487 5.92432 10.4253 5.92499 10.5027C5.92566 10.5802 5.9109 10.657 5.88157 10.7287C5.85224 10.8004 5.80893 10.8655 5.75416 10.9203C5.69939 10.9751 5.63426 11.0184 5.56257 11.0477C5.49088 11.077 5.41406 11.0918 5.33661 11.0911C5.25916 11.0905 5.18261 11.0744 5.11144 11.0438C5.04027 11.0132 4.9759 10.9688 4.92209 10.9131L2.58876 8.57973Z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
${this.renderShortcut(this.shortcuts.nextStep)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -614,13 +639,8 @@ export class MainHeader extends LitElement {
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Show/Hide</div>
|
||||
</div>
|
||||
<div class="icon-container showhide-icons">
|
||||
<div class="icon-box">⌘</div>
|
||||
<div class="icon-box">
|
||||
<svg viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.50391 1.32812L5.16391 10.673" stroke="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
${this.renderShortcut(this.shortcuts.toggleVisibility)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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)}
|
||||
></settings-view>`;
|
||||
case 'shortcut-settings':
|
||||
return html`<shortcut-settings-view></shortcut-settings-view>`;
|
||||
case 'history':
|
||||
return html`<history-view></history-view>`;
|
||||
case 'help':
|
||||
|
@ -67,6 +67,11 @@ class ModelStateService {
|
||||
};
|
||||
this.state = this.store.get(`users.${userId}`, defaultState);
|
||||
console.log(`[ModelStateService] State loaded for user: ${userId}`);
|
||||
for (const p of Object.keys(PROVIDERS)) {
|
||||
if (!(p in this.state.apiKeys)) {
|
||||
this.state.apiKeys[p] = null;
|
||||
}
|
||||
}
|
||||
this._autoSelectAvailableModels();
|
||||
this._saveState();
|
||||
this._logCurrentSelection();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }) {
|
||||
|
@ -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`<span class="shortcut-key">${keyMap[key] || key}</span>`)}`;
|
||||
}
|
||||
|
||||
const keys = accelerator.split('+');
|
||||
return html`${keys.map(key => html`<span class="shortcut-key">${keyMap[key] || key}</span>`)}`;
|
||||
}
|
||||
|
||||
togglePresets() {
|
||||
this.showPresets = !this.showPresets;
|
||||
}
|
||||
@ -1131,14 +1109,20 @@ export class SettingsView extends LitElement {
|
||||
|
||||
${apiKeyManagementHTML}
|
||||
${modelSelectionHTML}
|
||||
|
||||
<div class="buttons-section" style="border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 6px; margin-top: 6px;">
|
||||
<button class="settings-button full-width" @click=${this.openShortcutEditor}>
|
||||
Edit Shortcuts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="shortcuts-section">
|
||||
${this.getMainShortcuts().map(shortcut => html`
|
||||
<div class="shortcut-item">
|
||||
<span class="shortcut-name">${shortcut.name}</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="cmd-key">⌘</span>
|
||||
<span class="shortcut-key">${shortcut.key}</span>
|
||||
${this.renderShortcutKeys(shortcut.accelerator)}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
|
235
src/features/settings/ShortCutSettingsView.js
Normal file
235
src/features/settings/ShortCutSettingsView.js
Normal file
@ -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`<div class="container"><div class="loading-state">Loading Shortcuts...</div></div>`;
|
||||
}
|
||||
return html`
|
||||
<div class="container">
|
||||
<button class="close-button" @click=${this.handleClose} title="Close">×</button>
|
||||
<h1 class="title">Edit Shortcuts</h1>
|
||||
|
||||
<div class="scroll-area">
|
||||
${Object.keys(this.shortcuts).map(key=>html`
|
||||
<div>
|
||||
<div class="shortcut-entry">
|
||||
<span class="shortcut-name">${this.formatShortcutName(key)}</span>
|
||||
|
||||
<!-- Edit & Disable 버튼 -->
|
||||
<button class="action-btn" @click=${()=>this.startCapture(key)}>Edit</button>
|
||||
<button class="action-btn" @click=${()=>this.disableShortcut(key)}>Disable</button>
|
||||
|
||||
<input readonly
|
||||
class="shortcut-input ${this.capturingKey===key?'capturing':''}"
|
||||
.value=${this.shortcuts[key]||''}
|
||||
placeholder=${this.capturingKey===key?'Press new shortcut…':'Click to edit'}
|
||||
@click=${()=>this.startCapture(key)}
|
||||
@keydown=${e=>this.handleKeydown(e,key)}
|
||||
@blur=${()=>this.stopCapture()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
${this.feedback[key] ? html`
|
||||
<div class="feedback ${this.feedback[key].type}">
|
||||
${this.feedback[key].msg}
|
||||
</div>` : html`<div class="feedback"></div>`
|
||||
}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="settings-button" @click=${this.handleClose}>Cancel</button>
|
||||
<button class="settings-button danger" @click=${this.handleResetToDefault}>Reset to Default</button>
|
||||
<button class="settings-button primary" @click=${this.handleSave}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('shortcut-settings-view', ShortcutSettingsView);
|
Loading…
x
Reference in New Issue
Block a user