Add permission setup flow

This commit is contained in:
jhyang0 2025-07-05 23:34:15 +09:00
parent 552a6bebcd
commit d2ab6570ea
7 changed files with 861 additions and 163 deletions

View File

@ -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');
}

View File

@ -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
View 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);

View File

@ -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();

View File

@ -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;
}
});
}

View File

@ -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);
}
}
* {

View File

@ -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);
}
}
* {