add shortcut editing(beta)
This commit is contained in:
parent
ba3e8a153c
commit
843694daac
@ -117,7 +117,6 @@ We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is%
|
|||||||
|
|
||||||
| Status | Issue | Description |
|
| Status | Issue | Description |
|
||||||
|--------|--------------------------------|---------------------------------------------------|
|
|--------|--------------------------------|---------------------------------------------------|
|
||||||
| 🚧 WIP | Code Refactoring | Refactoring the entire codebase for better maintainability. |
|
|
||||||
| 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows |
|
| 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows |
|
||||||
| 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers |
|
| 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers |
|
||||||
| 🚧 WIP | AEC Improvement | Transcription is not working occasionally |
|
| 🚧 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
|
### Changelog
|
||||||
|
|
||||||
- Jul 5: Now support Gemini, Intel Mac supported
|
- 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
|
## About Pickle
|
||||||
|
@ -3,11 +3,12 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
|||||||
export class MainHeader extends LitElement {
|
export class MainHeader extends LitElement {
|
||||||
static properties = {
|
static properties = {
|
||||||
isSessionActive: { type: Boolean, state: true },
|
isSessionActive: { type: Boolean, state: true },
|
||||||
|
shortcuts: { type: Object, state: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: flex;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
|
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 {
|
.header {
|
||||||
width: 100%;
|
width: max-content;
|
||||||
height: 47px;
|
height: 47px;
|
||||||
padding: 2px 10px 2px 13px;
|
padding: 2px 10px 2px 13px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -212,16 +213,6 @@ export class MainHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-button,
|
.action-button,
|
||||||
.settings-button {
|
|
||||||
background: transparent;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-text {
|
.action-text {
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -275,9 +266,16 @@ export class MainHeader extends LitElement {
|
|||||||
.settings-button {
|
.settings-button {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button:hover {
|
.settings-button:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
@ -286,6 +284,7 @@ export class MainHeader extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-icon svg {
|
.settings-icon svg {
|
||||||
@ -346,6 +345,7 @@ export class MainHeader extends LitElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.shortcuts = {};
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
this.wasJustDragged = false;
|
this.wasJustDragged = false;
|
||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
@ -501,6 +501,11 @@ export class MainHeader extends LitElement {
|
|||||||
this.isSessionActive = isActive;
|
this.isSessionActive = isActive;
|
||||||
};
|
};
|
||||||
ipcRenderer.on('session-state-changed', this._sessionStateListener);
|
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) {
|
if (this._sessionStateListener) {
|
||||||
ipcRenderer.removeListener('session-state-changed', 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() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="header" @mousedown=${this.handleMouseDown}>
|
<div class="header" @mousedown=${this.handleMouseDown}>
|
||||||
@ -599,14 +630,8 @@ export class MainHeader extends LitElement {
|
|||||||
<div class="action-text">
|
<div class="action-text">
|
||||||
<div class="action-text-content">Ask</div>
|
<div class="action-text-content">Ask</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-container ask-icons">
|
<div class="icon-container">
|
||||||
<div class="icon-box">⌘</div>
|
${this.renderShortcut(this.shortcuts.nextStep)}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -614,13 +639,8 @@ export class MainHeader extends LitElement {
|
|||||||
<div class="action-text">
|
<div class="action-text">
|
||||||
<div class="action-text-content">Show/Hide</div>
|
<div class="action-text-content">Show/Hide</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-container showhide-icons">
|
<div class="icon-container">
|
||||||
<div class="icon-box">⌘</div>
|
${this.renderShortcut(this.shortcuts.toggleVisibility)}
|
||||||
<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>
|
</div>
|
||||||
</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 { SettingsView } from '../features/settings/SettingsView.js';
|
||||||
import { AssistantView } from '../features/listen/AssistantView.js';
|
import { AssistantView } from '../features/listen/AssistantView.js';
|
||||||
import { AskView } from '../features/ask/AskView.js';
|
import { AskView } from '../features/ask/AskView.js';
|
||||||
|
import { ShortcutSettingsView } from '../features/settings/ShortCutSettingsView.js';
|
||||||
|
|
||||||
import '../features/listen/renderer/renderer.js';
|
import '../features/listen/renderer/renderer.js';
|
||||||
|
|
||||||
@ -268,6 +269,8 @@ export class PickleGlassApp extends LitElement {
|
|||||||
.onProfileChange=${profile => (this.selectedProfile = profile)}
|
.onProfileChange=${profile => (this.selectedProfile = profile)}
|
||||||
.onLanguageChange=${lang => (this.selectedLanguage = lang)}
|
.onLanguageChange=${lang => (this.selectedLanguage = lang)}
|
||||||
></settings-view>`;
|
></settings-view>`;
|
||||||
|
case 'shortcut-settings':
|
||||||
|
return html`<shortcut-settings-view></shortcut-settings-view>`;
|
||||||
case 'history':
|
case 'history':
|
||||||
return html`<history-view></history-view>`;
|
return html`<history-view></history-view>`;
|
||||||
case 'help':
|
case 'help':
|
||||||
|
@ -11,7 +11,13 @@ const authService = require('../common/services/authService');
|
|||||||
const systemSettingsRepository = require('../common/repositories/systemSettings');
|
const systemSettingsRepository = require('../common/repositories/systemSettings');
|
||||||
const userRepository = require('../common/repositories/user');
|
const userRepository = require('../common/repositories/user');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const Store = require('electron-store');
|
||||||
|
const shortCutStore = new Store({
|
||||||
|
name: 'user-preferences',
|
||||||
|
defaults: {
|
||||||
|
customKeybinds: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||||
let liquidGlass;
|
let liquidGlass;
|
||||||
@ -51,6 +57,7 @@ let settingsHideTimer = null;
|
|||||||
|
|
||||||
let selectedCaptureSourceId = null;
|
let selectedCaptureSourceId = null;
|
||||||
|
|
||||||
|
// let shortcutEditorWindow = null;
|
||||||
let layoutManager = null;
|
let layoutManager = null;
|
||||||
function updateLayout() {
|
function updateLayout() {
|
||||||
if (layoutManager) {
|
if (layoutManager) {
|
||||||
@ -60,16 +67,16 @@ function updateLayout() {
|
|||||||
|
|
||||||
let movementManager = null;
|
let movementManager = null;
|
||||||
|
|
||||||
let storedProvider = 'openai';
|
|
||||||
|
|
||||||
const featureWindows = ['listen','ask','settings'];
|
const featureWindows = ['listen','ask','settings'];
|
||||||
|
// const featureWindows = ['listen','ask','settings','shortcut-settings'];
|
||||||
function isAllowed(name) {
|
function isAllowed(name) {
|
||||||
if (name === 'header') return true;
|
if (name === 'header') return true;
|
||||||
return featureWindows.includes(name) && currentHeaderState === 'main';
|
return featureWindows.includes(name) && currentHeaderState === 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFeatureWindows(header) {
|
function createFeatureWindows(header, namesToCreate) {
|
||||||
if (windowPool.has('listen')) return;
|
// if (windowPool.has('listen')) return;
|
||||||
|
|
||||||
const commonChildOptions = {
|
const commonChildOptions = {
|
||||||
parent: header,
|
parent: header,
|
||||||
@ -84,106 +91,207 @@ function createFeatureWindows(header) {
|
|||||||
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
// listen
|
const createFeatureWindow = (name) => {
|
||||||
const listen = new BrowserWindow({
|
if (windowPool.has(name)) return;
|
||||||
...commonChildOptions, width:400,minWidth:400,maxWidth:400,
|
|
||||||
maxHeight:700,
|
switch (name) {
|
||||||
});
|
case 'listen': {
|
||||||
listen.setContentProtection(isContentProtectionOn);
|
const listen = new BrowserWindow({
|
||||||
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
...commonChildOptions, width:400,minWidth:400,maxWidth:400,
|
||||||
if (process.platform === 'darwin') {
|
maxHeight:700,
|
||||||
listen.setWindowButtonVisibility(false);
|
});
|
||||||
}
|
listen.setContentProtection(isContentProtectionOn);
|
||||||
const listenLoadOptions = { query: { view: 'listen' } };
|
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||||
if (!shouldUseLiquidGlass) {
|
if (process.platform === 'darwin') {
|
||||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
listen.setWindowButtonVisibility(false);
|
||||||
}
|
}
|
||||||
else {
|
const listenLoadOptions = { query: { view: 'listen' } };
|
||||||
listenLoadOptions.query.glass = 'true';
|
if (!shouldUseLiquidGlass) {
|
||||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||||
listen.webContents.once('did-finish-load', () => {
|
}
|
||||||
const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), {
|
else {
|
||||||
cornerRadius: 12,
|
listenLoadOptions.query.glass = 'true';
|
||||||
tintColor: '#FF00001A', // Red tint
|
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||||
opaque: false,
|
listen.webContents.once('did-finish-load', () => {
|
||||||
});
|
const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), {
|
||||||
if (viewId !== -1) {
|
cornerRadius: 12,
|
||||||
liquidGlass.unstable_setVariant(viewId, 2);
|
tintColor: '#FF00001A', // Red tint
|
||||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
opaque: false,
|
||||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
});
|
||||||
|
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.on('blur',()=>ask.webContents.send('window-blur'));
|
||||||
|
|
||||||
// ask
|
// Open DevTools in development
|
||||||
const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
|
if (!app.isPackaged) {
|
||||||
ask.setContentProtection(isContentProtectionOn);
|
ask.webContents.openDevTools({ mode: 'detach' });
|
||||||
ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
}
|
||||||
if (process.platform === 'darwin') {
|
windowPool.set('ask', ask);
|
||||||
ask.setWindowButtonVisibility(false);
|
break;
|
||||||
}
|
|
||||||
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'));
|
// settings
|
||||||
|
case 'settings': {
|
||||||
// Open DevTools in development
|
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
||||||
if (!app.isPackaged) {
|
settings.setContentProtection(isContentProtectionOn);
|
||||||
ask.webContents.openDevTools({ mode: 'detach' });
|
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||||
}
|
if (process.platform === 'darwin') {
|
||||||
windowPool.set('ask', ask);
|
settings.setWindowButtonVisibility(false);
|
||||||
|
}
|
||||||
// settings
|
const settingsLoadOptions = { query: { view: 'settings' } };
|
||||||
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
if (!shouldUseLiquidGlass) {
|
||||||
settings.setContentProtection(isContentProtectionOn);
|
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||||
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
.catch(console.error);
|
||||||
if (process.platform === 'darwin') {
|
}
|
||||||
settings.setWindowButtonVisibility(false);
|
else {
|
||||||
}
|
settingsLoadOptions.query.glass = 'true';
|
||||||
const settingsLoadOptions = { query: { view: 'settings' } };
|
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||||
if (!shouldUseLiquidGlass) {
|
.catch(console.error);
|
||||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
settings.webContents.once('did-finish-load', () => {
|
||||||
.catch(console.error);
|
const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), {
|
||||||
}
|
cornerRadius: 12,
|
||||||
else {
|
tintColor: '#FF00001A', // Red tint
|
||||||
settingsLoadOptions.query.glass = 'true';
|
opaque: false,
|
||||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
});
|
||||||
.catch(console.error);
|
if (viewId !== -1) {
|
||||||
settings.webContents.once('did-finish-load', () => {
|
liquidGlass.unstable_setVariant(viewId, 2);
|
||||||
const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), {
|
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||||
cornerRadius: 12,
|
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||||
tintColor: '#FF00001A', // Red tint
|
}
|
||||||
opaque: false,
|
});
|
||||||
});
|
}
|
||||||
if (viewId !== -1) {
|
windowPool.set('settings', settings);
|
||||||
liquidGlass.unstable_setVariant(viewId, 2);
|
break;
|
||||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
|
||||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
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() {
|
function destroyFeatureWindows() {
|
||||||
@ -199,6 +307,7 @@ function destroyFeatureWindows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getCurrentDisplay(window) {
|
function getCurrentDisplay(window) {
|
||||||
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
|
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
|
||||||
|
|
||||||
@ -354,7 +463,7 @@ function createWindows() {
|
|||||||
setupIpcHandlers(movementManager);
|
setupIpcHandlers(movementManager);
|
||||||
|
|
||||||
if (currentHeaderState === 'main') {
|
if (currentHeaderState === 'main') {
|
||||||
createFeatureWindows(header);
|
createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
|
||||||
}
|
}
|
||||||
|
|
||||||
header.setContentProtection(isContentProtectionOn);
|
header.setContentProtection(isContentProtectionOn);
|
||||||
@ -385,10 +494,6 @@ function createWindows() {
|
|||||||
|
|
||||||
header.on('resize', updateLayout);
|
header.on('resize', updateLayout);
|
||||||
|
|
||||||
// header.webContents.once('dom-ready', () => {
|
|
||||||
// loadAndRegisterShortcuts();
|
|
||||||
// });
|
|
||||||
|
|
||||||
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
|
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
|
||||||
|
|
||||||
ipcMain.handle('toggle-feature', async (event, featureName) => {
|
ipcMain.handle('toggle-feature', async (event, featureName) => {
|
||||||
@ -584,37 +689,32 @@ function createWindows() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// setupIpcHandlers();
|
|
||||||
|
|
||||||
return windowPool;
|
return windowPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAndRegisterShortcuts(movementManager) {
|
function loadAndRegisterShortcuts(movementManager) {
|
||||||
|
if (windowPool.has('shortcut-settings')) {
|
||||||
|
console.log('[Shortcuts] Editing in progress, skipping registration.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultKeybinds = getDefaultKeybinds();
|
const defaultKeybinds = getDefaultKeybinds();
|
||||||
const header = windowPool.get('header');
|
const savedKeybinds = shortCutStore.get('customKeybinds', {});
|
||||||
|
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||||
|
|
||||||
const sendToRenderer = (channel, ...args) => {
|
const sendToRenderer = (channel, ...args) => {
|
||||||
windowPool.forEach(win => {
|
windowPool.forEach(win => {
|
||||||
try {
|
if (win && !win.isDestroyed()) {
|
||||||
if (win && !win.isDestroyed()) {
|
try {
|
||||||
win.webContents.send(channel, ...args);
|
win.webContents.send(channel, ...args);
|
||||||
|
} catch (e) {
|
||||||
|
// 창이 이미 닫혔을 수 있으므로 오류를 무시합니다.
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -768,6 +868,7 @@ function setupIpcHandlers(movementManager) {
|
|||||||
} else { // 'apikey' | 'permission'
|
} else { // 'apikey' | 'permission'
|
||||||
destroyFeatureWindows();
|
destroyFeatureWindows();
|
||||||
}
|
}
|
||||||
|
loadAndRegisterShortcuts(movementManager);
|
||||||
|
|
||||||
for (const [name, win] of windowPool) {
|
for (const [name, win] of windowPool) {
|
||||||
if (!isAllowed(name) && !win.isDestroyed()) {
|
if (!isAllowed(name) && !win.isDestroyed()) {
|
||||||
@ -777,36 +878,69 @@ function setupIpcHandlers(movementManager) {
|
|||||||
win.show();
|
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) => {
|
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
||||||
updateGlobalShortcuts(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', () => {
|
ipcMain.handle('open-login-page', () => {
|
||||||
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
||||||
const personalizeUrl = `${webUrl}/personalize?desktop=true`;
|
const personalizeUrl = `${webUrl}/personalize?desktop=true`;
|
||||||
@ -971,15 +1105,6 @@ function setupIpcHandlers(movementManager) {
|
|||||||
console.log('[WindowManager] Received request to log out.');
|
console.log('[WindowManager] Received request to log out.');
|
||||||
|
|
||||||
await authService.signOut();
|
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 () => {
|
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 ////////
|
//////// after_modelStateService ////////
|
||||||
async function getStoredApiKey() {
|
async function getStoredApiKey() {
|
||||||
@ -1227,15 +1257,15 @@ async function getStoredProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 렌더러에서 요청한 타입('llm' 또는 'stt')에 대한 모델 정보를 반환합니다.
|
*
|
||||||
* @param {IpcMainInvokeEvent} event - 일렉트론 IPC 이벤트 객체
|
* @param {IpcMainInvokeEvent} event
|
||||||
* @param {{type: 'llm' | 'stt'}} { type } - 요청할 모델 타입
|
* @param {{type: 'llm' | 'stt'}}
|
||||||
*/
|
*/
|
||||||
async function getCurrentModelInfo(event, { type }) {
|
async function getCurrentModelInfo(event, { type }) {
|
||||||
if (global.modelStateService && (type === 'llm' || type === 'stt')) {
|
if (global.modelStateService && (type === 'llm' || type === 'stt')) {
|
||||||
return global.modelStateService.getCurrentModelInfo(type);
|
return global.modelStateService.getCurrentModelInfo(type);
|
||||||
}
|
}
|
||||||
return null; // 서비스가 없거나 유효하지 않은 타입일 경우 null 반환
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupApiKeyIPC() {
|
function setupApiKeyIPC() {
|
||||||
@ -1279,33 +1309,26 @@ function getDefaultKeybinds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
|
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
|
||||||
// console.log('Updating global shortcuts with:', keybinds);
|
|
||||||
|
|
||||||
// Unregister all existing shortcuts
|
|
||||||
globalShortcut.unregisterAll();
|
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 isMac = process.platform === 'darwin';
|
||||||
const modifier = isMac ? 'Cmd' : 'Ctrl';
|
const modifier = isMac ? 'Cmd' : 'Ctrl';
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
const state = header?.currentHeaderState || currentHeaderState;
|
||||||
|
|
||||||
if (keybinds.toggleVisibility) {
|
// ✨ 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지)
|
||||||
try {
|
|
||||||
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager));
|
|
||||||
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displays = screen.getAllDisplays();
|
const displays = screen.getAllDisplays();
|
||||||
if (displays.length > 1) {
|
if (displays.length > 1) {
|
||||||
displays.forEach((display, index) => {
|
displays.forEach((display, index) => {
|
||||||
const key = `${modifier}+Shift+${index + 1}`;
|
const key = `${modifier}+Shift+${index + 1}`;
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(key, () => {
|
globalShortcut.register(key, () => movementManager.moveToDisplay(display.id));
|
||||||
movementManager.moveToDisplay(display.id);
|
|
||||||
});
|
|
||||||
console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`);
|
console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register display switch ${key}:`, 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');
|
console.log('ApiKeyHeader is active, skipping conditional shortcuts');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const directions = [
|
// ✨ 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지)
|
||||||
{ 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const edgeDirections = [
|
const edgeDirections = [
|
||||||
{ key: `${modifier}+Shift+Left`, direction: 'left' },
|
{ key: `${modifier}+Shift+Left`, direction: 'left' },
|
||||||
{ key: `${modifier}+Shift+Right`, direction: 'right' },
|
{ key: `${modifier}+Shift+Right`, direction: 'right' },
|
||||||
{ key: `${modifier}+Shift+Up`, direction: 'up' },
|
// { key: `${modifier}+Shift+Up`, direction: 'up' },
|
||||||
{ key: `${modifier}+Shift+Down`, direction: 'down' },
|
// { key: `${modifier}+Shift+Down`, direction: 'down' },
|
||||||
];
|
];
|
||||||
|
|
||||||
edgeDirections.forEach(({ key, direction }) => {
|
edgeDirections.forEach(({ key, direction }) => {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(key, () => {
|
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) {
|
} 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 {
|
try {
|
||||||
globalShortcut.register(keybinds.toggleClickThrough, () => {
|
let callback;
|
||||||
mouseEventsIgnored = !mouseEventsIgnored;
|
switch(action) {
|
||||||
if (mouseEventsIgnored) {
|
case 'toggleVisibility':
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
callback = () => toggleAllWindowsVisibility(movementManager);
|
||||||
console.log('Mouse events ignored');
|
break;
|
||||||
} else {
|
case 'nextStep':
|
||||||
mainWindow.setIgnoreMouseEvents(false);
|
callback = () => {
|
||||||
console.log('Mouse events enabled');
|
const askWindow = windowPool.get('ask');
|
||||||
}
|
if (!askWindow || askWindow.isDestroyed()) return;
|
||||||
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
if (askWindow.isVisible()) {
|
||||||
});
|
askWindow.webContents.send('ask-global-send');
|
||||||
// console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
} else {
|
||||||
} catch (error) {
|
askWindow.show();
|
||||||
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();
|
|
||||||
updateLayout();
|
updateLayout();
|
||||||
header.setPosition(currentHeaderPosition.x, currentHeaderPosition.y, false);
|
askWindow.webContents.send('window-show-animation');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
askWindow.webContents.send('window-show-animation');
|
break;
|
||||||
} catch (e) {
|
case 'scrollUp':
|
||||||
console.error('Error showing Ask window:', e);
|
callback = () => {
|
||||||
}
|
// 'ask' 창을 명시적으로 가져옵니다.
|
||||||
}
|
const askWindow = windowPool.get('ask');
|
||||||
});
|
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
|
||||||
// console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`);
|
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||||
} catch (error) {
|
askWindow.webContents.send('scroll-response-up');
|
||||||
console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error);
|
}
|
||||||
}
|
};
|
||||||
}
|
break;
|
||||||
|
case 'scrollDown':
|
||||||
if (keybinds.manualScreenshot) {
|
callback = () => {
|
||||||
try {
|
// 'ask' 창을 명시적으로 가져옵니다.
|
||||||
globalShortcut.register(keybinds.manualScreenshot, () => {
|
const askWindow = windowPool.get('ask');
|
||||||
console.log('Manual screenshot shortcut triggered');
|
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
|
||||||
mainWindow.webContents.executeJavaScript(`
|
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||||
if (window.captureManualScreenshot) {
|
askWindow.webContents.send('scroll-response-down');
|
||||||
window.captureManualScreenshot();
|
}
|
||||||
} else {
|
};
|
||||||
console.log('Manual screenshot function not available');
|
break;
|
||||||
}
|
case 'moveUp':
|
||||||
`);
|
callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); };
|
||||||
});
|
break;
|
||||||
// console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`);
|
case 'moveDown':
|
||||||
} catch (error) {
|
callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); };
|
||||||
console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error);
|
break;
|
||||||
}
|
case 'moveLeft':
|
||||||
}
|
callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); };
|
||||||
|
break;
|
||||||
if (keybinds.previousResponse) {
|
case 'moveRight':
|
||||||
try {
|
callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); };
|
||||||
globalShortcut.register(keybinds.previousResponse, () => {
|
break;
|
||||||
console.log('Previous response shortcut triggered');
|
case 'toggleClickThrough':
|
||||||
sendToRenderer('navigate-previous-response');
|
callback = () => {
|
||||||
});
|
mouseEventsIgnored = !mouseEventsIgnored;
|
||||||
// console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
if(mainWindow && !mainWindow.isDestroyed()){
|
||||||
} catch (error) {
|
mainWindow.setIgnoreMouseEvents(mouseEventsIgnored, { forward: true });
|
||||||
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
|
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
break;
|
||||||
if (keybinds.nextResponse) {
|
case 'manualScreenshot':
|
||||||
try {
|
callback = () => {
|
||||||
globalShortcut.register(keybinds.nextResponse, () => {
|
if(mainWindow && !mainWindow.isDestroyed()) {
|
||||||
console.log('Next response shortcut triggered');
|
mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();');
|
||||||
sendToRenderer('navigate-next-response');
|
}
|
||||||
});
|
};
|
||||||
// console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
break;
|
||||||
} catch (error) {
|
case 'previousResponse':
|
||||||
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
|
callback = () => sendToRenderer('navigate-previous-response');
|
||||||
}
|
break;
|
||||||
}
|
case 'nextResponse':
|
||||||
|
callback = () => sendToRenderer('navigate-next-response');
|
||||||
if (keybinds.scrollUp) {
|
break;
|
||||||
try {
|
}
|
||||||
globalShortcut.register(keybinds.scrollUp, () => {
|
|
||||||
console.log('Scroll up shortcut triggered');
|
if (callback) {
|
||||||
sendToRenderer('scroll-response-up');
|
globalShortcut.register(accelerator, callback);
|
||||||
});
|
}
|
||||||
// console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
} catch(e) {
|
||||||
} catch (error) {
|
console.error(`Failed to register shortcut for "${action}" (${accelerator}):`, e.message);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -658,6 +658,8 @@ export class AskView extends LitElement {
|
|||||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
|
|
||||||
|
this.handleScroll = this.handleScroll.bind(this);
|
||||||
|
|
||||||
this.loadLibraries();
|
this.loadLibraries();
|
||||||
|
|
||||||
// --- Resize helpers ---
|
// --- Resize helpers ---
|
||||||
@ -863,6 +865,9 @@ export class AskView extends LitElement {
|
|||||||
|
|
||||||
ipcRenderer.on('ask-response-chunk', this.handleStreamChunk);
|
ipcRenderer.on('ask-response-chunk', this.handleStreamChunk);
|
||||||
ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd);
|
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 이벤트 리스너 등록 완료');
|
console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -901,9 +906,24 @@ export class AskView extends LitElement {
|
|||||||
|
|
||||||
ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk);
|
ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk);
|
||||||
ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd);
|
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 이벤트 리스너 제거 완료');
|
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 }) {
|
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 ////////
|
//////// after_modelStateService ////////
|
||||||
static properties = {
|
static properties = {
|
||||||
|
shortcuts: { type: Object, state: true },
|
||||||
firebaseUser: { type: Object, state: true },
|
firebaseUser: { type: Object, state: true },
|
||||||
isLoading: { type: Boolean, state: true },
|
isLoading: { type: Boolean, state: true },
|
||||||
isContentProtectionOn: { type: Boolean, state: true },
|
isContentProtectionOn: { type: Boolean, state: true },
|
||||||
@ -473,20 +461,8 @@ export class SettingsView extends LitElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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 ////////
|
//////// after_modelStateService ////////
|
||||||
|
this.shortcuts = {};
|
||||||
this.firebaseUser = null;
|
this.firebaseUser = null;
|
||||||
this.apiKeys = { openai: '', gemini: '', anthropic: '' };
|
this.apiKeys = { openai: '', gemini: '', anthropic: '' };
|
||||||
this.providerConfig = {};
|
this.providerConfig = {};
|
||||||
@ -507,55 +483,13 @@ export class SettingsView extends LitElement {
|
|||||||
//////// after_modelStateService ////////
|
//////// 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 ////////
|
//////// after_modelStateService ////////
|
||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
if (!window.require) return;
|
if (!window.require) return;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require('electron');
|
||||||
try {
|
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('get-current-user'),
|
||||||
ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드
|
ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드
|
||||||
ipcRenderer.invoke('model:get-all-keys'),
|
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-available-models', { type: 'stt' }),
|
||||||
ipcRenderer.invoke('model:get-selected-models'),
|
ipcRenderer.invoke('model:get-selected-models'),
|
||||||
ipcRenderer.invoke('settings:getPresets'),
|
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;
|
if (userState && userState.isLoggedIn) this.firebaseUser = userState;
|
||||||
@ -575,6 +510,7 @@ export class SettingsView extends LitElement {
|
|||||||
this.selectedStt = selectedModels.stt;
|
this.selectedStt = selectedModels.stt;
|
||||||
this.presets = presets || [];
|
this.presets = presets || [];
|
||||||
this.isContentProtectionOn = contentProtection;
|
this.isContentProtectionOn = contentProtection;
|
||||||
|
this.shortcuts = shortcuts || {};
|
||||||
if (this.presets.length > 0) {
|
if (this.presets.length > 0) {
|
||||||
const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
||||||
if (firstUserPreset) this.selectedPreset = firstUserPreset;
|
if (firstUserPreset) this.selectedPreset = firstUserPreset;
|
||||||
@ -668,6 +604,13 @@ export class SettingsView extends LitElement {
|
|||||||
}
|
}
|
||||||
//////// after_modelStateService ////////
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
|
openShortcutEditor() {
|
||||||
|
if (window.require) {
|
||||||
|
const { ipcRenderer } = window.require('electron');
|
||||||
|
ipcRenderer.invoke('open-shortcut-editor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@ -732,10 +675,15 @@ export class SettingsView extends LitElement {
|
|||||||
console.error('[SettingsView] Failed to refresh presets:', error);
|
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('user-state-changed', this._userStateListener);
|
||||||
ipcRenderer.on('settings-updated', this._settingsUpdatedListener);
|
ipcRenderer.on('settings-updated', this._settingsUpdatedListener);
|
||||||
ipcRenderer.on('presets-updated', this._presetsUpdatedListener);
|
ipcRenderer.on('presets-updated', this._presetsUpdatedListener);
|
||||||
|
ipcRenderer.on('shortcuts-updated', this._shortcutListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupIpcListeners() {
|
cleanupIpcListeners() {
|
||||||
@ -752,6 +700,9 @@ export class SettingsView extends LitElement {
|
|||||||
if (this._presetsUpdatedListener) {
|
if (this._presetsUpdatedListener) {
|
||||||
ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener);
|
ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener);
|
||||||
}
|
}
|
||||||
|
if (this._shortcutListener) {
|
||||||
|
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupWindowResize() {
|
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() {
|
getMainShortcuts() {
|
||||||
return [
|
return [
|
||||||
{ name: 'Show / Hide', key: '\\' },
|
{ name: 'Show / Hide', accelerator: this.shortcuts.toggleVisibility },
|
||||||
{ name: 'Ask Anything', key: '↵' },
|
{ name: 'Ask Anything', accelerator: this.shortcuts.nextStep },
|
||||||
{ name: 'Scroll AI Response', key: '↕' }
|
{ 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() {
|
togglePresets() {
|
||||||
this.showPresets = !this.showPresets;
|
this.showPresets = !this.showPresets;
|
||||||
}
|
}
|
||||||
@ -1131,14 +1109,20 @@ export class SettingsView extends LitElement {
|
|||||||
|
|
||||||
${apiKeyManagementHTML}
|
${apiKeyManagementHTML}
|
||||||
${modelSelectionHTML}
|
${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">
|
<div class="shortcuts-section">
|
||||||
${this.getMainShortcuts().map(shortcut => html`
|
${this.getMainShortcuts().map(shortcut => html`
|
||||||
<div class="shortcut-item">
|
<div class="shortcut-item">
|
||||||
<span class="shortcut-name">${shortcut.name}</span>
|
<span class="shortcut-name">${shortcut.name}</span>
|
||||||
<div class="shortcut-keys">
|
<div class="shortcut-keys">
|
||||||
<span class="cmd-key">⌘</span>
|
${this.renderShortcutKeys(shortcut.accelerator)}
|
||||||
<span class="shortcut-key">${shortcut.key}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</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