808 lines
29 KiB
JavaScript
808 lines
29 KiB
JavaScript
const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron');
|
|
const WindowLayoutManager = require('./windowLayoutManager');
|
|
const SmoothMovementManager = require('./smoothMovementManager');
|
|
const path = require('node:path');
|
|
const os = require('os');
|
|
const shortcutsService = require('../features/shortcuts/shortcutsService');
|
|
const internalBridge = require('../bridge/internalBridge');
|
|
const permissionRepository = require('../features/common/repositories/permission');
|
|
|
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
|
let liquidGlass;
|
|
const isLiquidGlassSupported = () => {
|
|
if (process.platform !== 'darwin') {
|
|
return false;
|
|
}
|
|
const majorVersion = parseInt(os.release().split('.')[0], 10);
|
|
// return majorVersion >= 25; // macOS 26+ (Darwin 25+)
|
|
return majorVersion >= 26; // See you soon!
|
|
};
|
|
let shouldUseLiquidGlass = isLiquidGlassSupported();
|
|
if (shouldUseLiquidGlass) {
|
|
try {
|
|
liquidGlass = require('electron-liquid-glass');
|
|
} catch (e) {
|
|
console.warn('Could not load optional dependency "electron-liquid-glass". The feature will be disabled.');
|
|
shouldUseLiquidGlass = false;
|
|
}
|
|
}
|
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
|
|
|
let isContentProtectionOn = true;
|
|
let lastVisibleWindows = new Set(['header']);
|
|
|
|
let currentHeaderState = 'apikey';
|
|
const windowPool = new Map();
|
|
|
|
let settingsHideTimer = null;
|
|
|
|
|
|
let layoutManager = null;
|
|
let movementManager = null;
|
|
|
|
|
|
function updateChildWindowLayouts(animated = true) {
|
|
if (movementManager.isAnimating) return;
|
|
|
|
const visibleWindows = {};
|
|
const listenWin = windowPool.get('listen');
|
|
const askWin = windowPool.get('ask');
|
|
if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
|
|
visibleWindows.listen = true;
|
|
}
|
|
if (askWin && !askWin.isDestroyed() && askWin.isVisible()) {
|
|
visibleWindows.ask = true;
|
|
}
|
|
|
|
if (Object.keys(visibleWindows).length === 0) return;
|
|
|
|
const newLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows);
|
|
movementManager.animateLayout(newLayout, animated);
|
|
}
|
|
|
|
const showSettingsWindow = () => {
|
|
internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true });
|
|
};
|
|
|
|
const hideSettingsWindow = () => {
|
|
internalBridge.emit('window:requestVisibility', { name: 'settings', visible: false });
|
|
};
|
|
|
|
const cancelHideSettingsWindow = () => {
|
|
internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true });
|
|
};
|
|
|
|
const moveWindowStep = (direction) => {
|
|
internalBridge.emit('window:moveStep', { direction });
|
|
};
|
|
|
|
const resizeHeaderWindow = ({ width, height }) => {
|
|
internalBridge.emit('window:resizeHeaderWindow', { width, height });
|
|
};
|
|
|
|
const handleHeaderAnimationFinished = (state) => {
|
|
internalBridge.emit('window:headerAnimationFinished', state);
|
|
};
|
|
|
|
const getHeaderPosition = () => {
|
|
return new Promise((resolve) => {
|
|
internalBridge.emit('window:getHeaderPosition', (position) => {
|
|
resolve(position);
|
|
});
|
|
});
|
|
};
|
|
|
|
const moveHeaderTo = (newX, newY) => {
|
|
internalBridge.emit('window:moveHeaderTo', { newX, newY });
|
|
};
|
|
|
|
const adjustWindowHeight = (sender, targetHeight) => {
|
|
internalBridge.emit('window:adjustWindowHeight', { sender, targetHeight });
|
|
};
|
|
|
|
|
|
function setupWindowController(windowPool, layoutManager, movementManager) {
|
|
internalBridge.on('window:requestVisibility', ({ name, visible }) => {
|
|
handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, visible);
|
|
});
|
|
internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => {
|
|
changeAllWindowsVisibility(windowPool, targetVisibility);
|
|
});
|
|
internalBridge.on('window:moveToDisplay', ({ displayId }) => {
|
|
// movementManager.moveToDisplay(displayId);
|
|
const header = windowPool.get('header');
|
|
if (header) {
|
|
const newPosition = layoutManager.calculateNewPositionForDisplay(header, displayId);
|
|
if (newPosition) {
|
|
movementManager.animateWindowPosition(header, newPosition, {
|
|
onComplete: () => updateChildWindowLayouts(true)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
internalBridge.on('window:moveToEdge', ({ direction }) => {
|
|
const header = windowPool.get('header');
|
|
if (header) {
|
|
const newPosition = layoutManager.calculateEdgePosition(header, direction);
|
|
movementManager.animateWindowPosition(header, newPosition, {
|
|
onComplete: () => updateChildWindowLayouts(true)
|
|
});
|
|
}
|
|
});
|
|
|
|
internalBridge.on('window:moveStep', ({ direction }) => {
|
|
const header = windowPool.get('header');
|
|
if (header) {
|
|
const newHeaderPosition = layoutManager.calculateStepMovePosition(header, direction);
|
|
if (!newHeaderPosition) return;
|
|
|
|
const futureHeaderBounds = { ...header.getBounds(), ...newHeaderPosition };
|
|
const visibleWindows = {};
|
|
const listenWin = windowPool.get('listen');
|
|
const askWin = windowPool.get('ask');
|
|
if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
|
|
visibleWindows.listen = true;
|
|
}
|
|
if (askWin && !askWin.isDestroyed() && askWin.isVisible()) {
|
|
visibleWindows.ask = true;
|
|
}
|
|
|
|
const newChildLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows, futureHeaderBounds);
|
|
|
|
movementManager.animateWindowPosition(header, newHeaderPosition);
|
|
movementManager.animateLayout(newChildLayout);
|
|
}
|
|
});
|
|
|
|
internalBridge.on('window:resizeHeaderWindow', ({ width, height }) => {
|
|
const header = windowPool.get('header');
|
|
if (!header || movementManager.isAnimating) return;
|
|
|
|
const newHeaderBounds = layoutManager.calculateHeaderResize(header, { width, height });
|
|
|
|
const wasResizable = header.isResizable();
|
|
if (!wasResizable) header.setResizable(true);
|
|
|
|
movementManager.animateWindowBounds(header, newHeaderBounds, {
|
|
onComplete: () => {
|
|
if (!wasResizable) header.setResizable(false);
|
|
updateChildWindowLayouts(true);
|
|
}
|
|
});
|
|
});
|
|
internalBridge.on('window:headerAnimationFinished', (state) => {
|
|
const header = windowPool.get('header');
|
|
if (!header || header.isDestroyed()) return;
|
|
|
|
if (state === 'hidden') {
|
|
header.hide();
|
|
} else if (state === 'visible') {
|
|
updateChildWindowLayouts(false);
|
|
}
|
|
});
|
|
internalBridge.on('window:getHeaderPosition', (reply) => {
|
|
const header = windowPool.get('header');
|
|
if (header && !header.isDestroyed()) {
|
|
reply(header.getBounds());
|
|
} else {
|
|
reply({ x: 0, y: 0, width: 0, height: 0 });
|
|
}
|
|
});
|
|
internalBridge.on('window:moveHeaderTo', ({ newX, newY }) => {
|
|
const header = windowPool.get('header');
|
|
if (header) {
|
|
const newPosition = layoutManager.calculateClampedPosition(header, { x: newX, y: newY });
|
|
header.setPosition(newPosition.x, newPosition.y);
|
|
}
|
|
});
|
|
internalBridge.on('window:adjustWindowHeight', ({ sender, targetHeight }) => {
|
|
const senderWindow = windowPool.get(sender);
|
|
if (senderWindow) {
|
|
const newBounds = layoutManager.calculateWindowHeightAdjustment(senderWindow, targetHeight);
|
|
|
|
const wasResizable = senderWindow.isResizable();
|
|
if (!wasResizable) senderWindow.setResizable(true);
|
|
|
|
movementManager.animateWindowBounds(senderWindow, newBounds, {
|
|
onComplete: () => {
|
|
if (!wasResizable) senderWindow.setResizable(false);
|
|
updateChildWindowLayouts(true);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function changeAllWindowsVisibility(windowPool, targetVisibility) {
|
|
const header = windowPool.get('header');
|
|
if (!header) return;
|
|
|
|
if (typeof targetVisibility === 'boolean' &&
|
|
header.isVisible() === targetVisibility) {
|
|
return;
|
|
}
|
|
|
|
if (header.isVisible()) {
|
|
lastVisibleWindows.clear();
|
|
|
|
windowPool.forEach((win, name) => {
|
|
if (win && !win.isDestroyed() && win.isVisible()) {
|
|
lastVisibleWindows.add(name);
|
|
}
|
|
});
|
|
|
|
lastVisibleWindows.forEach(name => {
|
|
if (name === 'header') return;
|
|
const win = windowPool.get(name);
|
|
if (win && !win.isDestroyed()) win.hide();
|
|
});
|
|
header.hide();
|
|
|
|
return;
|
|
}
|
|
|
|
lastVisibleWindows.forEach(name => {
|
|
const win = windowPool.get(name);
|
|
if (win && !win.isDestroyed())
|
|
win.show();
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Map<string, BrowserWindow>} windowPool
|
|
* @param {WindowLayoutManager} layoutManager
|
|
* @param {SmoothMovementManager} movementManager
|
|
* @param {'listen' | 'ask' | 'settings' | 'shortcut-settings'} name
|
|
* @param {boolean} shouldBeVisible
|
|
*/
|
|
async function handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, shouldBeVisible) {
|
|
console.log(`[WindowManager] Request: set '${name}' visibility to ${shouldBeVisible}`);
|
|
const win = windowPool.get(name);
|
|
|
|
if (!win || win.isDestroyed()) {
|
|
console.warn(`[WindowManager] Window '${name}' not found or destroyed.`);
|
|
return;
|
|
}
|
|
|
|
if (name !== 'settings') {
|
|
const isCurrentlyVisible = win.isVisible();
|
|
if (isCurrentlyVisible === shouldBeVisible) {
|
|
console.log(`[WindowManager] Window '${name}' is already in the desired state.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const disableClicks = (selectedWindow) => {
|
|
for (const [name, win] of windowPool) {
|
|
if (win !== selectedWindow && !win.isDestroyed()) {
|
|
win.setIgnoreMouseEvents(true, { forward: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
const restoreClicks = () => {
|
|
for (const [, win] of windowPool) {
|
|
if (!win.isDestroyed()) win.setIgnoreMouseEvents(false);
|
|
}
|
|
};
|
|
|
|
if (name === 'settings') {
|
|
if (shouldBeVisible) {
|
|
// Cancel any pending hide operations
|
|
if (settingsHideTimer) {
|
|
clearTimeout(settingsHideTimer);
|
|
settingsHideTimer = null;
|
|
}
|
|
const position = layoutManager.calculateSettingsWindowPosition();
|
|
if (position) {
|
|
win.setBounds(position);
|
|
win.__lockedByButton = true;
|
|
win.show();
|
|
win.moveTop();
|
|
win.setAlwaysOnTop(true);
|
|
} else {
|
|
console.warn('[WindowManager] Could not calculate settings window position.');
|
|
}
|
|
} else {
|
|
// Hide after a delay
|
|
if (settingsHideTimer) {
|
|
clearTimeout(settingsHideTimer);
|
|
}
|
|
settingsHideTimer = setTimeout(() => {
|
|
if (win && !win.isDestroyed()) {
|
|
win.setAlwaysOnTop(false);
|
|
win.hide();
|
|
}
|
|
settingsHideTimer = null;
|
|
}, 200);
|
|
|
|
win.__lockedByButton = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
if (name === 'shortcut-settings') {
|
|
if (shouldBeVisible) {
|
|
// layoutManager.positionShortcutSettingsWindow();
|
|
const newBounds = layoutManager.calculateShortcutSettingsWindowPosition();
|
|
if (newBounds) win.setBounds(newBounds);
|
|
|
|
if (process.platform === 'darwin') {
|
|
win.setAlwaysOnTop(true, 'screen-saver');
|
|
} else {
|
|
win.setAlwaysOnTop(true);
|
|
}
|
|
// globalShortcut.unregisterAll();
|
|
disableClicks(win);
|
|
win.show();
|
|
} else {
|
|
if (process.platform === 'darwin') {
|
|
win.setAlwaysOnTop(false, 'screen-saver');
|
|
} else {
|
|
win.setAlwaysOnTop(false);
|
|
}
|
|
restoreClicks();
|
|
win.hide();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (name === 'listen' || name === 'ask') {
|
|
const win = windowPool.get(name);
|
|
const otherName = name === 'listen' ? 'ask' : 'listen';
|
|
const otherWin = windowPool.get(otherName);
|
|
const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible();
|
|
|
|
const ANIM_OFFSET_X = 50;
|
|
const ANIM_OFFSET_Y = 20;
|
|
|
|
const finalVisibility = {
|
|
listen: (name === 'listen' && shouldBeVisible) || (otherName === 'listen' && isOtherWinVisible),
|
|
ask: (name === 'ask' && shouldBeVisible) || (otherName === 'ask' && isOtherWinVisible),
|
|
};
|
|
if (!shouldBeVisible) {
|
|
finalVisibility[name] = false;
|
|
}
|
|
|
|
const targetLayout = layoutManager.calculateFeatureWindowLayout(finalVisibility);
|
|
|
|
if (shouldBeVisible) {
|
|
if (!win) return;
|
|
const targetBounds = targetLayout[name];
|
|
if (!targetBounds) return;
|
|
|
|
const startPos = { ...targetBounds };
|
|
if (name === 'listen') startPos.x -= ANIM_OFFSET_X;
|
|
else if (name === 'ask') startPos.y -= ANIM_OFFSET_Y;
|
|
|
|
win.setOpacity(0);
|
|
win.setBounds(startPos);
|
|
win.show();
|
|
|
|
movementManager.fade(win, { to: 1 });
|
|
movementManager.animateLayout(targetLayout);
|
|
|
|
} else {
|
|
if (!win || !win.isVisible()) return;
|
|
|
|
const currentBounds = win.getBounds();
|
|
const targetPos = { ...currentBounds };
|
|
if (name === 'listen') targetPos.x -= ANIM_OFFSET_X;
|
|
else if (name === 'ask') targetPos.y -= ANIM_OFFSET_Y;
|
|
|
|
movementManager.fade(win, { to: 0, onComplete: () => win.hide() });
|
|
movementManager.animateWindowPosition(win, targetPos);
|
|
|
|
// 다른 창들도 새 레이아웃으로 애니메이션
|
|
const otherWindowsLayout = { ...targetLayout };
|
|
delete otherWindowsLayout[name];
|
|
movementManager.animateLayout(otherWindowsLayout);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const setContentProtection = (status) => {
|
|
isContentProtectionOn = status;
|
|
console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
|
|
windowPool.forEach(win => {
|
|
if (win && !win.isDestroyed()) {
|
|
win.setContentProtection(isContentProtectionOn);
|
|
}
|
|
});
|
|
};
|
|
|
|
const getContentProtectionStatus = () => isContentProtectionOn;
|
|
|
|
const toggleContentProtection = () => {
|
|
const newStatus = !getContentProtectionStatus();
|
|
setContentProtection(newStatus);
|
|
return newStatus;
|
|
};
|
|
|
|
|
|
const openLoginPage = () => {
|
|
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
|
const personalizeUrl = `${webUrl}/personalize?desktop=true`;
|
|
shell.openExternal(personalizeUrl);
|
|
console.log('Opening personalization page:', personalizeUrl);
|
|
};
|
|
|
|
|
|
function createFeatureWindows(header, namesToCreate) {
|
|
// if (windowPool.has('listen')) return;
|
|
|
|
const commonChildOptions = {
|
|
parent: header,
|
|
show: false,
|
|
frame: false,
|
|
transparent: true,
|
|
vibrancy: false,
|
|
hasShadow: false,
|
|
skipTaskbar: true,
|
|
hiddenInMissionControl: true,
|
|
resizable: false,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
preload: path.join(__dirname, '../preload.js'),
|
|
},
|
|
};
|
|
|
|
const createFeatureWindow = (name) => {
|
|
if (windowPool.has(name)) return;
|
|
|
|
switch (name) {
|
|
case 'listen': {
|
|
const listen = new BrowserWindow({
|
|
...commonChildOptions, width:400,minWidth:400,maxWidth:900,
|
|
maxHeight:900,
|
|
});
|
|
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, '../ui/app/content.html'), listenLoadOptions);
|
|
}
|
|
else {
|
|
listenLoadOptions.query.glass = 'true';
|
|
listen.loadFile(path.join(__dirname, '../ui/app/content.html'), listenLoadOptions);
|
|
listen.webContents.once('did-finish-load', () => {
|
|
const viewId = liquidGlass.addView(listen.getNativeWindowHandle());
|
|
if (viewId !== -1) {
|
|
liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles);
|
|
// liquidGlass.unstable_setScrim(viewId, 1);
|
|
// liquidGlass.unstable_setSubdued(viewId, 1);
|
|
}
|
|
});
|
|
}
|
|
if (!app.isPackaged) {
|
|
listen.webContents.openDevTools({ mode: 'detach' });
|
|
}
|
|
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, '../ui/app/content.html'), askLoadOptions);
|
|
}
|
|
else {
|
|
askLoadOptions.query.glass = 'true';
|
|
ask.loadFile(path.join(__dirname, '../ui/app/content.html'), askLoadOptions);
|
|
ask.webContents.once('did-finish-load', () => {
|
|
const viewId = liquidGlass.addView(ask.getNativeWindowHandle());
|
|
if (viewId !== -1) {
|
|
liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles);
|
|
// liquidGlass.unstable_setScrim(viewId, 1);
|
|
// liquidGlass.unstable_setSubdued(viewId, 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Open DevTools in development
|
|
if (!app.isPackaged) {
|
|
ask.webContents.openDevTools({ mode: 'detach' });
|
|
}
|
|
windowPool.set('ask', ask);
|
|
break;
|
|
}
|
|
|
|
// 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,'../ui/app/content.html'), settingsLoadOptions)
|
|
.catch(console.error);
|
|
}
|
|
else {
|
|
settingsLoadOptions.query.glass = 'true';
|
|
settings.loadFile(path.join(__dirname,'../ui/app/content.html'), settingsLoadOptions)
|
|
.catch(console.error);
|
|
settings.webContents.once('did-finish-load', () => {
|
|
const viewId = liquidGlass.addView(settings.getNativeWindowHandle());
|
|
if (viewId !== -1) {
|
|
liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles);
|
|
// liquidGlass.unstable_setScrim(viewId, 1);
|
|
// liquidGlass.unstable_setSubdued(viewId, 1);
|
|
}
|
|
});
|
|
}
|
|
windowPool.set('settings', settings);
|
|
|
|
if (!app.isPackaged) {
|
|
settings.webContents.openDevTools({ mode: 'detach' });
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'shortcut-settings': {
|
|
const shortcutEditor = new BrowserWindow({
|
|
...commonChildOptions,
|
|
width: 353,
|
|
height: 720,
|
|
modal: false,
|
|
parent: undefined,
|
|
alwaysOnTop: true,
|
|
titleBarOverlay: false,
|
|
});
|
|
|
|
shortcutEditor.setContentProtection(isContentProtectionOn);
|
|
shortcutEditor.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
|
if (process.platform === 'darwin') {
|
|
shortcutEditor.setWindowButtonVisibility(false);
|
|
}
|
|
|
|
const loadOptions = { query: { view: 'shortcut-settings' } };
|
|
if (!shouldUseLiquidGlass) {
|
|
shortcutEditor.loadFile(path.join(__dirname, '../ui/app/content.html'), loadOptions);
|
|
} else {
|
|
loadOptions.query.glass = 'true';
|
|
shortcutEditor.loadFile(path.join(__dirname, '../ui/app/content.html'), loadOptions);
|
|
shortcutEditor.webContents.once('did-finish-load', () => {
|
|
const viewId = liquidGlass.addView(shortcutEditor.getNativeWindowHandle());
|
|
if (viewId !== -1) {
|
|
liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles);
|
|
}
|
|
});
|
|
}
|
|
|
|
windowPool.set('shortcut-settings', shortcutEditor);
|
|
if (!app.isPackaged) {
|
|
shortcutEditor.webContents.openDevTools({ mode: 'detach' });
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (Array.isArray(namesToCreate)) {
|
|
namesToCreate.forEach(name => createFeatureWindow(name));
|
|
} else if (typeof namesToCreate === 'string') {
|
|
createFeatureWindow(namesToCreate);
|
|
} else {
|
|
createFeatureWindow('listen');
|
|
createFeatureWindow('ask');
|
|
createFeatureWindow('settings');
|
|
createFeatureWindow('shortcut-settings');
|
|
}
|
|
}
|
|
|
|
function destroyFeatureWindows() {
|
|
const featureWindows = ['listen','ask','settings','shortcut-settings'];
|
|
if (settingsHideTimer) {
|
|
clearTimeout(settingsHideTimer);
|
|
settingsHideTimer = null;
|
|
}
|
|
featureWindows.forEach(name=>{
|
|
const win = windowPool.get(name);
|
|
if (win && !win.isDestroyed()) win.destroy();
|
|
windowPool.delete(name);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function getCurrentDisplay(window) {
|
|
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
|
|
|
|
const windowBounds = window.getBounds();
|
|
const windowCenter = {
|
|
x: windowBounds.x + windowBounds.width / 2,
|
|
y: windowBounds.y + windowBounds.height / 2,
|
|
};
|
|
|
|
return screen.getDisplayNearestPoint(windowCenter);
|
|
}
|
|
|
|
|
|
|
|
function createWindows() {
|
|
const HEADER_HEIGHT = 47;
|
|
const DEFAULT_WINDOW_WIDTH = 353;
|
|
|
|
const primaryDisplay = screen.getPrimaryDisplay();
|
|
const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea;
|
|
|
|
const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2);
|
|
const initialY = workAreaY + 21;
|
|
|
|
const header = new BrowserWindow({
|
|
width: DEFAULT_WINDOW_WIDTH,
|
|
height: HEADER_HEIGHT,
|
|
x: initialX,
|
|
y: initialY,
|
|
frame: false,
|
|
transparent: true,
|
|
vibrancy: false,
|
|
hasShadow: false,
|
|
alwaysOnTop: true,
|
|
skipTaskbar: true,
|
|
hiddenInMissionControl: true,
|
|
resizable: false,
|
|
focusable: true,
|
|
acceptFirstMouse: true,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
preload: path.join(__dirname, '../preload.js'),
|
|
backgroundThrottling: false,
|
|
webSecurity: false,
|
|
enableRemoteModule: false,
|
|
// Ensure proper rendering and prevent pixelation
|
|
experimentalFeatures: false,
|
|
},
|
|
// Prevent pixelation and ensure proper rendering
|
|
useContentSize: true,
|
|
disableAutoHideCursor: true,
|
|
});
|
|
if (process.platform === 'darwin') {
|
|
header.setWindowButtonVisibility(false);
|
|
}
|
|
const headerLoadOptions = {};
|
|
if (!shouldUseLiquidGlass) {
|
|
header.loadFile(path.join(__dirname, '../ui/app/header.html'), headerLoadOptions);
|
|
}
|
|
else {
|
|
headerLoadOptions.query = { glass: 'true' };
|
|
header.loadFile(path.join(__dirname, '../ui/app/header.html'), headerLoadOptions);
|
|
header.webContents.once('did-finish-load', () => {
|
|
const viewId = liquidGlass.addView(header.getNativeWindowHandle());
|
|
if (viewId !== -1) {
|
|
liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles);
|
|
// liquidGlass.unstable_setScrim(viewId, 1);
|
|
// liquidGlass.unstable_setSubdued(viewId, 1);
|
|
}
|
|
});
|
|
}
|
|
windowPool.set('header', header);
|
|
layoutManager = new WindowLayoutManager(windowPool);
|
|
movementManager = new SmoothMovementManager(windowPool);
|
|
|
|
|
|
header.on('moved', () => {
|
|
if (movementManager.isAnimating) {
|
|
return;
|
|
}
|
|
updateChildWindowLayouts(false);
|
|
});
|
|
|
|
header.webContents.once('dom-ready', () => {
|
|
shortcutsService.initialize(windowPool);
|
|
shortcutsService.registerShortcuts();
|
|
});
|
|
|
|
setupIpcHandlers(windowPool, layoutManager);
|
|
setupWindowController(windowPool, layoutManager, movementManager);
|
|
|
|
if (currentHeaderState === 'main') {
|
|
createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
|
|
}
|
|
|
|
header.setContentProtection(isContentProtectionOn);
|
|
header.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
|
|
// Open DevTools in development
|
|
if (!app.isPackaged) {
|
|
header.webContents.openDevTools({ mode: 'detach' });
|
|
}
|
|
|
|
header.on('focus', () => {
|
|
console.log('[WindowManager] Header gained focus');
|
|
});
|
|
|
|
header.on('blur', () => {
|
|
console.log('[WindowManager] Header lost focus');
|
|
});
|
|
|
|
header.webContents.on('before-input-event', (event, input) => {
|
|
if (input.type === 'mouseDown') {
|
|
const target = input.target;
|
|
if (target && (target.includes('input') || target.includes('apikey'))) {
|
|
header.focus();
|
|
}
|
|
}
|
|
});
|
|
|
|
header.on('resize', () => updateChildWindowLayouts(false));
|
|
|
|
return windowPool;
|
|
}
|
|
|
|
|
|
function setupIpcHandlers(windowPool, layoutManager) {
|
|
screen.on('display-added', (event, newDisplay) => {
|
|
console.log('[Display] New display added:', newDisplay.id);
|
|
});
|
|
|
|
screen.on('display-removed', (event, oldDisplay) => {
|
|
console.log('[Display] Display removed:', oldDisplay.id);
|
|
const header = windowPool.get('header');
|
|
|
|
if (header && getCurrentDisplay(header).id === oldDisplay.id) {
|
|
const primaryDisplay = screen.getPrimaryDisplay();
|
|
const newPosition = layoutManager.calculateNewPositionForDisplay(header, primaryDisplay.id);
|
|
if (newPosition) {
|
|
// 복구 상황이므로 애니메이션 없이 즉시 이동
|
|
header.setPosition(newPosition.x, newPosition.y, false);
|
|
updateChildWindowLayouts(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
|
|
// 레이아웃 업데이트 함수를 새 버전으로 호출
|
|
updateChildWindowLayouts(false);
|
|
});
|
|
}
|
|
|
|
|
|
const handleHeaderStateChanged = (state) => {
|
|
console.log(`[WindowManager] Header state changed to: ${state}`);
|
|
currentHeaderState = state;
|
|
|
|
if (state === 'main') {
|
|
createFeatureWindows(windowPool.get('header'));
|
|
} else { // 'apikey' | 'permission'
|
|
destroyFeatureWindows();
|
|
}
|
|
internalBridge.emit('reregister-shortcuts');
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
createWindows,
|
|
windowPool,
|
|
toggleContentProtection,
|
|
resizeHeaderWindow,
|
|
getContentProtectionStatus,
|
|
showSettingsWindow,
|
|
hideSettingsWindow,
|
|
cancelHideSettingsWindow,
|
|
openLoginPage,
|
|
moveWindowStep,
|
|
handleHeaderStateChanged,
|
|
handleHeaderAnimationFinished,
|
|
getHeaderPosition,
|
|
moveHeaderTo,
|
|
adjustWindowHeight,
|
|
}; |