minor fix

This commit is contained in:
jhyang0 2025-07-05 00:30:02 +09:00
parent 4c51d5133c
commit ba8401345b
8 changed files with 986 additions and 434 deletions

View File

@ -1,17 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

@ -1,15 +0,0 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -1,35 +0,0 @@
version: '3.8'
services:
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: pickleglass-backend
restart: always
ports:
- "8000:8000"
environment:
- DATABASE_URL=/app/data/pickleglass.db
volumes:
- ./backend:/app
- ./data:/app/data
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
container_name: pickleglass-frontend
restart: always
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8000
depends_on:
- backend
volumes:
- .:/app
- /app/node_modules
volumes:
mongodb_data:

View File

@ -346,9 +346,18 @@ export class ApiKeyHeader extends LitElement {
const isValid = await this.validateApiKey(this.apiKey.trim());
if (isValid) {
console.log('API key valid - starting slide out animation');
console.log('API key valid - checking system permissions...');
const permissionResult = await this.checkAndRequestPermissions();
if (permissionResult.success) {
console.log('All permissions granted - starting slide out animation');
this.startSlideOutAnimation();
this.validatedApiKey = this.apiKey.trim();
} else {
this.errorMessage = permissionResult.error || 'Permission setup required';
console.log('Permission setup incomplete:', permissionResult);
}
} else {
this.errorMessage = 'Invalid API key - please check and try again';
console.log('API key validation failed');
@ -398,6 +407,58 @@ export class ApiKeyHeader extends LitElement {
}
}
async checkAndRequestPermissions() {
if (!window.require) {
return { success: true };
}
const { ipcRenderer } = window.require('electron');
try {
const permissions = await ipcRenderer.invoke('check-system-permissions');
console.log('[Permissions] Current status:', permissions);
if (!permissions.needsSetup) {
return { success: true };
}
if (!permissions.microphone) {
console.log('[Permissions] Requesting microphone permission...');
const micResult = await ipcRenderer.invoke('request-microphone-permission');
if (!micResult.success) {
console.log('[Permissions] Microphone permission denied');
await ipcRenderer.invoke('open-system-preferences', 'microphone');
return {
success: false,
error: 'Please grant microphone access in System Preferences'
};
}
}
if (!permissions.screen) {
console.log('[Permissions] Screen recording permission needed');
await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
this.errorMessage = 'Please grant screen recording permission and try again';
this.requestUpdate();
return {
success: false,
error: 'Please grant screen recording access in System Preferences'
};
}
return { success: true };
} catch (error) {
console.error('[Permissions] Error checking/requesting permissions:', error);
return {
success: false,
error: 'Failed to check permissions'
};
}
}
startSlideOutAnimation() {
this.classList.add('sliding-out');
}

View File

@ -10,25 +10,25 @@ export class AppHeader extends LitElement {
display: block;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.25s ease-out;
transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
will-change: transform, opacity;
}
:host(.hiding) {
animation: slideUp 0.45s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.6, 1) forwards;
}
:host(.showing) {
animation: slideDown 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
animation: slideDown 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
:host(.sliding-in) {
animation: fadeIn 0.25s ease-out forwards;
will-change: opacity;
animation: fadeIn 0.2s ease-out forwards;
}
:host(.hidden) {
opacity: 0;
transform: translateY(-180%) scale(0.8);
transform: translateY(-150%) scale(0.85);
pointer-events: none;
}
@ -36,65 +36,50 @@ export class AppHeader extends LitElement {
0% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px) brightness(1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
filter: blur(0px);
}
25% {
opacity: 0.85;
transform: translateY(-20%) scale(0.96);
filter: blur(0px) brightness(0.95);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.25);
30% {
opacity: 0.7;
transform: translateY(-20%) scale(0.98);
filter: blur(0.5px);
}
50% {
opacity: 0.5;
transform: translateY(-60%) scale(0.9);
filter: blur(1px) brightness(0.85);
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.15);
}
75% {
opacity: 0.15;
transform: translateY(-120%) scale(0.85);
filter: blur(2px) brightness(0.75);
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
70% {
opacity: 0.3;
transform: translateY(-80%) scale(0.92);
filter: blur(1.5px);
}
100% {
opacity: 0;
transform: translateY(-180%) scale(0.8);
filter: blur(3px) brightness(0.7);
box-shadow: 0 0px 0px rgba(0, 0, 0, 0);
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
}
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(-180%) scale(0.8);
filter: blur(3px) brightness(0.7);
box-shadow: 0 0px 0px rgba(0, 0, 0, 0);
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
40% {
opacity: 0.6;
transform: translateY(-30%) scale(0.95);
filter: blur(1px) brightness(0.9);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
30% {
opacity: 0.5;
transform: translateY(-50%) scale(0.92);
filter: blur(1px);
}
70% {
65% {
opacity: 0.9;
transform: translateY(-5%) scale(1.01);
filter: blur(0.3px) brightness(1.02);
box-shadow: 0 7px 28px rgba(0, 0, 0, 0.28);
transform: translateY(-5%) scale(0.99);
filter: blur(0.2px);
}
85% {
opacity: 0.98;
transform: translateY(1%) scale(0.995);
filter: blur(0.1px) brightness(1.01);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.31);
transform: translateY(2%) scale(1.005);
filter: blur(0px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px) brightness(1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
filter: blur(0px);
}
}
@ -318,6 +303,7 @@ export class AppHeader extends LitElement {
this.hasSlidIn = false;
this.settingsHideTimer = null;
this.isSessionActive = false;
this.animationEndTimer = null;
if (window.require) {
const { ipcRenderer } = window.require('electron');
@ -388,7 +374,15 @@ export class AppHeader extends LitElement {
}
toggleVisibility() {
if (this.isAnimating) return;
if (this.isAnimating) {
console.log('[AppHeader] Animation already in progress, ignoring toggle');
return;
}
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
this.isAnimating = true;
@ -403,17 +397,34 @@ export class AppHeader extends LitElement {
this.classList.remove('showing', 'hidden');
this.classList.add('hiding');
this.isVisible = false;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('hiding')) {
this.handleAnimationEnd({ target: this });
}
}, 350);
}
show() {
this.classList.remove('hiding', 'hidden');
this.classList.add('showing');
this.isVisible = true;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('showing')) {
this.handleAnimationEnd({ target: this });
}
}, 400);
}
handleAnimationEnd(e) {
if (e.target !== this) return;
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
this.isAnimating = false;
if (this.classList.contains('hiding')) {
@ -434,7 +445,7 @@ export class AppHeader extends LitElement {
} else if (this.classList.contains('sliding-in')) {
this.classList.remove('sliding-in');
this.hasSlidIn = true;
console.log('AppHeader slide-in animation completed');
console.log('[AppHeader] Slide-in animation completed');
}
}
@ -460,6 +471,11 @@ export class AppHeader extends LitElement {
super.disconnectedCallback();
this.removeEventListener('animationend', this.handleAnimationEnd);
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('toggle-header-visibility');

View File

@ -81,11 +81,31 @@ class HeaderTransitionManager {
if (error) {
console.warn('[HeaderController] Login payload indicates verification failure. Proceeding to AppHeader UI only.');
// Check permissions before transitioning
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader();
} else {
console.log('[HeaderController] Permissions not granted after login error');
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = permissionResult.error || 'Permission setup required';
this.apiKeyHeader.requestUpdate();
}
}
}
} catch (error) {
console.error('[HeaderController] Sign-in failed', error);
// Check permissions before transitioning
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader();
} else {
console.log('[HeaderController] Permissions not granted after sign-in failure');
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = permissionResult.error || 'Permission setup required';
this.apiKeyHeader.requestUpdate();
}
}
}
});
@ -125,7 +145,17 @@ class HeaderTransitionManager {
console.log('[HeaderController] Firebase sign-in successful via ID token');
} else {
console.warn('[HeaderController] No ID token received from deeplink, virtual key request may fail');
// Check permissions before transitioning
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader();
} else {
console.log('[HeaderController] Permissions not granted after Firebase auth');
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = permissionResult.error || 'Permission setup required';
this.apiKeyHeader.requestUpdate();
}
}
}
} catch (error) {
console.error('[HeaderController] Firebase auth failed:', error);
@ -173,11 +203,25 @@ class HeaderTransitionManager {
}
if (user) {
console.log('[HeaderController] User is logged in, transitioning to AppHeader');
console.log('[HeaderController] User is logged in, checking permissions...');
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader(!this.hasApiKey);
} else {
console.log('[HeaderController] Permissions not granted, staying on ApiKeyHeader');
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = permissionResult.error || 'Permission setup required';
this.apiKeyHeader.requestUpdate();
}
}
} else if (this.hasApiKey) {
console.log('[HeaderController] No Firebase user but API key exists, showing AppHeader');
console.log('[HeaderController] No Firebase user but API key exists, checking permissions...');
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader(false);
} else {
console.log('[HeaderController] Permissions not granted, staying on ApiKeyHeader');
}
} else {
console.log('[HeaderController] No auth & no API key — showing ApiKeyHeader');
this.transitionToApiKeyHeader();
@ -185,7 +229,6 @@ class HeaderTransitionManager {
});
}
notifyHeaderState(stateOverride) {
const state = stateOverride || this.currentHeaderType || 'apikey';
if (window.require) {
@ -212,14 +255,27 @@ class HeaderTransitionManager {
});
if (user || this.hasApiKey) {
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
await this._resizeForApp();
this.ensureHeader('app');
} else {
await this._resizeForApiKey();
this.ensureHeader('apikey');
}
}
setTimeout(() => {
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = permissionResult.error || 'Permission setup required';
this.apiKeyHeader.requestUpdate();
}
}, 100);
}
} else {
await this._resizeForApiKey();
this.ensureHeader('apikey');
}
}
async transitionToAppHeader(animate = true) {
if (this.currentHeaderType === 'app') {
@ -256,9 +312,16 @@ class HeaderTransitionManager {
.catch(() => {});
}
async _resizeForApiKey() {
if (!window.require) return;
return window
.require('electron')
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 })
.catch(() => {});
}
async transitionToApiKeyHeader() {
await window.require('electron')
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 });
await this._resizeForApiKey();
if (this.currentHeaderType !== 'apikey') {
this.ensureHeader('apikey');
@ -266,6 +329,45 @@ class HeaderTransitionManager {
if (this.apiKeyHeader) this.apiKeyHeader.reset();
}
async checkPermissions() {
if (!window.require) {
return { success: true };
}
const { ipcRenderer } = window.require('electron');
try {
// Check permission status
const permissions = await ipcRenderer.invoke('check-system-permissions');
console.log('[HeaderController] Current permissions:', permissions);
if (!permissions.needsSetup) {
return { success: true };
}
// If permissions are not set up, return false
let errorMessage = '';
if (!permissions.microphone && !permissions.screen) {
errorMessage = 'Microphone and screen recording access required';
} else if (!permissions.microphone) {
errorMessage = 'Microphone access required';
} else if (!permissions.screen) {
errorMessage = 'Screen recording access required';
}
return {
success: false,
error: errorMessage
};
} catch (error) {
console.error('[HeaderController] Error checking permissions:', error);
return {
success: false,
error: 'Failed to check permissions'
};
}
}
}
window.addEventListener('DOMContentLoaded', () => {

View File

@ -224,7 +224,7 @@ class WindowLayoutManager {
const PAD = 8;
/* ① 헤더 중심 X를 “디스플레이 기준 상대좌표”로 변환 */
/* ① 헤더 중심 X를 "디스플레이 기준 상대좌표"로 변환 */
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
let askBounds = askVisible ? ask.getBounds() : null;
@ -418,6 +418,49 @@ class SmoothMovementManager {
this.hiddenPosition = null;
this.lastVisiblePosition = null;
this.currentDisplayId = null;
this.currentAnimationTimer = null;
this.animationAbortController = null;
this.animationFrameRate = 16; // ~60fps
}
safeSetPosition(window, x, y) {
if (!window || window.isDestroyed()) {
return false;
}
let safeX = Number.isFinite(x) ? Math.round(x) : 0;
let safeY = Number.isFinite(y) ? Math.round(y) : 0;
if (Object.is(safeX, -0)) safeX = 0;
if (Object.is(safeY, -0)) safeY = 0;
safeX = parseInt(safeX, 10);
safeY = parseInt(safeY, 10);
if (!Number.isInteger(safeX) || !Number.isInteger(safeY)) {
console.error('[Movement] Invalid position after conversion:', { x: safeX, y: safeY, originalX: x, originalY: y });
return false;
}
try {
window.setPosition(safeX, safeY);
return true;
} catch (err) {
console.error('[Movement] setPosition failed with values:', { x: safeX, y: safeY }, err);
return false;
}
}
cancelCurrentAnimation() {
if (this.currentAnimationTimer) {
clearTimeout(this.currentAnimationTimer);
this.currentAnimationTimer = null;
}
if (this.animationAbortController) {
this.animationAbortController.abort();
this.animationAbortController = null;
}
this.isAnimating = false;
}
moveToDisplay(displayId) {
@ -456,50 +499,83 @@ class SmoothMovementManager {
this.currentDisplayId = targetDisplay.id;
}
hideToEdge(edge, callback) {
hideToEdge(edge, callback, errorCallback) {
const header = windowPool.get('header');
if (!header || !header.isVisible() || this.isAnimating) return;
if (!header || !header.isVisible()) {
if (errorCallback) errorCallback(new Error('Header not available or not visible'));
return;
}
// cancel current animation
this.cancelCurrentAnimation();
console.log(`[Movement] Hiding to ${edge} edge`);
const currentBounds = header.getBounds();
let currentBounds;
try {
currentBounds = header.getBounds();
} catch (err) {
console.error('[Movement] Failed to get header bounds:', err);
if (errorCallback) errorCallback(err);
return;
}
this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y };
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
const display = getCurrentDisplay(header);
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
const { x: workAreaX, y: workAreaY } = display.workArea;
const headerBounds = header.getBounds();
let targetX = this.headerPosition.x;
let targetY = this.headerPosition.y;
switch (edge) {
case 'top':
targetY = workAreaY - headerBounds.height - 20;
targetY = workAreaY - currentBounds.height - 20;
break;
case 'bottom':
targetY = workAreaY + screenHeight + 20;
break;
case 'left':
targetX = workAreaX - headerBounds.width - 20;
targetX = workAreaX - currentBounds.width - 20;
break;
case 'right':
targetX = workAreaX + screenWidth + 20;
break;
}
// 대상 위치 유효성 검사
if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) {
console.error('[Movement] Invalid target position:', { targetX, targetY });
if (errorCallback) errorCallback(new Error('Invalid target position'));
return;
}
this.hiddenPosition = { x: targetX, y: targetY, edge };
// create AbortController
this.animationAbortController = new AbortController();
const signal = this.animationAbortController.signal;
this.isAnimating = true;
const startX = this.headerPosition.x;
const startY = this.headerPosition.y;
const duration = 400;
const duration = 300;
const startTime = Date.now();
const animate = () => {
if (!header || typeof header.setPosition !== 'function' || header.isDestroyed()) {
// check aborted
if (signal.aborted) {
this.isAnimating = false;
if (errorCallback) errorCallback(new Error('Animation aborted'));
return;
}
// check destroyed
if (!header || header.isDestroyed()) {
this.isAnimating = false;
this.currentAnimationTimer = null;
if (errorCallback) errorCallback(new Error('Window destroyed during animation'));
return;
}
@ -510,44 +586,33 @@ class SmoothMovementManager {
const currentX = startX + (targetX - startX) * eased;
const currentY = startY + (targetY - startY) * eased;
// Validate computed positions before using
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
console.error('[Movement] Invalid animation values for hide:', {
currentX, currentY, progress, eased, startX, startY, targetX, targetY
});
this.isAnimating = false;
return;
}
// Safely call setPosition
try {
header.setPosition(Math.round(currentX), Math.round(currentY));
} catch (err) {
console.error('[Movement] Failed to set position:', err);
// set position safe
const success = this.safeSetPosition(header, currentX, currentY);
if (!success) {
this.isAnimating = false;
this.currentAnimationTimer = null;
if (errorCallback) errorCallback(new Error('Failed to set position'));
return;
}
if (progress < 1) {
setTimeout(animate, 8);
this.currentAnimationTimer = setTimeout(animate, this.animationFrameRate);
} else {
this.headerPosition = { x: targetX, y: targetY };
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
try {
header.setPosition(Math.round(targetX), Math.round(targetY));
} catch (err) {
console.error('[Movement] Failed to set final position:', err);
}
}
// set final position
this.safeSetPosition(header, targetX, targetY);
this.isAnimating = false;
this.currentAnimationTimer = null;
this.animationAbortController = null;
if (typeof callback === 'function') {
if (typeof callback === 'function' && !signal.aborted) {
try {
callback();
} catch (err) {
console.error('[Movement] Callback error:', err);
if (errorCallback) errorCallback(err);
}
}
@ -555,30 +620,62 @@ class SmoothMovementManager {
}
};
try {
animate();
} catch (err) {
console.error('[Movement] Animation start error:', err);
this.isAnimating = false;
if (errorCallback) errorCallback(err);
}
}
showFromEdge(callback) {
showFromEdge(callback, errorCallback) {
const header = windowPool.get('header');
if (!header || this.isAnimating || !this.hiddenPosition || !this.lastVisiblePosition) return;
if (!header || !this.hiddenPosition || !this.lastVisiblePosition) {
if (errorCallback) errorCallback(new Error('Cannot show - missing required data'));
return;
}
this.cancelCurrentAnimation();
console.log(`[Movement] Showing from ${this.hiddenPosition.edge} edge`);
header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y);
if (!this.safeSetPosition(header, this.hiddenPosition.x, this.hiddenPosition.y)) {
if (errorCallback) errorCallback(new Error('Failed to set initial position'));
return;
}
this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y };
const targetX = this.lastVisiblePosition.x;
const targetY = this.lastVisiblePosition.y;
if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) {
console.error('[Movement] Invalid target position for show:', { targetX, targetY });
if (errorCallback) errorCallback(new Error('Invalid target position for show'));
return;
}
this.animationAbortController = new AbortController();
const signal = this.animationAbortController.signal;
this.isAnimating = true;
const startX = this.headerPosition.x;
const startY = this.headerPosition.y;
const duration = 500;
const duration = 400;
const startTime = Date.now();
const animate = () => {
if (signal.aborted) {
this.isAnimating = false;
if (errorCallback) errorCallback(new Error('Animation aborted'));
return;
}
if (!header || header.isDestroyed()) {
this.isAnimating = false;
this.currentAnimationTimer = null;
if (errorCallback) errorCallback(new Error('Window destroyed during animation'));
return;
}
@ -592,34 +689,47 @@ class SmoothMovementManager {
const currentX = startX + (targetX - startX) * eased;
const currentY = startY + (targetY - startY) * eased;
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
console.error('[Movement] Invalid animation values for show:', { currentX, currentY, progress, eased });
const success = this.safeSetPosition(header, currentX, currentY);
if (!success) {
this.isAnimating = false;
this.currentAnimationTimer = null;
if (errorCallback) errorCallback(new Error('Failed to set position'));
return;
}
header.setPosition(Math.round(currentX), Math.round(currentY));
if (progress < 1) {
setTimeout(animate, 8);
this.currentAnimationTimer = setTimeout(animate, this.animationFrameRate);
} else {
this.headerPosition = { x: targetX, y: targetY };
this.headerPosition = { x: targetX, y: targetY };
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
header.setPosition(Math.round(targetX), Math.round(targetY));
}
this.safeSetPosition(header, targetX, targetY);
this.isAnimating = false;
this.currentAnimationTimer = null;
this.animationAbortController = null;
this.hiddenPosition = null;
this.lastVisiblePosition = null;
if (callback) callback();
if (typeof callback === 'function' && !signal.aborted) {
try {
callback();
} catch (err) {
console.error('[Movement] Show callback error:', err);
if (errorCallback) errorCallback(err);
}
}
console.log(`[Movement] Show from edge completed`);
}
};
try {
animate();
} catch (err) {
console.error('[Movement] Animation start error:', err);
this.isAnimating = false;
if (errorCallback) errorCallback(err);
}
}
moveStep(direction) {
@ -682,6 +792,9 @@ class SmoothMovementManager {
}
animateToPosition(header, targetX, targetY) {
// cancel animation
this.cancelCurrentAnimation();
this.isAnimating = true;
const startX = this.headerPosition.x;
@ -694,9 +807,14 @@ class SmoothMovementManager {
return;
}
this.animationAbortController = new AbortController();
const signal = this.animationAbortController.signal;
const animate = () => {
if (!header || header.isDestroyed()) {
if (signal.aborted || !header || header.isDestroyed()) {
this.isAnimating = false;
this.currentAnimationTimer = null;
return;
}
@ -708,24 +826,24 @@ class SmoothMovementManager {
const currentX = startX + (targetX - startX) * eased;
const currentY = startY + (targetY - startY) * eased;
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
console.error('[Movement] Invalid animation values:', { currentX, currentY, progress, eased });
const success = this.safeSetPosition(header, currentX, currentY);
if (!success) {
this.isAnimating = false;
this.currentAnimationTimer = null;
return;
}
header.setPosition(Math.round(currentX), Math.round(currentY));
if (progress < 1) {
setTimeout(animate, 8);
this.currentAnimationTimer = setTimeout(animate, this.animationFrameRate);
} else {
this.headerPosition = { x: targetX, y: targetY };
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
header.setPosition(Math.round(targetX), Math.round(targetY));
} else {
console.warn('[Movement] Final position invalid, skip setPosition:', { targetX, targetY });
}
this.safeSetPosition(header, targetX, targetY);
this.isAnimating = false;
this.currentAnimationTimer = null;
this.animationAbortController = null;
updateLayout();
@ -738,16 +856,23 @@ class SmoothMovementManager {
moveToEdge(direction) {
const header = windowPool.get('header');
if (!header || !header.isVisible() || this.isAnimating) return;
if (!header || !header.isVisible()) return;
this.cancelCurrentAnimation();
console.log(`[Movement] Move to edge: ${direction}`);
const display = getCurrentDisplay(header);
const { width, height } = display.workAreaSize;
const { x: workAreaX, y: workAreaY } = display.workArea;
const headerBounds = header.getBounds();
const currentBounds = header.getBounds();
let currentBounds;
try {
currentBounds = header.getBounds();
} catch (err) {
console.error('[Movement] Failed to get header bounds:', err);
return;
}
let targetX = currentBounds.x;
let targetY = currentBounds.y;
@ -756,23 +881,26 @@ class SmoothMovementManager {
targetX = workAreaX;
break;
case 'right':
targetX = workAreaX + width - headerBounds.width;
targetX = workAreaX + width - currentBounds.width;
break;
case 'up':
targetY = workAreaY;
break;
case 'down':
targetY = workAreaY + height - headerBounds.height;
targetY = workAreaY + height - currentBounds.height;
break;
}
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
this.animationAbortController = new AbortController();
const signal = this.animationAbortController.signal;
this.isAnimating = true;
const startX = this.headerPosition.x;
const startY = this.headerPosition.y;
const duration = 400;
const startTime = Date.now(); // 이 줄을 animate 함수 정의 전으로 이동
const duration = 350;
const startTime = Date.now();
if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
console.error('[Movement] Invalid edge position values:', { startX, startY, targetX, targetY });
@ -781,8 +909,9 @@ class SmoothMovementManager {
}
const animate = () => {
if (!header || header.isDestroyed()) {
if (signal.aborted || !header || header.isDestroyed()) {
this.isAnimating = false;
this.currentAnimationTimer = null;
return;
}
@ -794,22 +923,24 @@ class SmoothMovementManager {
const currentX = startX + (targetX - startX) * eased;
const currentY = startY + (targetY - startY) * eased;
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
console.error('[Movement] Invalid edge animation values:', { currentX, currentY, progress, eased });
const success = this.safeSetPosition(header, currentX, currentY);
if (!success) {
this.isAnimating = false;
this.currentAnimationTimer = null;
return;
}
header.setPosition(Math.round(currentX), Math.round(currentY));
if (progress < 1) {
setTimeout(animate, 8);
this.currentAnimationTimer = setTimeout(animate, this.animationFrameRate);
} else {
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
header.setPosition(Math.round(targetX), Math.round(targetY));
}
this.safeSetPosition(header, targetX, targetY);
this.headerPosition = { x: targetX, y: targetY };
this.isAnimating = false;
this.currentAnimationTimer = null;
this.animationAbortController = null;
updateLayout();
@ -829,6 +960,7 @@ class SmoothMovementManager {
}
destroy() {
this.cancelCurrentAnimation();
this.isAnimating = false;
console.log('[Movement] Destroyed');
}
@ -837,19 +969,117 @@ class SmoothMovementManager {
const layoutManager = new WindowLayoutManager();
let movementManager = null;
function toggleAllWindowsVisibility() {
const header = windowPool.get('header');
if (!header) return;
function isWindowSafe(window) {
return window && !window.isDestroyed() && typeof window.getBounds === 'function';
}
function safeWindowOperation(window, operation, fallback = null) {
if (!isWindowSafe(window)) {
console.warn('[WindowManager] Window not safe for operation');
return fallback;
}
try {
return operation(window);
} catch (error) {
console.error('[WindowManager] Window operation failed:', error);
return fallback;
}
}
function safeSetPosition(window, x, y) {
return safeWindowOperation(window, (win) => {
win.setPosition(Math.round(x), Math.round(y));
return true;
}, false);
}
function safeGetBounds(window) {
return safeWindowOperation(window, (win) => win.getBounds(), null);
}
function safeShow(window) {
return safeWindowOperation(window, (win) => {
win.show();
return true;
}, false);
}
function safeHide(window) {
return safeWindowOperation(window, (win) => {
win.hide();
return true;
}, false);
}
let toggleState = {
isToggling: false,
lastToggleTime: 0,
pendingToggle: null,
toggleDebounceTimer: null,
failsafeTimer: null
};
function toggleAllWindowsVisibility() {
const now = Date.now();
const timeSinceLastToggle = now - toggleState.lastToggleTime;
if (timeSinceLastToggle < 200) {
console.log('[Visibility] Toggle ignored - too fast (debounced)');
return;
}
if (toggleState.isToggling) {
console.log('[Visibility] Toggle in progress, queueing request');
if (toggleState.toggleDebounceTimer) {
clearTimeout(toggleState.toggleDebounceTimer);
}
toggleState.toggleDebounceTimer = setTimeout(() => {
toggleState.toggleDebounceTimer = null;
if (!toggleState.isToggling) {
toggleAllWindowsVisibility();
}
}, 300);
return;
}
const header = windowPool.get('header');
if (!header || header.isDestroyed()) {
console.error('[Visibility] Header window not found or destroyed');
return;
}
toggleState.isToggling = true;
toggleState.lastToggleTime = now;
const resetToggleState = () => {
toggleState.isToggling = false;
if (toggleState.toggleDebounceTimer) {
clearTimeout(toggleState.toggleDebounceTimer);
toggleState.toggleDebounceTimer = null;
}
if (toggleState.failsafeTimer) {
clearTimeout(toggleState.failsafeTimer);
toggleState.failsafeTimer = null;
}
};
toggleState.failsafeTimer = setTimeout(() => {
console.warn('[Visibility] Toggle operation timed out, resetting state');
resetToggleState();
}, 2000);
try {
if (header.isVisible()) {
console.log('[Visibility] Smart hiding - calculating nearest edge');
const headerBounds = header.getBounds();
const display = screen.getPrimaryDisplay();
const display = getCurrentDisplay(header);
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
const { x: workAreaX, y: workAreaY } = display.workArea;
const centerX = headerBounds.x + headerBounds.width / 2;
const centerY = headerBounds.y + headerBounds.height / 2;
const centerX = headerBounds.x + headerBounds.width / 2 - workAreaX;
const centerY = headerBounds.y + headerBounds.height / 2 - workAreaY;
const distances = {
top: centerY,
@ -858,54 +1088,100 @@ function toggleAllWindowsVisibility() {
right: screenWidth - centerX,
};
const nearestEdge = Object.keys(distances).reduce((nearest, edge) => (distances[edge] < distances[nearest] ? edge : nearest));
const nearestEdge = Object.keys(distances).reduce((nearest, edge) =>
(distances[edge] < distances[nearest] ? edge : nearest)
);
console.log(`[Visibility] Nearest edge: ${nearestEdge} (distance: ${distances[nearestEdge].toFixed(1)}px)`);
lastVisibleWindows.clear();
lastVisibleWindows.add('header');
const hidePromises = [];
windowPool.forEach((win, name) => {
if (win.isVisible()) {
if (win && !win.isDestroyed() && win.isVisible() && name !== 'header') {
lastVisibleWindows.add(name);
if (name !== 'header') {
win.webContents.send('window-hide-animation');
hidePromises.push(new Promise(resolve => {
setTimeout(() => {
if (!win.isDestroyed()) {
win.hide();
}
}, 200);
}
resolve();
}, 180); // 200ms ->180ms
}));
}
});
console.log('[Visibility] Visible windows before hide:', Array.from(lastVisibleWindows));
Promise.all(hidePromises).then(() => {
if (!movementManager || header.isDestroyed()) {
resetToggleState();
return;
}
movementManager.hideToEdge(nearestEdge, () => {
if (!header.isDestroyed()) {
header.hide();
}
resetToggleState();
console.log('[Visibility] Smart hide completed');
}, (error) => {
console.error('[Visibility] Error in hideToEdge:', error);
resetToggleState();
});
}).catch(err => {
console.error('[Visibility] Error during hide:', err);
resetToggleState();
});
} else {
console.log('[Visibility] Smart showing from hidden position');
console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
header.show();
if (!movementManager) {
console.error('[Visibility] Movement manager not initialized');
resetToggleState();
return;
}
movementManager.showFromEdge(() => {
const showPromises = [];
lastVisibleWindows.forEach(name => {
if (name === 'header') return;
const win = windowPool.get(name);
if (win && !win.isDestroyed()) {
showPromises.push(new Promise(resolve => {
win.show();
win.webContents.send('window-show-animation');
setTimeout(resolve, 100);
}));
}
});
Promise.all(showPromises).then(() => {
setImmediate(updateLayout);
setTimeout(updateLayout, 120);
setTimeout(updateLayout, 100);
resetToggleState();
console.log('[Visibility] Smart show completed');
}).catch(err => {
console.error('[Visibility] Error during show:', err);
resetToggleState();
});
}, (error) => {
console.error('[Visibility] Error in showFromEdge:', error);
resetToggleState();
});
}
} catch (error) {
console.error('[Visibility] Unexpected error in toggle:', error);
resetToggleState();
}
}
@ -926,6 +1202,21 @@ function ensureDataDirectories() {
}
function createWindows() {
if (movementManager) {
movementManager.destroy();
movementManager = null;
}
toggleState.isToggling = false;
if (toggleState.toggleDebounceTimer) {
clearTimeout(toggleState.toggleDebounceTimer);
toggleState.toggleDebounceTimer = null;
}
if (toggleState.failsafeTimer) {
clearTimeout(toggleState.failsafeTimer);
toggleState.failsafeTimer = null;
}
const primaryDisplay = screen.getPrimaryDisplay();
const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea;
@ -994,11 +1285,25 @@ function createWindows() {
loadAndRegisterShortcuts();
});
ipcMain.handle('toggle-all-windows-visibility', toggleAllWindowsVisibility);
ipcMain.handle('toggle-all-windows-visibility', () => {
try {
toggleAllWindowsVisibility();
} catch (error) {
console.error('[WindowManager] Error in toggle-all-windows-visibility:', error);
toggleState.isToggling = false;
}
});
ipcMain.handle('toggle-feature', async (event, featureName) => {
try {
const header = windowPool.get('header');
if (!header || header.isDestroyed()) {
console.error('[WindowManager] Header window not available');
return;
}
if (!windowPool.get(featureName) && currentHeaderState === 'app') {
createFeatureWindows(windowPool.get('header'));
createFeatureWindows(header);
}
if (!windowPool.get(featureName) && currentHeaderState === 'app') {
@ -1142,6 +1447,10 @@ function createWindows() {
console.error('Available windows:', Array.from(windowPool.keys()));
}
}
} catch (error) {
console.error('[WindowManager] Error in toggle-feature:', error);
toggleState.isToggling = false;
}
});
ipcMain.handle('send-question-to-ask', (event, question) => {
@ -1228,13 +1537,51 @@ function loadAndRegisterShortcuts() {
}
function updateLayout() {
if (layoutManager._updateTimer) {
clearTimeout(layoutManager._updateTimer);
}
layoutManager._updateTimer = setTimeout(() => {
layoutManager._updateTimer = null;
layoutManager.updateLayout();
}, 16);
}
function setupIpcHandlers(openaiSessionRef) {
const layoutManager = new WindowLayoutManager();
// const movementManager = new SmoothMovementManager();
//cleanup
app.on('before-quit', () => {
console.log('[WindowManager] App is quitting, cleaning up...');
if (movementManager) {
movementManager.destroy();
}
if (toggleState.toggleDebounceTimer) {
clearTimeout(toggleState.toggleDebounceTimer);
toggleState.toggleDebounceTimer = null;
}
if (toggleState.failsafeTimer) {
clearTimeout(toggleState.failsafeTimer);
toggleState.failsafeTimer = null;
}
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
settingsHideTimer = null;
}
windowPool.forEach((win, name) => {
if (win && !win.isDestroyed()) {
win.destroy();
}
});
windowPool.clear();
});
screen.on('display-added', (event, newDisplay) => {
console.log('[Display] New display added:', newDisplay.id);
});
@ -1823,6 +2170,99 @@ function setupIpcHandlers(openaiSessionRef) {
header.webContents.send('request-firebase-logout');
}
});
ipcMain.handle('check-system-permissions', async () => {
const { systemPreferences } = require('electron');
const permissions = {
microphone: false,
screen: false,
needsSetup: false
};
try {
if (process.platform === 'darwin') {
// Check microphone permission on macOS
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
permissions.microphone = micStatus === 'granted';
try {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width: 1, height: 1 }
});
permissions.screen = sources && sources.length > 0;
} catch (err) {
console.log('[Permissions] Screen capture test failed:', err);
permissions.screen = false;
}
permissions.needsSetup = !permissions.microphone || !permissions.screen;
} else {
permissions.microphone = true;
permissions.screen = true;
permissions.needsSetup = false;
}
console.log('[Permissions] System permissions status:', permissions);
return permissions;
} catch (error) {
console.error('[Permissions] Error checking permissions:', error);
return {
microphone: false,
screen: false,
needsSetup: true,
error: error.message
};
}
});
ipcMain.handle('request-microphone-permission', async () => {
if (process.platform !== 'darwin') {
return { success: true };
}
const { systemPreferences } = require('electron');
try {
const status = systemPreferences.getMediaAccessStatus('microphone');
if (status === 'granted') {
return { success: true, status: 'already-granted' };
}
// Req mic permission
const granted = await systemPreferences.askForMediaAccess('microphone');
return {
success: granted,
status: granted ? 'granted' : 'denied'
};
} catch (error) {
console.error('[Permissions] Error requesting microphone permission:', error);
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('open-system-preferences', async (event, section) => {
if (process.platform !== 'darwin') {
return { success: false, error: 'Not supported on this platform' };
}
try {
// Open System Preferences to Privacy & Security > Screen Recording
if (section === 'screen-recording') {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
} else if (section === 'microphone') {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');
} else {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy');
}
return { success: true };
} catch (error) {
console.error('[Permissions] Error opening system preferences:', error);
return { success: false, error: error.message };
}
});
}
let storedApiKey = null;

View File

@ -1,9 +1,9 @@
try {
const reloader = require('electron-reloader');
reloader(module, {
});
} catch (err) {
}
// try {
// const reloader = require('electron-reloader');
// reloader(module, {
// });
// } catch (err) {
// }
require('dotenv').config();