// src/renderer/components/SettingsModal.tsx
// 설계서 03: Settings React Modal — General(음성 모드+핫키)/Audio/STT/LLM 탭
import { useState, useEffect, useCallback } from 'react'
import {
Dialog,
DialogTitle,
DialogContent,
Tabs,
Tab,
Box,
TextField,
Select,
MenuItem,
Switch,
FormControlLabel,
Typography,
IconButton,
Divider,
InputLabel,
FormControl,
Button,
Chip,
Stack,
Paper,
} from '@mui/material'
import CloseIcon from '@mui/icons-material/Close'
import KeyboardIcon from '@mui/icons-material/Keyboard'
import EditIcon from '@mui/icons-material/Edit'
import MicIcon from '@mui/icons-material/Mic'
import LockIcon from '@mui/icons-material/Lock'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import { d3roPalette, d3roFontMono, d3roShadow } from '../theme'
import { HotkeyRecordModal } from './HotkeyRecordModal'
import { LicenseTab } from './LicenseTab'
import { useI18n, LOCALE_META } from '../i18n'
import type { Locale } from '../i18n'
import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice } from '@shared/types'
import { Feature } 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 {children}
}
// ── 핫키 표시 컴포넌트 ──────────────────────────────────
function HotkeyDisplay({
binding,
onEdit,
label,
notSetLabel,
}: {
binding: HotkeyBinding | null
onEdit: () => void
label: string
notSetLabel: string
}): React.ReactElement {
return (
{label}
{binding ? (
{binding.displayLabel.split(' + ').map((key) => (
))}
) : (
{notSetLabel}
)}
)
}
// ── 음성 모드 카드 ──────────────────────────────────────
function VoiceModeCard({
title,
description,
enabled,
onToggle,
disabled,
enabledLabel,
disabledLabel,
children,
}: {
title: string
description: string
enabled: boolean
onToggle: (enabled: boolean) => void
disabled?: boolean
enabledLabel: string
disabledLabel: string
children?: React.ReactNode
}): React.ReactElement {
return (
{title}
onToggle(e.target.checked)}
disabled={disabled}
size="small"
sx={{
'& .MuiSwitch-switchBase.Mui-checked': { color: d3roPalette.tag.green },
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
backgroundColor: d3roPalette.tag.green,
},
}}
/>
}
label={
}
sx={{ ml: 0, mr: 0 }}
/>
{description}
{children && {children}}
)
}
export function SettingsModal({ open, onClose }: SettingsModalProps): React.ReactElement {
const { t, locale, setLocale } = useI18n()
const [activeTab, setActiveTab] = useState(0)
const [config, setConfig] = useState>({})
const [loading, setLoading] = useState(true)
// 음성 모드 상태
const [dictationEnabled, setDictationEnabled] = useState(true)
const [dictationBinding, setDictationBinding] = useState(null)
const [handsFreeEnabled, setHandsFreeEnabled] = useState(false)
const [handsFreeBinding, setHandsFreeBinding] = useState(null)
const [captionBinding, setCaptionBinding] = useState(null)
const [hotkeyGlobalEnabled, setHotkeyGlobalEnabled] = useState(true)
// 핫키 녹화 모달
const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false)
const [hotkeyModalTarget, setHotkeyModalTarget] = useState<'dictation' | 'handsFree' | 'caption'>('dictation')
// 오디오 디바이스
const [audioDevices, setAudioDevices] = useState([])
const [selectedDeviceId, setSelectedDeviceId] = useState('default')
const [micTesting, setMicTesting] = useState(false)
const [micLevel, setMicLevel] = useState(0)
// 설정 로드
useEffect(() => {
if (!open) return
setLoading(true)
Promise.all([
window.electronAPI.config.getAll(),
window.electronAPI.hotkey.getDictationShortcut(),
window.electronAPI.hotkey.getHandsFreeShortcut(),
window.electronAPI.hotkey.getCaptionShortcut(),
window.electronAPI.hotkey.isEnabled(),
])
.then(([configResult, dictResult, hfResult, capResult, enabledResult]) => {
if (configResult.success) setConfig(configResult.data)
if (dictResult.success && dictResult.data) setDictationBinding(dictResult.data)
if (hfResult.success && hfResult.data) setHandsFreeBinding(hfResult.data)
if (capResult.success && capResult.data) setCaptionBinding(capResult.data)
if (enabledResult.success) {
setHotkeyGlobalEnabled(enabledResult.data)
setDictationEnabled(enabledResult.data)
}
})
.finally(() => setLoading(false))
Promise.all([
window.electronAPI.audio.getDevices(),
window.electronAPI.audio.getSelectedDevice(),
]).then(([devicesResult, selectedResult]) => {
if (devicesResult.success) setAudioDevices(devicesResult.data)
if (selectedResult.success && selectedResult.data) setSelectedDeviceId(selectedResult.data)
})
}, [open])
const updateConfig = useCallback((key: keyof AppConfig, value: AppConfig[keyof AppConfig]) => {
setConfig((prev) => ({ ...prev, [key]: value }))
window.electronAPI.config.set({ key, value })
}, [])
const handleDictationToggle = useCallback(
(enabled: boolean) => {
setDictationEnabled(enabled)
window.electronAPI.hotkey.setEnabled({ enabled })
if (!enabled && handsFreeEnabled) {
setHandsFreeEnabled(false)
}
},
[handsFreeEnabled]
)
const handleHandsFreeToggle = useCallback((enabled: boolean) => {
if (enabled && !handsFreeBinding) {
setHotkeyModalTarget('handsFree')
setHotkeyModalOpen(true)
return
}
setHandsFreeEnabled(enabled)
}, [handsFreeBinding])
const handleHotkeySave = useCallback(
(binding: HotkeyBinding) => {
if (hotkeyModalTarget === 'dictation') {
setDictationBinding(binding)
window.electronAPI.hotkey.setDictationShortcut({ binding })
setDictationEnabled(true)
window.electronAPI.hotkey.setEnabled({ enabled: true })
} else if (hotkeyModalTarget === 'handsFree') {
setHandsFreeBinding(binding)
window.electronAPI.hotkey.setHandsFreeShortcut({ binding })
setHandsFreeEnabled(true)
} else {
setCaptionBinding(binding)
window.electronAPI.hotkey.setCaptionShortcut({ binding })
}
},
[hotkeyModalTarget]
)
const openHotkeyModal = useCallback((target: 'dictation' | 'handsFree' | 'caption') => {
setHotkeyModalTarget(target)
setHotkeyModalOpen(true)
}, [])
const handleLanguageChange = useCallback((newLocale: string) => {
setLocale(newLocale as Locale)
setConfig((prev) => ({ ...prev, language: newLocale }))
}, [setLocale])
if (loading) return
return (
<>
setHotkeyModalOpen(false)}
onSave={handleHotkeySave}
currentBinding={hotkeyModalTarget === 'dictation' ? dictationBinding : hotkeyModalTarget === 'handsFree' ? handsFreeBinding : captionBinding}
title={hotkeyModalTarget === 'dictation' ? t('hotkey.dictationTitle') : hotkeyModalTarget === 'handsFree' ? t('hotkey.oneTouchTitle') : t('hotkey.captionTitle')}
/>
>
)
}