feat(icon)+fix(audio): 앱 아이콘 적용 + 트레이 아이콘 + 마이크 테스트 조기 종료 수정
- 앱 아이콘: icon.svg 마스터(d3ro 브랜드 — 메탈 섀시+오렌지 웨이브) → png/ico 생성 스크립트(generate-icons.mjs, sharp) + electron-builder win/mac 연결 - 트레이: createEmpty() 빈 아이콘 → 실제 앱 아이콘 (getAppIconPath) - 마이크 테스트: 초기 무음 level:0을 종료로 오인하던 조기 종료 버그 — AUDIO.TEST_LEVEL에 done 플래그 신설(SSOT), STOP 버튼이 실제 캡처 중지, testDevice 실패 시 상태 롤백
This commit is contained in:
parent
26b3fd1f63
commit
cbed451209
12 changed files with 184 additions and 11 deletions
|
|
@ -71,12 +71,13 @@ export function registerAudioHandlers(): void {
|
|||
}
|
||||
service.on('audio-data', testAudioHandler)
|
||||
|
||||
// 100ms 간격으로 레벨을 렌더러에 전송
|
||||
// 100ms 간격으로 레벨을 렌더러에 전송.
|
||||
// level 0은 "무음/초기화 중"일 뿐 종료가 아님 — 종료는 done:true로만 알린다.
|
||||
testLevelInterval = setInterval(() => {
|
||||
const level = testLastRms > 0 ? Math.min(1.0, Math.pow(testLastRms, 0.28)) : 0
|
||||
const win = getMainWindow()
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('audio:testLevel', { level })
|
||||
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level, done: false })
|
||||
}
|
||||
}, 100)
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ export function registerAudioHandlers(): void {
|
|||
stopAudioTest()
|
||||
const win = getMainWindow()
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('audio:testLevel', { level: 0 })
|
||||
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true })
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
|
|
@ -98,4 +99,14 @@ export function registerAudioHandlers(): void {
|
|||
)
|
||||
}
|
||||
})
|
||||
|
||||
// 테스트 수동 중지 (STOP 버튼)
|
||||
ipcMain.handle(IPC_CHANNELS.AUDIO.STOP_TEST, async () => {
|
||||
stopAudioTest()
|
||||
const win = getMainWindow()
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true })
|
||||
}
|
||||
return ipcSuccess(undefined)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,3 +151,17 @@ export function getUserDataPath(): string {
|
|||
export function getWhisperModelsDir(): string {
|
||||
return path.join(app.getPath('userData'), 'whisper-models')
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱 아이콘 경로 (트레이/윈도우용).
|
||||
* - production: extraResources로 번들된 icons/ (win은 멀티사이즈 ico — DPI 대응)
|
||||
* - dev: build/icon.png
|
||||
* 존재하지 않으면 null (호출측에서 폴백 처리).
|
||||
*/
|
||||
export function getAppIconPath(): string | null {
|
||||
const filename = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
|
||||
const candidate = isPackaged()
|
||||
? path.join(process.resourcesPath, 'icons', filename)
|
||||
: path.join(app.getAppPath(), 'build', filename)
|
||||
return existsSync(candidate) ? candidate : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// src/main/windows/TrayManager.ts
|
||||
|
||||
import { Tray, Menu, app, nativeImage } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { getMainWindow } from './WindowManager'
|
||||
import { getLogger } from '../services/LoggerService'
|
||||
import { getAppIconPath } from '../utils/paths'
|
||||
import { setIsQuitting } from '../lifecycle'
|
||||
|
||||
const logger = getLogger('TrayManager')
|
||||
|
|
@ -11,8 +11,9 @@ const logger = getLogger('TrayManager')
|
|||
let tray: Tray | null = null
|
||||
|
||||
export function createTray(): void {
|
||||
// 16x16 빈 아이콘 생성 (리소스 아이콘이 없을 때 폴백)
|
||||
const icon = nativeImage.createEmpty()
|
||||
// 앱 아이콘 (win=멀티사이즈 ico, 그 외=png). 없으면 빈 아이콘 폴백.
|
||||
const iconPath = getAppIconPath()
|
||||
const icon = iconPath ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty()
|
||||
tray = new Tray(icon)
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
|
|
|
|||
|
|
@ -203,10 +203,11 @@ const electronAPI = {
|
|||
invoke<void>(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, params),
|
||||
testDevice: (params: TestDeviceParams) =>
|
||||
invoke<TestDeviceResult>(IPC_CHANNELS.AUDIO.TEST_DEVICE, params),
|
||||
stopTest: () => invoke<void>(IPC_CHANNELS.AUDIO.STOP_TEST),
|
||||
onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb),
|
||||
onTestLevel: (cb: (e: { level: number }) => void): Unsubscribe =>
|
||||
on('audio:testLevel', cb),
|
||||
onTestLevel: (cb: (e: { level: number; done: boolean }) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.AUDIO.TEST_LEVEL, cb),
|
||||
},
|
||||
|
||||
// ── Config ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -547,19 +547,28 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
|||
size="small"
|
||||
onClick={async () => {
|
||||
if (micTesting) {
|
||||
// 메인 프로세스 캡처까지 실제로 중지 — done 이벤트가 구독 해제를 마무리
|
||||
await window.electronAPI.audio.stopTest()
|
||||
setMicTesting(false)
|
||||
setMicLevel(0)
|
||||
} else {
|
||||
setMicTesting(true)
|
||||
setMicLevel(0)
|
||||
// level 0은 무음/초기화 중일 뿐 — 종료 판정은 done 플래그로만
|
||||
const unsub = window.electronAPI.audio.onTestLevel((e) => {
|
||||
setMicLevel(e.level)
|
||||
if (e.level === 0) {
|
||||
if (e.done) {
|
||||
setMicTesting(false)
|
||||
setMicLevel(0)
|
||||
unsub()
|
||||
}
|
||||
})
|
||||
await window.electronAPI.audio.testDevice({ deviceId: 'default' })
|
||||
const result = await window.electronAPI.audio.testDevice({ deviceId: 'default' })
|
||||
if (!result.success) {
|
||||
setMicTesting(false)
|
||||
setMicLevel(0)
|
||||
unsub()
|
||||
}
|
||||
}
|
||||
}}
|
||||
sx={{ fontFamily: d3roFontMono, fontSize: '11px', minWidth: 80 }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue