- HistoryPopup: Vanilla JS, 다크 카드(#242427), 앰버 악센트(#f25b29) - Ctrl+Shift+V → 커서 위치에 최근 10건 히스토리 팝업 - Arrow↑↓ 선택, Enter 붙여넣기, 1-9 직접 선택, ESC 닫기 - focusable: false → 활성 앱 포커스 유지 - 2-phase 리사이즈, 등장/퇴장 애니메이션 - WindowManager: HistoryPopup 관리 + 프리로딩 - Bootstrap: globalShortcut 등록 + IPC 연동
196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
// src/main/bootstrap.ts — 초기화 시퀀스
|
|
|
|
import { app, dialog, globalShortcut, ipcMain as ipcMainRef } from 'electron'
|
|
import { initLoggerService, getLogger } from './services/LoggerService'
|
|
import { initConfigService } from './services/ConfigService'
|
|
import { getHotkeyService } from './services/HotkeyService'
|
|
import { getVoiceModeService } from './services/VoiceModeService'
|
|
import { getLocalLLMService } from './services/LocalLLMService'
|
|
import { getHistoryService } from './services/HistoryService'
|
|
import { initDatabase } from './db'
|
|
import {
|
|
createMainWindow,
|
|
preloadPopupWindows,
|
|
showRecordingTip,
|
|
hideRecordingTip,
|
|
updateRecordingTipState,
|
|
sendAudioLevelToTip,
|
|
showResultPopup,
|
|
showHistoryPopup,
|
|
hideHistoryPopup,
|
|
sendKeyToHistoryPopup,
|
|
isHistoryPopupVisible
|
|
} from './windows/WindowManager'
|
|
import { createTray } from './windows/TrayManager'
|
|
import { registerAllIpcHandlers } from './ipc'
|
|
|
|
const logger = getLogger('bootstrap')
|
|
|
|
interface BootstrapStep {
|
|
name: string
|
|
critical: boolean
|
|
fn: () => Promise<void>
|
|
}
|
|
|
|
export async function bootstrap(): Promise<void> {
|
|
const steps: BootstrapStep[] = [
|
|
{ name: 'logger', critical: false, fn: initLogger },
|
|
{ name: 'config', critical: false, fn: initConfig },
|
|
{ name: 'database', critical: true, fn: initDB },
|
|
{ name: 'create-windows', critical: true, fn: createWindows },
|
|
{ name: 'tray', critical: false, fn: initTray },
|
|
{ name: 'ipc-handlers', critical: true, fn: initIpcHandlers },
|
|
{ name: 'popup-preload', critical: false, fn: initPopupWindows },
|
|
{ name: 'hotkey', critical: false, fn: initHotkey },
|
|
{ name: 'voice-mode', critical: false, fn: initVoiceMode },
|
|
{ name: 'llm-polling', critical: false, fn: initLLMPolling }
|
|
]
|
|
|
|
for (const step of steps) {
|
|
try {
|
|
await step.fn()
|
|
logger.info(`[bootstrap] ${step.name} initialized`)
|
|
} catch (error) {
|
|
logger.error(`[bootstrap] ${step.name} failed:`, error)
|
|
if (step.critical) {
|
|
dialog.showErrorBox(
|
|
'D3RO-VOICE 초기화 실패',
|
|
`${step.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
)
|
|
app.quit()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function initLogger(): Promise<void> {
|
|
initLoggerService()
|
|
}
|
|
|
|
async function initConfig(): Promise<void> {
|
|
initConfigService()
|
|
}
|
|
|
|
async function initDB(): Promise<void> {
|
|
initDatabase()
|
|
}
|
|
|
|
async function createWindows(): Promise<void> {
|
|
createMainWindow()
|
|
}
|
|
|
|
async function initTray(): Promise<void> {
|
|
createTray()
|
|
}
|
|
|
|
async function initIpcHandlers(): Promise<void> {
|
|
registerAllIpcHandlers()
|
|
}
|
|
|
|
async function initHotkey(): Promise<void> {
|
|
const hotkey = getHotkeyService()
|
|
hotkey.loadFromConfig()
|
|
hotkey.start()
|
|
}
|
|
|
|
async function initPopupWindows(): Promise<void> {
|
|
preloadPopupWindows()
|
|
setupHistoryPopupIPC()
|
|
|
|
// Ctrl+Shift+V → 히스토리 팝업 토글
|
|
globalShortcut.register('Ctrl+Shift+V', () => {
|
|
if (isHistoryPopupVisible()) {
|
|
hideHistoryPopup()
|
|
} else {
|
|
const entries = getHistoryService().list({ page: 0, pageSize: 10 }).entries
|
|
showHistoryPopup(entries as unknown as Array<Record<string, unknown>>)
|
|
}
|
|
})
|
|
}
|
|
|
|
async function initVoiceMode(): Promise<void> {
|
|
const voiceMode = getVoiceModeService()
|
|
voiceMode.connectHotkey()
|
|
|
|
// RecordingTip 연동: 세션 시작/종료 시 팝업 표시/숨김
|
|
voiceMode.on('session-started', () => {
|
|
showRecordingTip('recording')
|
|
})
|
|
|
|
voiceMode.on('audio-level', ({ level }) => {
|
|
sendAudioLevelToTip(level)
|
|
})
|
|
|
|
voiceMode.on('recognition-state-changed', ({ current }) => {
|
|
if (current === 'recognizing') {
|
|
updateRecordingTipState('thinking')
|
|
}
|
|
})
|
|
|
|
voiceMode.on('session-completed', ({ session, finalText }) => {
|
|
hideRecordingTip()
|
|
if (finalText.length > 0) {
|
|
showResultPopup(finalText)
|
|
}
|
|
|
|
// 이력 저장
|
|
try {
|
|
const wordCount = finalText.split(/\s+/).filter((w) => w.length > 0).length
|
|
getHistoryService().create({
|
|
originalText: session.transcription || finalText,
|
|
polishedText: session.processedText,
|
|
mode: session.mode === 'hands-free' ? 'dictation' : session.mode,
|
|
status: 'completed',
|
|
duration: (Date.now() - session.startedAt) / 1000,
|
|
wordCount
|
|
})
|
|
} catch (error) {
|
|
logger.warn(`Failed to save history: ${error instanceof Error ? error.message : String(error)}`)
|
|
}
|
|
})
|
|
|
|
voiceMode.on('session-cancelled', () => {
|
|
hideRecordingTip()
|
|
})
|
|
|
|
voiceMode.on('error', ({ error }) => {
|
|
updateRecordingTipState('error', { errorMessage: error.message })
|
|
setTimeout(() => hideRecordingTip(), 3000)
|
|
})
|
|
}
|
|
|
|
async function initLLMPolling(): Promise<void> {
|
|
const llm = getLocalLLMService()
|
|
llm.startPolling()
|
|
}
|
|
|
|
// ── HistoryPopup IPC 연동 ────────────────────────────
|
|
|
|
function setupHistoryPopupIPC(): void {
|
|
const { ipcMain } = require('electron')
|
|
const { getTextInsertService } = require('./services/TextInsertService')
|
|
|
|
// 히스토리 팝업 열기 (핫키에서 호출)
|
|
ipcMain.on('history:showPopup', () => {
|
|
const entries = getHistoryService().list({ page: 0, pageSize: 10 }).entries
|
|
showHistoryPopup(entries as unknown as Array<Record<string, unknown>>)
|
|
})
|
|
|
|
// 아이템 선택 → 텍스트 삽입
|
|
ipcMain.on('history:itemSelected', (_event: Electron.IpcMainEvent, data: { text: string }) => {
|
|
hideHistoryPopup()
|
|
setTimeout(async () => {
|
|
try {
|
|
await getTextInsertService().insertText(data.text)
|
|
} catch (error) {
|
|
logger.warn(`History popup insert failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
}
|
|
}, 200)
|
|
})
|
|
|
|
// 팝업 닫기
|
|
ipcMain.on('history:popupDismissed', () => {
|
|
hideHistoryPopup()
|
|
})
|
|
}
|