Add permission setup flow
This commit is contained in:
		
							parent
							
								
									552a6bebcd
								
							
						
					
					
						commit
						d2ab6570ea
					
				@ -268,7 +268,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
 | 
			
		||||
        this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this);
 | 
			
		||||
        this.handleProviderChange = this.handleProviderChange.bind(this);
 | 
			
		||||
        this.checkAndRequestPermissions = this.checkAndRequestPermissions.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reset() {
 | 
			
		||||
@ -407,18 +406,10 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider);
 | 
			
		||||
            
 | 
			
		||||
            if (isValid) {
 | 
			
		||||
                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');
 | 
			
		||||
                console.log('API key valid - starting slide out animation');
 | 
			
		||||
                    this.startSlideOutAnimation();
 | 
			
		||||
                    this.validatedApiKey = this.apiKey.trim();
 | 
			
		||||
                    this.validatedProvider = this.selectedProvider;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.errorMessage = permissionResult.error || 'Permission setup required';
 | 
			
		||||
                    console.log('Permission setup incomplete:', permissionResult);
 | 
			
		||||
                }
 | 
			
		||||
                this.validatedProvider = this.selectedProvider;
 | 
			
		||||
            } else {
 | 
			
		||||
                this.errorMessage = 'Invalid API key - please check and try again';
 | 
			
		||||
                console.log('API key validation failed');
 | 
			
		||||
@ -497,45 +488,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
                    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');
 | 
			
		||||
                return {
 | 
			
		||||
                    success: false,
 | 
			
		||||
                    error: 'Please grant screen recording access in System Preferences',
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
    
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.error('[Permissions] Error checking/requesting permissions:', err);
 | 
			
		||||
            return { success: false, error: 'Failed to check permissions' };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    startSlideOutAnimation() {
 | 
			
		||||
        this.classList.add('sliding-out');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithCredential,
 | 
			
		||||
 | 
			
		||||
import './AppHeader.js';
 | 
			
		||||
import './ApiKeyHeader.js';
 | 
			
		||||
import './PermissionSetup.js';
 | 
			
		||||
 | 
			
		||||
const firebaseConfig = {
 | 
			
		||||
    apiKey: 'AIzaSyAgtJrmsFWG1C7m9S55HyT1laICEzuUS2g',
 | 
			
		||||
@ -21,31 +22,40 @@ class HeaderTransitionManager {
 | 
			
		||||
    constructor() {
 | 
			
		||||
 | 
			
		||||
        this.headerContainer      = document.getElementById('header-container');
 | 
			
		||||
        this.currentHeaderType    = null;   // 'apikey' | 'app'
 | 
			
		||||
        this.currentHeaderType    = null;   // 'apikey' | 'app' | 'permission'
 | 
			
		||||
        this.apiKeyHeader         = null;
 | 
			
		||||
        this.appHeader            = null;
 | 
			
		||||
        this.permissionSetup      = null;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * only one header window is allowed
 | 
			
		||||
         * @param {'apikey'|'app'} type
 | 
			
		||||
         * @param {'apikey'|'app'|'permission'} type
 | 
			
		||||
         */
 | 
			
		||||
        this.ensureHeader = (type) => {
 | 
			
		||||
            if (this.currentHeaderType === type) return;
 | 
			
		||||
 | 
			
		||||
            if (this.apiKeyHeader) { this.apiKeyHeader.remove(); this.apiKeyHeader = null; }
 | 
			
		||||
            if (this.appHeader)    { this.appHeader.remove();    this.appHeader   = null; }
 | 
			
		||||
            this.headerContainer.innerHTML = '';
 | 
			
		||||
            
 | 
			
		||||
            this.apiKeyHeader = null;
 | 
			
		||||
            this.appHeader = null;
 | 
			
		||||
            this.permissionSetup = null;
 | 
			
		||||
 | 
			
		||||
            // Create new header element
 | 
			
		||||
            if (type === 'apikey') {
 | 
			
		||||
                this.apiKeyHeader      = document.createElement('apikey-header');
 | 
			
		||||
                this.apiKeyHeader = document.createElement('apikey-header');
 | 
			
		||||
                this.headerContainer.appendChild(this.apiKeyHeader);
 | 
			
		||||
            } else if (type === 'permission') {
 | 
			
		||||
                this.permissionSetup = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionSetup.continueCallback = () => this.transitionToAppHeader();
 | 
			
		||||
                this.headerContainer.appendChild(this.permissionSetup);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.appHeader         = document.createElement('app-header');
 | 
			
		||||
                this.appHeader = document.createElement('app-header');
 | 
			
		||||
                this.headerContainer.appendChild(this.appHeader);
 | 
			
		||||
                this.appHeader.startSlideInAnimation?.();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentHeaderType = type;
 | 
			
		||||
            this.notifyHeaderState(type);
 | 
			
		||||
            this.notifyHeaderState(type === 'permission' ? 'apikey' : type); // Keep permission state as apikey for compatibility
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        console.log('[HeaderController] Manager initialized');
 | 
			
		||||
@ -80,32 +90,14 @@ 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();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        console.warn('[HeaderController] Login payload indicates verification failure. Showing permission setup.');
 | 
			
		||||
                        // Show permission setup after login error
 | 
			
		||||
                        this.transitionToPermissionSetup();
 | 
			
		||||
                    }
 | 
			
		||||
                } 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();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    // Show permission setup after sign-in failure
 | 
			
		||||
                    this.transitionToPermissionSetup();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
@ -122,7 +114,10 @@ class HeaderTransitionManager {
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('api-key-validated', () => {
 | 
			
		||||
                this.hasApiKey = true;
 | 
			
		||||
                this.transitionToAppHeader();
 | 
			
		||||
                // Wait for animation to complete before transitioning
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    this.transitionToPermissionSetup();
 | 
			
		||||
                }, 350); // Give time for slide-out animation to complete
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('api-key-removed', () => {
 | 
			
		||||
@ -133,7 +128,7 @@ class HeaderTransitionManager {
 | 
			
		||||
            ipcRenderer.on('api-key-updated', () => {
 | 
			
		||||
                this.hasApiKey = true;
 | 
			
		||||
                if (!auth.currentUser) {
 | 
			
		||||
                    this.transitionToAppHeader();
 | 
			
		||||
                    this.transitionToPermissionSetup();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@ -145,22 +140,13 @@ class HeaderTransitionManager {
 | 
			
		||||
                        await signInWithCredential(auth, credential);
 | 
			
		||||
                        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();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        console.warn('[HeaderController] No ID token received from deeplink, showing permission setup');
 | 
			
		||||
                        // Show permission setup after Firebase auth
 | 
			
		||||
                        this.transitionToPermissionSetup();
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[HeaderController] Firebase auth failed:', error);
 | 
			
		||||
                    this.transitionToAppHeader();
 | 
			
		||||
                    this.transitionToPermissionSetup();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
@ -201,28 +187,18 @@ class HeaderTransitionManager {
 | 
			
		||||
 | 
			
		||||
            if (!this.isInitialized) {
 | 
			
		||||
                this.isInitialized = true;
 | 
			
		||||
                return; // Skip on initial load - bootstrap handles it
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Only handle state changes after initial load
 | 
			
		||||
            if (user) {
 | 
			
		||||
                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();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                console.log('[HeaderController] User logged in, updating hasApiKey and checking permissions...');
 | 
			
		||||
                this.hasApiKey = true; // User login should provide API key
 | 
			
		||||
                // Delay permission check to ensure smooth login flow
 | 
			
		||||
                setTimeout(() => this.transitionToPermissionSetup(), 500);
 | 
			
		||||
            } else if (this.hasApiKey) {
 | 
			
		||||
                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');
 | 
			
		||||
                }
 | 
			
		||||
                console.log('[HeaderController] No Firebase user but API key exists, checking if permission setup is needed...');
 | 
			
		||||
                setTimeout(() => this.transitionToPermissionSetup(), 500);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log('[HeaderController] No auth & no API key — showing ApiKeyHeader');
 | 
			
		||||
                this.transitionToApiKeyHeader();
 | 
			
		||||
@ -255,29 +231,60 @@ class HeaderTransitionManager {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (user || this.hasApiKey) {
 | 
			
		||||
        // check flow order: API key -> Permissions -> App
 | 
			
		||||
        if (!user && !this.hasApiKey) {
 | 
			
		||||
            // No auth and no API key -> show API key input
 | 
			
		||||
            await this._resizeForApiKey();
 | 
			
		||||
            this.ensureHeader('apikey');
 | 
			
		||||
        } else {
 | 
			
		||||
            // Has API key or user -> check permissions first
 | 
			
		||||
            const permissionResult = await this.checkPermissions();
 | 
			
		||||
            
 | 
			
		||||
            if (permissionResult.success) {
 | 
			
		||||
                // All permissions granted -> go to app
 | 
			
		||||
                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);
 | 
			
		||||
                // Permissions needed -> show permission setup
 | 
			
		||||
                await this._resizeForPermissionSetup();
 | 
			
		||||
                this.ensureHeader('permission');
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            await this._resizeForApiKey();
 | 
			
		||||
            this.ensureHeader('apikey');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async transitionToPermissionSetup() {
 | 
			
		||||
        // Prevent duplicate transitions
 | 
			
		||||
        if (this.currentHeaderType === 'permission') {
 | 
			
		||||
            console.log('[HeaderController] Already showing permission setup, skipping transition');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if permissions were previously completed
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            try {
 | 
			
		||||
                const permissionsCompleted = await ipcRenderer.invoke('check-permissions-completed');
 | 
			
		||||
                if (permissionsCompleted) {
 | 
			
		||||
                    console.log('[HeaderController] Permissions were previously completed, checking current status...');
 | 
			
		||||
                    
 | 
			
		||||
                    // Double check current permission status
 | 
			
		||||
                    const permissionResult = await this.checkPermissions();
 | 
			
		||||
                    if (permissionResult.success) {
 | 
			
		||||
                        // Skip permission setup if already granted
 | 
			
		||||
                        this.transitionToAppHeader();
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    console.log('[HeaderController] Permissions were revoked, showing setup again');
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('[HeaderController] Error checking permissions completed status:', error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this._resizeForPermissionSetup();
 | 
			
		||||
        this.ensureHeader('permission');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async transitionToAppHeader(animate = true) {
 | 
			
		||||
        if (this.currentHeaderType === 'app') {
 | 
			
		||||
            return this._resizeForApp();
 | 
			
		||||
@ -285,11 +292,10 @@ class HeaderTransitionManager {
 | 
			
		||||
 | 
			
		||||
        const canAnimate =
 | 
			
		||||
            animate &&
 | 
			
		||||
            this.apiKeyHeader &&
 | 
			
		||||
            !this.apiKeyHeader.classList.contains('hidden') &&
 | 
			
		||||
            typeof this.apiKeyHeader.startSlideOutAnimation === 'function';
 | 
			
		||||
            (this.apiKeyHeader || this.permissionSetup) &&
 | 
			
		||||
            this.currentHeaderType !== 'app';
 | 
			
		||||
    
 | 
			
		||||
        if (canAnimate) {
 | 
			
		||||
        if (canAnimate && this.apiKeyHeader?.startSlideOutAnimation) {
 | 
			
		||||
            const old = this.apiKeyHeader;
 | 
			
		||||
            const onEnd = () => {
 | 
			
		||||
                clearTimeout(fallback);
 | 
			
		||||
@ -321,6 +327,14 @@ class HeaderTransitionManager {
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _resizeForPermissionSetup() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async transitionToApiKeyHeader() {
 | 
			
		||||
        await this._resizeForApiKey();
 | 
			
		||||
        
 | 
			
		||||
@ -351,10 +365,6 @@ class HeaderTransitionManager {
 | 
			
		||||
            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 { 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										533
									
								
								src/app/PermissionSetup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										533
									
								
								src/app/PermissionSetup.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,533 @@
 | 
			
		||||
import { LitElement, html, css } from '../assets/lit-core-2.7.4.min.js';
 | 
			
		||||
 | 
			
		||||
export class PermissionSetup extends LitElement {
 | 
			
		||||
    static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: opacity 0.25s ease-out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.sliding-out) {
 | 
			
		||||
            animation: slideOutUp 0.3s ease-in forwards;
 | 
			
		||||
            will-change: opacity, transform;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hidden) {
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideOutUp {
 | 
			
		||||
            from {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
            to {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-20px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
            cursor: default;
 | 
			
		||||
            user-select: none;
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            width: 285px;
 | 
			
		||||
            height: 220px;
 | 
			
		||||
            padding: 18px 20px;
 | 
			
		||||
            background: rgba(0, 0, 0, 0.3);
 | 
			
		||||
            border-radius: 16px;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container::after {
 | 
			
		||||
            content: '';
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 0;
 | 
			
		||||
            left: 0;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
            border-radius: 16px;
 | 
			
		||||
            padding: 1px;
 | 
			
		||||
            background: linear-gradient(169deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.5) 100%);
 | 
			
		||||
            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
 | 
			
		||||
            -webkit-mask-composite: destination-out;
 | 
			
		||||
            mask-composite: exclude;
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .close-button {
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 10px;
 | 
			
		||||
            right: 10px;
 | 
			
		||||
            width: 14px;
 | 
			
		||||
            height: 14px;
 | 
			
		||||
            background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 3px;
 | 
			
		||||
            color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            transition: all 0.15s ease;
 | 
			
		||||
            z-index: 10;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            line-height: 1;
 | 
			
		||||
            padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .close-button:hover {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
            color: rgba(255, 255, 255, 0.9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .close-button:active {
 | 
			
		||||
            transform: scale(0.95);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .title {
 | 
			
		||||
            color: white;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            flex-shrink: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .form-content {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            margin-top: auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .subtitle {
 | 
			
		||||
            color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
            font-size: 11px;
 | 
			
		||||
            font-weight: 400;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            margin-bottom: 12px;
 | 
			
		||||
            line-height: 1.3;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .permission-status {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            gap: 8px;
 | 
			
		||||
            margin-bottom: 12px;
 | 
			
		||||
            min-height: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .permission-item {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            gap: 6px;
 | 
			
		||||
            color: rgba(255, 255, 255, 0.8);
 | 
			
		||||
            font-size: 11px;
 | 
			
		||||
            font-weight: 400;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .permission-item.granted {
 | 
			
		||||
            color: rgba(34, 197, 94, 0.9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .permission-icon {
 | 
			
		||||
            width: 12px;
 | 
			
		||||
            height: 12px;
 | 
			
		||||
            opacity: 0.8;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .check-icon {
 | 
			
		||||
            width: 12px;
 | 
			
		||||
            height: 12px;
 | 
			
		||||
            color: rgba(34, 197, 94, 0.9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .action-button {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            color: white;
 | 
			
		||||
            font-size: 12px;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: background 0.15s ease;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            margin-bottom: 6px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .action-button::after {
 | 
			
		||||
            content: '';
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 0;
 | 
			
		||||
            left: 0;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            padding: 1px;
 | 
			
		||||
            background: linear-gradient(169deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.5) 100%);
 | 
			
		||||
            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
 | 
			
		||||
            -webkit-mask-composite: destination-out;
 | 
			
		||||
            mask-composite: exclude;
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .action-button:hover:not(:disabled) {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .action-button:disabled {
 | 
			
		||||
            opacity: 0.5;
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .continue-button {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(34, 197, 94, 0.8);
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            color: white;
 | 
			
		||||
            font-size: 12px;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: background 0.15s ease;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            margin-top: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .continue-button::after {
 | 
			
		||||
            content: '';
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 0;
 | 
			
		||||
            left: 0;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            padding: 1px;
 | 
			
		||||
            background: linear-gradient(169deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.5) 100%);
 | 
			
		||||
            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
 | 
			
		||||
            -webkit-mask-composite: destination-out;
 | 
			
		||||
            mask-composite: exclude;
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .continue-button:hover:not(:disabled) {
 | 
			
		||||
            background: rgba(34, 197, 94, 0.9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .continue-button:disabled {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    static properties = {
 | 
			
		||||
        microphoneGranted: { type: String },
 | 
			
		||||
        screenGranted: { type: String },
 | 
			
		||||
        isChecking: { type: String },
 | 
			
		||||
        continueCallback: { type: Function }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.microphoneGranted = 'unknown';
 | 
			
		||||
        this.screenGranted = 'unknown';
 | 
			
		||||
        this.isChecking = false;
 | 
			
		||||
        this.continueCallback = null;
 | 
			
		||||
 | 
			
		||||
        this.handleMouseMove = this.handleMouseMove.bind(this);
 | 
			
		||||
        this.handleMouseUp = this.handleMouseUp.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        await this.checkPermissions();
 | 
			
		||||
        
 | 
			
		||||
        // Set up periodic permission check
 | 
			
		||||
        this.permissionCheckInterval = setInterval(() => {
 | 
			
		||||
            this.checkPermissions();
 | 
			
		||||
        }, 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (this.permissionCheckInterval) {
 | 
			
		||||
            clearInterval(this.permissionCheckInterval);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMouseDown(e) {
 | 
			
		||||
        if (e.target.tagName === 'BUTTON') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const initialPosition = await ipcRenderer.invoke('get-header-position');
 | 
			
		||||
 | 
			
		||||
        this.dragState = {
 | 
			
		||||
            initialMouseX: e.screenX,
 | 
			
		||||
            initialMouseY: e.screenY,
 | 
			
		||||
            initialWindowX: initialPosition.x,
 | 
			
		||||
            initialWindowY: initialPosition.y,
 | 
			
		||||
            moved: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        window.addEventListener('mousemove', this.handleMouseMove);
 | 
			
		||||
        window.addEventListener('mouseup', this.handleMouseUp, { once: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseMove(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
 | 
			
		||||
        const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
 | 
			
		||||
 | 
			
		||||
        if (deltaX > 3 || deltaY > 3) {
 | 
			
		||||
            this.dragState.moved = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
 | 
			
		||||
        const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseUp(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const wasDragged = this.dragState.moved;
 | 
			
		||||
 | 
			
		||||
        window.removeEventListener('mousemove', this.handleMouseMove);
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
 | 
			
		||||
        if (wasDragged) {
 | 
			
		||||
            this.wasJustDragged = true;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                this.wasJustDragged = false;
 | 
			
		||||
            }, 200);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkPermissions() {
 | 
			
		||||
        if (!window.require || this.isChecking) return;
 | 
			
		||||
        
 | 
			
		||||
        this.isChecking = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            const prevMic = this.microphoneGranted;
 | 
			
		||||
            const prevScreen = this.screenGranted;
 | 
			
		||||
            
 | 
			
		||||
            this.microphoneGranted = permissions.microphone;
 | 
			
		||||
            this.screenGranted = permissions.screen;
 | 
			
		||||
            
 | 
			
		||||
            // if permissions changed == UI update
 | 
			
		||||
            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
 | 
			
		||||
                console.log('[PermissionSetup] Permission status changed, updating UI');
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // if all permissions granted == automatically continue
 | 
			
		||||
            if (this.microphoneGranted === 'granted' && 
 | 
			
		||||
                this.screenGranted === 'granted' && 
 | 
			
		||||
                this.continueCallback) {
 | 
			
		||||
                console.log('[PermissionSetup] All permissions granted, proceeding automatically');
 | 
			
		||||
                setTimeout(() => this.handleContinue(), 500);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error checking permissions:', error);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.isChecking = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMicrophoneClick() {
 | 
			
		||||
        if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionSetup] Requesting microphone permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Microphone permission result:', result);
 | 
			
		||||
            
 | 
			
		||||
            if (result.microphone === 'granted') {
 | 
			
		||||
                this.microphoneGranted = 'granted';
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
            
 | 
			
		||||
              if (result.microphone === 'not-determined' || result.microphone === 'denied' || result.microphone === 'unknown' || result.microphone === 'restricted') {
 | 
			
		||||
                const res = await ipcRenderer.invoke('request-microphone-permission');
 | 
			
		||||
                if (res.status === 'granted' || res.success === true) {
 | 
			
		||||
                    this.microphoneGranted = 'granted';
 | 
			
		||||
                    this.requestUpdate();
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            
 | 
			
		||||
            
 | 
			
		||||
            // Check permissions again after a delay
 | 
			
		||||
            // setTimeout(() => this.checkPermissions(), 1000);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error requesting microphone permission:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleScreenClick() {
 | 
			
		||||
        if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionSetup] Checking screen recording permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Screen permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            if (permissions.screen === 'granted') {
 | 
			
		||||
                this.screenGranted = 'granted';
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') {
 | 
			
		||||
            console.log('[PermissionSetup] Opening screen recording preferences...');
 | 
			
		||||
            await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Check permissions again after a delay
 | 
			
		||||
            // (This may not execute if app restarts after permission grant)
 | 
			
		||||
            // setTimeout(() => this.checkPermissions(), 2000);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error opening screen recording preferences:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleContinue() {
 | 
			
		||||
        if (this.continueCallback && 
 | 
			
		||||
            this.microphoneGranted === 'granted' && 
 | 
			
		||||
            this.screenGranted === 'granted' && 
 | 
			
		||||
            !this.wasJustDragged) {
 | 
			
		||||
            // Mark permissions as completed
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
                try {
 | 
			
		||||
                    await ipcRenderer.invoke('mark-permissions-completed');
 | 
			
		||||
                    console.log('[PermissionSetup] Marked permissions as completed');
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[PermissionSetup] Error marking permissions as completed:', error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            this.continueCallback();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleClose() {
 | 
			
		||||
        console.log('Close button clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.invoke('quit-application');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="container" @mousedown=${this.handleMouseDown}>
 | 
			
		||||
                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
			
		||||
                    <svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
 | 
			
		||||
                        <path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                </button>
 | 
			
		||||
                <h1 class="title">Permission Setup Required</h1>
 | 
			
		||||
 | 
			
		||||
                <div class="form-content">
 | 
			
		||||
                    <div class="subtitle">Grant access to microphone and screen recording to continue</div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="permission-status">
 | 
			
		||||
                        <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
                            ${this.microphoneGranted === 'granted' ? html`
 | 
			
		||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Microphone ✓</span>
 | 
			
		||||
                            ` : html`
 | 
			
		||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Microphone</span>
 | 
			
		||||
                            `}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="permission-item ${this.screenGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
                            ${this.screenGranted === 'granted' ? html`
 | 
			
		||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Screen ✓</span>
 | 
			
		||||
                            ` : html`
 | 
			
		||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Screen Recording</span>
 | 
			
		||||
                            `}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    ${this.microphoneGranted !== 'granted' ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="action-button" 
 | 
			
		||||
                            @click=${this.handleMicrophoneClick}
 | 
			
		||||
                        >
 | 
			
		||||
                            Grant Microphone Access
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${this.screenGranted !== 'granted' ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="action-button" 
 | 
			
		||||
                            @click=${this.handleScreenClick}
 | 
			
		||||
                        >
 | 
			
		||||
                            Grant Screen Recording Access
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${allGranted ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="continue-button" 
 | 
			
		||||
                            @click=${this.handleContinue}
 | 
			
		||||
                        >
 | 
			
		||||
                            Continue to Pickle Glass
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('permission-setup', PermissionSetup); 
 | 
			
		||||
@ -433,6 +433,34 @@ class SQLiteClient {
 | 
			
		||||
            this.db = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async query(sql, params = []) {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
            if (!this.db) {
 | 
			
		||||
                return reject(new Error('Database not connected'));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (sql.toUpperCase().startsWith('SELECT')) {
 | 
			
		||||
                this.db.all(sql, params, (err, rows) => {
 | 
			
		||||
                    if (err) {
 | 
			
		||||
                        console.error('Query error:', err);
 | 
			
		||||
                        reject(err);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        resolve(rows);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                this.db.run(sql, params, function(err) {
 | 
			
		||||
                    if (err) {
 | 
			
		||||
                        console.error('Query error:', err);
 | 
			
		||||
                        reject(err);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        resolve({ changes: this.changes, lastID: this.lastID });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const sqliteClient = new SQLiteClient();
 | 
			
		||||
 | 
			
		||||
@ -1845,32 +1845,27 @@ function setupIpcHandlers(openaiSessionRef) {
 | 
			
		||||
    ipcMain.handle('check-system-permissions', async () => {
 | 
			
		||||
        const { systemPreferences } = require('electron');
 | 
			
		||||
        const permissions = {
 | 
			
		||||
            microphone: false,
 | 
			
		||||
            screen: false,
 | 
			
		||||
            needsSetup: false
 | 
			
		||||
            microphone: 'unknown',
 | 
			
		||||
            screen: 'unknown',
 | 
			
		||||
            needsSetup: true
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (process.platform === 'darwin') {
 | 
			
		||||
                // Check microphone permission on macOS
 | 
			
		||||
                const micStatus = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
                permissions.microphone = micStatus === 'granted';
 | 
			
		||||
                console.log('[Permissions] Microphone status:', micStatus);
 | 
			
		||||
                permissions.microphone = micStatus;
 | 
			
		||||
 | 
			
		||||
                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;
 | 
			
		||||
                }
 | 
			
		||||
                // Check screen recording permission using the system API
 | 
			
		||||
                const screenStatus = systemPreferences.getMediaAccessStatus('screen');
 | 
			
		||||
                console.log('[Permissions] Screen status:', screenStatus);
 | 
			
		||||
                permissions.screen = screenStatus;
 | 
			
		||||
 | 
			
		||||
                permissions.needsSetup = !permissions.microphone || !permissions.screen;
 | 
			
		||||
                permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
 | 
			
		||||
            } else {
 | 
			
		||||
                permissions.microphone = true;
 | 
			
		||||
                permissions.screen = true;
 | 
			
		||||
                permissions.microphone = 'granted';
 | 
			
		||||
                permissions.screen = 'granted';
 | 
			
		||||
                permissions.needsSetup = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -1879,8 +1874,8 @@ function setupIpcHandlers(openaiSessionRef) {
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error checking permissions:', error);
 | 
			
		||||
            return {
 | 
			
		||||
                microphone: false,
 | 
			
		||||
                screen: false,
 | 
			
		||||
                microphone: 'unknown',
 | 
			
		||||
                screen: 'unknown',
 | 
			
		||||
                needsSetup: true,
 | 
			
		||||
                error: error.message
 | 
			
		||||
            };
 | 
			
		||||
@ -1895,15 +1890,16 @@ function setupIpcHandlers(openaiSessionRef) {
 | 
			
		||||
        const { systemPreferences } = require('electron');
 | 
			
		||||
        try {
 | 
			
		||||
            const status = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
            console.log('[Permissions] Microphone status:', status);
 | 
			
		||||
            if (status === 'granted') {
 | 
			
		||||
                return { success: true, status: 'already-granted' };
 | 
			
		||||
                return { success: true, status: 'granted' };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Req mic permission
 | 
			
		||||
            const granted = await systemPreferences.askForMediaAccess('microphone');
 | 
			
		||||
            return { 
 | 
			
		||||
                success: granted, 
 | 
			
		||||
                status: granted ? 'granted' : 'denied' 
 | 
			
		||||
                success: granted,
 | 
			
		||||
                status: granted ? 'granted' : 'denied'
 | 
			
		||||
            };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error requesting microphone permission:', error);
 | 
			
		||||
@ -1920,20 +1916,61 @@ function setupIpcHandlers(openaiSessionRef) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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');
 | 
			
		||||
                // First trigger screen capture request to register the app in system preferences
 | 
			
		||||
                try {
 | 
			
		||||
                    console.log('[Permissions] Triggering screen capture request to register app...');
 | 
			
		||||
                    await desktopCapturer.getSources({ 
 | 
			
		||||
                        types: ['screen'], 
 | 
			
		||||
                        thumbnailSize: { width: 1, height: 1 } 
 | 
			
		||||
                    });
 | 
			
		||||
                    console.log('[Permissions] App registered for screen recording');
 | 
			
		||||
                } catch (captureError) {
 | 
			
		||||
                    console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Then open system preferences
 | 
			
		||||
                // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
 | 
			
		||||
            }
 | 
			
		||||
            // if (section === 'microphone') {
 | 
			
		||||
            //     await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');
 | 
			
		||||
            // }
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error opening system preferences:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('mark-permissions-completed', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            // Store in SQLite that permissions have been completed
 | 
			
		||||
            await sqliteClient.query(
 | 
			
		||||
                'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
 | 
			
		||||
                ['permissions_completed', 'true']
 | 
			
		||||
            );
 | 
			
		||||
            console.log('[Permissions] Marked permissions as completed');
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error marking permissions as completed:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('check-permissions-completed', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await sqliteClient.query(
 | 
			
		||||
                'SELECT value FROM system_settings WHERE key = ?',
 | 
			
		||||
                ['permissions_completed']
 | 
			
		||||
            );
 | 
			
		||||
            const completed = result.length > 0 && result[0].value === 'true';
 | 
			
		||||
            console.log('[Permissions] Permissions completed status:', completed);
 | 
			
		||||
            return completed;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error checking permissions completed status:', error);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,75 @@ export class AskView extends LitElement {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            color: white;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
 | 
			
		||||
            will-change: transform, opacity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hiding) {
 | 
			
		||||
            animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.6, 1) forwards;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.showing) {
 | 
			
		||||
            animation: slideDown 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hidden) {
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            transform: translateY(-150%) scale(0.85);
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideUp {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.7;
 | 
			
		||||
                transform: translateY(-20%) scale(0.98);
 | 
			
		||||
                filter: blur(0.5px);
 | 
			
		||||
            }
 | 
			
		||||
            70% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: translateY(-80%) scale(0.92);
 | 
			
		||||
                filter: blur(1.5px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideDown {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
                transform: translateY(-50%) scale(0.92);
 | 
			
		||||
                filter: blur(1px);
 | 
			
		||||
            }
 | 
			
		||||
            65% {
 | 
			
		||||
                opacity: 0.9;
 | 
			
		||||
                transform: translateY(-5%) scale(0.99);
 | 
			
		||||
                filter: blur(0.2px);
 | 
			
		||||
            }
 | 
			
		||||
            85% {
 | 
			
		||||
                opacity: 0.98;
 | 
			
		||||
                transform: translateY(2%) scale(1.005);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,75 @@ export class AssistantView extends LitElement {
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 400px;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
 | 
			
		||||
            will-change: transform, opacity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hiding) {
 | 
			
		||||
            animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.6, 1) forwards;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.showing) {
 | 
			
		||||
            animation: slideDown 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hidden) {
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            transform: translateY(-150%) scale(0.85);
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideUp {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.7;
 | 
			
		||||
                transform: translateY(-20%) scale(0.98);
 | 
			
		||||
                filter: blur(0.5px);
 | 
			
		||||
            }
 | 
			
		||||
            70% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: translateY(-80%) scale(0.92);
 | 
			
		||||
                filter: blur(1.5px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideDown {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
                transform: translateY(-50%) scale(0.92);
 | 
			
		||||
                filter: blur(1px);
 | 
			
		||||
            }
 | 
			
		||||
            65% {
 | 
			
		||||
                opacity: 0.9;
 | 
			
		||||
                transform: translateY(-5%) scale(0.99);
 | 
			
		||||
                filter: blur(0.2px);
 | 
			
		||||
            }
 | 
			
		||||
            85% {
 | 
			
		||||
                opacity: 0.98;
 | 
			
		||||
                transform: translateY(2%) scale(1.005);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user