import { useState, useMemo } from 'react' import { Button, Box, ToggleButtonGroup, ToggleButton, useMediaQuery, Tooltip, } from '@mui/material' import { Save, Cancel } from '@mui/icons-material' import { diffLines, Change } from 'diff' import { useI18nStore } from '../stores/i18n.store' import P3xrDialog from '../components/P3xrDialog' interface DiffBlock { type: 'added' | 'removed' | 'unchanged' | 'collapse' lines: string[] collapsedCount?: number } const CONTEXT_LINES = 3 function buildBlocks(changes: Change[]): DiffBlock[] { const blocks: DiffBlock[] = [] for (const change of changes) { const lines = change.value.replace(/\n$/, '').split('\n') if (change.added) { blocks.push({ type: 'added', lines }) } else if (change.removed) { blocks.push({ type: 'removed', lines }) } else { if (lines.length <= CONTEXT_LINES * 2 + 1) { blocks.push({ type: 'unchanged', lines }) } else { blocks.push({ type: 'unchanged', lines: lines.slice(0, CONTEXT_LINES) }) const collapsed = lines.slice(CONTEXT_LINES, -CONTEXT_LINES) blocks.push({ type: 'collapse', lines: collapsed, collapsedCount: collapsed.length }) blocks.push({ type: 'unchanged', lines: lines.slice(-CONTEXT_LINES) }) } } } return blocks } interface DiffDialogProps { open: boolean keyName: string fieldName?: string oldValue: string newValue: string onConfirm: () => void onCancel: () => void } export default function DiffDialog({ open, keyName, fieldName, oldValue, newValue, onConfirm, onCancel }: DiffDialogProps) { const strings = useI18nStore(s => s.strings) const d = strings?.diff || {} as any const isWide = useMediaQuery('(min-width: 600px)') const [mode, setMode] = useState<'inline' | 'side-by-side'>('inline') const [expanded, setExpanded] = useState>(new Set()) const changes = useMemo(() => diffLines(String(oldValue ?? ''), String(newValue ?? '')), [oldValue, newValue]) const blocks = useMemo(() => buildBlocks(changes), [changes]) const additions = useMemo(() => changes.filter(c => c.added).reduce((n, c) => n + (c.value.split('\n').length - 1 || 1), 0), [changes]) const deletions = useMemo(() => changes.filter(c => c.removed).reduce((n, c) => n + (c.value.split('\n').length - 1 || 1), 0), [changes]) const toggleExpand = (i: number) => setExpanded(prev => { const s = new Set(prev); s.has(i) ? s.delete(i) : s.add(i); return s }) const lineSx = (type: string) => ({ px: 1, py: '1px', whiteSpace: 'pre-wrap', wordBreak: 'break-all', fontFamily: "'Roboto Mono', monospace", fontSize: 13, ...(type === 'added' ? { bgcolor: 'rgba(76,175,80,0.12)' } : {}), ...(type === 'removed' ? { bgcolor: 'rgba(244,67,54,0.12)' } : {}), ...(type === 'unchanged' || type === 'collapse' ? { opacity: 0.6 } : {}), }) const collapseSx = { px: 1, py: '4px', opacity: 0.4, fontStyle: 'italic', cursor: 'pointer', fontFamily: "'Roboto Mono', monospace", fontSize: 13, '&:hover': { opacity: 0.7 }, } const renderInline = () => blocks.map((block, i) => { if (block.type === 'collapse' && !expanded.has(i)) { return toggleExpand(i)}>... {block.collapsedCount} {d.unchangedLines} ... } return block.lines.map((line, j) => ( {block.type === 'added' ? '+' : block.type === 'removed' ? '-' : ' '} {line} )) }) const renderSide = (side: 'before' | 'after') => { const skipType = side === 'before' ? 'added' : 'removed' return (<> {side === 'before' ? d.before : d.after} {blocks.map((block, i) => { if (block.type === 'collapse' && !expanded.has(i)) { return toggleExpand(i)}>... {block.collapsedCount} {d.unchangedLines} ... } if (block.type === skipType) return null return block.lines.map((line, j) => {line}) })} ) } const title = `${d.reviewChanges} — ${fieldName ? `${fieldName} @ ` : ''}${keyName}` return ( v && setMode(v)} sx={{ mr: 0.5, '& .MuiToggleButton-root': { py: '2px', px: 1.5, fontSize: 12, borderRadius: '4px', textTransform: 'none', color: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,255,255,0.3)' } }}> {d.inline} {d.sideBySide} +{additions}{' '}{d.additions},{' '} -{deletions}{' '}{d.deletions} } actions={<> } > {mode === 'inline' ? ( {renderInline()} ) : (<> {renderSide('before')} {renderSide('after')} )} ) }