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()); const isValid = await this.validateApiKey(this.apiKey.trim());
if (isValid) { if (isValid) {
console.log('API key valid - starting slide out animation'); console.log('API key valid - checking system permissions...');
this.startSlideOutAnimation();
this.validatedApiKey = this.apiKey.trim(); 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 { } else {
this.errorMessage = 'Invalid API key - please check and try again'; this.errorMessage = 'Invalid API key - please check and try again';
console.log('API key validation failed'); 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() { startSlideOutAnimation() {
this.classList.add('sliding-out'); this.classList.add('sliding-out');
} }

View File

@ -10,25 +10,25 @@ export class AppHeader extends LitElement {
display: block; display: block;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
backface-visibility: hidden; 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) { :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) { :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) { :host(.sliding-in) {
animation: fadeIn 0.25s ease-out forwards; animation: fadeIn 0.2s ease-out forwards;
will-change: opacity;
} }
:host(.hidden) { :host(.hidden) {
opacity: 0; opacity: 0;
transform: translateY(-180%) scale(0.8); transform: translateY(-150%) scale(0.85);
pointer-events: none; pointer-events: none;
} }
@ -36,65 +36,50 @@ export class AppHeader extends LitElement {
0% { 0% {
opacity: 1; opacity: 1;
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
filter: blur(0px) brightness(1); filter: blur(0px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
} }
25% { 30% {
opacity: 0.85; opacity: 0.7;
transform: translateY(-20%) scale(0.96); transform: translateY(-20%) scale(0.98);
filter: blur(0px) brightness(0.95); filter: blur(0.5px);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.25);
} }
50% { 70% {
opacity: 0.5; opacity: 0.3;
transform: translateY(-60%) scale(0.9); transform: translateY(-80%) scale(0.92);
filter: blur(1px) brightness(0.85); filter: blur(1.5px);
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);
} }
100% { 100% {
opacity: 0; opacity: 0;
transform: translateY(-180%) scale(0.8); transform: translateY(-150%) scale(0.85);
filter: blur(3px) brightness(0.7); filter: blur(2px);
box-shadow: 0 0px 0px rgba(0, 0, 0, 0);
} }
} }
@keyframes slideDown { @keyframes slideDown {
0% { 0% {
opacity: 0; opacity: 0;
transform: translateY(-180%) scale(0.8); transform: translateY(-150%) scale(0.85);
filter: blur(3px) brightness(0.7); filter: blur(2px);
box-shadow: 0 0px 0px rgba(0, 0, 0, 0);
} }
40% { 30% {
opacity: 0.6; opacity: 0.5;
transform: translateY(-30%) scale(0.95); transform: translateY(-50%) scale(0.92);
filter: blur(1px) brightness(0.9); filter: blur(1px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
} }
70% { 65% {
opacity: 0.9; opacity: 0.9;
transform: translateY(-5%) scale(1.01); transform: translateY(-5%) scale(0.99);
filter: blur(0.3px) brightness(1.02); filter: blur(0.2px);
box-shadow: 0 7px 28px rgba(0, 0, 0, 0.28);
} }
85% { 85% {
opacity: 0.98; opacity: 0.98;
transform: translateY(1%) scale(0.995); transform: translateY(2%) scale(1.005);
filter: blur(0.1px) brightness(1.01); filter: blur(0px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.31);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
filter: blur(0px) brightness(1); filter: blur(0px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
} }
} }
@ -318,6 +303,7 @@ export class AppHeader extends LitElement {
this.hasSlidIn = false; this.hasSlidIn = false;
this.settingsHideTimer = null; this.settingsHideTimer = null;
this.isSessionActive = false; this.isSessionActive = false;
this.animationEndTimer = null;
if (window.require) { if (window.require) {
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
@ -388,7 +374,15 @@ export class AppHeader extends LitElement {
} }
toggleVisibility() { 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; this.isAnimating = true;
@ -403,17 +397,34 @@ export class AppHeader extends LitElement {
this.classList.remove('showing', 'hidden'); this.classList.remove('showing', 'hidden');
this.classList.add('hiding'); this.classList.add('hiding');
this.isVisible = false; this.isVisible = false;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('hiding')) {
this.handleAnimationEnd({ target: this });
}
}, 350);
} }
show() { show() {
this.classList.remove('hiding', 'hidden'); this.classList.remove('hiding', 'hidden');
this.classList.add('showing'); this.classList.add('showing');
this.isVisible = true; this.isVisible = true;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('showing')) {
this.handleAnimationEnd({ target: this });
}
}, 400);
} }
handleAnimationEnd(e) { handleAnimationEnd(e) {
if (e.target !== this) return; if (e.target !== this) return;
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
this.isAnimating = false; this.isAnimating = false;
if (this.classList.contains('hiding')) { if (this.classList.contains('hiding')) {
@ -434,7 +445,7 @@ export class AppHeader extends LitElement {
} else if (this.classList.contains('sliding-in')) { } else if (this.classList.contains('sliding-in')) {
this.classList.remove('sliding-in'); this.classList.remove('sliding-in');
this.hasSlidIn = true; 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(); super.disconnectedCallback();
this.removeEventListener('animationend', this.handleAnimationEnd); this.removeEventListener('animationend', this.handleAnimationEnd);
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
if (window.require) { if (window.require) {
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('toggle-header-visibility'); ipcRenderer.removeAllListeners('toggle-header-visibility');

View File

@ -81,11 +81,31 @@ class HeaderTransitionManager {
if (error) { if (error) {
console.warn('[HeaderController] Login payload indicates verification failure. Proceeding to AppHeader UI only.'); console.warn('[HeaderController] Login payload indicates verification failure. Proceeding to AppHeader UI only.');
this.transitionToAppHeader(); // 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) { } catch (error) {
console.error('[HeaderController] Sign-in failed', error); console.error('[HeaderController] Sign-in failed', error);
this.transitionToAppHeader(); // 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'); console.log('[HeaderController] Firebase sign-in successful via ID token');
} else { } else {
console.warn('[HeaderController] No ID token received from deeplink, virtual key request may fail'); console.warn('[HeaderController] No ID token received from deeplink, virtual key request may fail');
this.transitionToAppHeader(); // 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) { } catch (error) {
console.error('[HeaderController] Firebase auth failed:', error); console.error('[HeaderController] Firebase auth failed:', error);
@ -173,11 +203,25 @@ class HeaderTransitionManager {
} }
if (user) { if (user) {
console.log('[HeaderController] User is logged in, transitioning to AppHeader'); console.log('[HeaderController] User is logged in, checking permissions...');
this.transitionToAppHeader(!this.hasApiKey); 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) { } 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...');
this.transitionToAppHeader(false); const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
this.transitionToAppHeader(false);
} else {
console.log('[HeaderController] Permissions not granted, staying on ApiKeyHeader');
}
} else { } else {
console.log('[HeaderController] No auth & no API key — showing ApiKeyHeader'); console.log('[HeaderController] No auth & no API key — showing ApiKeyHeader');
this.transitionToApiKeyHeader(); this.transitionToApiKeyHeader();
@ -185,7 +229,6 @@ class HeaderTransitionManager {
}); });
} }
notifyHeaderState(stateOverride) { notifyHeaderState(stateOverride) {
const state = stateOverride || this.currentHeaderType || 'apikey'; const state = stateOverride || this.currentHeaderType || 'apikey';
if (window.require) { if (window.require) {
@ -193,33 +236,46 @@ class HeaderTransitionManager {
} }
} }
async _bootstrap() { async _bootstrap() {
let storedKey = null; let storedKey = null;
if (window.require) { if (window.require) {
try { try {
storedKey = await window storedKey = await window
.require('electron') .require('electron')
.ipcRenderer.invoke('get-current-api-key'); .ipcRenderer.invoke('get-current-api-key');
} catch (_) {} } catch (_) {}
} }
this.hasApiKey = !!storedKey; this.hasApiKey = !!storedKey;
const user = await new Promise(resolve => {
const unsubscribe = onAuthStateChanged(auth, u => {
unsubscribe();
resolve(u);
});
});
if (user || this.hasApiKey) {
await this._resizeForApp();
this.ensureHeader('app');
} else {
await this._resizeForApiKey();
this.ensureHeader('apikey');
}
}
const user = await new Promise(resolve => {
const unsubscribe = onAuthStateChanged(auth, u => {
unsubscribe();
resolve(u);
});
});
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) { async transitionToAppHeader(animate = true) {
if (this.currentHeaderType === 'app') { if (this.currentHeaderType === 'app') {
@ -249,23 +305,69 @@ class HeaderTransitionManager {
} }
_resizeForApp() { _resizeForApp() {
if (!window.require) return; if (!window.require) return;
return window return window
.require('electron') .require('electron')
.ipcRenderer.invoke('resize-header-window', { width: 353, height: 60 }) .ipcRenderer.invoke('resize-header-window', { width: 353, height: 60 })
.catch(() => {}); .catch(() => {});
}
async _resizeForApiKey() {
if (!window.require) return;
return window
.require('electron')
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 })
.catch(() => {});
}
async transitionToApiKeyHeader() {
await this._resizeForApiKey();
if (this.currentHeaderType !== 'apikey') {
this.ensureHeader('apikey');
}
if (this.apiKeyHeader) this.apiKeyHeader.reset();
}
async checkPermissions() {
if (!window.require) {
return { success: true };
} }
async transitionToApiKeyHeader() { const { ipcRenderer } = window.require('electron');
await window.require('electron')
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 }); try {
// Check permission status
const permissions = await ipcRenderer.invoke('check-system-permissions');
console.log('[HeaderController] Current permissions:', permissions);
if (this.currentHeaderType !== 'apikey') { if (!permissions.needsSetup) {
this.ensureHeader('apikey'); return { success: true };
}
if (this.apiKeyHeader) this.apiKeyHeader.reset();
} }
// 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', () => { window.addEventListener('DOMContentLoaded', () => {

File diff suppressed because it is too large Load Diff

View File

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