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
BIN
apps/desktop/build/icon.ico
Normal file
BIN
apps/desktop/build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
apps/desktop/build/icon.png
Normal file
BIN
apps/desktop/build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
61
apps/desktop/build/icon.svg
Normal file
61
apps/desktop/build/icon.svg
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- D3RO Voice 앱 아이콘 — 정밀 계측기 정체성: 메탈 섀시 + 다크 글래스 + 앰버 포스포 웨이브
|
||||
색상 SSOT: d3roPalette (accent #f25b29 / light #ff7a4d / chassis #242528 / crtGlass #050605) -->
|
||||
<defs>
|
||||
<linearGradient id="chassis" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#35363a"/>
|
||||
<stop offset="0.5" stop-color="#242528"/>
|
||||
<stop offset="1" stop-color="#18191b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="glass" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#101113"/>
|
||||
<stop offset="1" stop-color="#050605"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bar" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ff7a4d"/>
|
||||
<stop offset="1" stop-color="#f25b29"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="topSheen" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.10"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
|
||||
<feGaussianBlur stdDeviation="30"/>
|
||||
</filter>
|
||||
<clipPath id="glassClip">
|
||||
<rect x="88" y="88" width="848" height="848" rx="176"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- 메탈 섀시 -->
|
||||
<rect x="16" y="16" width="992" height="992" rx="230" fill="url(#chassis)"/>
|
||||
<!-- 섀시 상단 하이라이트 (inset 촉감) -->
|
||||
<rect x="20" y="20" width="984" height="984" rx="226" fill="none"
|
||||
stroke="#ffffff" stroke-opacity="0.10" stroke-width="3"/>
|
||||
|
||||
<!-- 다크 글래스 패널 -->
|
||||
<rect x="88" y="88" width="848" height="848" rx="176" fill="url(#glass)"/>
|
||||
<rect x="88" y="88" width="848" height="848" rx="176" fill="none"
|
||||
stroke="#000000" stroke-opacity="0.55" stroke-width="6"/>
|
||||
|
||||
<!-- 포스포 글로우 (블러 레이어) -->
|
||||
<g filter="url(#glow)" opacity="0.55" clip-path="url(#glassClip)">
|
||||
<rect x="207" y="424.5" width="74" height="175" rx="37" fill="#f25b29"/>
|
||||
<rect x="341" y="355.5" width="74" height="313" rx="37" fill="#f25b29"/>
|
||||
<rect x="475" y="282" width="74" height="460" rx="37" fill="#f25b29"/>
|
||||
<rect x="609" y="355.5" width="74" height="313" rx="37" fill="#f25b29"/>
|
||||
<rect x="743" y="424.5" width="74" height="175" rx="37" fill="#f25b29"/>
|
||||
</g>
|
||||
|
||||
<!-- 웨이브 바 (cos 분포 5개) -->
|
||||
<g>
|
||||
<rect x="207" y="424.5" width="74" height="175" rx="37" fill="url(#bar)"/>
|
||||
<rect x="341" y="355.5" width="74" height="313" rx="37" fill="url(#bar)"/>
|
||||
<rect x="475" y="282" width="74" height="460" rx="37" fill="url(#bar)"/>
|
||||
<rect x="609" y="355.5" width="74" height="313" rx="37" fill="url(#bar)"/>
|
||||
<rect x="743" y="424.5" width="74" height="175" rx="37" fill="url(#bar)"/>
|
||||
</g>
|
||||
|
||||
<!-- 글래스 상단 반사 -->
|
||||
<rect x="88" y="88" width="848" height="300" rx="176" fill="url(#topSheen)" clip-path="url(#glassClip)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -43,6 +43,7 @@ asarUnpack:
|
|||
# electron 기본 아이콘 사용(아이콘 준비되면 win.icon 라인 추가).
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
win:
|
||||
icon: build/icon.ico
|
||||
target:
|
||||
- target: nsis
|
||||
arch:
|
||||
|
|
@ -52,6 +53,8 @@ win:
|
|||
# macOS
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
mac:
|
||||
# icns는 electron-builder가 build/icon.png(1024)에서 자동 생성
|
||||
icon: build/icon.png
|
||||
category: public.app-category.productivity
|
||||
# 파일명 공백 금지 — GitLab Generic Package Registry가 공백을 불허해
|
||||
# latest-mac.yml의 url과 레지스트리 업로드명이 어긋나면 업데이트 다운로드 404.
|
||||
|
|
@ -98,6 +101,12 @@ linux:
|
|||
# 공통 리소스 번들
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
extraResources:
|
||||
# 앱 아이콘 (트레이/런타임용 — paths.ts getAppIconPath)
|
||||
- from: build/icon.ico
|
||||
to: icons/icon.ico
|
||||
- from: build/icon.png
|
||||
to: icons/icon.png
|
||||
|
||||
# 효과음 (플랫폼 공통)
|
||||
- from: resources/sounds/
|
||||
to: sounds/
|
||||
|
|
|
|||
58
apps/desktop/scripts/generate-icons.mjs
Normal file
58
apps/desktop/scripts/generate-icons.mjs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// scripts/generate-icons.mjs
|
||||
// build/icon.svg → build/icon.png(1024) + build/icon.ico(멀티사이즈)
|
||||
// 사용: node apps/desktop/scripts/generate-icons.mjs
|
||||
// 의존: sharp (모노레포 루트 node_modules), Pillow는 불필요(ico도 sharp 산출 PNG로 조립)
|
||||
|
||||
import sharp from 'sharp'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
const buildDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'build')
|
||||
const SVG = join(buildDir, 'icon.svg')
|
||||
|
||||
/** ICO 컨테이너 조립 — PNG 임베드 방식 (Vista+ 표준, 모든 현대 Windows 지원) */
|
||||
function buildIco(pngs) {
|
||||
const count = pngs.length
|
||||
const header = Buffer.alloc(6)
|
||||
header.writeUInt16LE(0, 0) // reserved
|
||||
header.writeUInt16LE(1, 2) // type: icon
|
||||
header.writeUInt16LE(count, 4)
|
||||
|
||||
const entries = []
|
||||
let offset = 6 + 16 * count
|
||||
for (const { size, buf } of pngs) {
|
||||
const e = Buffer.alloc(16)
|
||||
e.writeUInt8(size >= 256 ? 0 : size, 0) // width (0=256)
|
||||
e.writeUInt8(size >= 256 ? 0 : size, 1) // height
|
||||
e.writeUInt8(0, 2) // palette
|
||||
e.writeUInt8(0, 3) // reserved
|
||||
e.writeUInt16LE(1, 4) // planes
|
||||
e.writeUInt16LE(32, 6) // bpp
|
||||
e.writeUInt32LE(buf.length, 8)
|
||||
e.writeUInt32LE(offset, 12)
|
||||
entries.push(e)
|
||||
offset += buf.length
|
||||
}
|
||||
return Buffer.concat([header, ...entries, ...pngs.map((p) => p.buf)])
|
||||
}
|
||||
|
||||
async function render(size) {
|
||||
return sharp(SVG, { density: (72 * size) / 1024 })
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
// 1) 마스터 PNG (electron-builder가 mac icns/linux 아이콘 자동 생성에 사용)
|
||||
writeFileSync(join(buildDir, 'icon.png'), await render(1024))
|
||||
console.log('build/icon.png (1024)')
|
||||
|
||||
// 2) Windows ICO — 표준 사이즈 세트
|
||||
const icoSizes = [256, 128, 64, 48, 32, 16]
|
||||
const pngs = []
|
||||
for (const size of icoSizes) {
|
||||
pngs.push({ size, buf: await render(size) })
|
||||
}
|
||||
writeFileSync(join(buildDir, 'icon.ico'), buildIco(pngs))
|
||||
console.log(`build/icon.ico (${icoSizes.join('/')})`)
|
||||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
- **G: 온보딩 UX 수정 (v0.1.4-alpha) — 릴리스 완료(#69756 전 잡 그린), 차등 24MB로 설치 앱 전달 확인**: ①total 없는 상태(pulling/verifying/writing manifest)에서 인디터미넌트 바 + 친화적 문구(i18n 3키), ②중복 클릭 가드(runningRef), ③LocalLLMService.pullModel 동시 호출 dedupe(in-flight map), ④STT /download 409 시 기존 진행 폴링 합류
|
||||
- 참고: 온보딩 STT 단계는 사용자의 기존 설정 모델(medium)을 존중해 다운로드함 — turbo 강제 아님 (설계 의도)
|
||||
|
||||
### H: 앱 아이콘 + 트레이 + 마이크 테스트 버튼 수정 (v0.1.5-alpha)
|
||||
- **앱 아이콘 신규**: build/icon.svg(마스터, d3roPalette SSOT — 메탈 섀시+다크 글래스+오렌지 웨이브 5바 cos 분포) → icon.png(1024)+icon.ico(6사이즈). `scripts/generate-icons.mjs`(sharp, ico 컨테이너 직접 조립). electron-builder win.icon/mac.icon 연결 + extraResources icons/
|
||||
- **트레이 아이콘 버그**: createEmpty()로 빈 이미지였음 → getAppIconPath()(paths.ts, win=ico/기타=png, dev는 build/) 사용
|
||||
- **마이크 테스트 버튼 조기 종료 버그** (mic-test-debug 에이전트 진단): 렌더러가 초기 무음 `level:0`을 종료 신호로 오인 → 즉시 구독 해제. 수정: TEST_LEVEL 이벤트에 `done` 플래그(SSOT 채널 `AUDIO.TEST_LEVEL`/`STOP_TEST` 신설, 하드코딩 'audio:testLevel' 제거), 렌더러는 done으로만 종료, STOP 버튼이 실제로 메인 캡처 중지, testDevice 실패 시 상태 롤백
|
||||
- 잔여(경미): 설계서 02의 audio:testDevice 명세는 "짧은 캡처 후 결과 반환"인데 실제는 스트리밍 — 명세 갱신 후보. AudioCaptureService.testCapture() dead code 정리 후보
|
||||
|
||||
#### 남은 것
|
||||
1. 설치 디렉토리명 `@d3rodesktop` 정리 (electron-builder가 package name에서 유도 — 다음 릴리스에서 명시)
|
||||
2. VIVEN runner의 signtool 서명이 어떤 인증서인지 확인 (twentyoz 조직 인증서면 유지)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ export const IPC_CHANNELS = {
|
|||
GET_SELECTED_DEVICE: 'audio:getSelectedDevice',
|
||||
SET_SELECTED_DEVICE: 'audio:setSelectedDevice',
|
||||
TEST_DEVICE: 'audio:testDevice',
|
||||
STOP_TEST: 'audio:stopTest',
|
||||
// Main → Renderer events
|
||||
DEVICE_CHANGED: 'audio:deviceChanged'
|
||||
DEVICE_CHANGED: 'audio:deviceChanged',
|
||||
/** 마이크 테스트 레벨 스트림 — done=true가 테스트 종료 신호 (level 0은 무음일 뿐) */
|
||||
TEST_LEVEL: 'audio:testLevel'
|
||||
},
|
||||
|
||||
STT: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue