// src/renderer/pages/DictionaryPage.tsx // Precision Vocabulary & Phonetic Dictionary Editor import React, { useState, useEffect, useCallback, useRef } from 'react' import { Box, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Tooltip } from '@mui/material' import { Plus, Trash2, Pencil, BookA, Download, Upload } from 'lucide-react' import { MetalCard, PhosphorText, PhysicalButton, TactileBadge } from '@d3ro/ui/components/ds' import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontSans, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' import type { DictionaryEntry, DictionaryPage as DictPageData } from '@d3ro/core/types' export function DictionaryPage(): React.ReactElement { const { t } = useI18n() const [data, setData] = useState(null) const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [dialogOpen, setDialogOpen] = useState(false) const [editId, setEditId] = useState(null) const [formWord, setFormWord] = useState('') const [formPronunciation, setFormPronunciation] = useState('') const [saving, setSaving] = useState(false) const isSavingRef = useRef(false) const [ioBusy, setIoBusy] = useState(false) const [ioMessage, setIoMessage] = useState(null) const loadData = useCallback(async () => { setLoading(true) const trimmed = search.trim() const result = trimmed ? await window.electronAPI.dictionary.search({ query: trimmed, page: 0, pageSize: 50 }) : await window.electronAPI.dictionary.getAll({ page: 0, pageSize: 50 }) if (result.success) setData(result.data) setLoading(false) }, [search]) useEffect(() => { loadData() }, [loadData]) const openAdd = () => { if (dialogOpen) return setEditId(null) setFormWord('') setFormPronunciation('') setDialogOpen(true) } const openEdit = (entry: DictionaryEntry) => { if (dialogOpen) return setEditId(entry.id) setFormWord(entry.word) setFormPronunciation(entry.pronunciation ?? '') setDialogOpen(true) } const handleSave = async () => { if (!formWord.trim() || saving || isSavingRef.current) return isSavingRef.current = true setSaving(true) try { if (editId) { await window.electronAPI.dictionary.update({ id: editId, word: formWord.trim(), pronunciation: formPronunciation.trim() || undefined, }) } else { await window.electronAPI.dictionary.add({ word: formWord.trim(), pronunciation: formPronunciation.trim() || undefined, }) } setDialogOpen(false) loadData() } finally { setSaving(false) isSavingRef.current = false } } const handleExport = async (format: 'json' | 'csv'): Promise => { if (ioBusy) return setIoBusy(true) setIoMessage(null) try { const result = await window.electronAPI.dictionary.export({ format }) setIoMessage(result.success ? t('dictionary.exported') : t('dictionary.exportFailed')) } finally { setIoBusy(false) } } const handleImport = async (format: 'json' | 'csv'): Promise => { if (ioBusy) return setIoBusy(true) setIoMessage(null) try { const result = await window.electronAPI.dictionary.import({ filePath: '', format }) if (!result.success) { setIoMessage(t('dictionary.importFailed')) return } const { imported, skipped, errors } = result.data setIoMessage( t('dictionary.importedCount', { imported: String(imported), skipped: String(skipped), errors: String(errors), }) ) await loadData() } finally { setIoBusy(false) } } const ioButtonSx = { p: 0.7, color: d3roPalette.text.inactive, bgcolor: d3roPalette.glass.raised, border: `1px solid ${d3roPalette.glass.hairline}`, '&:hover': { color: d3roPalette.accent.light, bgcolor: d3roPalette.glass.hairlineStrong }, } as const return ( void handleExport('json')} sx={ioButtonSx} > void handleExport('csv')} sx={ioButtonSx} > void handleImport('json')} sx={ioButtonSx} > void handleImport('csv')} sx={ioButtonSx} > }> {t('dictionary.add')} } /> {ioMessage && ( {ioMessage} )} {loading ? ( {t('common.loading')} ) : !data || data.entries.length === 0 ? ( } action={ {t('dictionary.add')} } /> ) : ( {data.entries.map((entry: DictionaryEntry) => ( {entry.word} {entry.pronunciation && ( [{entry.pronunciation}] )} {entry.category.toUpperCase()} {t('dictionary.used', { count: entry.usageCount })} openEdit(entry)} sx={{ p: 0.6, color: d3roPalette.text.inactive, bgcolor: d3roPalette.glass.raised, border: `1px solid ${d3roPalette.glass.hairline}`, '&:hover': { color: d3roPalette.accent.light, bgcolor: d3roPalette.glass.hairlineStrong }, }} > { await window.electronAPI.dictionary.delete({ id: entry.id }) loadData() }} sx={{ p: 0.6, color: d3roPalette.text.inactive, bgcolor: d3roPalette.glass.raised, border: `1px solid ${d3roPalette.glass.hairline}`, '&:hover': { color: d3roPalette.tag.red, bgcolor: d3roPalette.tag.redBg }, }} > ))} )} {/* Add / Edit Dialog */} setDialogOpen(false)} maxWidth="xs" fullWidth PaperProps={{ sx: { bgcolor: d3roPalette.bg.card, borderRadius: d3roRadius.doubleBezelOuter, border: `1px solid ${d3roPalette.glass.hairlineStrong}`, boxShadow: d3roShadow.dialog, p: 1, }, }} > {editId ? t('dictionary.editTitle') : t('dictionary.addTitle')} setFormWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1.5 }} /> setFormPronunciation(e.target.value)} fullWidth sx={{ mt: 2.5 }} helperText="e.g. 디쓰리오" /> setDialogOpen(false)}> {t('common.cancel')} {saving ? t('common.saving') || '저장 중...' : t('common.save')} ) }