Compare commits
No commits in common. "main" and "refactor/localmodel" have entirely different histories.
main
...
refactor/l
@ -11,7 +11,6 @@ const localAIManager = require('../features/common/services/localAIManager');
|
|||||||
const askService = require('../features/ask/askService');
|
const askService = require('../features/ask/askService');
|
||||||
const listenService = require('../features/listen/listenService');
|
const listenService = require('../features/listen/listenService');
|
||||||
const permissionService = require('../features/common/services/permissionService');
|
const permissionService = require('../features/common/services/permissionService');
|
||||||
const encryptionService = require('../features/common/services/encryptionService');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Renderer로부터의 요청을 수신하고 서비스로 전달
|
// Renderer로부터의 요청을 수신하고 서비스로 전달
|
||||||
@ -21,6 +20,7 @@ module.exports = {
|
|||||||
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
||||||
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));
|
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));
|
||||||
ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
|
ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
|
||||||
|
ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key));
|
||||||
ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
|
ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
|
||||||
ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));
|
ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));
|
||||||
|
|
||||||
@ -40,13 +40,10 @@ module.exports = {
|
|||||||
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
||||||
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
||||||
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
||||||
ipcMain.handle('mark-keychain-completed', async () => await permissionService.markKeychainCompleted());
|
|
||||||
ipcMain.handle('check-keychain-completed', async () => await permissionService.checkKeychainCompleted());
|
//TODO: Need to Remove this
|
||||||
ipcMain.handle('initialize-encryption-key', async () => {
|
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
|
||||||
const userId = authService.getCurrentUserId();
|
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
|
||||||
await encryptionService.initializeKey(userId);
|
|
||||||
return { success: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
// User/Auth
|
// User/Auth
|
||||||
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
||||||
@ -96,7 +93,6 @@ module.exports = {
|
|||||||
ipcMain.handle('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio());
|
ipcMain.handle('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio());
|
||||||
ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio());
|
ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio());
|
||||||
ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
|
ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
|
||||||
ipcMain.handle('listen:isSessionActive', async () => await listenService.isSessionActive());
|
|
||||||
ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
|
ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
|
||||||
console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText);
|
console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText);
|
||||||
try {
|
try {
|
||||||
@ -110,15 +106,14 @@ 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', async () => await modelStateService.getAllApiKeys());
|
ipcMain.handle('model:get-all-keys', () => 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', async () => await modelStateService.getSelectedModels());
|
ipcMain.handle('model:get-selected-models', () => 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', async (e, { type }) => await modelStateService.getAvailableModels(type));
|
ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
|
||||||
ipcMain.handle('model:are-providers-configured', async () => await modelStateService.areProvidersConfigured());
|
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||||
ipcMain.handle('model:re-initialize-state', async () => await modelStateService.initialize());
|
|
||||||
|
|
||||||
// LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
|
// LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
|
||||||
localAIManager.on('install-progress', (service, data) => {
|
localAIManager.on('install-progress', (service, data) => {
|
||||||
|
@ -24,8 +24,9 @@ module.exports = {
|
|||||||
ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
|
ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
|
||||||
ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
|
ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
|
||||||
ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
|
ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
|
||||||
|
ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY));
|
||||||
ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
|
ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
|
||||||
ipcMain.handle('adjust-window-height', (event, { winName, height }) => windowManager.adjustWindowHeight(winName, height));
|
ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
|
||||||
},
|
},
|
||||||
|
|
||||||
notifyFocusChange(win, isFocused) {
|
notifyFocusChange(win, isFocused) {
|
||||||
|
@ -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 = await modelStateService.getCurrentModelInfo('llm');
|
const modelInfo = 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,6 +91,7 @@ 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' },
|
||||||
@ -100,7 +101,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 (provider)']
|
constraints: ['PRIMARY KEY (uid, provider)']
|
||||||
},
|
},
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
columns: [
|
columns: [
|
||||||
@ -108,12 +109,6 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'accelerator', type: 'TEXT NOT NULL' },
|
{ name: 'accelerator', type: 'TEXT NOT NULL' },
|
||||||
{ name: 'created_at', type: 'INTEGER' }
|
{ name: 'created_at', type: 'INTEGER' }
|
||||||
]
|
]
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
columns: [
|
|
||||||
{ name: 'uid', type: 'TEXT PRIMARY KEY' },
|
|
||||||
{ name: 'keychain_completed', type: 'INTEGER DEFAULT 0' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,13 +33,7 @@ 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) {
|
||||||
try {
|
appObject[field] = encryptionService.decrypt(appObject[field]);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,6 @@ function getRepository() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
markKeychainCompleted: (...args) => getRepository().markKeychainCompleted(...args),
|
markPermissionsAsCompleted: (...args) => getRepository().markPermissionsAsCompleted(...args),
|
||||||
checkKeychainCompleted: (...args) => getRepository().checkKeychainCompleted(...args),
|
checkPermissionsCompleted: (...args) => getRepository().checkPermissionsCompleted(...args),
|
||||||
};
|
};
|
@ -1,18 +1,14 @@
|
|||||||
const sqliteClient = require('../../services/sqliteClient');
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
|
|
||||||
function markKeychainCompleted(uid) {
|
async function markPermissionsAsCompleted() {
|
||||||
return sqliteClient.query(
|
return sqliteClient.markPermissionsAsCompleted();
|
||||||
'INSERT OR REPLACE INTO permissions (uid, keychain_completed) VALUES (?, 1)',
|
|
||||||
[uid]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkKeychainCompleted(uid) {
|
async function checkPermissionsCompleted() {
|
||||||
const row = sqliteClient.query('SELECT keychain_completed FROM permissions WHERE uid = ?', [uid]);
|
return sqliteClient.checkPermissionsCompleted();
|
||||||
return row.length > 0 && row[0].keychain_completed === 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
markKeychainCompleted,
|
markPermissionsAsCompleted,
|
||||||
checkKeychainCompleted
|
checkPermissionsCompleted,
|
||||||
};
|
};
|
@ -0,0 +1,161 @@
|
|||||||
|
const { collection, doc, getDoc, getDocs, setDoc, deleteDoc, query, where } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance: getFirestore } = require('../../services/firebaseClient');
|
||||||
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
|
||||||
|
// Create encrypted converter for provider settings
|
||||||
|
const providerSettingsConverter = createEncryptedConverter([
|
||||||
|
'api_key', // Encrypt API keys
|
||||||
|
'selected_llm_model', // Encrypt model selections for privacy
|
||||||
|
'selected_stt_model'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function providerSettingsCol() {
|
||||||
|
const db = getFirestore();
|
||||||
|
return collection(db, 'provider_settings').withConverter(providerSettingsConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getByProvider(uid, provider) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
const docSnap = await getDoc(docRef);
|
||||||
|
return docSnap.exists() ? { id: docSnap.id, ...docSnap.data() } : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting provider settings:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllByUid(uid) {
|
||||||
|
try {
|
||||||
|
const q = query(providerSettingsCol(), where('uid', '==', uid));
|
||||||
|
const querySnapshot = await getDocs(q);
|
||||||
|
return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting all provider settings:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsert(uid, provider, settings) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
await setDoc(docRef, settings, { merge: true });
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error upserting provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(uid, provider) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
await deleteDoc(docRef);
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error removing provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAllByUid(uid) {
|
||||||
|
try {
|
||||||
|
const settings = await getAllByUid(uid);
|
||||||
|
const deletePromises = settings.map(setting => {
|
||||||
|
const docRef = doc(providerSettingsCol(), setting.id);
|
||||||
|
return deleteDoc(docRef);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
return { changes: settings.length };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error removing all provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active provider for a specific type (llm or stt)
|
||||||
|
async function getActiveProvider(uid, type) {
|
||||||
|
try {
|
||||||
|
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
||||||
|
const q = query(providerSettingsCol(),
|
||||||
|
where('uid', '==', uid),
|
||||||
|
where(column, '==', true)
|
||||||
|
);
|
||||||
|
const querySnapshot = await getDocs(q);
|
||||||
|
|
||||||
|
if (querySnapshot.empty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = querySnapshot.docs[0];
|
||||||
|
return { id: doc.id, ...doc.data() };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting active provider:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set active provider for a specific type
|
||||||
|
async function setActiveProvider(uid, provider, type) {
|
||||||
|
try {
|
||||||
|
const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
|
||||||
|
|
||||||
|
// First, deactivate all providers for this type
|
||||||
|
const allSettings = await getAllByUid(uid);
|
||||||
|
const updatePromises = allSettings.map(setting => {
|
||||||
|
const docRef = doc(providerSettingsCol(), setting.id);
|
||||||
|
return setDoc(docRef, { [column]: false }, { merge: true });
|
||||||
|
});
|
||||||
|
await Promise.all(updatePromises);
|
||||||
|
|
||||||
|
// Then activate the specified provider
|
||||||
|
if (provider) {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
await setDoc(docRef, { [column]: true }, { merge: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error setting active provider:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all active settings (both llm and stt)
|
||||||
|
async function getActiveSettings(uid) {
|
||||||
|
try {
|
||||||
|
// Firebase doesn't support OR queries in this way, so we'll get all settings and filter
|
||||||
|
const allSettings = await getAllByUid(uid);
|
||||||
|
|
||||||
|
const activeSettings = {
|
||||||
|
llm: null,
|
||||||
|
stt: null
|
||||||
|
};
|
||||||
|
|
||||||
|
allSettings.forEach(setting => {
|
||||||
|
if (setting.is_active_llm) {
|
||||||
|
activeSettings.llm = setting;
|
||||||
|
}
|
||||||
|
if (setting.is_active_stt) {
|
||||||
|
activeSettings.stt = setting;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return activeSettings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting active settings:', error);
|
||||||
|
return { llm: null, stt: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getByProvider,
|
||||||
|
getAllByUid,
|
||||||
|
upsert,
|
||||||
|
remove,
|
||||||
|
removeAllByUid,
|
||||||
|
getActiveProvider,
|
||||||
|
setActiveProvider,
|
||||||
|
getActiveSettings
|
||||||
|
};
|
@ -1,68 +1,83 @@
|
|||||||
|
const firebaseRepository = require('./firebase.repository');
|
||||||
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.
|
if (!authService) {
|
||||||
return sqliteRepository;
|
throw new Error('AuthService not set for providerSettings repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = authService.getCurrentUser();
|
||||||
|
return user.isLoggedIn ? firebaseRepository : sqliteRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerSettingsRepositoryAdapter = {
|
const providerSettingsRepositoryAdapter = {
|
||||||
// Core CRUD operations
|
// Core CRUD operations
|
||||||
async getByProvider(provider) {
|
async getByProvider(provider) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.getByProvider(provider);
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.getByProvider(uid, provider);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAll() {
|
async getAllByUid() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.getAll();
|
const uid = authService.getCurrentUserId();
|
||||||
|
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(provider, settingsWithMeta);
|
return await repo.upsert(uid, provider, settingsWithMeta);
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(provider) {
|
async remove(provider) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.remove(provider);
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.remove(uid, provider);
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeAll() {
|
async removeAllByUid() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.removeAll();
|
const uid = authService.getCurrentUserId();
|
||||||
},
|
return await repo.removeAllByUid(uid);
|
||||||
|
|
||||||
async getRawApiKeys() {
|
|
||||||
// This function should always target the local sqlite DB,
|
|
||||||
// as it's part of the local-first boot sequence.
|
|
||||||
return await sqliteRepository.getRawApiKeys();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActiveProvider(type) {
|
async getActiveProvider(type) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.getActiveProvider(type);
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.getActiveProvider(uid, type);
|
||||||
},
|
},
|
||||||
|
|
||||||
async setActiveProvider(provider, type) {
|
async setActiveProvider(provider, type) {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.setActiveProvider(provider, type);
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.setActiveProvider(uid, provider, type);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActiveSettings() {
|
async getActiveSettings() {
|
||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
return await repo.getActiveSettings();
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.getActiveSettings(uid);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...providerSettingsRepositoryAdapter
|
...providerSettingsRepositoryAdapter,
|
||||||
|
setAuthService
|
||||||
};
|
};
|
@ -1,32 +1,34 @@
|
|||||||
const sqliteClient = require('../../services/sqliteClient');
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
const encryptionService = require('../../services/encryptionService');
|
|
||||||
|
|
||||||
function getByProvider(provider) {
|
function getByProvider(uid, provider) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('SELECT * FROM provider_settings WHERE provider = ?');
|
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
|
||||||
const result = stmt.get(provider) || null;
|
const result = stmt.get(uid, provider) || null;
|
||||||
|
|
||||||
if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
// if (result && result.api_key) {
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
// // Decrypt API key if it exists
|
||||||
}
|
// result.api_key = result.api_key;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAll() {
|
function getAllByUid(uid) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('SELECT * FROM provider_settings ORDER BY provider');
|
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
|
||||||
const results = stmt.all();
|
const results = stmt.all(uid);
|
||||||
|
|
||||||
|
// Decrypt API keys for all results
|
||||||
return results.map(result => {
|
return results.map(result => {
|
||||||
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
if (result.api_key) {
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
result.api_key = result.api_key;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(provider, settings) {
|
function upsert(uid, 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.');
|
||||||
@ -34,11 +36,17 @@ function upsert(provider, settings) {
|
|||||||
|
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
|
|
||||||
|
// Encrypt API key if it exists
|
||||||
|
// const encryptedSettings = { ...settings };
|
||||||
|
// if (encryptedSettings.api_key) {
|
||||||
|
// encryptedSettings.api_key = encryptedSettings.api_key;
|
||||||
|
// }
|
||||||
|
|
||||||
// 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 (provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
|
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 (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(provider) DO UPDATE SET
|
ON CONFLICT(uid, 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,
|
||||||
@ -48,6 +56,7 @@ function upsert(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 +70,49 @@ function upsert(provider, settings) {
|
|||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(provider) {
|
function remove(uid, provider) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('DELETE FROM provider_settings WHERE provider = ?');
|
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
|
||||||
const result = stmt.run(provider);
|
const result = stmt.run(uid, provider);
|
||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAll() {
|
function removeAllByUid(uid) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const stmt = db.prepare('DELETE FROM provider_settings');
|
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
|
||||||
const result = stmt.run();
|
const result = stmt.run(uid);
|
||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRawApiKeys() {
|
|
||||||
const db = sqliteClient.getDb();
|
|
||||||
const stmt = db.prepare('SELECT api_key FROM provider_settings');
|
|
||||||
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(type) {
|
function getActiveProvider(uid, 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 ${column} = 1`);
|
const stmt = db.prepare(`SELECT * FROM provider_settings WHERE uid = ? AND ${column} = 1`);
|
||||||
const result = stmt.get() || null;
|
const result = stmt.get(uid) || null;
|
||||||
|
|
||||||
if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
if (result && result.api_key) {
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
result.api_key = result.api_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set active provider for a specific type
|
// Set active provider for a specific type
|
||||||
function setActiveProvider(provider, type) {
|
function setActiveProvider(uid, 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`);
|
const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0 WHERE uid = ?`);
|
||||||
deactivateStmt.run();
|
deactivateStmt.run(uid);
|
||||||
|
|
||||||
// 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 provider = ?`);
|
const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE uid = ? AND provider = ?`);
|
||||||
activateStmt.run(provider);
|
activateStmt.run(uid, provider);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -117,14 +120,14 @@ function setActiveProvider(provider, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all active settings (both llm and stt)
|
// Get all active settings (both llm and stt)
|
||||||
function getActiveSettings() {
|
function getActiveSettings(uid) {
|
||||||
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 (is_active_llm = 1 OR is_active_stt = 1)
|
WHERE uid = ? AND (is_active_llm = 1 OR is_active_stt = 1)
|
||||||
ORDER BY provider
|
ORDER BY provider
|
||||||
`);
|
`);
|
||||||
const results = stmt.all();
|
const results = stmt.all(uid);
|
||||||
|
|
||||||
// Decrypt API keys and organize by type
|
// Decrypt API keys and organize by type
|
||||||
const activeSettings = {
|
const activeSettings = {
|
||||||
@ -133,8 +136,8 @@ function getActiveSettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
|
if (result.api_key) {
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
result.api_key = result.api_key;
|
||||||
}
|
}
|
||||||
if (result.is_active_llm) {
|
if (result.is_active_llm) {
|
||||||
activeSettings.llm = result;
|
activeSettings.llm = result;
|
||||||
@ -149,11 +152,10 @@ function getActiveSettings() {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getByProvider,
|
getByProvider,
|
||||||
getAll,
|
getAllByUid,
|
||||||
upsert,
|
upsert,
|
||||||
remove,
|
remove,
|
||||||
removeAll,
|
removeAllByUid,
|
||||||
getRawApiKeys,
|
|
||||||
getActiveProvider,
|
getActiveProvider,
|
||||||
setActiveProvider,
|
setActiveProvider,
|
||||||
getActiveSettings
|
getActiveSettings
|
||||||
|
@ -6,7 +6,6 @@ const encryptionService = require('./encryptionService');
|
|||||||
const migrationService = require('./migrationService');
|
const migrationService = require('./migrationService');
|
||||||
const sessionRepository = require('../repositories/session');
|
const sessionRepository = require('../repositories/session');
|
||||||
const providerSettingsRepository = require('../repositories/providerSettings');
|
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||||
const permissionService = require('./permissionService');
|
|
||||||
|
|
||||||
async function getVirtualKeyByEmail(email, idToken) {
|
async function getVirtualKeyByEmail(email, idToken) {
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
@ -43,9 +42,11 @@ class AuthService {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
// This ensures the key is ready before any login/logout state change.
|
// This ensures the key is ready before any login/logout state change.
|
||||||
|
encryptionService.initializeKey(this.currentUserId);
|
||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
|
|
||||||
sessionRepository.setAuthService(this);
|
sessionRepository.setAuthService(this);
|
||||||
|
providerSettingsRepository.setAuthService(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
@ -66,32 +67,29 @@ class AuthService {
|
|||||||
// Clean up any zombie sessions from a previous run for this user.
|
// Clean up any zombie sessions from a previous run for this user.
|
||||||
await sessionRepository.endAllActiveSessions();
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
// ** Initialize encryption key for the logged-in user if permissions are already granted **
|
// ** Initialize encryption key for the logged-in user **
|
||||||
if (process.platform === 'darwin' && !(await permissionService.checkKeychainCompleted(this.currentUserId))) {
|
await encryptionService.initializeKey(user.uid);
|
||||||
console.warn('[AuthService] Keychain permission not yet completed for this user. Deferring key initialization.');
|
|
||||||
} else {
|
|
||||||
await encryptionService.initializeKey(user.uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ** Check for and run data migration for the user **
|
// ** Check for and run data migration for the user **
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (global.modelStateService) {
|
// Start background task to fetch and save virtual key
|
||||||
// The model state service now writes directly to the DB, no in-memory state.
|
(async () => {
|
||||||
await global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
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);
|
||||||
}
|
}
|
||||||
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
|
||||||
@ -99,8 +97,7 @@ 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) {
|
||||||
// The model state service now writes directly to the DB.
|
global.modelStateService.setFirebaseVirtualKey(null);
|
||||||
await global.modelStateService.setFirebaseVirtualKey(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
@ -110,7 +107,8 @@ class AuthService {
|
|||||||
// End active sessions for the local/default user as well.
|
// End active sessions for the local/default user as well.
|
||||||
await sessionRepository.endAllActiveSessions();
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
encryptionService.resetSessionKey();
|
// ** Initialize encryption key for the default/local user **
|
||||||
|
await encryptionService.initializeKey(this.currentUserId);
|
||||||
}
|
}
|
||||||
this.broadcastUserState();
|
this.broadcastUserState();
|
||||||
|
|
||||||
@ -175,6 +173,7 @@ class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getCurrentUserId() {
|
getCurrentUserId() {
|
||||||
return this.currentUserId;
|
return this.currentUserId;
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,10 @@ class DatabaseInitializer {
|
|||||||
|
|
||||||
// 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
|
// 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
// In both development and production mode, the database is stored in the userData directory:
|
|
||||||
// macOS: ~/Library/Application Support/Glass/pickleglass.db
|
|
||||||
// Windows: %APPDATA%\Glass\pickleglass.db
|
|
||||||
this.dbPath = path.join(userDataPath, 'pickleglass.db');
|
this.dbPath = path.join(userDataPath, 'pickleglass.db');
|
||||||
this.dataDir = userDataPath;
|
this.dataDir = userDataPath;
|
||||||
|
|
||||||
// The original DB path (read-only location in the package)
|
// 원본 DB 경로 (패키지 내 읽기 전용 위치)
|
||||||
this.sourceDbPath = app.isPackaged
|
this.sourceDbPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, 'data', 'pickleglass.db')
|
? path.join(process.resourcesPath, 'data', 'pickleglass.db')
|
||||||
: path.join(app.getAppPath(), 'data', 'pickleglass.db');
|
: path.join(app.getAppPath(), 'data', 'pickleglass.db');
|
||||||
@ -55,7 +52,7 @@ class DatabaseInitializer {
|
|||||||
try {
|
try {
|
||||||
this.ensureDatabaseExists();
|
this.ensureDatabaseExists();
|
||||||
|
|
||||||
sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
|
await sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
|
||||||
|
|
||||||
// This single call will now synchronize the schema and then init default data.
|
// This single call will now synchronize the schema and then init default data.
|
||||||
await sqliteClient.initTables();
|
await sqliteClient.initTables();
|
||||||
|
@ -9,8 +9,6 @@ try {
|
|||||||
keytar = null;
|
keytar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionService = require('./permissionService');
|
|
||||||
|
|
||||||
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
|
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
|
||||||
let sessionKey = null; // In-memory fallback key
|
let sessionKey = null; // In-memory fallback key
|
||||||
|
|
||||||
@ -33,8 +31,6 @@ async function initializeKey(userId) {
|
|||||||
throw new Error('A user ID must be provided to initialize the encryption key.');
|
throw new Error('A user ID must be provided to initialize the encryption key.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyRetrieved = false;
|
|
||||||
|
|
||||||
if (keytar) {
|
if (keytar) {
|
||||||
try {
|
try {
|
||||||
let key = await keytar.getPassword(SERVICE_NAME, userId);
|
let key = await keytar.getPassword(SERVICE_NAME, userId);
|
||||||
@ -45,7 +41,6 @@ async function initializeKey(userId) {
|
|||||||
console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
|
console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
|
console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
|
||||||
keyRetrieved = true;
|
|
||||||
}
|
}
|
||||||
sessionKey = key;
|
sessionKey = key;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -60,26 +55,12 @@ async function initializeKey(userId) {
|
|||||||
sessionKey = crypto.randomBytes(32).toString('hex');
|
sessionKey = crypto.randomBytes(32).toString('hex');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark keychain completed in permissions DB if this is the first successful retrieval or storage
|
|
||||||
try {
|
|
||||||
await permissionService.markKeychainCompleted(userId);
|
|
||||||
if (keyRetrieved) {
|
|
||||||
console.log(`[EncryptionService] Keychain completion marked in DB for ${userId}.`);
|
|
||||||
}
|
|
||||||
} catch (permErr) {
|
|
||||||
console.error('[EncryptionService] Failed to mark keychain completion:', permErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionKey) {
|
if (!sessionKey) {
|
||||||
throw new Error('Failed to initialize encryption key.');
|
throw new Error('Failed to initialize encryption key.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSessionKey() {
|
|
||||||
sessionKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a given text using AES-256-GCM.
|
* Encrypts a given text using AES-256-GCM.
|
||||||
* @param {string} text The text to encrypt.
|
* @param {string} text The text to encrypt.
|
||||||
@ -148,28 +129,12 @@ function decrypt(encryptedText) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// It's common for this to fail if the data is not encrypted (e.g., legacy data).
|
// It's common for this to fail if the data is not encrypted (e.g., legacy data).
|
||||||
// In that case, we return the original value.
|
// In that case, we return the original value.
|
||||||
console.error('[EncryptionService] Decryption failed:', error);
|
|
||||||
return encryptedText;
|
return encryptedText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function looksEncrypted(str) {
|
|
||||||
if (!str || typeof str !== 'string') return false;
|
|
||||||
// Base64 chars + optional '=' padding
|
|
||||||
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(str)) return false;
|
|
||||||
try {
|
|
||||||
const buf = Buffer.from(str, 'base64');
|
|
||||||
// Our AES-GCM cipher text must be at least 32 bytes (IV 16 + TAG 16)
|
|
||||||
return buf.length >= 32;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initializeKey,
|
initializeKey,
|
||||||
resetSessionKey,
|
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
looksEncrypted,
|
|
||||||
};
|
};
|
File diff suppressed because it is too large
Load Diff
@ -1066,7 +1066,7 @@ class OllamaService extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedModels = await modelStateService.getSelectedModels();
|
const selectedModels = modelStateService.getSelectedModels();
|
||||||
const llmModelId = selectedModels.llm;
|
const llmModelId = selectedModels.llm;
|
||||||
|
|
||||||
// Check if it's an Ollama model
|
// Check if it's an Ollama model
|
||||||
|
@ -2,28 +2,27 @@ const { systemPreferences, shell, desktopCapturer } = require('electron');
|
|||||||
const permissionRepository = require('../repositories/permission');
|
const permissionRepository = require('../repositories/permission');
|
||||||
|
|
||||||
class PermissionService {
|
class PermissionService {
|
||||||
_getAuthService() {
|
|
||||||
return require('./authService');
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkSystemPermissions() {
|
async checkSystemPermissions() {
|
||||||
const permissions = {
|
const permissions = {
|
||||||
microphone: 'unknown',
|
microphone: 'unknown',
|
||||||
screen: 'unknown',
|
screen: 'unknown',
|
||||||
keychain: 'unknown',
|
|
||||||
needsSetup: true
|
needsSetup: true
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
permissions.microphone = systemPreferences.getMediaAccessStatus('microphone');
|
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
|
||||||
permissions.screen = systemPreferences.getMediaAccessStatus('screen');
|
console.log('[Permissions] Microphone status:', micStatus);
|
||||||
permissions.keychain = await this.checkKeychainCompleted(this._getAuthService().getCurrentUserId()) ? 'granted' : 'unknown';
|
permissions.microphone = micStatus;
|
||||||
permissions.needsSetup = permissions.microphone !== 'granted' || permissions.screen !== 'granted' || permissions.keychain !== 'granted';
|
|
||||||
|
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
|
||||||
|
console.log('[Permissions] Screen status:', screenStatus);
|
||||||
|
permissions.screen = screenStatus;
|
||||||
|
|
||||||
|
permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
|
||||||
} else {
|
} else {
|
||||||
permissions.microphone = 'granted';
|
permissions.microphone = 'granted';
|
||||||
permissions.screen = 'granted';
|
permissions.screen = 'granted';
|
||||||
permissions.keychain = 'granted';
|
|
||||||
permissions.needsSetup = false;
|
permissions.needsSetup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +33,6 @@ class PermissionService {
|
|||||||
return {
|
return {
|
||||||
microphone: 'unknown',
|
microphone: 'unknown',
|
||||||
screen: 'unknown',
|
screen: 'unknown',
|
||||||
keychain: 'unknown',
|
|
||||||
needsSetup: true,
|
needsSetup: true,
|
||||||
error: error.message
|
error: error.message
|
||||||
};
|
};
|
||||||
@ -94,27 +92,24 @@ class PermissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async markKeychainCompleted() {
|
async markPermissionsAsCompleted() {
|
||||||
try {
|
try {
|
||||||
await permissionRepository.markKeychainCompleted(this._getAuthService().getCurrentUserId());
|
await permissionRepository.markPermissionsAsCompleted();
|
||||||
console.log('[Permissions] Marked keychain as completed');
|
console.log('[Permissions] Marked permissions as completed');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Permissions] Error marking keychain as completed:', error);
|
console.error('[Permissions] Error marking permissions as completed:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkKeychainCompleted(uid) {
|
async checkPermissionsCompleted() {
|
||||||
if (uid === "default_user") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const completed = permissionRepository.checkKeychainCompleted(uid);
|
const completed = await permissionRepository.checkPermissionsCompleted();
|
||||||
console.log('[Permissions] Keychain completed status:', completed);
|
console.log('[Permissions] Permissions completed status:', completed);
|
||||||
return completed;
|
return completed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Permissions] Error checking keychain completed status:', error);
|
console.error('[Permissions] Error checking permissions completed status:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,82 +40,8 @@ class SQLiteClient {
|
|||||||
return `"${identifier}"`;
|
return `"${identifier}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_migrateProviderSettings() {
|
synchronizeSchema() {
|
||||||
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...');
|
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)) {
|
||||||
@ -206,8 +132,8 @@ class SQLiteClient {
|
|||||||
console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
|
console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initTables() {
|
initTables() {
|
||||||
await this.synchronizeSchema();
|
this.synchronizeSchema();
|
||||||
this.initDefaultData();
|
this.initDefaultData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +166,21 @@ class SQLiteClient {
|
|||||||
console.log('Default data initialized.');
|
console.log('Default data initialized.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markPermissionsAsCompleted() {
|
||||||
|
return this.query(
|
||||||
|
'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
|
||||||
|
['permissions_completed', 'true']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermissionsCompleted() {
|
||||||
|
const result = this.query(
|
||||||
|
'SELECT value FROM system_settings WHERE key = ?',
|
||||||
|
['permissions_completed']
|
||||||
|
);
|
||||||
|
return result.length > 0 && result[0].value === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
try {
|
try {
|
||||||
|
@ -54,7 +54,53 @@ class ListenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleListenRequest(listenButtonText) {
|
async handleListenRequest(listenButtonText) {
|
||||||
const { windowPool } = require('../../window/windowManager');
|
const { windowPool, updateLayout } = require('../../window/windowManager');
|
||||||
|
const listenWindow = windowPool.get('listen');
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (listenButtonText) {
|
||||||
|
case 'Listen':
|
||||||
|
console.log('[ListenService] changeSession to "Listen"');
|
||||||
|
listenWindow.show();
|
||||||
|
updateLayout();
|
||||||
|
listenWindow.webContents.send('window-show-animation');
|
||||||
|
await this.initializeSession();
|
||||||
|
listenWindow.webContents.send('session-state-changed', { isActive: true });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Stop':
|
||||||
|
console.log('[ListenService] changeSession to "Stop"');
|
||||||
|
await this.closeSession();
|
||||||
|
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Done':
|
||||||
|
console.log('[ListenService] changeSession to "Done"');
|
||||||
|
listenWindow.webContents.send('window-hide-animation');
|
||||||
|
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`[ListenService] unknown listenButtonText: ${listenButtonText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
header.webContents.send('listen:changeSessionResult', { success: true });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ListenService] error in handleListenRequest:', error);
|
||||||
|
header.webContents.send('listen:changeSessionResult', { success: false });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.setupIpcHandlers();
|
||||||
|
console.log('[ListenService] Initialized and ready.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleListenRequest(listenButtonText) {
|
||||||
|
const { windowPool, updateLayout } = require('../../window/windowManager');
|
||||||
const listenWindow = windowPool.get('listen');
|
const listenWindow = windowPool.get('listen');
|
||||||
const header = windowPool.get('header');
|
const header = windowPool.get('header');
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ 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;
|
||||||
|
|
||||||
@ -133,7 +134,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 = await modelStateService.getCurrentModelInfo('stt');
|
const modelInfo = 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.');
|
||||||
}
|
}
|
||||||
@ -466,7 +467,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 = await modelStateService.getCurrentModelInfo('stt');
|
modelInfo = 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.');
|
||||||
@ -491,7 +492,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 = await modelStateService.getCurrentModelInfo('stt');
|
modelInfo = 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.');
|
||||||
@ -577,7 +578,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 = await modelStateService.getCurrentModelInfo('stt');
|
modelInfo = 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,6 +4,7 @@ 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() {
|
||||||
@ -98,7 +99,7 @@ Please build upon this context while analyzing the new conversation segments.
|
|||||||
await sessionRepository.touch(this.currentSessionId);
|
await sessionRepository.touch(this.currentSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelInfo = await modelStateService.getCurrentModelInfo('llm');
|
const modelInfo = 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,14 +26,16 @@ 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, availableLlm, availableStt] = await Promise.all([
|
const [config, storedKeys, selectedModels] = 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);
|
||||||
@ -41,6 +43,10 @@ async function getModelSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateAndSaveKey(provider, key) {
|
||||||
|
return modelStateService.handleValidateKey(provider, key);
|
||||||
|
}
|
||||||
|
|
||||||
async function clearApiKey(provider) {
|
async function clearApiKey(provider) {
|
||||||
const success = await modelStateService.handleRemoveApiKey(provider);
|
const success = await modelStateService.handleRemoveApiKey(provider);
|
||||||
return { success };
|
return { success };
|
||||||
@ -458,6 +464,7 @@ module.exports = {
|
|||||||
setAutoUpdateSetting,
|
setAutoUpdateSetting,
|
||||||
// Model settings facade
|
// Model settings facade
|
||||||
getModelSettings,
|
getModelSettings,
|
||||||
|
validateAndSaveKey,
|
||||||
clearApiKey,
|
clearApiKey,
|
||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
// Ollama facade
|
// Ollama facade
|
||||||
|
@ -8,11 +8,13 @@ class ShortcutsService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.lastVisibleWindows = new Set(['header']);
|
this.lastVisibleWindows = new Set(['header']);
|
||||||
this.mouseEventsIgnored = false;
|
this.mouseEventsIgnored = false;
|
||||||
|
this.movementManager = null;
|
||||||
this.windowPool = null;
|
this.windowPool = null;
|
||||||
this.allWindowVisibility = true;
|
this.allWindowVisibility = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(windowPool) {
|
initialize(movementManager, windowPool) {
|
||||||
|
this.movementManager = movementManager;
|
||||||
this.windowPool = windowPool;
|
this.windowPool = windowPool;
|
||||||
internalBridge.on('reregister-shortcuts', () => {
|
internalBridge.on('reregister-shortcuts', () => {
|
||||||
console.log('[ShortcutsService] Reregistering shortcuts due to header state change.');
|
console.log('[ShortcutsService] Reregistering shortcuts due to header state change.');
|
||||||
@ -136,7 +138,7 @@ class ShortcutsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async registerShortcuts(registerOnlyToggleVisibility = false) {
|
async registerShortcuts(registerOnlyToggleVisibility = false) {
|
||||||
if (!this.windowPool) {
|
if (!this.movementManager || !this.windowPool) {
|
||||||
console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.');
|
console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -177,7 +179,7 @@ class ShortcutsService {
|
|||||||
if (displays.length > 1) {
|
if (displays.length > 1) {
|
||||||
displays.forEach((display, index) => {
|
displays.forEach((display, index) => {
|
||||||
const key = `${modifier}+Shift+${index + 1}`;
|
const key = `${modifier}+Shift+${index + 1}`;
|
||||||
globalShortcut.register(key, () => internalBridge.emit('window:moveToDisplay', { displayId: display.id }));
|
globalShortcut.register(key, () => this.movementManager.moveToDisplay(display.id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +190,7 @@ class ShortcutsService {
|
|||||||
];
|
];
|
||||||
edgeDirections.forEach(({ key, direction }) => {
|
edgeDirections.forEach(({ key, direction }) => {
|
||||||
globalShortcut.register(key, () => {
|
globalShortcut.register(key, () => {
|
||||||
if (header && header.isVisible()) internalBridge.emit('window:moveToEdge', { direction });
|
if (header && header.isVisible()) this.movementManager.moveToEdge(direction);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,16 +232,16 @@ class ShortcutsService {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'moveUp':
|
case 'moveUp':
|
||||||
callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'up' }); };
|
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('up'); };
|
||||||
break;
|
break;
|
||||||
case 'moveDown':
|
case 'moveDown':
|
||||||
callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'down' }); };
|
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('down'); };
|
||||||
break;
|
break;
|
||||||
case 'moveLeft':
|
case 'moveLeft':
|
||||||
callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'left' }); };
|
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('left'); };
|
||||||
break;
|
break;
|
||||||
case 'moveRight':
|
case 'moveRight':
|
||||||
callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'right' }); };
|
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('right'); };
|
||||||
break;
|
break;
|
||||||
case 'toggleClickThrough':
|
case 'toggleClickThrough':
|
||||||
callback = () => {
|
callback = () => {
|
||||||
|
82
src/index.js
82
src/index.js
@ -686,43 +686,75 @@ 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:`);
|
||||||
Frontend: http://localhost:${frontendPort}
|
console.log(` Frontend: http://localhost:${frontendPort}`);
|
||||||
API: http://localhost:${apiPort}`);
|
console.log(` 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 {
|
||||||
await autoUpdater.checkForUpdates();
|
const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
|
||||||
autoUpdater.on('update-available', () => {
|
if (!autoUpdateEnabled) {
|
||||||
console.log('Update available!');
|
console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
|
||||||
autoUpdater.downloadUpdate();
|
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',
|
||||||
});
|
});
|
||||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, date, url) => {
|
|
||||||
console.log('Update downloaded:', releaseNotes, releaseName, date, url);
|
// Immediately check for updates & notify
|
||||||
dialog.showMessageBox({
|
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 = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Application Update',
|
buttons: ['Install now', 'Install on next launch'],
|
||||||
message: `A new version of PickleGlass (${releaseName}) has been downloaded. It will be installed the next time you launch the application.`,
|
title: 'Update Available',
|
||||||
buttons: ['Restart', 'Later']
|
message: 'A new version of Glass is ready to be installed.',
|
||||||
}).then(response => {
|
defaultId: 0,
|
||||||
if (response.response === 0) {
|
cancelId: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.showMessageBox(dialogOpts).then((returnValue) => {
|
||||||
|
// returnValue.response 0 is for 'Install Now'
|
||||||
|
if (returnValue.response === 0) {
|
||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
autoUpdater.on('error', (err) => {
|
} catch (e) {
|
||||||
console.error('Error in auto-updater:', err);
|
console.error('[AutoUpdater] Failed to initialise:', e);
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error initializing auto-updater:', err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -82,7 +82,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
headerController: {
|
headerController: {
|
||||||
// State Management
|
// State Management
|
||||||
sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
|
sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
|
||||||
reInitializeModelState: () => ipcRenderer.invoke('model:re-initialize-state'),
|
|
||||||
|
|
||||||
// Window Management
|
// Window Management
|
||||||
resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
|
resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
|
||||||
@ -97,7 +96,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
|
onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
|
||||||
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)
|
||||||
},
|
},
|
||||||
|
|
||||||
// src/ui/app/MainHeader.js
|
// src/ui/app/MainHeader.js
|
||||||
@ -131,9 +130,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
||||||
requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
|
requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
|
||||||
openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
|
openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
|
||||||
markKeychainCompleted: () => ipcRenderer.invoke('mark-keychain-completed'),
|
markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed')
|
||||||
checkKeychainCompleted: (uid) => ipcRenderer.invoke('check-keychain-completed', uid),
|
|
||||||
initializeEncryptionKey: () => ipcRenderer.invoke('initialize-encryption-key') // New for keychain
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// src/ui/app/PickleGlassApp.js
|
// src/ui/app/PickleGlassApp.js
|
||||||
@ -148,7 +145,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
askView: {
|
askView: {
|
||||||
// Window Management
|
// Window Management
|
||||||
closeAskWindow: () => ipcRenderer.invoke('ask:closeAskWindow'),
|
closeAskWindow: () => ipcRenderer.invoke('ask:closeAskWindow'),
|
||||||
adjustWindowHeight: (winName, height) => ipcRenderer.invoke('adjust-window-height', { winName, height }),
|
adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text),
|
sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text),
|
||||||
@ -173,7 +170,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// src/ui/listen/ListenView.js
|
// src/ui/listen/ListenView.js
|
||||||
listenView: {
|
listenView: {
|
||||||
// Window Management
|
// Window Management
|
||||||
adjustWindowHeight: (winName, height) => ipcRenderer.invoke('adjust-window-height', { winName, height }),
|
adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
onSessionStateChanged: (callback) => ipcRenderer.on('session-state-changed', callback),
|
onSessionStateChanged: (callback) => ipcRenderer.on('session-state-changed', callback),
|
||||||
@ -290,7 +287,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
|
stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
|
||||||
|
|
||||||
// Session Management
|
// Session Management
|
||||||
isSessionActive: () => ipcRenderer.invoke('listen:isSessionActive'),
|
isSessionActive: () => ipcRenderer.invoke('is-session-active'),
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback),
|
onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback),
|
||||||
|
@ -48,16 +48,7 @@ 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.permissionHeader.continueCallback = () => this.transitionToMainHeader();
|
||||||
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...');
|
|
||||||
await window.api.headerController.reInitializeModelState();
|
|
||||||
}
|
|
||||||
this.transitionToMainHeader();
|
|
||||||
};
|
|
||||||
this.headerContainer.appendChild(this.permissionHeader);
|
this.headerContainer.appendChild(this.permissionHeader);
|
||||||
} else {
|
} else {
|
||||||
this.mainHeader = document.createElement('main-header');
|
this.mainHeader = document.createElement('main-header');
|
||||||
@ -130,15 +121,19 @@ class HeaderTransitionManager {
|
|||||||
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
||||||
|
|
||||||
if (isConfigured) {
|
if (isConfigured) {
|
||||||
// If providers are configured, always check permissions regardless of login state.
|
const { isLoggedIn } = userState;
|
||||||
const permissionResult = await this.checkPermissions();
|
if (isLoggedIn) {
|
||||||
if (permissionResult.success) {
|
const permissionResult = await this.checkPermissions();
|
||||||
this.transitionToMainHeader();
|
if (permissionResult.success) {
|
||||||
|
this.transitionToMainHeader();
|
||||||
|
} else {
|
||||||
|
this.transitionToPermissionHeader();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.transitionToPermissionHeader();
|
this.transitionToMainHeader();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no providers are configured, show the welcome header to prompt for setup.
|
// 프로바이더가 설정되지 않았으면 WelcomeHeader 먼저 표시
|
||||||
await this._resizeForWelcome();
|
await this._resizeForWelcome();
|
||||||
this.ensureHeader('welcome');
|
this.ensureHeader('welcome');
|
||||||
}
|
}
|
||||||
@ -201,19 +196,7 @@ class HeaderTransitionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialHeight = 220;
|
await this._resizeForPermissionHeader();
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,10 +221,9 @@ class HeaderTransitionManager {
|
|||||||
return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
|
return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resizeForPermissionHeader(height) {
|
async _resizeForPermissionHeader() {
|
||||||
if (!window.api) return;
|
if (!window.api) return;
|
||||||
const finalHeight = height || 220;
|
return window.api.headerController.resizeHeaderWindow({ width: 285, 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 is now set dynamically */
|
height: 220px;
|
||||||
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,12 +103,6 @@ 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;
|
||||||
@ -264,60 +258,24 @@ export class PermissionHeader extends LitElement {
|
|||||||
static properties = {
|
static properties = {
|
||||||
microphoneGranted: { type: String },
|
microphoneGranted: { type: String },
|
||||||
screenGranted: { type: String },
|
screenGranted: { 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() {
|
||||||
super();
|
super();
|
||||||
this.microphoneGranted = 'unknown';
|
this.microphoneGranted = 'unknown';
|
||||||
this.screenGranted = 'unknown';
|
this.screenGranted = '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(async () => {
|
this.permissionCheckInterval = setInterval(() => {
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -340,25 +298,19 @@ export class PermissionHeader extends LitElement {
|
|||||||
|
|
||||||
const prevMic = this.microphoneGranted;
|
const prevMic = this.microphoneGranted;
|
||||||
const prevScreen = this.screenGranted;
|
const prevScreen = this.screenGranted;
|
||||||
const prevKeychain = this.keychainGranted;
|
|
||||||
|
|
||||||
this.microphoneGranted = permissions.microphone;
|
this.microphoneGranted = permissions.microphone;
|
||||||
this.screenGranted = permissions.screen;
|
this.screenGranted = permissions.screen;
|
||||||
this.keychainGranted = permissions.keychain;
|
|
||||||
|
|
||||||
// if permissions changed == UI update
|
// if permissions changed == UI update
|
||||||
if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted || prevKeychain !== this.keychainGranted) {
|
if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
|
||||||
console.log('[PermissionHeader] Permission status changed, updating UI');
|
console.log('[PermissionHeader] Permission status changed, updating UI');
|
||||||
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' &&
|
||||||
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);
|
||||||
@ -429,39 +381,17 @@ export class PermissionHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleKeychainClick() {
|
|
||||||
if (!window.api || this.keychainGranted === 'granted') return;
|
|
||||||
|
|
||||||
console.log('[PermissionHeader] Requesting keychain permission...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Trigger initializeKey to prompt for keychain access
|
|
||||||
// Assuming encryptionService is accessible or via API
|
|
||||||
await window.api.permissionHeader.initializeEncryptionKey(); // New IPC handler needed
|
|
||||||
|
|
||||||
// After success, update status
|
|
||||||
this.keychainGranted = 'granted';
|
|
||||||
this.requestUpdate();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[PermissionHeader] Error requesting keychain permission:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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') {
|
||||||
keychainOk) {
|
|
||||||
// Mark permissions as completed
|
// Mark permissions as completed
|
||||||
if (window.api && isKeychainRequired) {
|
if (window.api) {
|
||||||
try {
|
try {
|
||||||
await window.api.permissionHeader.markKeychainCompleted();
|
await window.api.permissionHeader.markPermissionsCompleted();
|
||||||
console.log('[PermissionHeader] Marked keychain as completed');
|
console.log('[PermissionHeader] Marked permissions as completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[PermissionHeader] Error marking keychain as completed:', error);
|
console.error('[PermissionHeader] Error marking permissions as completed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,13 +407,10 @@ export class PermissionHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isKeychainRequired = this.userMode === 'firebase';
|
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
|
||||||
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" style="height: ${containerHeight}px">
|
<div class="container">
|
||||||
<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" />
|
||||||
@ -491,92 +418,65 @@ 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 ${allGranted ? 'all-granted' : ''}">
|
<div class="form-content">
|
||||||
${!allGranted ? html`
|
<div class="subtitle">Grant access to microphone and screen recording to continue</div>
|
||||||
<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>
|
||||||
|
|
||||||
|
${this.microphoneGranted !== 'granted' ? html`
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
@click=${this.handleMicrophoneClick}
|
@click=${this.handleMicrophoneClick}
|
||||||
?disabled=${this.microphoneGranted === 'granted'}
|
|
||||||
>
|
>
|
||||||
${this.microphoneGranted === 'granted' ? 'Microphone Access Granted' : 'Grant Microphone Access'}
|
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'}
|
|
||||||
>
|
>
|
||||||
${this.screenGranted === 'granted' ? 'Screen Recording Granted' : 'Grant Screen Recording Access'}
|
Grant Screen Recording Access
|
||||||
</button>
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
${isKeychainRequired ? html`
|
${allGranted ? 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
|
<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>
|
||||||
`;
|
`;
|
||||||
|
@ -503,7 +503,6 @@ export class AskView extends LitElement {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input-container.no-response {
|
.text-input-container.no-response {
|
||||||
@ -1422,7 +1421,7 @@ export class AskView extends LitElement {
|
|||||||
|
|
||||||
const targetHeight = Math.min(700, idealHeight);
|
const targetHeight = Math.min(700, idealHeight);
|
||||||
|
|
||||||
window.api.askView.adjustWindowHeight("ask", targetHeight);
|
window.api.askView.adjustWindowHeight(targetHeight);
|
||||||
|
|
||||||
}).catch(err => console.error('AskView adjustWindowHeight error:', err));
|
}).catch(err => console.error('AskView adjustWindowHeight error:', err));
|
||||||
}
|
}
|
||||||
|
@ -536,7 +536,7 @@ export class ListenView extends LitElement {
|
|||||||
`[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
|
`[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
|
||||||
);
|
);
|
||||||
|
|
||||||
window.api.listenView.adjustWindowHeight('listen', targetHeight);
|
window.api.listenView.adjustWindowHeight(targetHeight);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error in adjustWindowHeight:', error);
|
console.error('Error in adjustWindowHeight:', error);
|
||||||
|
@ -422,12 +422,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
|
|
||||||
const sessionActive = await window.api.listenCapture.isSessionActive();
|
|
||||||
if (!sessionActive) {
|
|
||||||
throw new Error('STT sessions not initialized - please wait for initialization to complete');
|
|
||||||
}
|
|
||||||
|
|
||||||
// On macOS, use SystemAudioDump for audio and getDisplayMedia for screen
|
// On macOS, use SystemAudioDump for audio and getDisplayMedia for screen
|
||||||
console.log('Starting macOS capture with SystemAudioDump...');
|
console.log('Starting macOS capture with SystemAudioDump...');
|
||||||
|
|
||||||
@ -472,12 +466,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
|||||||
|
|
||||||
console.log('macOS screen capture started - audio handled by SystemAudioDump');
|
console.log('macOS screen capture started - audio handled by SystemAudioDump');
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
|
|
||||||
const sessionActive = await window.api.listenCapture.isSessionActive();
|
|
||||||
if (!sessionActive) {
|
|
||||||
throw new Error('STT sessions not initialized - please wait for initialization to complete');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linux - use display media for screen capture and getUserMedia for microphone
|
// Linux - use display media for screen capture and getUserMedia for microphone
|
||||||
mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
video: {
|
video: {
|
||||||
|
@ -918,8 +918,6 @@ 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() {
|
||||||
@ -1032,13 +1030,11 @@ export class SettingsView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateScrollHeight() {
|
updateScrollHeight() {
|
||||||
// Electron 일부 시점에서 window.innerHeight 가 0 으로 보고되는 버그 보호
|
const windowHeight = window.innerHeight;
|
||||||
const rawHeight = window.innerHeight || (window.screen ? window.screen.height : 0);
|
const maxHeight = windowHeight;
|
||||||
const MIN_HEIGHT = 300; // 최소 보장 높이
|
|
||||||
const maxHeight = Math.max(MIN_HEIGHT, rawHeight);
|
|
||||||
|
|
||||||
this.style.maxHeight = `${maxHeight}px`;
|
this.style.maxHeight = `${maxHeight}px`;
|
||||||
|
|
||||||
const container = this.shadowRoot?.querySelector('.settings-container');
|
const container = this.shadowRoot?.querySelector('.settings-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style.maxHeight = `${maxHeight}px`;
|
container.style.maxHeight = `${maxHeight}px`;
|
||||||
@ -1047,8 +1043,6 @@ 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 = () => {
|
||||||
|
@ -171,7 +171,6 @@ export class ShortcutSettingsView extends LitElement {
|
|||||||
|
|
||||||
async handleSave() {
|
async handleSave() {
|
||||||
if (!window.api) return;
|
if (!window.api) return;
|
||||||
this.feedback = {};
|
|
||||||
const result = await window.api.shortcutSettingsView.saveShortcuts(this.shortcuts);
|
const result = await window.api.shortcutSettingsView.saveShortcuts(this.shortcuts);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
alert('Failed to save shortcuts: ' + result.error);
|
alert('Failed to save shortcuts: ' + result.error);
|
||||||
@ -180,7 +179,6 @@ export class ShortcutSettingsView extends LitElement {
|
|||||||
|
|
||||||
handleClose() {
|
handleClose() {
|
||||||
if (!window.api) return;
|
if (!window.api) return;
|
||||||
this.feedback = {};
|
|
||||||
window.api.shortcutSettingsView.closeShortcutSettingsWindow();
|
window.api.shortcutSettingsView.closeShortcutSettingsWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
const { screen } = require('electron');
|
const { screen } = require('electron');
|
||||||
|
|
||||||
class SmoothMovementManager {
|
class SmoothMovementManager {
|
||||||
constructor(windowPool) {
|
constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) {
|
||||||
this.windowPool = windowPool;
|
this.windowPool = windowPool;
|
||||||
|
this.getDisplayById = getDisplayById;
|
||||||
|
this.getCurrentDisplay = getCurrentDisplay;
|
||||||
|
this.updateLayout = updateLayout;
|
||||||
this.stepSize = 80;
|
this.stepSize = 80;
|
||||||
this.animationDuration = 300;
|
this.animationDuration = 300;
|
||||||
this.headerPosition = { x: 0, y: 0 };
|
this.headerPosition = { x: 0, y: 0 };
|
||||||
@ -11,8 +14,6 @@ class SmoothMovementManager {
|
|||||||
this.lastVisiblePosition = null;
|
this.lastVisiblePosition = null;
|
||||||
this.currentDisplayId = null;
|
this.currentDisplayId = null;
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
|
|
||||||
this.animationTimers = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,25 +22,164 @@ class SmoothMovementManager {
|
|||||||
*/
|
*/
|
||||||
_isWindowValid(win) {
|
_isWindowValid(win) {
|
||||||
if (!win || win.isDestroyed()) {
|
if (!win || win.isDestroyed()) {
|
||||||
// 해당 창의 타이머가 있으면 정리
|
if (this.isAnimating) {
|
||||||
if (this.animationTimers.has(win)) {
|
console.warn('[MovementManager] Window destroyed mid-animation. Halting.');
|
||||||
clearTimeout(this.animationTimers.get(win));
|
this.isAnimating = false;
|
||||||
this.animationTimers.delete(win);
|
if (this.animationFrameId) {
|
||||||
|
clearTimeout(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveToDisplay(displayId) {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||||
|
|
||||||
|
const targetDisplay = this.getDisplayById(displayId);
|
||||||
|
if (!targetDisplay) return;
|
||||||
|
|
||||||
|
const currentBounds = header.getBounds();
|
||||||
|
const currentDisplay = this.getCurrentDisplay(header);
|
||||||
|
|
||||||
|
if (currentDisplay.id === targetDisplay.id) return;
|
||||||
|
|
||||||
|
const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width;
|
||||||
|
const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height;
|
||||||
|
const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX;
|
||||||
|
const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY;
|
||||||
|
|
||||||
|
const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX));
|
||||||
|
const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY));
|
||||||
|
|
||||||
|
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||||
|
this.animateToPosition(header, finalX, finalY);
|
||||||
|
this.currentDisplayId = targetDisplay.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideToEdge(edge, callback, { instant = false } = {}) {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!header || header.isDestroyed()) {
|
||||||
|
if (typeof callback === 'function') callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = header.getBounds();
|
||||||
|
this.lastVisiblePosition = { x, y };
|
||||||
|
this.hiddenPosition = { edge };
|
||||||
|
|
||||||
|
if (instant) {
|
||||||
|
header.hide();
|
||||||
|
if (typeof callback === 'function') callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
header.webContents.send('window-hide-animation');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!header.isDestroyed()) header.hide();
|
||||||
|
if (typeof callback === 'function') callback();
|
||||||
|
}, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
showFromEdge(callback) {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!header || header.isDestroyed()) {
|
||||||
|
if (typeof callback === 'function') callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 숨기기 전에 기억해둔 위치 복구
|
||||||
|
if (this.lastVisiblePosition) {
|
||||||
|
header.setPosition(
|
||||||
|
this.lastVisiblePosition.x,
|
||||||
|
this.lastVisiblePosition.y,
|
||||||
|
false // animate: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
header.show();
|
||||||
|
header.webContents.send('window-show-animation');
|
||||||
|
|
||||||
|
// 내부 상태 초기화
|
||||||
|
this.hiddenPosition = null;
|
||||||
|
this.lastVisiblePosition = null;
|
||||||
|
|
||||||
|
if (typeof callback === 'function') callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveStep(direction) {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||||
|
|
||||||
|
const currentBounds = header.getBounds();
|
||||||
|
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||||
|
let targetX = this.headerPosition.x;
|
||||||
|
let targetY = this.headerPosition.y;
|
||||||
|
|
||||||
|
console.log(`[MovementManager] Moving ${direction} from (${targetX}, ${targetY})`);
|
||||||
|
|
||||||
|
const windowSize = {
|
||||||
|
width: currentBounds.width,
|
||||||
|
height: currentBounds.height
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'left': targetX -= this.stepSize; break;
|
||||||
|
case 'right': targetX += this.stepSize; break;
|
||||||
|
case 'up': targetY -= this.stepSize; break;
|
||||||
|
case 'down': targetY += this.stepSize; break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the display that contains or is nearest to the target position
|
||||||
|
const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
|
||||||
|
const { x: workAreaX, y: workAreaY, width: workAreaWidth, height: workAreaHeight } = nearestDisplay.workArea;
|
||||||
|
|
||||||
|
// Only clamp if the target position would actually go out of bounds
|
||||||
|
let clampedX = targetX;
|
||||||
|
let clampedY = targetY;
|
||||||
|
|
||||||
|
// Check horizontal bounds
|
||||||
|
if (targetX < workAreaX) {
|
||||||
|
clampedX = workAreaX;
|
||||||
|
} else if (targetX + currentBounds.width > workAreaX + workAreaWidth) {
|
||||||
|
clampedX = workAreaX + workAreaWidth - currentBounds.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check vertical bounds
|
||||||
|
if (targetY < workAreaY) {
|
||||||
|
clampedY = workAreaY;
|
||||||
|
console.log(`[MovementManager] Clamped Y to top edge: ${clampedY}`);
|
||||||
|
} else if (targetY + currentBounds.height > workAreaY + workAreaHeight) {
|
||||||
|
clampedY = workAreaY + workAreaHeight - currentBounds.height;
|
||||||
|
console.log(`[MovementManager] Clamped Y to bottom edge: ${clampedY}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MovementManager] Final position: (${clampedX}, ${clampedY}), Work area: ${workAreaX},${workAreaY} ${workAreaWidth}x${workAreaHeight}`);
|
||||||
|
|
||||||
|
// Only move if there's an actual change in position
|
||||||
|
if (clampedX === this.headerPosition.x && clampedY === this.headerPosition.y) {
|
||||||
|
console.log(`[MovementManager] No position change, skipping animation`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animateToPosition(header, clampedX, clampedY, windowSize);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* [수정됨] 창을 목표 지점으로 부드럽게 애니메이션합니다.
|
||||||
* @param {BrowserWindow} win
|
* 완료 콜백 및 기타 옵션을 지원합니다.
|
||||||
* @param {number} targetX
|
* @param {BrowserWindow} win - 애니메이션할 창
|
||||||
* @param {number} targetY
|
* @param {number} targetX - 목표 X 좌표
|
||||||
* @param {object} [options]
|
* @param {number} targetY - 목표 Y 좌표
|
||||||
* @param {object} [options.sizeOverride]
|
* @param {object} [options] - 추가 옵션
|
||||||
* @param {function} [options.onComplete]
|
* @param {object} [options.sizeOverride] - 애니메이션 중 사용할 창 크기
|
||||||
* @param {number} [options.duration]
|
* @param {function} [options.onComplete] - 애니메이션 완료 후 실행할 콜백
|
||||||
|
* @param {number} [options.duration] - 애니메이션 지속 시간 (ms)
|
||||||
*/
|
*/
|
||||||
animateWindow(win, targetX, targetY, options = {}) {
|
animateWindow(win, targetX, targetY, options = {}) {
|
||||||
if (!this._isWindowValid(win)) {
|
if (!this._isWindowValid(win)) {
|
||||||
@ -54,6 +194,7 @@ class SmoothMovementManager {
|
|||||||
const { width, height } = sizeOverride || start;
|
const { width, height } = sizeOverride || start;
|
||||||
|
|
||||||
const step = () => {
|
const step = () => {
|
||||||
|
// 애니메이션 중간에 창이 파괴될 경우 콜백을 실행하고 중단
|
||||||
if (!this._isWindowValid(win)) {
|
if (!this._isWindowValid(win)) {
|
||||||
if (onComplete) onComplete();
|
if (onComplete) onComplete();
|
||||||
return;
|
return;
|
||||||
@ -67,116 +208,112 @@ class SmoothMovementManager {
|
|||||||
win.setBounds({ x: Math.round(x), y: Math.round(y), width, height });
|
win.setBounds({ x: Math.round(x), y: Math.round(y), width, height });
|
||||||
|
|
||||||
if (p < 1) {
|
if (p < 1) {
|
||||||
setTimeout(step, 8);
|
setTimeout(step, 8); // requestAnimationFrame 대신 setTimeout으로 간결하게 처리
|
||||||
} else {
|
} else {
|
||||||
this.layoutManager.updateLayout();
|
// 애니메이션 종료
|
||||||
|
this.updateLayout(); // 레이아웃 재정렬
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete();
|
onComplete(); // 완료 콜백 실행
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
step();
|
step();
|
||||||
}
|
}
|
||||||
|
|
||||||
fade(win, { from, to, duration = 250, onComplete }) {
|
animateToPosition(header, targetX, targetY, windowSize) {
|
||||||
if (!this._isWindowValid(win)) {
|
if (!this._isWindowValid(header)) return;
|
||||||
if (onComplete) onComplete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const startOpacity = from ?? win.getOpacity();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const step = () => {
|
this.isAnimating = true;
|
||||||
if (!this._isWindowValid(win)) {
|
const startX = this.headerPosition.x;
|
||||||
if (onComplete) onComplete(); return;
|
const startY = this.headerPosition.y;
|
||||||
}
|
const startTime = Date.now();
|
||||||
const progress = Math.min(1, (Date.now() - startTime) / duration);
|
|
||||||
const eased = 1 - Math.pow(1 - progress, 3);
|
|
||||||
win.setOpacity(startOpacity + (to - startOpacity) * eased);
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
setTimeout(step, 8);
|
|
||||||
} else {
|
|
||||||
win.setOpacity(to);
|
|
||||||
if (onComplete) onComplete();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
step();
|
|
||||||
}
|
|
||||||
|
|
||||||
animateWindowBounds(win, targetBounds, options = {}) {
|
|
||||||
if (this.animationTimers.has(win)) {
|
|
||||||
clearTimeout(this.animationTimers.get(win));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._isWindowValid(win)) {
|
if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
|
||||||
if (options.onComplete) options.onComplete();
|
this.isAnimating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isAnimating = true;
|
const animate = () => {
|
||||||
|
if (!this._isWindowValid(header)) return;
|
||||||
|
|
||||||
const startBounds = win.getBounds();
|
const elapsed = Date.now() - startTime;
|
||||||
const startTime = Date.now();
|
const progress = Math.min(elapsed / this.animationDuration, 1);
|
||||||
const duration = options.duration || this.animationDuration;
|
const eased = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const currentX = startX + (targetX - startX) * eased;
|
||||||
const step = () => {
|
const currentY = startY + (targetY - startY) * eased;
|
||||||
if (!this._isWindowValid(win)) {
|
|
||||||
if (options.onComplete) options.onComplete();
|
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
|
||||||
|
this.isAnimating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress = Math.min(1, (Date.now() - startTime) / duration);
|
if (!this._isWindowValid(header)) return;
|
||||||
const eased = 1 - Math.pow(1 - progress, 3);
|
const { width, height } = windowSize || header.getBounds();
|
||||||
|
header.setBounds({
|
||||||
const newBounds = {
|
x: Math.round(currentX),
|
||||||
x: Math.round(startBounds.x + (targetBounds.x - startBounds.x) * eased),
|
y: Math.round(currentY),
|
||||||
y: Math.round(startBounds.y + (targetBounds.y - startBounds.y) * eased),
|
width,
|
||||||
width: Math.round(startBounds.width + ((targetBounds.width ?? startBounds.width) - startBounds.width) * eased),
|
height
|
||||||
height: Math.round(startBounds.height + ((targetBounds.height ?? startBounds.height) - startBounds.height) * eased),
|
});
|
||||||
};
|
|
||||||
win.setBounds(newBounds);
|
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
const timerId = setTimeout(step, 8);
|
this.animationFrameId = setTimeout(animate, 8);
|
||||||
this.animationTimers.set(win, timerId);
|
|
||||||
} else {
|
} else {
|
||||||
win.setBounds(targetBounds);
|
this.animationFrameId = null;
|
||||||
this.animationTimers.delete(win);
|
this.isAnimating = false;
|
||||||
|
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
||||||
if (this.animationTimers.size === 0) {
|
if (!this._isWindowValid(header)) return;
|
||||||
this.isAnimating = false;
|
header.setPosition(Math.round(targetX), Math.round(targetY));
|
||||||
|
// Update header position to the actual final position
|
||||||
|
this.headerPosition = { x: Math.round(targetX), y: Math.round(targetY) };
|
||||||
}
|
}
|
||||||
|
this.updateLayout();
|
||||||
if (options.onComplete) options.onComplete();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
step();
|
animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
animateWindowPosition(win, targetPosition, options = {}) {
|
moveToEdge(direction) {
|
||||||
if (!this._isWindowValid(win)) {
|
const header = this.windowPool.get('header');
|
||||||
if (options.onComplete) options.onComplete();
|
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||||
return;
|
|
||||||
}
|
const display = this.getCurrentDisplay(header);
|
||||||
const currentBounds = win.getBounds();
|
const { width, height } = display.workAreaSize;
|
||||||
const targetBounds = { ...currentBounds, ...targetPosition };
|
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||||
this.animateWindowBounds(win, targetBounds, options);
|
const currentBounds = header.getBounds();
|
||||||
}
|
|
||||||
|
const windowSize = {
|
||||||
animateLayout(layout, animated = true) {
|
width: currentBounds.width,
|
||||||
if (!layout) return;
|
height: currentBounds.height
|
||||||
for (const winName in layout) {
|
};
|
||||||
const win = this.windowPool.get(winName);
|
|
||||||
const targetBounds = layout[winName];
|
let targetX = currentBounds.x;
|
||||||
if (win && !win.isDestroyed() && targetBounds) {
|
let targetY = currentBounds.y;
|
||||||
if (animated) {
|
|
||||||
this.animateWindowBounds(win, targetBounds);
|
switch (direction) {
|
||||||
} else {
|
case 'left':
|
||||||
win.setBounds(targetBounds);
|
targetX = workAreaX;
|
||||||
}
|
break;
|
||||||
}
|
case 'right':
|
||||||
|
targetX = workAreaX + width - windowSize.width;
|
||||||
|
break;
|
||||||
|
case 'up':
|
||||||
|
targetY = workAreaY;
|
||||||
|
break;
|
||||||
|
case 'down':
|
||||||
|
targetY = workAreaY + height - windowSize.height;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header.setBounds({
|
||||||
|
x: Math.round(targetX),
|
||||||
|
y: Math.round(targetY),
|
||||||
|
width: windowSize.width,
|
||||||
|
height: windowSize.height
|
||||||
|
});
|
||||||
|
|
||||||
|
this.headerPosition = { x: targetX, y: targetY };
|
||||||
|
this.updateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -27,15 +27,130 @@ class WindowLayoutManager {
|
|||||||
this.PADDING = 80;
|
this.PADDING = 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderPosition = () => {
|
updateLayout() {
|
||||||
const header = this.windowPool.get('header');
|
if (this.isUpdating) return;
|
||||||
if (header) {
|
this.isUpdating = true;
|
||||||
const [x, y] = header.getPosition();
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
this.positionWindows();
|
||||||
|
this.isUpdating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} [visibilityOverride] - { listen: true, ask: true }
|
||||||
|
* @returns {{listen: {x:number, y:number}|null, ask: {x:number, y:number}|null}}
|
||||||
|
*/
|
||||||
|
getTargetBoundsForFeatureWindows(visibilityOverride = {}) {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!header?.getBounds) return {};
|
||||||
|
|
||||||
|
const headerBounds = header.getBounds();
|
||||||
|
const display = getCurrentDisplay(header);
|
||||||
|
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
|
||||||
|
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||||
|
|
||||||
|
const ask = this.windowPool.get('ask');
|
||||||
|
const listen = this.windowPool.get('listen');
|
||||||
|
|
||||||
|
const askVis = visibilityOverride.ask !== undefined ?
|
||||||
|
visibilityOverride.ask :
|
||||||
|
(ask && ask.isVisible() && !ask.isDestroyed());
|
||||||
|
const listenVis = visibilityOverride.listen !== undefined ?
|
||||||
|
visibilityOverride.listen :
|
||||||
|
(listen && listen.isVisible() && !listen.isDestroyed());
|
||||||
|
|
||||||
|
if (!askVis && !listenVis) return {};
|
||||||
|
|
||||||
|
const PAD = 8;
|
||||||
|
const headerTopRel = headerBounds.y - workAreaY;
|
||||||
|
const headerBottomRel = headerTopRel + headerBounds.height;
|
||||||
|
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
|
||||||
|
|
||||||
|
const relativeX = headerCenterXRel / screenWidth;
|
||||||
|
const relativeY = (headerBounds.y - workAreaY) / screenHeight;
|
||||||
|
const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
|
||||||
|
|
||||||
|
const askB = ask ? ask.getBounds() : null;
|
||||||
|
const listenB = listen ? listen.getBounds() : null;
|
||||||
|
|
||||||
|
const result = { listen: null, ask: null };
|
||||||
|
|
||||||
|
if (askVis && listenVis) {
|
||||||
|
let askXRel = headerCenterXRel - (askB.width / 2);
|
||||||
|
let listenXRel = askXRel - listenB.width - PAD;
|
||||||
|
|
||||||
|
if (listenXRel < PAD) {
|
||||||
|
listenXRel = PAD;
|
||||||
|
askXRel = listenXRel + listenB.width + PAD;
|
||||||
|
}
|
||||||
|
if (askXRel + askB.width > screenWidth - PAD) {
|
||||||
|
askXRel = screenWidth - PAD - askB.width;
|
||||||
|
listenXRel = askXRel - listenB.width - PAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬
|
||||||
|
if (strategy.primary === 'above') {
|
||||||
|
const windowBottomAbs = headerBounds.y - PAD;
|
||||||
|
const askY = windowBottomAbs - askB.height;
|
||||||
|
const listenY = windowBottomAbs - listenB.height;
|
||||||
|
result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(askY) };
|
||||||
|
result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(listenY) };
|
||||||
|
} else { // 'below'
|
||||||
|
const yPos = headerBottomRel + PAD;
|
||||||
|
const yAbs = yPos + workAreaY;
|
||||||
|
result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs) };
|
||||||
|
result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs) };
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인)
|
||||||
|
const winB = askVis ? askB : listenB;
|
||||||
|
let xRel = headerCenterXRel - winB.width / 2;
|
||||||
|
xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel));
|
||||||
|
|
||||||
|
let yPos;
|
||||||
|
if (strategy.primary === 'above') {
|
||||||
|
const windowBottomRel = headerTopRel - PAD;
|
||||||
|
yPos = windowBottomRel - winB.height;
|
||||||
|
} else { // 'below'
|
||||||
|
yPos = headerBottomRel + PAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abs = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY) };
|
||||||
|
if (askVis) result.ask = abs;
|
||||||
|
if (listenVis) result.listen = abs;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
positionWindows() {
|
||||||
|
const header = this.windowPool.get('header');
|
||||||
|
if (!header?.getBounds) return;
|
||||||
|
|
||||||
|
const headerBounds = header.getBounds();
|
||||||
|
const display = getCurrentDisplay(header);
|
||||||
|
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
|
||||||
|
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||||
|
|
||||||
|
const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2;
|
||||||
|
const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2;
|
||||||
|
|
||||||
|
const relativeX = headerCenterX / screenWidth;
|
||||||
|
const relativeY = headerCenterY / screenHeight;
|
||||||
|
|
||||||
|
const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
|
||||||
|
|
||||||
|
this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
|
||||||
|
const settings = this.windowPool.get('settings');
|
||||||
|
if (settings && !settings.isDestroyed() && settings.isVisible()) {
|
||||||
|
const settingPos = this.calculateSettingsWindowPosition();
|
||||||
|
if (settingPos) {
|
||||||
|
const { width, height } = settings.getBounds();
|
||||||
|
settings.setBounds({ x: settingPos.x, y: settingPos.y, width, height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -64,6 +179,67 @@ class WindowLayoutManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
|
||||||
|
const ask = this.windowPool.get('ask');
|
||||||
|
const listen = this.windowPool.get('listen');
|
||||||
|
const askVisible = ask && ask.isVisible() && !ask.isDestroyed();
|
||||||
|
const listenVisible = listen && listen.isVisible() && !listen.isDestroyed();
|
||||||
|
|
||||||
|
if (!askVisible && !listenVisible) return;
|
||||||
|
|
||||||
|
const PAD = 8;
|
||||||
|
const headerTopRel = headerBounds.y - workAreaY;
|
||||||
|
const headerBottomRel = headerTopRel + headerBounds.height;
|
||||||
|
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
|
||||||
|
|
||||||
|
let askBounds = askVisible ? ask.getBounds() : null;
|
||||||
|
let listenBounds = listenVisible ? listen.getBounds() : null;
|
||||||
|
|
||||||
|
if (askVisible && listenVisible) {
|
||||||
|
let askXRel = headerCenterXRel - (askBounds.width / 2);
|
||||||
|
let listenXRel = askXRel - listenBounds.width - PAD;
|
||||||
|
|
||||||
|
if (listenXRel < PAD) {
|
||||||
|
listenXRel = PAD;
|
||||||
|
askXRel = listenXRel + listenBounds.width + PAD;
|
||||||
|
}
|
||||||
|
if (askXRel + askBounds.width > screenWidth - PAD) {
|
||||||
|
askXRel = screenWidth - PAD - askBounds.width;
|
||||||
|
listenXRel = askXRel - listenBounds.width - PAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬
|
||||||
|
if (strategy.primary === 'above') {
|
||||||
|
const windowBottomAbs = headerBounds.y - PAD;
|
||||||
|
const askY = windowBottomAbs - askBounds.height;
|
||||||
|
const listenY = windowBottomAbs - listenBounds.height;
|
||||||
|
ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(askY), width: askBounds.width, height: askBounds.height });
|
||||||
|
listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(listenY), width: listenBounds.width, height: listenBounds.height });
|
||||||
|
} else { // 'below'
|
||||||
|
const yPos = headerBottomRel + PAD;
|
||||||
|
const yAbs = yPos + workAreaY;
|
||||||
|
ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askBounds.width, height: askBounds.height });
|
||||||
|
listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenBounds.width, height: listenBounds.height });
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인)
|
||||||
|
const win = askVisible ? ask : listen;
|
||||||
|
const winBounds = askVisible ? askBounds : listenBounds;
|
||||||
|
let xRel = headerCenterXRel - winBounds.width / 2;
|
||||||
|
xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
|
||||||
|
|
||||||
|
let yPos;
|
||||||
|
if (strategy.primary === 'above') {
|
||||||
|
const windowBottomRel = headerTopRel - PAD;
|
||||||
|
yPos = windowBottomRel - winBounds.height;
|
||||||
|
} else { // 'below'
|
||||||
|
yPos = headerBottomRel + PAD;
|
||||||
|
}
|
||||||
|
const yAbs = yPos + workAreaY;
|
||||||
|
|
||||||
|
win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yAbs), width: winBounds.width, height: winBounds.height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {{x: number, y: number} | null}
|
* @returns {{x: number, y: number} | null}
|
||||||
@ -93,206 +269,26 @@ class WindowLayoutManager {
|
|||||||
return { x: Math.round(clampedX), y: Math.round(clampedY) };
|
return { x: Math.round(clampedX), y: Math.round(clampedY) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
positionShortcutSettingsWindow() {
|
||||||
calculateHeaderResize(header, { width, height }) {
|
|
||||||
if (!header) return null;
|
|
||||||
const currentBounds = header.getBounds();
|
|
||||||
const centerX = currentBounds.x + currentBounds.width / 2;
|
|
||||||
const newX = Math.round(centerX - width / 2);
|
|
||||||
const display = getCurrentDisplay(header);
|
|
||||||
const { x: workAreaX, width: workAreaWidth } = display.workArea;
|
|
||||||
const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
|
|
||||||
return { x: clampedX, y: currentBounds.y, width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateClampedPosition(header, { x: newX, y: newY }) {
|
|
||||||
if (!header) return null;
|
|
||||||
const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
|
|
||||||
const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
|
|
||||||
const headerBounds = header.getBounds();
|
|
||||||
const clampedX = Math.max(workAreaX, Math.min(newX, workAreaX + width - headerBounds.width));
|
|
||||||
const clampedY = Math.max(workAreaY, Math.min(newY, workAreaY + height - headerBounds.height));
|
|
||||||
return { x: clampedX, y: clampedY };
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateWindowHeightAdjustment(senderWindow, targetHeight) {
|
|
||||||
if (!senderWindow) return null;
|
|
||||||
const currentBounds = senderWindow.getBounds();
|
|
||||||
const minHeight = senderWindow.getMinimumSize()[1];
|
|
||||||
const maxHeight = senderWindow.getMaximumSize()[1];
|
|
||||||
let adjustedHeight = Math.max(minHeight, targetHeight);
|
|
||||||
if (maxHeight > 0) {
|
|
||||||
adjustedHeight = Math.min(maxHeight, adjustedHeight);
|
|
||||||
}
|
|
||||||
console.log(`[Layout Debug] calculateWindowHeightAdjustment: targetHeight=${targetHeight}`);
|
|
||||||
return { ...currentBounds, height: adjustedHeight };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 getTargetBoundsForFeatureWindows를 이 함수로 대체합니다.
|
|
||||||
calculateFeatureWindowLayout(visibility, headerBoundsOverride = null) {
|
|
||||||
const header = this.windowPool.get('header');
|
|
||||||
const headerBounds = headerBoundsOverride || (header ? header.getBounds() : null);
|
|
||||||
|
|
||||||
if (!headerBounds) return {};
|
|
||||||
|
|
||||||
let display;
|
|
||||||
if (headerBoundsOverride) {
|
|
||||||
const boundsCenter = {
|
|
||||||
x: headerBounds.x + headerBounds.width / 2,
|
|
||||||
y: headerBounds.y + headerBounds.height / 2,
|
|
||||||
};
|
|
||||||
display = screen.getDisplayNearestPoint(boundsCenter);
|
|
||||||
} else {
|
|
||||||
display = getCurrentDisplay(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width: screenWidth, height: screenHeight, x: workAreaX, y: workAreaY } = display.workArea;
|
|
||||||
|
|
||||||
const ask = this.windowPool.get('ask');
|
|
||||||
const listen = this.windowPool.get('listen');
|
|
||||||
|
|
||||||
const askVis = visibility.ask && ask && !ask.isDestroyed();
|
|
||||||
const listenVis = visibility.listen && listen && !listen.isDestroyed();
|
|
||||||
|
|
||||||
if (!askVis && !listenVis) return {};
|
|
||||||
|
|
||||||
const PAD = 8;
|
|
||||||
const headerTopRel = headerBounds.y - workAreaY;
|
|
||||||
const headerBottomRel = headerTopRel + headerBounds.height;
|
|
||||||
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
|
|
||||||
|
|
||||||
const relativeX = headerCenterXRel / screenWidth;
|
|
||||||
const relativeY = (headerBounds.y - workAreaY) / screenHeight;
|
|
||||||
const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
|
|
||||||
|
|
||||||
const askB = askVis ? ask.getBounds() : null;
|
|
||||||
const listenB = listenVis ? listen.getBounds() : null;
|
|
||||||
|
|
||||||
if (askVis) {
|
|
||||||
console.log(`[Layout Debug] Ask Window Bounds: height=${askB.height}, width=${askB.width}`);
|
|
||||||
}
|
|
||||||
if (listenVis) {
|
|
||||||
console.log(`[Layout Debug] Listen Window Bounds: height=${listenB.height}, width=${listenB.width}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const layout = {};
|
|
||||||
|
|
||||||
if (askVis && listenVis) {
|
|
||||||
let askXRel = headerCenterXRel - (askB.width / 2);
|
|
||||||
let listenXRel = askXRel - listenB.width - PAD;
|
|
||||||
|
|
||||||
if (listenXRel < PAD) {
|
|
||||||
listenXRel = PAD;
|
|
||||||
askXRel = listenXRel + listenB.width + PAD;
|
|
||||||
}
|
|
||||||
if (askXRel + askB.width > screenWidth - PAD) {
|
|
||||||
askXRel = screenWidth - PAD - askB.width;
|
|
||||||
listenXRel = askXRel - listenB.width - PAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strategy.primary === 'above') {
|
|
||||||
const windowBottomAbs = headerBounds.y - PAD;
|
|
||||||
layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(windowBottomAbs - askB.height), width: askB.width, height: askB.height };
|
|
||||||
layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(windowBottomAbs - listenB.height), width: listenB.width, height: listenB.height };
|
|
||||||
} else { // 'below'
|
|
||||||
const yAbs = headerBounds.y + headerBounds.height + PAD;
|
|
||||||
layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askB.width, height: askB.height };
|
|
||||||
layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenB.width, height: listenB.height };
|
|
||||||
}
|
|
||||||
} else { // Single window
|
|
||||||
const winName = askVis ? 'ask' : 'listen';
|
|
||||||
const winB = askVis ? askB : listenB;
|
|
||||||
if (!winB) return {};
|
|
||||||
|
|
||||||
let xRel = headerCenterXRel - winB.width / 2;
|
|
||||||
xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel));
|
|
||||||
|
|
||||||
let yPos;
|
|
||||||
if (strategy.primary === 'above') {
|
|
||||||
yPos = (headerBounds.y - workAreaY) - PAD - winB.height;
|
|
||||||
} else { // 'below'
|
|
||||||
yPos = (headerBounds.y - workAreaY) + headerBounds.height + PAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
layout[winName] = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY), width: winB.width, height: winB.height };
|
|
||||||
}
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateShortcutSettingsWindowPosition() {
|
|
||||||
const header = this.windowPool.get('header');
|
const header = this.windowPool.get('header');
|
||||||
const shortcutSettings = this.windowPool.get('shortcut-settings');
|
const shortcutSettings = this.windowPool.get('shortcut-settings');
|
||||||
if (!header || !shortcutSettings) return null;
|
|
||||||
|
if (!header || header.isDestroyed() || !shortcutSettings || shortcutSettings.isDestroyed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const headerBounds = header.getBounds();
|
const headerBounds = header.getBounds();
|
||||||
const shortcutBounds = shortcutSettings.getBounds();
|
const shortcutBounds = shortcutSettings.getBounds();
|
||||||
const { workArea } = getCurrentDisplay(header);
|
|
||||||
|
|
||||||
let newX = Math.round(headerBounds.x + (headerBounds.width / 2) - (shortcutBounds.width / 2));
|
|
||||||
let newY = Math.round(headerBounds.y);
|
|
||||||
|
|
||||||
newX = Math.max(workArea.x, Math.min(newX, workArea.x + workArea.width - shortcutBounds.width));
|
|
||||||
newY = Math.max(workArea.y, Math.min(newY, workArea.y + workArea.height - shortcutBounds.height));
|
|
||||||
|
|
||||||
return { x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height };
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateStepMovePosition(header, direction) {
|
|
||||||
if (!header) return null;
|
|
||||||
const currentBounds = header.getBounds();
|
|
||||||
const stepSize = 80; // 이동 간격
|
|
||||||
let targetX = currentBounds.x;
|
|
||||||
let targetY = currentBounds.y;
|
|
||||||
|
|
||||||
switch (direction) {
|
|
||||||
case 'left': targetX -= stepSize; break;
|
|
||||||
case 'right': targetX += stepSize; break;
|
|
||||||
case 'up': targetY -= stepSize; break;
|
|
||||||
case 'down': targetY += stepSize; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.calculateClampedPosition(header, { x: targetX, y: targetY });
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateEdgePosition(header, direction) {
|
|
||||||
if (!header) return null;
|
|
||||||
const display = getCurrentDisplay(header);
|
const display = getCurrentDisplay(header);
|
||||||
const { workArea } = display;
|
const { workArea } = display;
|
||||||
const currentBounds = header.getBounds();
|
|
||||||
|
let newX = Math.round(headerBounds.x + (headerBounds.width / 2) - (shortcutBounds.width / 2));
|
||||||
let targetX = currentBounds.x;
|
let newY = Math.round(headerBounds.y);
|
||||||
let targetY = currentBounds.y;
|
|
||||||
|
newX = Math.max(workArea.x, Math.min(newX, workArea.x + workArea.width - shortcutBounds.width));
|
||||||
switch (direction) {
|
newY = Math.max(workArea.y, Math.min(newY, workArea.y + workArea.height - shortcutBounds.height));
|
||||||
case 'left': targetX = workArea.x; break;
|
|
||||||
case 'right': targetX = workArea.x + workArea.width - currentBounds.width; break;
|
shortcutSettings.setBounds({ x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height });
|
||||||
case 'up': targetY = workArea.y; break;
|
|
||||||
case 'down': targetY = workArea.y + workArea.height - currentBounds.height; break;
|
|
||||||
}
|
|
||||||
return { x: targetX, y: targetY };
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateNewPositionForDisplay(window, targetDisplayId) {
|
|
||||||
if (!window) return null;
|
|
||||||
|
|
||||||
const targetDisplay = screen.getAllDisplays().find(d => d.id === targetDisplayId);
|
|
||||||
if (!targetDisplay) return null;
|
|
||||||
|
|
||||||
const currentBounds = window.getBounds();
|
|
||||||
const currentDisplay = getCurrentDisplay(window);
|
|
||||||
|
|
||||||
if (currentDisplay.id === targetDisplay.id) return { x: currentBounds.x, y: currentBounds.y };
|
|
||||||
|
|
||||||
const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workArea.width;
|
|
||||||
const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workArea.height;
|
|
||||||
|
|
||||||
const targetX = targetDisplay.workArea.x + targetDisplay.workArea.width * relativeX;
|
|
||||||
const targetY = targetDisplay.workArea.y + targetDisplay.workArea.height * relativeY;
|
|
||||||
|
|
||||||
const clampedX = Math.max(targetDisplay.workArea.x, Math.min(targetX, targetDisplay.workArea.x + targetDisplay.workArea.width - currentBounds.width));
|
|
||||||
const clampedY = Math.max(targetDisplay.workArea.y, Math.min(targetY, targetDisplay.workArea.y + targetDisplay.workArea.height - currentBounds.height));
|
|
||||||
|
|
||||||
return { x: Math.round(clampedX), y: Math.round(clampedY) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,6 +30,8 @@ if (shouldUseLiquidGlass) {
|
|||||||
|
|
||||||
let isContentProtectionOn = true;
|
let isContentProtectionOn = true;
|
||||||
let lastVisibleWindows = new Set(['header']);
|
let lastVisibleWindows = new Set(['header']);
|
||||||
|
const HEADER_HEIGHT = 47;
|
||||||
|
const DEFAULT_WINDOW_WIDTH = 353;
|
||||||
|
|
||||||
let currentHeaderState = 'apikey';
|
let currentHeaderState = 'apikey';
|
||||||
const windowPool = new Map();
|
const windowPool = new Map();
|
||||||
@ -38,26 +40,50 @@ let settingsHideTimer = null;
|
|||||||
|
|
||||||
|
|
||||||
let layoutManager = null;
|
let layoutManager = null;
|
||||||
|
function updateLayout() {
|
||||||
|
if (layoutManager) {
|
||||||
|
layoutManager.updateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
let movementManager = null;
|
let movementManager = null;
|
||||||
|
|
||||||
|
|
||||||
function updateChildWindowLayouts(animated = true) {
|
const FADE_DURATION = 250;
|
||||||
// if (movementManager.isAnimating) return;
|
const FADE_FPS = 60;
|
||||||
|
|
||||||
const visibleWindows = {};
|
/**
|
||||||
const listenWin = windowPool.get('listen');
|
* 윈도우 투명도를 서서히 변경한다.
|
||||||
const askWin = windowPool.get('ask');
|
* @param {BrowserWindow} win
|
||||||
if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
|
* @param {number} from
|
||||||
visibleWindows.listen = true;
|
* @param {number} to
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {Function=} onComplete
|
||||||
|
*/
|
||||||
|
function fadeWindow(win, from, to, duration = FADE_DURATION, onComplete) {
|
||||||
|
if (!win || win.isDestroyed()) return;
|
||||||
|
|
||||||
|
const steps = Math.max(1, Math.round(duration / (1000 / FADE_FPS)));
|
||||||
|
let currentStep = 0;
|
||||||
|
|
||||||
|
win.setOpacity(from);
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (win.isDestroyed()) { clearInterval(timer); return; }
|
||||||
|
|
||||||
|
currentStep += 1;
|
||||||
|
const progress = currentStep / steps;
|
||||||
|
const eased = progress < 1
|
||||||
|
? 1 - Math.pow(1 - progress, 3)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
win.setOpacity(from + (to - from) * eased);
|
||||||
|
|
||||||
|
if (currentStep >= steps) {
|
||||||
|
clearInterval(timer);
|
||||||
|
win.setOpacity(to);
|
||||||
|
onComplete && onComplete();
|
||||||
}
|
}
|
||||||
if (askWin && !askWin.isDestroyed() && askWin.isVisible()) {
|
}, 1000 / FADE_FPS);
|
||||||
visibleWindows.ask = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(visibleWindows).length === 0) return;
|
|
||||||
|
|
||||||
const newLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows);
|
|
||||||
movementManager.animateLayout(newLayout, animated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSettingsWindow = () => {
|
const showSettingsWindow = () => {
|
||||||
@ -72,34 +98,6 @@ const cancelHideSettingsWindow = () => {
|
|||||||
internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true });
|
internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveWindowStep = (direction) => {
|
|
||||||
internalBridge.emit('window:moveStep', { direction });
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeHeaderWindow = ({ width, height }) => {
|
|
||||||
internalBridge.emit('window:resizeHeaderWindow', { width, height });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeaderAnimationFinished = (state) => {
|
|
||||||
internalBridge.emit('window:headerAnimationFinished', state);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeaderPosition = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
internalBridge.emit('window:getHeaderPosition', (position) => {
|
|
||||||
resolve(position);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveHeaderTo = (newX, newY) => {
|
|
||||||
internalBridge.emit('window:moveHeaderTo', { newX, newY });
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustWindowHeight = (winName, targetHeight) => {
|
|
||||||
internalBridge.emit('window:adjustWindowHeight', { winName, targetHeight });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function setupWindowController(windowPool, layoutManager, movementManager) {
|
function setupWindowController(windowPool, layoutManager, movementManager) {
|
||||||
internalBridge.on('window:requestVisibility', ({ name, visible }) => {
|
internalBridge.on('window:requestVisibility', ({ name, visible }) => {
|
||||||
@ -108,110 +106,6 @@ function setupWindowController(windowPool, layoutManager, movementManager) {
|
|||||||
internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => {
|
internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => {
|
||||||
changeAllWindowsVisibility(windowPool, targetVisibility);
|
changeAllWindowsVisibility(windowPool, targetVisibility);
|
||||||
});
|
});
|
||||||
internalBridge.on('window:moveToDisplay', ({ displayId }) => {
|
|
||||||
// movementManager.moveToDisplay(displayId);
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (header) {
|
|
||||||
const newPosition = layoutManager.calculateNewPositionForDisplay(header, displayId);
|
|
||||||
if (newPosition) {
|
|
||||||
movementManager.animateWindowPosition(header, newPosition, {
|
|
||||||
onComplete: () => updateChildWindowLayouts(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
internalBridge.on('window:moveToEdge', ({ direction }) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (header) {
|
|
||||||
const newPosition = layoutManager.calculateEdgePosition(header, direction);
|
|
||||||
movementManager.animateWindowPosition(header, newPosition, {
|
|
||||||
onComplete: () => updateChildWindowLayouts(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
internalBridge.on('window:moveStep', ({ direction }) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (header) {
|
|
||||||
const newHeaderPosition = layoutManager.calculateStepMovePosition(header, direction);
|
|
||||||
if (!newHeaderPosition) return;
|
|
||||||
|
|
||||||
const futureHeaderBounds = { ...header.getBounds(), ...newHeaderPosition };
|
|
||||||
const visibleWindows = {};
|
|
||||||
const listenWin = windowPool.get('listen');
|
|
||||||
const askWin = windowPool.get('ask');
|
|
||||||
if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
|
|
||||||
visibleWindows.listen = true;
|
|
||||||
}
|
|
||||||
if (askWin && !askWin.isDestroyed() && askWin.isVisible()) {
|
|
||||||
visibleWindows.ask = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newChildLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows, futureHeaderBounds);
|
|
||||||
|
|
||||||
movementManager.animateWindowPosition(header, newHeaderPosition);
|
|
||||||
movementManager.animateLayout(newChildLayout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
internalBridge.on('window:resizeHeaderWindow', ({ width, height }) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (!header || movementManager.isAnimating) return;
|
|
||||||
|
|
||||||
const newHeaderBounds = layoutManager.calculateHeaderResize(header, { width, height });
|
|
||||||
|
|
||||||
const wasResizable = header.isResizable();
|
|
||||||
if (!wasResizable) header.setResizable(true);
|
|
||||||
|
|
||||||
movementManager.animateWindowBounds(header, newHeaderBounds, {
|
|
||||||
onComplete: () => {
|
|
||||||
if (!wasResizable) header.setResizable(false);
|
|
||||||
updateChildWindowLayouts(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
internalBridge.on('window:headerAnimationFinished', (state) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (!header || header.isDestroyed()) return;
|
|
||||||
|
|
||||||
if (state === 'hidden') {
|
|
||||||
header.hide();
|
|
||||||
} else if (state === 'visible') {
|
|
||||||
updateChildWindowLayouts(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
internalBridge.on('window:getHeaderPosition', (reply) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (header && !header.isDestroyed()) {
|
|
||||||
reply(header.getBounds());
|
|
||||||
} else {
|
|
||||||
reply({ x: 0, y: 0, width: 0, height: 0 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
internalBridge.on('window:moveHeaderTo', ({ newX, newY }) => {
|
|
||||||
const header = windowPool.get('header');
|
|
||||||
if (header) {
|
|
||||||
const newPosition = layoutManager.calculateClampedPosition(header, { x: newX, y: newY });
|
|
||||||
header.setPosition(newPosition.x, newPosition.y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
internalBridge.on('window:adjustWindowHeight', ({ winName, targetHeight }) => {
|
|
||||||
console.log(`[Layout Debug] adjustWindowHeight: targetHeight=${targetHeight}`);
|
|
||||||
const senderWindow = windowPool.get(winName);
|
|
||||||
if (senderWindow) {
|
|
||||||
const newBounds = layoutManager.calculateWindowHeightAdjustment(senderWindow, targetHeight);
|
|
||||||
|
|
||||||
const wasResizable = senderWindow.isResizable();
|
|
||||||
if (!wasResizable) senderWindow.setResizable(true);
|
|
||||||
|
|
||||||
movementManager.animateWindowBounds(senderWindow, newBounds, {
|
|
||||||
onComplete: () => {
|
|
||||||
if (!wasResizable) senderWindow.setResizable(false);
|
|
||||||
updateChildWindowLayouts(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeAllWindowsVisibility(windowPool, targetVisibility) {
|
function changeAllWindowsVisibility(windowPool, targetVisibility) {
|
||||||
@ -326,10 +220,7 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement
|
|||||||
|
|
||||||
if (name === 'shortcut-settings') {
|
if (name === 'shortcut-settings') {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
// layoutManager.positionShortcutSettingsWindow();
|
layoutManager.positionShortcutSettingsWindow();
|
||||||
const newBounds = layoutManager.calculateShortcutSettingsWindowPosition();
|
|
||||||
if (newBounds) win.setBounds(newBounds);
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
win.setAlwaysOnTop(true, 'screen-saver');
|
win.setAlwaysOnTop(true, 'screen-saver');
|
||||||
} else {
|
} else {
|
||||||
@ -351,55 +242,91 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'listen' || name === 'ask') {
|
if (name === 'listen' || name === 'ask') {
|
||||||
const win = windowPool.get(name);
|
|
||||||
const otherName = name === 'listen' ? 'ask' : 'listen';
|
const otherName = name === 'listen' ? 'ask' : 'listen';
|
||||||
const otherWin = windowPool.get(otherName);
|
const otherWin = windowPool.get(otherName);
|
||||||
const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible();
|
const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible();
|
||||||
|
|
||||||
const ANIM_OFFSET_X = 50;
|
|
||||||
const ANIM_OFFSET_Y = 20;
|
|
||||||
|
|
||||||
const finalVisibility = {
|
const ANIM_OFFSET_X = 100;
|
||||||
listen: (name === 'listen' && shouldBeVisible) || (otherName === 'listen' && isOtherWinVisible),
|
const ANIM_OFFSET_Y = 20;
|
||||||
ask: (name === 'ask' && shouldBeVisible) || (otherName === 'ask' && isOtherWinVisible),
|
|
||||||
};
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
finalVisibility[name] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetLayout = layoutManager.calculateFeatureWindowLayout(finalVisibility);
|
|
||||||
|
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
if (!win) return;
|
|
||||||
const targetBounds = targetLayout[name];
|
|
||||||
if (!targetBounds) return;
|
|
||||||
|
|
||||||
const startPos = { ...targetBounds };
|
|
||||||
if (name === 'listen') startPos.x -= ANIM_OFFSET_X;
|
|
||||||
else if (name === 'ask') startPos.y -= ANIM_OFFSET_Y;
|
|
||||||
|
|
||||||
win.setOpacity(0);
|
win.setOpacity(0);
|
||||||
win.setBounds(startPos);
|
|
||||||
win.show();
|
|
||||||
|
|
||||||
movementManager.fade(win, { to: 1 });
|
if (name === 'listen') {
|
||||||
movementManager.animateLayout(targetLayout);
|
if (!isOtherWinVisible) {
|
||||||
|
const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false });
|
||||||
|
if (!targets.listen) return;
|
||||||
|
|
||||||
|
const startPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y };
|
||||||
|
win.setBounds(startPos);
|
||||||
|
win.show();
|
||||||
|
fadeWindow(win, 0, 1);
|
||||||
|
movementManager.animateWindow(win, targets.listen.x, targets.listen.y);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true });
|
||||||
|
if (!targets.listen || !targets.ask) return;
|
||||||
|
|
||||||
|
const startListenPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y };
|
||||||
|
win.setBounds(startListenPos);
|
||||||
|
|
||||||
|
win.show();
|
||||||
|
fadeWindow(win, 0, 1);
|
||||||
|
movementManager.animateWindow(otherWin, targets.ask.x, targets.ask.y);
|
||||||
|
movementManager.animateWindow(win, targets.listen.x, targets.listen.y);
|
||||||
|
}
|
||||||
|
} else if (name === 'ask') {
|
||||||
|
if (!isOtherWinVisible) {
|
||||||
|
const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: false, ask: true });
|
||||||
|
if (!targets.ask) return;
|
||||||
|
|
||||||
|
const startPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y };
|
||||||
|
win.setBounds(startPos);
|
||||||
|
win.show();
|
||||||
|
fadeWindow(win, 0, 1);
|
||||||
|
movementManager.animateWindow(win, targets.ask.x, targets.ask.y);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true });
|
||||||
|
if (!targets.listen || !targets.ask) return;
|
||||||
|
|
||||||
|
const startAskPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y };
|
||||||
|
win.setBounds(startAskPos);
|
||||||
|
|
||||||
|
win.show();
|
||||||
|
fadeWindow(win, 0, 1);
|
||||||
|
movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
|
||||||
|
movementManager.animateWindow(win, targets.ask.x, targets.ask.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!win || !win.isVisible()) return;
|
|
||||||
|
|
||||||
const currentBounds = win.getBounds();
|
const currentBounds = win.getBounds();
|
||||||
const targetPos = { ...currentBounds };
|
fadeWindow(
|
||||||
if (name === 'listen') targetPos.x -= ANIM_OFFSET_X;
|
win, 1, 0, FADE_DURATION,
|
||||||
else if (name === 'ask') targetPos.y -= ANIM_OFFSET_Y;
|
() => win.hide()
|
||||||
|
);
|
||||||
|
if (name === 'listen') {
|
||||||
|
if (!isOtherWinVisible) {
|
||||||
|
const targetX = currentBounds.x - ANIM_OFFSET_X;
|
||||||
|
movementManager.animateWindow(win, targetX, currentBounds.y);
|
||||||
|
} else {
|
||||||
|
const targetX = currentBounds.x - currentBounds.width;
|
||||||
|
movementManager.animateWindow(win, targetX, currentBounds.y);
|
||||||
|
}
|
||||||
|
} else if (name === 'ask') {
|
||||||
|
if (!isOtherWinVisible) {
|
||||||
|
const targetY = currentBounds.y - ANIM_OFFSET_Y;
|
||||||
|
movementManager.animateWindow(win, currentBounds.x, targetY);
|
||||||
|
} else {
|
||||||
|
const targetAskY = currentBounds.y - ANIM_OFFSET_Y;
|
||||||
|
movementManager.animateWindow(win, currentBounds.x, targetAskY);
|
||||||
|
|
||||||
movementManager.fade(win, { to: 0, onComplete: () => win.hide() });
|
const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false });
|
||||||
movementManager.animateWindowPosition(win, targetPos);
|
if (targets.listen) {
|
||||||
|
movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
|
||||||
// 다른 창들도 새 레이아웃으로 애니메이션
|
}
|
||||||
const otherWindowsLayout = { ...targetLayout };
|
}
|
||||||
delete otherWindowsLayout[name];
|
}
|
||||||
movementManager.animateLayout(otherWindowsLayout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -423,6 +350,52 @@ const toggleContentProtection = () => {
|
|||||||
return newStatus;
|
return newStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resizeHeaderWindow = ({ width, height }) => {
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
if (header) {
|
||||||
|
console.log(`[WindowManager] Resize request: ${width}x${height}`);
|
||||||
|
|
||||||
|
if (movementManager && movementManager.isAnimating) {
|
||||||
|
console.log('[WindowManager] Skipping resize during animation');
|
||||||
|
return { success: false, error: 'Cannot resize during animation' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBounds = header.getBounds();
|
||||||
|
console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`);
|
||||||
|
|
||||||
|
if (currentBounds.width === width && currentBounds.height === height) {
|
||||||
|
console.log('[WindowManager] Already at target size, skipping resize');
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasResizable = header.isResizable();
|
||||||
|
if (!wasResizable) {
|
||||||
|
header.setResizable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerX = currentBounds.x + currentBounds.width / 2;
|
||||||
|
const newX = Math.round(centerX - width / 2);
|
||||||
|
|
||||||
|
const display = getCurrentDisplay(header);
|
||||||
|
const { x: workAreaX, width: workAreaWidth } = display.workArea;
|
||||||
|
|
||||||
|
const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
|
||||||
|
|
||||||
|
header.setBounds({ x: clampedX, y: currentBounds.y, width, height });
|
||||||
|
|
||||||
|
if (!wasResizable) {
|
||||||
|
header.setResizable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateLayout) {
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false, error: 'Header window not found' };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const openLoginPage = () => {
|
const openLoginPage = () => {
|
||||||
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
||||||
@ -431,6 +404,12 @@ const openLoginPage = () => {
|
|||||||
console.log('Opening personalization page:', personalizeUrl);
|
console.log('Opening personalization page:', personalizeUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const moveWindowStep = (direction) => {
|
||||||
|
if (movementManager) {
|
||||||
|
movementManager.moveStep(direction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function createFeatureWindows(header, namesToCreate) {
|
function createFeatureWindows(header, namesToCreate) {
|
||||||
// if (windowPool.has('listen')) return;
|
// if (windowPool.has('listen')) return;
|
||||||
@ -444,7 +423,7 @@ function createFeatureWindows(header, namesToCreate) {
|
|||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
hiddenInMissionControl: true,
|
hiddenInMissionControl: true,
|
||||||
resizable: false,
|
resizable: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@ -635,18 +614,24 @@ function getCurrentDisplay(window) {
|
|||||||
return screen.getDisplayNearestPoint(windowCenter);
|
return screen.getDisplayNearestPoint(windowCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayById(displayId) {
|
||||||
|
const displays = screen.getAllDisplays();
|
||||||
|
return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function createWindows() {
|
function createWindows() {
|
||||||
const HEADER_HEIGHT = 47;
|
|
||||||
const DEFAULT_WINDOW_WIDTH = 353;
|
|
||||||
|
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea;
|
const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea;
|
||||||
|
|
||||||
const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2);
|
const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2);
|
||||||
const initialY = workAreaY + 21;
|
const initialY = workAreaY + 21;
|
||||||
|
movementManager = new SmoothMovementManager(windowPool, getDisplayById, getCurrentDisplay, updateLayout);
|
||||||
|
|
||||||
const header = new BrowserWindow({
|
const header = new BrowserWindow({
|
||||||
width: DEFAULT_WINDOW_WIDTH,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: HEADER_HEIGHT,
|
height: HEADER_HEIGHT,
|
||||||
@ -696,23 +681,15 @@ function createWindows() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
windowPool.set('header', header);
|
windowPool.set('header', header);
|
||||||
|
header.on('moved', updateLayout);
|
||||||
layoutManager = new WindowLayoutManager(windowPool);
|
layoutManager = new WindowLayoutManager(windowPool);
|
||||||
movementManager = new SmoothMovementManager(windowPool);
|
|
||||||
|
|
||||||
|
|
||||||
header.on('moved', () => {
|
|
||||||
if (movementManager.isAnimating) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateChildWindowLayouts(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
header.webContents.once('dom-ready', () => {
|
header.webContents.once('dom-ready', () => {
|
||||||
shortcutsService.initialize(windowPool);
|
shortcutsService.initialize(movementManager, windowPool);
|
||||||
shortcutsService.registerShortcuts();
|
shortcutsService.registerShortcuts();
|
||||||
});
|
});
|
||||||
|
|
||||||
setupIpcHandlers(windowPool, layoutManager);
|
setupIpcHandlers(movementManager);
|
||||||
setupWindowController(windowPool, layoutManager, movementManager);
|
setupWindowController(windowPool, layoutManager, movementManager);
|
||||||
|
|
||||||
if (currentHeaderState === 'main') {
|
if (currentHeaderState === 'main') {
|
||||||
@ -744,13 +721,16 @@ function createWindows() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
header.on('resize', () => updateChildWindowLayouts(false));
|
header.on('resize', () => {
|
||||||
|
console.log('[WindowManager] Header resize event triggered');
|
||||||
|
updateLayout();
|
||||||
|
});
|
||||||
|
|
||||||
return windowPool;
|
return windowPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupIpcHandlers(movementManager) {
|
||||||
function setupIpcHandlers(windowPool, layoutManager) {
|
// quit-application handler moved to windowBridge.js to avoid duplication
|
||||||
screen.on('display-added', (event, newDisplay) => {
|
screen.on('display-added', (event, newDisplay) => {
|
||||||
console.log('[Display] New display added:', newDisplay.id);
|
console.log('[Display] New display added:', newDisplay.id);
|
||||||
});
|
});
|
||||||
@ -758,25 +738,18 @@ function setupIpcHandlers(windowPool, layoutManager) {
|
|||||||
screen.on('display-removed', (event, oldDisplay) => {
|
screen.on('display-removed', (event, oldDisplay) => {
|
||||||
console.log('[Display] Display removed:', oldDisplay.id);
|
console.log('[Display] Display removed:', oldDisplay.id);
|
||||||
const header = windowPool.get('header');
|
const header = windowPool.get('header');
|
||||||
|
|
||||||
if (header && getCurrentDisplay(header).id === oldDisplay.id) {
|
if (header && getCurrentDisplay(header).id === oldDisplay.id) {
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const newPosition = layoutManager.calculateNewPositionForDisplay(header, primaryDisplay.id);
|
movementManager.moveToDisplay(primaryDisplay.id);
|
||||||
if (newPosition) {
|
|
||||||
// 복구 상황이므로 애니메이션 없이 즉시 이동
|
|
||||||
header.setPosition(newPosition.x, newPosition.y, false);
|
|
||||||
updateChildWindowLayouts(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
|
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
|
||||||
// 레이아웃 업데이트 함수를 새 버전으로 호출
|
// console.log('[Display] Display metrics changed:', display.id, changedMetrics);
|
||||||
updateChildWindowLayouts(false);
|
updateLayout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleHeaderStateChanged = (state) => {
|
const handleHeaderStateChanged = (state) => {
|
||||||
console.log(`[WindowManager] Header state changed to: ${state}`);
|
console.log(`[WindowManager] Header state changed to: ${state}`);
|
||||||
currentHeaderState = state;
|
currentHeaderState = state;
|
||||||
@ -789,8 +762,96 @@ const handleHeaderStateChanged = (state) => {
|
|||||||
internalBridge.emit('reregister-shortcuts');
|
internalBridge.emit('reregister-shortcuts');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHeaderAnimationFinished = (state) => {
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
if (!header || header.isDestroyed()) return;
|
||||||
|
|
||||||
|
if (state === 'hidden') {
|
||||||
|
header.hide();
|
||||||
|
console.log('[WindowManager] Header hidden after animation.');
|
||||||
|
} else if (state === 'visible') {
|
||||||
|
console.log('[WindowManager] Header shown after animation.');
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeaderPosition = () => {
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
if (header) {
|
||||||
|
const [x, y] = header.getPosition();
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveHeader = (newX, newY) => {
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
if (header) {
|
||||||
|
const currentY = newY !== undefined ? newY : header.getBounds().y;
|
||||||
|
header.setPosition(newX, currentY, false);
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveHeaderTo = (newX, newY) => {
|
||||||
|
const header = windowPool.get('header');
|
||||||
|
if (header) {
|
||||||
|
const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
|
||||||
|
const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
|
||||||
|
const headerBounds = header.getBounds();
|
||||||
|
|
||||||
|
let clampedX = newX;
|
||||||
|
let clampedY = newY;
|
||||||
|
|
||||||
|
if (newX < workAreaX) {
|
||||||
|
clampedX = workAreaX;
|
||||||
|
} else if (newX + headerBounds.width > workAreaX + width) {
|
||||||
|
clampedX = workAreaX + width - headerBounds.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newY < workAreaY) {
|
||||||
|
clampedY = workAreaY;
|
||||||
|
} else if (newY + headerBounds.height > workAreaY + height) {
|
||||||
|
clampedY = workAreaY + height - headerBounds.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
header.setPosition(clampedX, clampedY, false);
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustWindowHeight = (sender, targetHeight) => {
|
||||||
|
const senderWindow = BrowserWindow.fromWebContents(sender);
|
||||||
|
if (senderWindow) {
|
||||||
|
const wasResizable = senderWindow.isResizable();
|
||||||
|
if (!wasResizable) {
|
||||||
|
senderWindow.setResizable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBounds = senderWindow.getBounds();
|
||||||
|
const minHeight = senderWindow.getMinimumSize()[1];
|
||||||
|
const maxHeight = senderWindow.getMaximumSize()[1];
|
||||||
|
|
||||||
|
let adjustedHeight;
|
||||||
|
if (maxHeight === 0) {
|
||||||
|
adjustedHeight = Math.max(minHeight, targetHeight);
|
||||||
|
} else {
|
||||||
|
adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
senderWindow.setSize(currentBounds.width, adjustedHeight, false);
|
||||||
|
|
||||||
|
if (!wasResizable) {
|
||||||
|
senderWindow.setResizable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
updateLayout,
|
||||||
createWindows,
|
createWindows,
|
||||||
windowPool,
|
windowPool,
|
||||||
toggleContentProtection,
|
toggleContentProtection,
|
||||||
@ -804,6 +865,7 @@ module.exports = {
|
|||||||
handleHeaderStateChanged,
|
handleHeaderStateChanged,
|
||||||
handleHeaderAnimationFinished,
|
handleHeaderAnimationFinished,
|
||||||
getHeaderPosition,
|
getHeaderPosition,
|
||||||
|
moveHeader,
|
||||||
moveHeaderTo,
|
moveHeaderTo,
|
||||||
adjustWindowHeight,
|
adjustWindowHeight,
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user