Phase 3 구현: 텍스트 삽입 + RecordingTip/ResultPopup + Settings
- TextInsertService: clipboard save→set→Ctrl+V→restore (@nut-tree-fork/nut-js) - RecordingTip 팝업: 9개 웨이브바 cos분포, thinking 점근수렴, 2-phase 리사이즈 - ResultPopup 팝업: 복사 버튼, auto-close, 마우스 호버 유지, 다크모드 - WindowManager: 팝업 프리로딩, 커서 위치 표시, 멀티모니터 보정 - Settings 모달: General/Audio/STT/LLM 탭 - VoiceModeService: 전사 완료 시 자동 텍스트 삽입 + 팝업 연동 - electron-vite: 팝업 HTML 멀티 엔트리 + popup preload 빌드
This commit is contained in:
parent
1d152d01a1
commit
517210af2f
16 changed files with 1366 additions and 10 deletions
221
src/renderer/components/SettingsModal.tsx
Normal file
221
src/renderer/components/SettingsModal.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// src/renderer/components/SettingsModal.tsx
|
||||
// 설계서 03: Settings React Modal (일반/오디오/핫키 탭)
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Typography,
|
||||
IconButton,
|
||||
Divider,
|
||||
InputLabel,
|
||||
FormControl
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import type { ThemeMode, AppConfig } from '@shared/types'
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface TabPanelProps {
|
||||
children: React.ReactNode
|
||||
value: number
|
||||
index: number
|
||||
}
|
||||
|
||||
function TabPanel({ children, value, index }: TabPanelProps): React.ReactElement | null {
|
||||
if (value !== index) return null
|
||||
return <Box sx={{ pt: 2 }}>{children}</Box>
|
||||
}
|
||||
|
||||
export function SettingsModal({ open, onClose }: SettingsModalProps): React.ReactElement {
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [config, setConfig] = useState<Partial<AppConfig>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setLoading(true)
|
||||
window.electronAPI.config
|
||||
.getAll()
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
setConfig(result.data)
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [open])
|
||||
|
||||
const updateConfig = (key: keyof AppConfig, value: AppConfig[keyof AppConfig]) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }))
|
||||
window.electronAPI.config.set({ key, value })
|
||||
}
|
||||
|
||||
if (loading) return <Dialog open={open} onClose={onClose}><DialogContent /></Dialog>
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
Settings
|
||||
<IconButton onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Audio" />
|
||||
<Tab label="STT" />
|
||||
<Tab label="LLM" />
|
||||
</Tabs>
|
||||
|
||||
{/* General */}
|
||||
<TabPanel value={activeTab} index={0}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControl size="small">
|
||||
<InputLabel>Theme</InputLabel>
|
||||
<Select
|
||||
label="Theme"
|
||||
value={config.theme ?? 'auto'}
|
||||
onChange={(e) => updateConfig('theme', e.target.value as ThemeMode)}
|
||||
>
|
||||
<MenuItem value="auto">System</MenuItem>
|
||||
<MenuItem value="light">Light</MenuItem>
|
||||
<MenuItem value="dark">Dark</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>Language</InputLabel>
|
||||
<Select
|
||||
label="Language"
|
||||
value={config.language ?? 'ko'}
|
||||
onChange={(e) => updateConfig('language', e.target.value)}
|
||||
>
|
||||
<MenuItem value="ko">한국어</MenuItem>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.closeToTray ?? true}
|
||||
onChange={(e) => updateConfig('closeToTray', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Close to tray"
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.autoInsert ?? true}
|
||||
onChange={(e) => updateConfig('autoInsert', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Auto-insert text after transcription"
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.soundEnabled ?? true}
|
||||
onChange={(e) => updateConfig('soundEnabled', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Sound effects"
|
||||
/>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Audio */}
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Microphone device selection will be available in a future update.
|
||||
Currently using the system default microphone.
|
||||
</Typography>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>Insert Method</InputLabel>
|
||||
<Select
|
||||
label="Insert Method"
|
||||
value={config.insertMethod ?? 'clipboard'}
|
||||
onChange={(e) =>
|
||||
updateConfig('insertMethod', e.target.value as 'clipboard' | 'keyboard')
|
||||
}
|
||||
>
|
||||
<MenuItem value="clipboard">Clipboard (Ctrl+V)</MenuItem>
|
||||
<MenuItem value="keyboard">Keyboard typing</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* STT */}
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControl size="small">
|
||||
<InputLabel>Whisper Model</InputLabel>
|
||||
<Select
|
||||
label="Whisper Model"
|
||||
value={config.sttModelId ?? 'base'}
|
||||
onChange={(e) => updateConfig('sttModelId', e.target.value)}
|
||||
>
|
||||
<MenuItem value="tiny">tiny (39 MB, fastest)</MenuItem>
|
||||
<MenuItem value="base">base (74 MB, balanced)</MenuItem>
|
||||
<MenuItem value="small">small (244 MB, better)</MenuItem>
|
||||
<MenuItem value="medium">medium (769 MB, good)</MenuItem>
|
||||
<MenuItem value="large-v3">large-v3 (1.5 GB, best)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>Language</InputLabel>
|
||||
<Select
|
||||
label="Language"
|
||||
value={config.sttLanguage ?? 'auto'}
|
||||
onChange={(e) => updateConfig('sttLanguage', e.target.value)}
|
||||
>
|
||||
<MenuItem value="auto">Auto-detect</MenuItem>
|
||||
<MenuItem value="ko">한국어</MenuItem>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="ja">日本語</MenuItem>
|
||||
<MenuItem value="zh">中文</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* LLM */}
|
||||
<TabPanel value={activeTab} index={3}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Ollama Server URL"
|
||||
value={config.ollamaServerUrl ?? 'http://localhost:11434'}
|
||||
onChange={(e) => updateConfig('ollamaServerUrl', e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
LLM model selection will be available after Ollama integration (Phase 4).
|
||||
</Typography>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue