keychain permission + modelStateService rely only on db
This commit is contained in:
parent
a27ab05fa8
commit
dad74875a0
@ -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) => {
|
||||
|
@ -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.');
|
||||
}
|
||||
|
@ -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: [
|
||||
|
@ -33,7 +33,13 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
|
||||
|
||||
for (const field of fieldsToEncrypt) {
|
||||
if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
|
||||
appObject[field] = encryptionService.decrypt(appObject[field]);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
// ***** 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);
|
||||
|
||||
// Start background task to fetch and save virtual key
|
||||
(async () => {
|
||||
try {
|
||||
const idToken = await user.getIdToken(true);
|
||||
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
|
||||
|
||||
if (global.modelStateService) {
|
||||
global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||
}
|
||||
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
|
||||
if (global.modelStateService) {
|
||||
// The model state service now writes directly to the DB, no in-memory state.
|
||||
await global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||
}
|
||||
})();
|
||||
console.log(`[AuthService] Virtual key for ${user.email} has been processed and state updated.`);
|
||||
|
||||
} catch (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
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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)) {
|
||||
|
@ -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.');
|
||||
|
@ -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.');
|
||||
}
|
||||
|
@ -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);
|
||||
|
82
src/index.js
82
src/index.js
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(() => {});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
@ -311,11 +351,14 @@ export class PermissionHeader extends LitElement {
|
||||
console.log('[PermissionHeader] Permission status changed, updating UI');
|
||||
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,89 +491,92 @@ 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="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="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-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 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>
|
||||
|
||||
${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>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>Data Encryption</span>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
` : 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>
|
||||
`}
|
||||
</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`
|
||||
<button
|
||||
class="action-button"
|
||||
@click=${this.handleKeychainClick}
|
||||
>
|
||||
Grant Keychain Access
|
||||
</button>
|
||||
<div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
|
||||
` : ''}
|
||||
|
||||
${allGranted ? html`
|
||||
${isKeychainRequired ? html`
|
||||
<button
|
||||
class="action-button"
|
||||
@click=${this.handleKeychainClick}
|
||||
?disabled=${this.keychainGranted === 'granted'}
|
||||
>
|
||||
${this.keychainGranted === 'granted' ? 'Encryption Enabled' : 'Enable Encryption'}
|
||||
</button>
|
||||
<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>
|
||||
` : ''}
|
||||
` : html`
|
||||
<button
|
||||
class="continue-button"
|
||||
@click=${this.handleContinue}
|
||||
>
|
||||
Continue to Pickle Glass
|
||||
</button>
|
||||
` : ''}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -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,11 +1032,13 @@ 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`;
|
||||
|
||||
|
||||
const container = this.shadowRoot?.querySelector('.settings-container');
|
||||
if (container) {
|
||||
container.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 = () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user