keychain permission + modelStateService rely only on db

This commit is contained in:
samtiz 2025-07-15 20:16:38 +09:00
parent a27ab05fa8
commit dad74875a0
19 changed files with 667 additions and 887 deletions

View File

@ -109,15 +109,15 @@ module.exports = {
// ModelStateService
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
ipcMain.handle('model:get-all-keys', async () => await modelStateService.getAllApiKeys());
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
ipcMain.handle('model:get-selected-models', async () => await modelStateService.getSelectedModels());
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
ipcMain.handle('model:get-available-models', async (e, { type }) => await modelStateService.getAvailableModels(type));
ipcMain.handle('model:are-providers-configured', async () => await modelStateService.areProvidersConfigured());
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
ipcMain.handle('model:re-initialize-state', async () => await modelStateService.initialize());
// LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
localAIManager.on('install-progress', (service, data) => {

View File

@ -243,7 +243,7 @@ class AskService {
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
const modelInfo = modelStateService.getCurrentModelInfo('llm');
const modelInfo = await modelStateService.getCurrentModelInfo('llm');
if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key not configured.');
}

View File

@ -91,7 +91,6 @@ const LATEST_SCHEMA = {
},
provider_settings: {
columns: [
{ name: 'uid', type: 'TEXT NOT NULL' },
{ name: 'provider', type: 'TEXT NOT NULL' },
{ name: 'api_key', type: 'TEXT' },
{ name: 'selected_llm_model', type: 'TEXT' },
@ -101,7 +100,7 @@ const LATEST_SCHEMA = {
{ name: 'created_at', type: 'INTEGER' },
{ name: 'updated_at', type: 'INTEGER' }
],
constraints: ['PRIMARY KEY (uid, provider)']
constraints: ['PRIMARY KEY (provider)']
},
shortcuts: {
columns: [

View File

@ -33,7 +33,13 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
for (const field of fieldsToEncrypt) {
if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
try {
appObject[field] = encryptionService.decrypt(appObject[field]);
} catch (error) {
console.warn(`[FirestoreConverter] Failed to decrypt field '${field}' (possibly plaintext or key mismatch):`, error.message);
// Keep the original value instead of failing
// appObject[field] remains as is
}
}
}

View File

@ -1,12 +1,7 @@
const sqliteRepository = require('./sqlite.repository');
let authService = null;
function setAuthService(service) {
authService = service;
}
function getBaseRepository() {
// For now, we only have sqlite. This could be expanded later.
return sqliteRepository;
}
@ -14,71 +9,60 @@ const providerSettingsRepositoryAdapter = {
// Core CRUD operations
async getByProvider(provider) {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.getByProvider(uid, provider);
return await repo.getByProvider(provider);
},
async getAllByUid() {
async getAll() {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.getAllByUid(uid);
return await repo.getAll();
},
async upsert(provider, settings) {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
const now = Date.now();
const settingsWithMeta = {
...settings,
uid,
provider,
updated_at: now,
created_at: settings.created_at || now
};
return await repo.upsert(uid, provider, settingsWithMeta);
return await repo.upsert(provider, settingsWithMeta);
},
async remove(provider) {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.remove(uid, provider);
return await repo.remove(provider);
},
async removeAllByUid() {
async removeAll() {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.removeAllByUid(uid);
return await repo.removeAll();
},
async getRawApiKeysByUid() {
async getRawApiKeys() {
// This function should always target the local sqlite DB,
// as it's part of the local-first boot sequence.
const uid = authService.getCurrentUserId();
return await sqliteRepository.getRawApiKeysByUid(uid);
return await sqliteRepository.getRawApiKeys();
},
async getActiveProvider(type) {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.getActiveProvider(uid, type);
return await repo.getActiveProvider(type);
},
async setActiveProvider(provider, type) {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.setActiveProvider(uid, provider, type);
return await repo.setActiveProvider(provider, type);
},
async getActiveSettings() {
const repo = getBaseRepository();
const uid = authService.getCurrentUserId();
return await repo.getActiveSettings(uid);
return await repo.getActiveSettings();
}
};
module.exports = {
...providerSettingsRepositoryAdapter,
setAuthService
...providerSettingsRepositoryAdapter
};

View File

@ -1,31 +1,32 @@
const sqliteClient = require('../../services/sqliteClient');
const encryptionService = require('../../services/encryptionService');
function getByProvider(uid, provider) {
function getByProvider(provider) {
const db = sqliteClient.getDb();
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
const result = stmt.get(uid, provider) || null;
const stmt = db.prepare('SELECT * FROM provider_settings WHERE provider = ?');
const result = stmt.get(provider) || null;
if (result && result.api_key) {
if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
result.api_key = encryptionService.decrypt(result.api_key);
}
return result;
}
function getAllByUid(uid) {
function getAll() {
const db = sqliteClient.getDb();
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
const results = stmt.all(uid);
const stmt = db.prepare('SELECT * FROM provider_settings ORDER BY provider');
const results = stmt.all();
return results.map(result => {
if (result.api_key) {
result.api_key = result.api_key;
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
result.api_key = encryptionService.decrypt(result.api_key);
}
return result;
});
}
function upsert(uid, provider, settings) {
function upsert(provider, settings) {
// Validate: prevent direct setting of active status
if (settings.is_active_llm || settings.is_active_stt) {
console.warn('[ProviderSettings] Warning: is_active_llm/is_active_stt should not be set directly. Use setActiveProvider() instead.');
@ -35,9 +36,9 @@ function upsert(uid, provider, settings) {
// Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
const stmt = db.prepare(`
INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(uid, provider) DO UPDATE SET
INSERT INTO provider_settings (provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(provider) DO UPDATE SET
api_key = excluded.api_key,
selected_llm_model = excluded.selected_llm_model,
selected_stt_model = excluded.selected_stt_model,
@ -47,7 +48,6 @@ function upsert(uid, provider, settings) {
`);
const result = stmt.run(
uid,
provider,
settings.api_key || null,
settings.selected_llm_model || null,
@ -61,55 +61,55 @@ function upsert(uid, provider, settings) {
return { changes: result.changes };
}
function remove(uid, provider) {
function remove(provider) {
const db = sqliteClient.getDb();
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
const result = stmt.run(uid, provider);
const stmt = db.prepare('DELETE FROM provider_settings WHERE provider = ?');
const result = stmt.run(provider);
return { changes: result.changes };
}
function removeAllByUid(uid) {
function removeAll() {
const db = sqliteClient.getDb();
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
const result = stmt.run(uid);
const stmt = db.prepare('DELETE FROM provider_settings');
const result = stmt.run();
return { changes: result.changes };
}
function getRawApiKeysByUid(uid) {
function getRawApiKeys() {
const db = sqliteClient.getDb();
const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
return stmt.all(uid);
const stmt = db.prepare('SELECT api_key FROM provider_settings');
return stmt.all();
}
// Get active provider for a specific type (llm or stt)
function getActiveProvider(uid, type) {
function getActiveProvider(type) {
const db = sqliteClient.getDb();
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
const stmt = db.prepare(`SELECT * FROM provider_settings WHERE uid = ? AND ${column} = 1`);
const result = stmt.get(uid) || null;
const stmt = db.prepare(`SELECT * FROM provider_settings WHERE ${column} = 1`);
const result = stmt.get() || null;
if (result && result.api_key) {
result.api_key = result.api_key;
if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
result.api_key = encryptionService.decrypt(result.api_key);
}
return result;
}
// Set active provider for a specific type
function setActiveProvider(uid, provider, type) {
function setActiveProvider(provider, type) {
const db = sqliteClient.getDb();
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
// Start transaction to ensure only one provider is active
db.transaction(() => {
// First, deactivate all providers for this type
const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0 WHERE uid = ?`);
deactivateStmt.run(uid);
const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0`);
deactivateStmt.run();
// Then activate the specified provider
if (provider) {
const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE uid = ? AND provider = ?`);
activateStmt.run(uid, provider);
const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE provider = ?`);
activateStmt.run(provider);
}
})();
@ -117,14 +117,14 @@ function setActiveProvider(uid, provider, type) {
}
// Get all active settings (both llm and stt)
function getActiveSettings(uid) {
function getActiveSettings() {
const db = sqliteClient.getDb();
const stmt = db.prepare(`
SELECT * FROM provider_settings
WHERE uid = ? AND (is_active_llm = 1 OR is_active_stt = 1)
WHERE (is_active_llm = 1 OR is_active_stt = 1)
ORDER BY provider
`);
const results = stmt.all(uid);
const results = stmt.all();
// Decrypt API keys and organize by type
const activeSettings = {
@ -133,8 +133,8 @@ function getActiveSettings(uid) {
};
results.forEach(result => {
if (result.api_key) {
result.api_key = result.api_key;
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
result.api_key = encryptionService.decrypt(result.api_key);
}
if (result.is_active_llm) {
activeSettings.llm = result;
@ -149,11 +149,11 @@ function getActiveSettings(uid) {
module.exports = {
getByProvider,
getAllByUid,
getAll,
upsert,
remove,
removeAllByUid,
getRawApiKeysByUid,
removeAll,
getRawApiKeys,
getActiveProvider,
setActiveProvider,
getActiveSettings

View File

@ -46,7 +46,6 @@ class AuthService {
this.initializationPromise = null;
sessionRepository.setAuthService(this);
providerSettingsRepository.setAuthService(this);
}
initialize() {
@ -78,22 +77,21 @@ class AuthService {
// No 'await' here, so it runs in the background without blocking startup.
migrationService.checkAndRunMigration(user);
// Start background task to fetch and save virtual key
(async () => {
// ***** CRITICAL: Wait for the virtual key and model state update to complete *****
try {
const idToken = await user.getIdToken(true);
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(virtualKey);
// The model state service now writes directly to the DB, no in-memory state.
await global.modelStateService.setFirebaseVirtualKey(virtualKey);
}
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
console.log(`[AuthService] Virtual key for ${user.email} has been processed and state updated.`);
} catch (error) {
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
console.error('[AuthService] Failed to fetch or save virtual key:', error);
// This is not critical enough to halt the login, but we should log it.
}
})();
} else {
// User signed OUT
@ -101,7 +99,8 @@ class AuthService {
if (previousUser) {
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(null);
// The model state service now writes directly to the DB.
await global.modelStateService.setFirebaseVirtualKey(null);
}
}
this.currentUser = null;

File diff suppressed because it is too large Load Diff

View File

@ -1066,7 +1066,7 @@ class OllamaService extends EventEmitter {
return false;
}
const selectedModels = modelStateService.getSelectedModels();
const selectedModels = await modelStateService.getSelectedModels();
const llmModelId = selectedModels.llm;
// Check if it's an Ollama model

View File

@ -106,6 +106,9 @@ class PermissionService {
}
async checkKeychainCompleted(uid) {
if (uid === "default_user") {
return true;
}
try {
const completed = permissionRepository.checkKeychainCompleted(uid);
console.log('[Permissions] Keychain completed status:', completed);

View File

@ -40,8 +40,82 @@ class SQLiteClient {
return `"${identifier}"`;
}
_migrateProviderSettings() {
const tablesInDb = this.getTablesFromDb();
if (!tablesInDb.includes('provider_settings')) {
return; // Table doesn't exist, no migration needed.
}
const providerSettingsInfo = this.db.prepare(`PRAGMA table_info(provider_settings)`).all();
const hasUidColumn = providerSettingsInfo.some(col => col.name === 'uid');
if (hasUidColumn) {
console.log('[DB Migration] Old provider_settings schema detected. Starting robust migration...');
try {
this.db.transaction(() => {
this.db.exec('ALTER TABLE provider_settings RENAME TO provider_settings_old');
console.log('[DB Migration] Renamed provider_settings to provider_settings_old');
this.createTable('provider_settings', LATEST_SCHEMA.provider_settings);
console.log('[DB Migration] Created new provider_settings table');
// Dynamically build the migration query for robustness
const oldColumnNames = this.db.prepare(`PRAGMA table_info(provider_settings_old)`).all().map(c => c.name);
const newColumnNames = LATEST_SCHEMA.provider_settings.columns.map(c => c.name);
const commonColumns = newColumnNames.filter(name => oldColumnNames.includes(name));
if (!commonColumns.includes('provider')) {
console.warn('[DB Migration] Old table is missing the "provider" column. Aborting migration for this table.');
this.db.exec('DROP TABLE provider_settings_old');
return;
}
const orderParts = [];
if (oldColumnNames.includes('updated_at')) orderParts.push('updated_at DESC');
if (oldColumnNames.includes('created_at')) orderParts.push('created_at DESC');
const orderByClause = orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
const columnsForInsert = commonColumns.map(c => this._validateAndQuoteIdentifier(c)).join(', ');
const migrationQuery = `
INSERT INTO provider_settings (${columnsForInsert})
SELECT ${columnsForInsert}
FROM (
SELECT *, ROW_NUMBER() OVER(PARTITION BY provider ${orderByClause}) as rn
FROM provider_settings_old
)
WHERE rn = 1
`;
console.log(`[DB Migration] Executing robust migration query for columns: ${commonColumns.join(', ')}`);
const result = this.db.prepare(migrationQuery).run();
console.log(`[DB Migration] Migrated ${result.changes} rows to the new provider_settings table.`);
this.db.exec('DROP TABLE provider_settings_old');
console.log('[DB Migration] Dropped provider_settings_old table.');
})();
console.log('[DB Migration] provider_settings migration completed successfully.');
} catch (error) {
console.error('[DB Migration] Failed to migrate provider_settings table.', error);
// Try to recover by dropping the temp table if it exists
const oldTableExists = this.getTablesFromDb().includes('provider_settings_old');
if (oldTableExists) {
this.db.exec('DROP TABLE provider_settings_old');
console.warn('[DB Migration] Cleaned up temporary old table after failure.');
}
throw error;
}
}
}
async synchronizeSchema() {
console.log('[DB Sync] Starting schema synchronization...');
// Run special migration for provider_settings before the generic sync logic
this._migrateProviderSettings();
const tablesInDb = this.getTablesFromDb();
for (const tableName of Object.keys(LATEST_SCHEMA)) {

View File

@ -2,7 +2,6 @@ const { BrowserWindow } = require('electron');
const { spawn } = require('child_process');
const { createSTT } = require('../../common/ai/factory');
const modelStateService = require('../../common/services/modelStateService');
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
const COMPLETION_DEBOUNCE_MS = 2000;
@ -134,7 +133,7 @@ class SttService {
async initializeSttSessions(language = 'en') {
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
const modelInfo = modelStateService.getCurrentModelInfo('stt');
const modelInfo = await modelStateService.getCurrentModelInfo('stt');
if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key is not configured.');
}
@ -467,7 +466,7 @@ class SttService {
let modelInfo = this.modelInfo;
if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await modelStateService.getCurrentModelInfo('stt');
}
if (!modelInfo) {
throw new Error('STT model info could not be retrieved.');
@ -492,7 +491,7 @@ class SttService {
let modelInfo = this.modelInfo;
if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await modelStateService.getCurrentModelInfo('stt');
}
if (!modelInfo) {
throw new Error('STT model info could not be retrieved.');
@ -578,7 +577,7 @@ class SttService {
let modelInfo = this.modelInfo;
if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await modelStateService.getCurrentModelInfo('stt');
}
if (!modelInfo) {
throw new Error('STT model info could not be retrieved.');

View File

@ -4,7 +4,6 @@ const { createLLM } = require('../../common/ai/factory');
const sessionRepository = require('../../common/repositories/session');
const summaryRepository = require('./repositories');
const modelStateService = require('../../common/services/modelStateService');
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
class SummaryService {
constructor() {
@ -99,7 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
await sessionRepository.touch(this.currentSessionId);
}
const modelInfo = modelStateService.getCurrentModelInfo('llm');
const modelInfo = await modelStateService.getCurrentModelInfo('llm');
if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key is not configured.');
}

View File

@ -26,16 +26,14 @@ const NOTIFICATION_CONFIG = {
// New facade functions for model state management
async function getModelSettings() {
try {
const [config, storedKeys, selectedModels] = await Promise.all([
const [config, storedKeys, selectedModels, availableLlm, availableStt] = await Promise.all([
modelStateService.getProviderConfig(),
modelStateService.getAllApiKeys(),
modelStateService.getSelectedModels(),
modelStateService.getAvailableModels('llm'),
modelStateService.getAvailableModels('stt')
]);
// 동기 함수들은 별도로 호출
const availableLlm = modelStateService.getAvailableModels('llm');
const availableStt = modelStateService.getAvailableModels('stt');
return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
} catch (error) {
console.error('[SettingsService] Error getting model settings:', error);

View File

@ -686,75 +686,43 @@ async function startWebStack() {
console.log(`✅ API server started on http://localhost:${apiPort}`);
console.log(`🚀 All services ready:`);
console.log(` Frontend: http://localhost:${frontendPort}`);
console.log(` API: http://localhost:${apiPort}`);
console.log(`🚀 All services ready:
Frontend: http://localhost:${frontendPort}
API: http://localhost:${apiPort}`);
return frontendPort;
}
// Auto-update initialization
async function initAutoUpdater() {
if (process.env.NODE_ENV === 'development') {
console.log('Development environment, skipping auto-updater.');
return;
}
try {
const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
if (!autoUpdateEnabled) {
console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
return;
}
// Skip auto-updater in development mode
if (!app.isPackaged) {
console.log('[AutoUpdater] Skipped in development (app is not packaged)');
return;
}
autoUpdater.setFeedURL({
provider: 'github',
owner: 'pickle-com',
repo: 'glass',
await autoUpdater.checkForUpdates();
autoUpdater.on('update-available', () => {
console.log('Update available!');
autoUpdater.downloadUpdate();
});
// Immediately check for updates & notify
autoUpdater.checkForUpdatesAndNotify()
.catch(err => {
console.error('[AutoUpdater] Error checking for updates:', err);
});
autoUpdater.on('checking-for-update', () => {
console.log('[AutoUpdater] Checking for updates…');
});
autoUpdater.on('update-available', (info) => {
console.log('[AutoUpdater] Update available:', info.version);
});
autoUpdater.on('update-not-available', () => {
console.log('[AutoUpdater] Application is up-to-date');
});
autoUpdater.on('error', (err) => {
console.error('[AutoUpdater] Error while updating:', err);
});
autoUpdater.on('update-downloaded', (info) => {
console.log(`[AutoUpdater] Update downloaded: ${info.version}`);
const dialogOpts = {
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, date, url) => {
console.log('Update downloaded:', releaseNotes, releaseName, date, url);
dialog.showMessageBox({
type: 'info',
buttons: ['Install now', 'Install on next launch'],
title: 'Update Available',
message: 'A new version of Glass is ready to be installed.',
defaultId: 0,
cancelId: 1
};
dialog.showMessageBox(dialogOpts).then((returnValue) => {
// returnValue.response 0 is for 'Install Now'
if (returnValue.response === 0) {
title: 'Application Update',
message: `A new version of PickleGlass (${releaseName}) has been downloaded. It will be installed the next time you launch the application.`,
buttons: ['Restart', 'Later']
}).then(response => {
if (response.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
} catch (e) {
console.error('[AutoUpdater] Failed to initialise:', e);
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
});
} catch (err) {
console.error('Error initializing auto-updater:', err);
}
}

View File

@ -98,8 +98,6 @@ contextBridge.exposeInMainWorld('api', {
removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
onForceShowPermissionHeader: (callback) => ipcRenderer.on('force-show-permission-header', callback),
removeOnForceShowPermissionHeader: (callback) => ipcRenderer.removeListener('force-show-permission-header', callback)
},
// src/ui/app/MainHeader.js

View File

@ -48,6 +48,9 @@ class HeaderTransitionManager {
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
} else if (type === 'permission') {
this.permissionHeader = document.createElement('permission-setup');
this.permissionHeader.addEventListener('request-resize', e => {
this._resizeForPermissionHeader(e.detail.height);
});
this.permissionHeader.continueCallback = async () => {
if (window.api && window.api.headerController) {
console.log('[HeaderController] Re-initializing model state after permission grant...');
@ -198,7 +201,19 @@ class HeaderTransitionManager {
}
}
await this._resizeForPermissionHeader();
let initialHeight = 220;
if (window.api) {
try {
const userState = await window.api.common.getCurrentUser();
if (userState.mode === 'firebase') {
initialHeight = 280;
}
} catch (e) {
console.error('Could not get user state for resize', e);
}
}
await this._resizeForPermissionHeader(initialHeight);
this.ensureHeader('permission');
}
@ -223,9 +238,10 @@ class HeaderTransitionManager {
return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
}
async _resizeForPermissionHeader() {
async _resizeForPermissionHeader(height) {
if (!window.api) return;
return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 })
const finalHeight = height || 220;
return window.api.headerController.resizeHeaderWindow({ width: 285, height: finalHeight })
.catch(() => {});
}

View File

@ -28,7 +28,7 @@ export class PermissionHeader extends LitElement {
.container {
-webkit-app-region: drag;
width: 285px;
height: 220px;
/* height is now set dynamically */
padding: 18px 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 16px;
@ -103,6 +103,12 @@ export class PermissionHeader extends LitElement {
margin-top: auto;
}
.form-content.all-granted {
flex-grow: 1;
justify-content: center;
margin-top: 0;
}
.subtitle {
color: rgba(255, 255, 255, 0.7);
font-size: 11px;
@ -260,7 +266,8 @@ export class PermissionHeader extends LitElement {
screenGranted: { type: String },
keychainGranted: { type: String },
isChecking: { type: String },
continueCallback: { type: Function }
continueCallback: { type: Function },
userMode: { type: String }, // 'local' or 'firebase'
};
constructor() {
@ -270,14 +277,47 @@ export class PermissionHeader extends LitElement {
this.keychainGranted = 'unknown';
this.isChecking = false;
this.continueCallback = null;
this.userMode = 'local'; // Default to local
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('userMode')) {
const newHeight = this.userMode === 'firebase' ? 280 : 220;
console.log(`[PermissionHeader] User mode changed to ${this.userMode}, requesting resize to ${newHeight}px`);
this.dispatchEvent(new CustomEvent('request-resize', {
detail: { height: newHeight },
bubbles: true,
composed: true
}));
}
}
async connectedCallback() {
super.connectedCallback();
if (window.api) {
try {
const userState = await window.api.common.getCurrentUser();
this.userMode = userState.mode;
} catch (e) {
console.error('[PermissionHeader] Failed to get user state', e);
this.userMode = 'local'; // Fallback to local
}
}
await this.checkPermissions();
// Set up periodic permission check
this.permissionCheckInterval = setInterval(() => {
this.permissionCheckInterval = setInterval(async () => {
if (window.api) {
try {
const userState = await window.api.common.getCurrentUser();
this.userMode = userState.mode;
} catch (e) {
this.userMode = 'local';
}
}
this.checkPermissions();
}, 1000);
}
@ -312,10 +352,13 @@ export class PermissionHeader extends LitElement {
this.requestUpdate();
}
const isKeychainRequired = this.userMode === 'firebase';
const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
// if all permissions granted == automatically continue
if (this.microphoneGranted === 'granted' &&
this.screenGranted === 'granted' &&
this.keychainGranted === 'granted' &&
keychainOk &&
this.continueCallback) {
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
setTimeout(() => this.handleContinue(), 500);
@ -405,12 +448,15 @@ export class PermissionHeader extends LitElement {
}
async handleContinue() {
const isKeychainRequired = this.userMode === 'firebase';
const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
if (this.continueCallback &&
this.microphoneGranted === 'granted' &&
this.screenGranted === 'granted' &&
this.keychainGranted === 'granted') {
keychainOk) {
// Mark permissions as completed
if (window.api) {
if (window.api && isKeychainRequired) {
try {
await window.api.permissionHeader.markKeychainCompleted();
console.log('[PermissionHeader] Marked keychain as completed');
@ -431,10 +477,13 @@ export class PermissionHeader extends LitElement {
}
render() {
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
const isKeychainRequired = this.userMode === 'firebase';
const containerHeight = isKeychainRequired ? 280 : 220;
const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && keychainOk;
return html`
<div class="container">
<div class="container" style="height: ${containerHeight}px">
<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" />
@ -442,8 +491,9 @@ export class PermissionHeader extends LitElement {
</button>
<h1 class="title">Permission Setup Required</h1>
<div class="form-content">
<div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
<div class="form-content ${allGranted ? 'all-granted' : ''}">
${!allGranted ? html`
<div class="subtitle">Grant access to microphone, screen recording${isKeychainRequired ? ' and keychain' : ''} to continue</div>
<div class="permission-status">
<div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
@ -474,57 +524,59 @@ export class PermissionHeader extends LitElement {
`}
</div>
${isKeychainRequired ? html`
<div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
${this.keychainGranted === '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>Keychain </span>
<span>Data Encryption </span>
` : html`
<svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
</svg>
<span>Keychain Access</span>
<span>Data Encryption</span>
`}
</div>
` : ''}
</div>
${this.microphoneGranted !== 'granted' ? html`
<button
class="action-button"
@click=${this.handleMicrophoneClick}
?disabled=${this.microphoneGranted === 'granted'}
>
Grant Microphone Access
${this.microphoneGranted === 'granted' ? 'Microphone Access Granted' : 'Grant Microphone Access'}
</button>
` : ''}
${this.screenGranted !== 'granted' ? html`
<button
class="action-button"
@click=${this.handleScreenClick}
?disabled=${this.screenGranted === 'granted'}
>
Grant Screen Recording Access
${this.screenGranted === 'granted' ? 'Screen Recording Granted' : 'Grant Screen Recording Access'}
</button>
` : ''}
${this.keychainGranted !== 'granted' ? html`
${isKeychainRequired ? html`
<button
class="action-button"
@click=${this.handleKeychainClick}
?disabled=${this.keychainGranted === 'granted'}
>
Grant Keychain Access
${this.keychainGranted === 'granted' ? 'Encryption Enabled' : 'Enable Encryption'}
</button>
<div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
<div class="subtitle" style="visibility: ${this.keychainGranted === 'granted' ? 'hidden' : 'visible'}">
Stores the key to encrypt your data. Press "<b>Always Allow</b>" to continue.
</div>
` : ''}
${allGranted ? html`
` : html`
<button
class="continue-button"
@click=${this.handleContinue}
>
Continue to Pickle Glass
</button>
` : ''}
`}
</div>
</div>
`;

View File

@ -918,6 +918,8 @@ export class SettingsView extends LitElement {
this.setupIpcListeners();
this.setupWindowResize();
this.loadAutoUpdateSetting();
// Force one height calculation immediately (innerHeight may be 0 at first)
setTimeout(() => this.updateScrollHeight(), 0);
}
disconnectedCallback() {
@ -1030,8 +1032,10 @@ export class SettingsView extends LitElement {
}
updateScrollHeight() {
const windowHeight = window.innerHeight;
const maxHeight = windowHeight;
// Electron 일부 시점에서 window.innerHeight 가 0 으로 보고되는 버그 보호
const rawHeight = window.innerHeight || (window.screen ? window.screen.height : 0);
const MIN_HEIGHT = 300; // 최소 보장 높이
const maxHeight = Math.max(MIN_HEIGHT, rawHeight);
this.style.maxHeight = `${maxHeight}px`;
@ -1043,6 +1047,8 @@ export class SettingsView extends LitElement {
handleMouseEnter = () => {
window.api.settingsView.cancelHideSettingsWindow();
// Recalculate height in case it was set to 0 before
this.updateScrollHeight();
}
handleMouseLeave = () => {