d3ro-voice/apps/desktop/src/renderer/App.tsx
Yun Chan 9a8f7e6aa6 feat(desktop): sync knowledge, recordings and shared settings; play any recording
Knowledge documents travel as source-text chunks; each surface embeds them
with its own model, the server index is requested through embed-chunks, and
documents from the phone are stored without a file and indexed from their
chunks. Chunk text is now kept when local embedding fails, so reindexing no
longer needs the original file.

Recordings upload to the mobile storage contract (audio bucket under the
user's folder plus an audio_files row, 50 MiB cap, a Settings > Cloud
toggle) and are removed with their record. The history card gains a play
button that uses the local file or, for phone recordings, a signed URL.

Language (ko/en), system/light/dark theme, auto-polish and the active user
command follow the phone's user_settings with its revision rule; changes that
arrive from the phone reach the open window.
2026-09-27 14:44:56 +09:00

102 lines
3.5 KiB
TypeScript

// src/renderer/App.tsx — 루트 컴포넌트
// 테마 시스템: auto(시스템) / dark / light. 기본은 auto → 다크.
// i18n: I18nProvider가 전체 트리를 감쌈. ConfigService에서 언어 로드.
//
// 인증 정책: 무료 로컬 모드가 사용자 entry point.
// 앱은 로그인 없이 바로 메인 UI에 진입해서 로컬 STT/LLM을 사용할 수 있다.
// OAuth 로그인은 Settings → Cloud Sync 섹션에서 선택적으로 수행 (유료 SaaS 경로).
import { useState, useEffect, useMemo, useRef } from 'react'
import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material'
import { getTheme } from '@d3ro/ui/theme'
import { I18nProvider, useI18n, type I18nStorage, type Locale } from '@d3ro/i18n'
import { AppLayout } from './components/AppLayout'
import { UpgradePromptModal } from './components/UpgradePromptModal'
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types'
// Electron ConfigService에 바인딩된 i18n 영속화 어댑터
const electronI18nStorage: I18nStorage = {
load: async () => {
const result = await window.electronAPI.config.get({ key: 'language' })
return result.success && result.data ? (result.data as string) : null
},
save: (locale: Locale) => {
window.electronAPI.config.set({ key: 'language', value: locale })
}
}
/** 다른 기기(모바일)에서 바꾼 언어가 동기화로 들어오면 화면 언어도 바꾼다. */
function SyncedLocale(): null {
const { locale, setLocale } = useI18n()
useEffect(
() =>
window.electronAPI.config.onChanged((e: ConfigChangedEvent) => {
if (e.key === 'language' && typeof e.value === 'string' && e.value !== locale) {
setLocale(e.value as Locale)
}
}),
[locale, setLocale],
)
return null
}
export function App(): React.ReactElement {
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
const systemAudioCleanupRef = useRef<(() => void) | null>(null)
// 설정에서 테마 로드 + 변경 감지
useEffect(() => {
window.electronAPI.config.getTheme().then((result) => {
if (result.success) {
setThemeMode(result.data)
}
})
const unsub = window.electronAPI.config.onChanged((e: ConfigChangedEvent) => {
if (e.key === 'theme') {
setThemeMode(e.value as ThemeMode)
}
})
return unsub
}, [])
// 시스템 오디오 캡처: 메인 프로세스의 시작/중지 요청에 응답
useEffect(() => {
const unsubStart = window.electronAPI.caption.onStartSystemAudio(() => {
startSystemAudioCapture((pcm16Buffer) => {
window.electronAPI.caption.sendSystemAudioData(pcm16Buffer)
}).catch(() => {
// 시스템 오디오 캡처 실패 시 무시 — 마이크 자막만 사용
})
})
const unsubStop = window.electronAPI.caption.onStopSystemAudio(() => {
stopSystemAudioCapture()
})
systemAudioCleanupRef.current = () => {
stopSystemAudioCapture()
}
return () => {
unsubStart()
unsubStop()
systemAudioCleanupRef.current?.()
}
}, [])
const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark])
return (
<I18nProvider storage={electronI18nStorage}>
<SyncedLocale />
<ThemeProvider theme={theme}>
<CssBaseline />
<AppLayout />
<UpgradePromptModal />
</ThemeProvider>
</I18nProvider>
)
}