keychain permission + modelStateService rely only on db
This commit is contained in:
parent
a27ab05fa8
commit
dad74875a0
@ -109,15 +109,15 @@ module.exports = {
|
|||||||
|
|
||||||
// ModelStateService
|
// ModelStateService
|
||||||
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
|
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: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: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: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:get-available-models', async (e, { type }) => await modelStateService.getAvailableModels(type));
|
||||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
ipcMain.handle('model:are-providers-configured', async () => await modelStateService.areProvidersConfigured());
|
||||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
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 이벤트를 모든 윈도우에 브로드캐스트
|
||||||
localAIManager.on('install-progress', (service, data) => {
|
localAIManager.on('install-progress', (service, data) => {
|
||||||
|
@ -243,7 +243,7 @@ class AskService {
|
|||||||
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
||||||
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
|
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) {
|
if (!modelInfo || !modelInfo.apiKey) {
|
||||||
throw new Error('AI model or API key not configured.');
|
throw new Error('AI model or API key not configured.');
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,6 @@ const LATEST_SCHEMA = {
|
|||||||
},
|
},
|
||||||
provider_settings: {
|
provider_settings: {
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'uid', type: 'TEXT NOT NULL' },
|
|
||||||
{ name: 'provider', type: 'TEXT NOT NULL' },
|
{ name: 'provider', type: 'TEXT NOT NULL' },
|
||||||
{ name: 'api_key', type: 'TEXT' },
|
{ name: 'api_key', type: 'TEXT' },
|
||||||
{ name: 'selected_llm_model', type: 'TEXT' },
|
{ name: 'selected_llm_model', type: 'TEXT' },
|
||||||
@ -101,7 +100,7 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'created_at', type: 'INTEGER' },
|
{ name: 'created_at', type: 'INTEGER' },
|
||||||
{ name: 'updated_at', type: 'INTEGER' }
|
{ name: 'updated_at', type: 'INTEGER' }
|
||||||
],
|
],
|
||||||
constraints: ['PRIMARY KEY (uid, provider)']
|
constraints: ['PRIMARY KEY (provider)']
|
||||||
},
|
},
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -33,7 +33,13 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
|
|||||||
|
|
||||||
for (const field of fieldsToEncrypt) {
|
for (const field of fieldsToEncrypt) {
|
||||||
if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
|
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');
|
const sqliteRepository = require('./sqlite.repository');
|
||||||
|
|
||||||
let authService = null;
|
|
||||||
|
|
||||||
function setAuthService(service) {
|
|
||||||
authService = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBaseRepository() {
|
function getBaseRepository() {
|
||||||
|
// For now, we only have sqlite. This could be expanded later.
|
||||||
return sqliteRepository;
|
return sqliteRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,71 +9,60 @@ const providerSettingsRepositoryAdapter = {
|
|||||||
// Core CRUD operations
|
// Core CRUD operations
|
||||||
async getByProvider(provider) {
|
async getByProvider(provider) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.getByProvider(provider);
|
||||||
return await repo.getByProvider(uid, provider);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllByUid() {
|
async getAll() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.getAll();
|
||||||
return await repo.getAllByUid(uid);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async upsert(provider, settings) {
|
async upsert(provider, settings) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const settingsWithMeta = {
|
const settingsWithMeta = {
|
||||||
...settings,
|
...settings,
|
||||||
uid,
|
|
||||||
provider,
|
provider,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
created_at: settings.created_at || now
|
created_at: settings.created_at || now
|
||||||
};
|
};
|
||||||
|
|
||||||
return await repo.upsert(uid, provider, settingsWithMeta);
|
return await repo.upsert(provider, settingsWithMeta);
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(provider) {
|
async remove(provider) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.remove(provider);
|
||||||
return await repo.remove(uid, provider);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeAllByUid() {
|
async removeAll() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.removeAll();
|
||||||
return await repo.removeAllByUid(uid);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getRawApiKeysByUid() {
|
async getRawApiKeys() {
|
||||||
// This function should always target the local sqlite DB,
|
// This function should always target the local sqlite DB,
|
||||||
// as it's part of the local-first boot sequence.
|
// as it's part of the local-first boot sequence.
|
||||||
const uid = authService.getCurrentUserId();
|
return await sqliteRepository.getRawApiKeys();
|
||||||
return await sqliteRepository.getRawApiKeysByUid(uid);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActiveProvider(type) {
|
async getActiveProvider(type) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.getActiveProvider(type);
|
||||||
return await repo.getActiveProvider(uid, type);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async setActiveProvider(provider, type) {
|
async setActiveProvider(provider, type) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.setActiveProvider(provider, type);
|
||||||
return await repo.setActiveProvider(uid, provider, type);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActiveSettings() {
|
async getActiveSettings() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
return await repo.getActiveSettings();
|
||||||
return await repo.getActiveSettings(uid);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...providerSettingsRepositoryAdapter,
|
...providerSettingsRepositoryAdapter
|
||||||
setAuthService
|
|
||||||
};
|
};
|
@ -1,31 +1,32 @@
|
|||||||
const sqliteClient = require('../../services/sqliteClient');
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
|
const encryptionService = require('../../services/encryptionService');
|
||||||
|
|
||||||
function getByProvider(uid, provider) {
|
function getByProvider(provider) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
|
const stmt = db.prepare('SELECT * FROM provider_settings WHERE provider = ?');
|
||||||
const result = stmt.get(uid, provider) || null;
|
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);
|
result.api_key = encryptionService.decrypt(result.api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllByUid(uid) {
|
function getAll() {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
|
const stmt = db.prepare('SELECT * FROM provider_settings ORDER BY provider');
|
||||||
const results = stmt.all(uid);
|
const results = stmt.all();
|
||||||
|
|
||||||
return results.map(result => {
|
return results.map(result => {
|
||||||
if (result.api_key) {
|
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
||||||
result.api_key = result.api_key;
|
result.api_key = encryptionService.decrypt(result.api_key);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(uid, provider, settings) {
|
function upsert(provider, settings) {
|
||||||
// Validate: prevent direct setting of active status
|
// Validate: prevent direct setting of active status
|
||||||
if (settings.is_active_llm || settings.is_active_stt) {
|
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.');
|
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)
|
// Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
|
||||||
const stmt = db.prepare(`
|
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)
|
INSERT INTO provider_settings (provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(uid, provider) DO UPDATE SET
|
ON CONFLICT(provider) DO UPDATE SET
|
||||||
api_key = excluded.api_key,
|
api_key = excluded.api_key,
|
||||||
selected_llm_model = excluded.selected_llm_model,
|
selected_llm_model = excluded.selected_llm_model,
|
||||||
selected_stt_model = excluded.selected_stt_model,
|
selected_stt_model = excluded.selected_stt_model,
|
||||||
@ -47,7 +48,6 @@ function upsert(uid, provider, settings) {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const result = stmt.run(
|
const result = stmt.run(
|
||||||
uid,
|
|
||||||
provider,
|
provider,
|
||||||
settings.api_key || null,
|
settings.api_key || null,
|
||||||
settings.selected_llm_model || null,
|
settings.selected_llm_model || null,
|
||||||
@ -61,55 +61,55 @@ function upsert(uid, provider, settings) {
|
|||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(uid, provider) {
|
function remove(provider) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
|
const stmt = db.prepare('DELETE FROM provider_settings WHERE provider = ?');
|
||||||
const result = stmt.run(uid, provider);
|
const result = stmt.run(provider);
|
||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAllByUid(uid) {
|
function removeAll() {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
|
const stmt = db.prepare('DELETE FROM provider_settings');
|
||||||
const result = stmt.run(uid);
|
const result = stmt.run();
|
||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRawApiKeysByUid(uid) {
|
function getRawApiKeys() {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
|
const stmt = db.prepare('SELECT api_key FROM provider_settings');
|
||||||
return stmt.all(uid);
|
return stmt.all();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active provider for a specific type (llm or stt)
|
// Get active provider for a specific type (llm or stt)
|
||||||
function getActiveProvider(uid, type) {
|
function getActiveProvider(type) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
||||||
const stmt = db.prepare(`SELECT * FROM provider_settings WHERE uid = ? AND ${column} = 1`);
|
const stmt = db.prepare(`SELECT * FROM provider_settings WHERE ${column} = 1`);
|
||||||
const result = stmt.get(uid) || null;
|
const result = stmt.get() || null;
|
||||||
|
|
||||||
if (result && result.api_key) {
|
if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
||||||
result.api_key = result.api_key;
|
result.api_key = encryptionService.decrypt(result.api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set active provider for a specific type
|
// Set active provider for a specific type
|
||||||
function setActiveProvider(uid, provider, type) {
|
function setActiveProvider(provider, type) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
||||||
|
|
||||||
// Start transaction to ensure only one provider is active
|
// Start transaction to ensure only one provider is active
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
// First, deactivate all providers for this type
|
// First, deactivate all providers for this type
|
||||||
const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0 WHERE uid = ?`);
|
const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0`);
|
||||||
deactivateStmt.run(uid);
|
deactivateStmt.run();
|
||||||
|
|
||||||
// Then activate the specified provider
|
// Then activate the specified provider
|
||||||
if (provider) {
|
if (provider) {
|
||||||
const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE uid = ? AND provider = ?`);
|
const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE provider = ?`);
|
||||||
activateStmt.run(uid, provider);
|
activateStmt.run(provider);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -117,14 +117,14 @@ function setActiveProvider(uid, provider, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all active settings (both llm and stt)
|
// Get all active settings (both llm and stt)
|
||||||
function getActiveSettings(uid) {
|
function getActiveSettings() {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
SELECT * FROM provider_settings
|
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
|
ORDER BY provider
|
||||||
`);
|
`);
|
||||||
const results = stmt.all(uid);
|
const results = stmt.all();
|
||||||
|
|
||||||
// Decrypt API keys and organize by type
|
// Decrypt API keys and organize by type
|
||||||
const activeSettings = {
|
const activeSettings = {
|
||||||
@ -133,8 +133,8 @@ function getActiveSettings(uid) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
if (result.api_key) {
|
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
||||||
result.api_key = result.api_key;
|
result.api_key = encryptionService.decrypt(result.api_key);
|
||||||
}
|
}
|
||||||
if (result.is_active_llm) {
|
if (result.is_active_llm) {
|
||||||
activeSettings.llm = result;
|
activeSettings.llm = result;
|
||||||
@ -149,11 +149,11 @@ function getActiveSettings(uid) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getByProvider,
|
getByProvider,
|
||||||
getAllByUid,
|
getAll,
|
||||||
upsert,
|
upsert,
|
||||||
remove,
|
remove,
|
||||||
removeAllByUid,
|
removeAll,
|
||||||
getRawApiKeysByUid,
|
getRawApiKeys,
|
||||||
getActiveProvider,
|
getActiveProvider,
|
||||||
setActiveProvider,
|
setActiveProvider,
|
||||||
getActiveSettings
|
getActiveSettings
|
||||||
|
@ -46,7 +46,6 @@ class AuthService {
|
|||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
|
|
||||||
sessionRepository.setAuthService(this);
|
sessionRepository.setAuthService(this);
|
||||||
providerSettingsRepository.setAuthService(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
@ -78,22 +77,21 @@ class AuthService {
|
|||||||
// No 'await' here, so it runs in the background without blocking startup.
|
// No 'await' here, so it runs in the background without blocking startup.
|
||||||
migrationService.checkAndRunMigration(user);
|
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
|
if (global.modelStateService) {
|
||||||
(async () => {
|
// The model state service now writes directly to the DB, no in-memory state.
|
||||||
try {
|
await global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
})();
|
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 {
|
} else {
|
||||||
// User signed OUT
|
// User signed OUT
|
||||||
@ -101,7 +99,8 @@ class AuthService {
|
|||||||
if (previousUser) {
|
if (previousUser) {
|
||||||
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
|
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
|
||||||
if (global.modelStateService) {
|
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;
|
this.currentUser = null;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1066,7 +1066,7 @@ class OllamaService extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedModels = modelStateService.getSelectedModels();
|
const selectedModels = await modelStateService.getSelectedModels();
|
||||||
const llmModelId = selectedModels.llm;
|
const llmModelId = selectedModels.llm;
|
||||||
|
|
||||||
// Check if it's an Ollama model
|
// Check if it's an Ollama model
|
||||||
|
@ -106,6 +106,9 @@ class PermissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkKeychainCompleted(uid) {
|
async checkKeychainCompleted(uid) {
|
||||||
|
if (uid === "default_user") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const completed = permissionRepository.checkKeychainCompleted(uid);
|
const completed = permissionRepository.checkKeychainCompleted(uid);
|
||||||
console.log('[Permissions] Keychain completed status:', completed);
|
console.log('[Permissions] Keychain completed status:', completed);
|
||||||
|
@ -40,8 +40,82 @@ class SQLiteClient {
|
|||||||
return `"${identifier}"`;
|
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() {
|
async synchronizeSchema() {
|
||||||
console.log('[DB Sync] Starting schema synchronization...');
|
console.log('[DB Sync] Starting schema synchronization...');
|
||||||
|
|
||||||
|
// Run special migration for provider_settings before the generic sync logic
|
||||||
|
this._migrateProviderSettings();
|
||||||
|
|
||||||
const tablesInDb = this.getTablesFromDb();
|
const tablesInDb = this.getTablesFromDb();
|
||||||
|
|
||||||
for (const tableName of Object.keys(LATEST_SCHEMA)) {
|
for (const tableName of Object.keys(LATEST_SCHEMA)) {
|
||||||
|
@ -2,7 +2,6 @@ const { BrowserWindow } = require('electron');
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { createSTT } = require('../../common/ai/factory');
|
const { createSTT } = require('../../common/ai/factory');
|
||||||
const modelStateService = require('../../common/services/modelStateService');
|
const modelStateService = require('../../common/services/modelStateService');
|
||||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
|
|
||||||
|
|
||||||
const COMPLETION_DEBOUNCE_MS = 2000;
|
const COMPLETION_DEBOUNCE_MS = 2000;
|
||||||
|
|
||||||
@ -134,7 +133,7 @@ class SttService {
|
|||||||
async initializeSttSessions(language = 'en') {
|
async initializeSttSessions(language = 'en') {
|
||||||
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || 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) {
|
if (!modelInfo || !modelInfo.apiKey) {
|
||||||
throw new Error('AI model or API key is not configured.');
|
throw new Error('AI model or API key is not configured.');
|
||||||
}
|
}
|
||||||
@ -467,7 +466,7 @@ class SttService {
|
|||||||
let modelInfo = this.modelInfo;
|
let modelInfo = this.modelInfo;
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
||||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
modelInfo = await modelStateService.getCurrentModelInfo('stt');
|
||||||
}
|
}
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
throw new Error('STT model info could not be retrieved.');
|
throw new Error('STT model info could not be retrieved.');
|
||||||
@ -492,7 +491,7 @@ class SttService {
|
|||||||
let modelInfo = this.modelInfo;
|
let modelInfo = this.modelInfo;
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
||||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
modelInfo = await modelStateService.getCurrentModelInfo('stt');
|
||||||
}
|
}
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
throw new Error('STT model info could not be retrieved.');
|
throw new Error('STT model info could not be retrieved.');
|
||||||
@ -578,7 +577,7 @@ class SttService {
|
|||||||
let modelInfo = this.modelInfo;
|
let modelInfo = this.modelInfo;
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
|
||||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
modelInfo = await modelStateService.getCurrentModelInfo('stt');
|
||||||
}
|
}
|
||||||
if (!modelInfo) {
|
if (!modelInfo) {
|
||||||
throw new Error('STT model info could not be retrieved.');
|
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 sessionRepository = require('../../common/repositories/session');
|
||||||
const summaryRepository = require('./repositories');
|
const summaryRepository = require('./repositories');
|
||||||
const modelStateService = require('../../common/services/modelStateService');
|
const modelStateService = require('../../common/services/modelStateService');
|
||||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
|
|
||||||
|
|
||||||
class SummaryService {
|
class SummaryService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -99,7 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
|
|||||||
await sessionRepository.touch(this.currentSessionId);
|
await sessionRepository.touch(this.currentSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelInfo = modelStateService.getCurrentModelInfo('llm');
|
const modelInfo = await modelStateService.getCurrentModelInfo('llm');
|
||||||
if (!modelInfo || !modelInfo.apiKey) {
|
if (!modelInfo || !modelInfo.apiKey) {
|
||||||
throw new Error('AI model or API key is not configured.');
|
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
|
// New facade functions for model state management
|
||||||
async function getModelSettings() {
|
async function getModelSettings() {
|
||||||
try {
|
try {
|
||||||
const [config, storedKeys, selectedModels] = await Promise.all([
|
const [config, storedKeys, selectedModels, availableLlm, availableStt] = await Promise.all([
|
||||||
modelStateService.getProviderConfig(),
|
modelStateService.getProviderConfig(),
|
||||||
modelStateService.getAllApiKeys(),
|
modelStateService.getAllApiKeys(),
|
||||||
modelStateService.getSelectedModels(),
|
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 } };
|
return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SettingsService] Error getting model settings:', 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(`✅ API server started on http://localhost:${apiPort}`);
|
||||||
|
|
||||||
console.log(`🚀 All services ready:`);
|
console.log(`🚀 All services ready:
|
||||||
console.log(` Frontend: http://localhost:${frontendPort}`);
|
Frontend: http://localhost:${frontendPort}
|
||||||
console.log(` API: http://localhost:${apiPort}`);
|
API: http://localhost:${apiPort}`);
|
||||||
|
|
||||||
return frontendPort;
|
return frontendPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-update initialization
|
// Auto-update initialization
|
||||||
async function initAutoUpdater() {
|
async function initAutoUpdater() {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('Development environment, skipping auto-updater.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
|
await autoUpdater.checkForUpdates();
|
||||||
if (!autoUpdateEnabled) {
|
autoUpdater.on('update-available', () => {
|
||||||
console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
|
console.log('Update available!');
|
||||||
return;
|
autoUpdater.downloadUpdate();
|
||||||
}
|
|
||||||
// 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',
|
|
||||||
});
|
});
|
||||||
|
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, date, url) => {
|
||||||
// Immediately check for updates & notify
|
console.log('Update downloaded:', releaseNotes, releaseName, date, url);
|
||||||
autoUpdater.checkForUpdatesAndNotify()
|
dialog.showMessageBox({
|
||||||
.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 = {
|
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['Install now', 'Install on next launch'],
|
title: 'Application Update',
|
||||||
title: 'Update Available',
|
message: `A new version of PickleGlass (${releaseName}) has been downloaded. It will be installed the next time you launch the application.`,
|
||||||
message: 'A new version of Glass is ready to be installed.',
|
buttons: ['Restart', 'Later']
|
||||||
defaultId: 0,
|
}).then(response => {
|
||||||
cancelId: 1
|
if (response.response === 0) {
|
||||||
};
|
|
||||||
|
|
||||||
dialog.showMessageBox(dialogOpts).then((returnValue) => {
|
|
||||||
// returnValue.response 0 is for 'Install Now'
|
|
||||||
if (returnValue.response === 0) {
|
|
||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e) {
|
autoUpdater.on('error', (err) => {
|
||||||
console.error('[AutoUpdater] Failed to initialise:', e);
|
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),
|
removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
|
||||||
onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
|
onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
|
||||||
removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('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
|
// src/ui/app/MainHeader.js
|
||||||
|
@ -48,6 +48,9 @@ class HeaderTransitionManager {
|
|||||||
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
|
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
|
||||||
} else if (type === 'permission') {
|
} else if (type === 'permission') {
|
||||||
this.permissionHeader = document.createElement('permission-setup');
|
this.permissionHeader = document.createElement('permission-setup');
|
||||||
|
this.permissionHeader.addEventListener('request-resize', e => {
|
||||||
|
this._resizeForPermissionHeader(e.detail.height);
|
||||||
|
});
|
||||||
this.permissionHeader.continueCallback = async () => {
|
this.permissionHeader.continueCallback = async () => {
|
||||||
if (window.api && window.api.headerController) {
|
if (window.api && window.api.headerController) {
|
||||||
console.log('[HeaderController] Re-initializing model state after permission grant...');
|
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');
|
this.ensureHeader('permission');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,9 +238,10 @@ class HeaderTransitionManager {
|
|||||||
return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
|
return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resizeForPermissionHeader() {
|
async _resizeForPermissionHeader(height) {
|
||||||
if (!window.api) return;
|
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(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ export class PermissionHeader extends LitElement {
|
|||||||
.container {
|
.container {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
width: 285px;
|
width: 285px;
|
||||||
height: 220px;
|
/* height is now set dynamically */
|
||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@ -103,6 +103,12 @@ export class PermissionHeader extends LitElement {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-content.all-granted {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -260,7 +266,8 @@ export class PermissionHeader extends LitElement {
|
|||||||
screenGranted: { type: String },
|
screenGranted: { type: String },
|
||||||
keychainGranted: { type: String },
|
keychainGranted: { type: String },
|
||||||
isChecking: { type: String },
|
isChecking: { type: String },
|
||||||
continueCallback: { type: Function }
|
continueCallback: { type: Function },
|
||||||
|
userMode: { type: String }, // 'local' or 'firebase'
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -270,14 +277,47 @@ export class PermissionHeader extends LitElement {
|
|||||||
this.keychainGranted = 'unknown';
|
this.keychainGranted = 'unknown';
|
||||||
this.isChecking = false;
|
this.isChecking = false;
|
||||||
this.continueCallback = null;
|
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() {
|
async connectedCallback() {
|
||||||
super.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();
|
await this.checkPermissions();
|
||||||
|
|
||||||
// Set up periodic permission check
|
// 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();
|
this.checkPermissions();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@ -312,10 +352,13 @@ export class PermissionHeader extends LitElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isKeychainRequired = this.userMode === 'firebase';
|
||||||
|
const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
|
||||||
|
|
||||||
// if all permissions granted == automatically continue
|
// if all permissions granted == automatically continue
|
||||||
if (this.microphoneGranted === 'granted' &&
|
if (this.microphoneGranted === 'granted' &&
|
||||||
this.screenGranted === 'granted' &&
|
this.screenGranted === 'granted' &&
|
||||||
this.keychainGranted === 'granted' &&
|
keychainOk &&
|
||||||
this.continueCallback) {
|
this.continueCallback) {
|
||||||
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
|
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
|
||||||
setTimeout(() => this.handleContinue(), 500);
|
setTimeout(() => this.handleContinue(), 500);
|
||||||
@ -405,12 +448,15 @@ export class PermissionHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleContinue() {
|
async handleContinue() {
|
||||||
|
const isKeychainRequired = this.userMode === 'firebase';
|
||||||
|
const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
|
||||||
|
|
||||||
if (this.continueCallback &&
|
if (this.continueCallback &&
|
||||||
this.microphoneGranted === 'granted' &&
|
this.microphoneGranted === 'granted' &&
|
||||||
this.screenGranted === 'granted' &&
|
this.screenGranted === 'granted' &&
|
||||||
this.keychainGranted === 'granted') {
|
keychainOk) {
|
||||||
// Mark permissions as completed
|
// Mark permissions as completed
|
||||||
if (window.api) {
|
if (window.api && isKeychainRequired) {
|
||||||
try {
|
try {
|
||||||
await window.api.permissionHeader.markKeychainCompleted();
|
await window.api.permissionHeader.markKeychainCompleted();
|
||||||
console.log('[PermissionHeader] Marked keychain as completed');
|
console.log('[PermissionHeader] Marked keychain as completed');
|
||||||
@ -431,10 +477,13 @@ export class PermissionHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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`
|
return html`
|
||||||
<div class="container">
|
<div class="container" style="height: ${containerHeight}px">
|
||||||
<button class="close-button" @click=${this.handleClose} title="Close application">
|
<button class="close-button" @click=${this.handleClose} title="Close application">
|
||||||
<svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
|
<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" />
|
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
|
||||||
@ -442,89 +491,92 @@ export class PermissionHeader extends LitElement {
|
|||||||
</button>
|
</button>
|
||||||
<h1 class="title">Permission Setup Required</h1>
|
<h1 class="title">Permission Setup Required</h1>
|
||||||
|
|
||||||
<div class="form-content">
|
<div class="form-content ${allGranted ? 'all-granted' : ''}">
|
||||||
<div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
|
${!allGranted ? html`
|
||||||
|
<div class="subtitle">Grant access to microphone, screen recording${isKeychainRequired ? ' and keychain' : ''} to continue</div>
|
||||||
|
|
||||||
<div class="permission-status">
|
<div class="permission-status">
|
||||||
<div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
|
<div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
|
||||||
${this.microphoneGranted === 'granted' ? html`
|
${this.microphoneGranted === 'granted' ? html`
|
||||||
<svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span>Microphone ✓</span>
|
<span>Microphone ✓</span>
|
||||||
` : html`
|
` : html`
|
||||||
<svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span>Microphone</span>
|
<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>
|
||||||
|
|
||||||
<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 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
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
@click=${this.handleMicrophoneClick}
|
@click=${this.handleMicrophoneClick}
|
||||||
|
?disabled=${this.microphoneGranted === 'granted'}
|
||||||
>
|
>
|
||||||
Grant Microphone Access
|
${this.microphoneGranted === 'granted' ? 'Microphone Access Granted' : 'Grant Microphone Access'}
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.screenGranted !== 'granted' ? html`
|
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
@click=${this.handleScreenClick}
|
@click=${this.handleScreenClick}
|
||||||
|
?disabled=${this.screenGranted === 'granted'}
|
||||||
>
|
>
|
||||||
Grant Screen Recording Access
|
${this.screenGranted === 'granted' ? 'Screen Recording Granted' : 'Grant Screen Recording Access'}
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.keychainGranted !== 'granted' ? html`
|
${isKeychainRequired ? html`
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
@click=${this.handleKeychainClick}
|
@click=${this.handleKeychainClick}
|
||||||
>
|
?disabled=${this.keychainGranted === 'granted'}
|
||||||
Grant Keychain Access
|
>
|
||||||
</button>
|
${this.keychainGranted === 'granted' ? 'Encryption Enabled' : 'Enable Encryption'}
|
||||||
<div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
|
</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.
|
||||||
${allGranted ? html`
|
</div>
|
||||||
|
` : ''}
|
||||||
|
` : html`
|
||||||
<button
|
<button
|
||||||
class="continue-button"
|
class="continue-button"
|
||||||
@click=${this.handleContinue}
|
@click=${this.handleContinue}
|
||||||
>
|
>
|
||||||
Continue to Pickle Glass
|
Continue to Pickle Glass
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -918,6 +918,8 @@ export class SettingsView extends LitElement {
|
|||||||
this.setupIpcListeners();
|
this.setupIpcListeners();
|
||||||
this.setupWindowResize();
|
this.setupWindowResize();
|
||||||
this.loadAutoUpdateSetting();
|
this.loadAutoUpdateSetting();
|
||||||
|
// Force one height calculation immediately (innerHeight may be 0 at first)
|
||||||
|
setTimeout(() => this.updateScrollHeight(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@ -1030,8 +1032,10 @@ export class SettingsView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateScrollHeight() {
|
updateScrollHeight() {
|
||||||
const windowHeight = window.innerHeight;
|
// Electron 일부 시점에서 window.innerHeight 가 0 으로 보고되는 버그 보호
|
||||||
const maxHeight = windowHeight;
|
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`;
|
this.style.maxHeight = `${maxHeight}px`;
|
||||||
|
|
||||||
@ -1043,6 +1047,8 @@ export class SettingsView extends LitElement {
|
|||||||
|
|
||||||
handleMouseEnter = () => {
|
handleMouseEnter = () => {
|
||||||
window.api.settingsView.cancelHideSettingsWindow();
|
window.api.settingsView.cancelHideSettingsWindow();
|
||||||
|
// Recalculate height in case it was set to 0 before
|
||||||
|
this.updateScrollHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave = () => {
|
handleMouseLeave = () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user