diff --git a/renderer/components/proofread/ProofreadEditor.tsx b/renderer/components/proofread/ProofreadEditor.tsx index 30f68752..65b7658c 100644 --- a/renderer/components/proofread/ProofreadEditor.tsx +++ b/renderer/components/proofread/ProofreadEditor.tsx @@ -99,6 +99,7 @@ export default function ProofreadEditor({ canRedo, handleMergeSubtitles, handleSplitSubtitle, + handleDeleteSubtitle, handleTimeChange, // 光标位置 handleCursorPositionChange, @@ -222,6 +223,13 @@ export default function ProofreadEditor({ [handleSubtitleClick], ); + const handleDeleteClick = useCallback( + (index: number) => { + handleDeleteSubtitle(index); + }, + [handleDeleteSubtitle], + ); + // 重置触发器 const handleTriggerHandled = useCallback(() => { setTriggerAiOptimize(false); @@ -462,6 +470,7 @@ export default function ProofreadEditor({ onCursorPositionChange={handleCursorPositionChange} onAiOptimizeClick={handleAiOptimizeClick} onSplitClick={handleSplitClick} + onDeleteClick={handleDeleteClick} onTimeChange={handleTimeChange} retranslate={retranslate} onMergeRange={handleMergeSubtitles} diff --git a/renderer/components/subtitle/SubtitleList.tsx b/renderer/components/subtitle/SubtitleList.tsx index 506ef368..edcb7d2f 100644 --- a/renderer/components/subtitle/SubtitleList.tsx +++ b/renderer/components/subtitle/SubtitleList.tsx @@ -22,6 +22,7 @@ import { Combine, CircleStop, X, + Trash2, } from 'lucide-react'; import { Tooltip, @@ -51,6 +52,7 @@ interface SubtitleListProps { onCursorPositionChange?: (position: number) => void; onAiOptimizeClick?: (index: number) => void; onSplitClick?: (index: number) => void; + onDeleteClick?: (index: number) => void; /** 行内时间编辑提交;返回错误文案(不应用)或 null(已应用) */ onTimeChange?: ( index: number, @@ -74,6 +76,7 @@ interface RowLabels { translationFailedPlaceholder: string; aiOptimize: string; split: string; + deleteSubtitle: string; timeInvalidFormat: string; timeEditHint: string; } @@ -104,6 +107,7 @@ interface SubtitleRowProps { fontScale: 's' | 'm' | 'l'; showAiOptimize: boolean; showSplit: boolean; + showDelete: boolean; labels: RowLabels; onRowClick: (index: number, shiftKey: boolean) => void; onFieldChange: ( @@ -122,6 +126,7 @@ interface SubtitleRowProps { ) => void; onAiOptimize: (index: number) => void; onSplit: (index: number) => void; + onDelete: (index: number) => void; onTimeCommit: ( index: number, startSec: number, @@ -141,6 +146,7 @@ const SubtitleRow = memo(function SubtitleRow({ fontScale, showAiOptimize, showSplit, + showDelete, labels, onRowClick, onFieldChange, @@ -149,6 +155,7 @@ const SubtitleRow = memo(function SubtitleRow({ onTargetKeyDown, onAiOptimize, onSplit, + onDelete, onTimeCommit, }: SubtitleRowProps) { // 失败行降噪:左缘红条 + ⚠,不再整行红底 @@ -269,6 +276,27 @@ const SubtitleRow = memo(function SubtitleRow({ {labels.split} )} + {showDelete && ( + + + + + + {labels.deleteSubtitle} + + + )} @@ -321,6 +349,7 @@ const SubtitleList: React.FC = ({ onCursorPositionChange, onAiOptimizeClick, onSplitClick, + onDeleteClick, onTimeChange, retranslate, onMergeRange, @@ -386,6 +415,7 @@ const SubtitleList: React.FC = ({ onCursorPositionChange, onAiOptimizeClick, onSplitClick, + onDeleteClick, onTimeChange, currentSubtitleIndex, }); @@ -395,6 +425,7 @@ const SubtitleList: React.FC = ({ onCursorPositionChange, onAiOptimizeClick, onSplitClick, + onDeleteClick, onTimeChange, currentSubtitleIndex, }; @@ -537,6 +568,10 @@ const SubtitleList: React.FC = ({ latestRef.current.onSplitClick?.(index); }, []); + const onDelete = useCallback((index: number) => { + latestRef.current.onDeleteClick?.(index); + }, []); + const onTimeCommit = useCallback( (index: number, startSec: number, endSec: number): string | null => { return latestRef.current.onTimeChange?.(index, startSec, endSec) ?? null; @@ -554,6 +589,7 @@ const SubtitleList: React.FC = ({ translationFailedPlaceholder: t('translationFailedPlaceholder'), aiOptimize: t('aiOptimize'), split: t('split'), + deleteSubtitle: t('deleteSubtitle'), timeInvalidFormat: t('timeEditInvalidFormat'), timeEditHint: t('timeEditHint'), }), @@ -773,6 +809,7 @@ const SubtitleList: React.FC = ({ fontScale={fontScale} showAiOptimize={!!onAiOptimizeClick} showSplit={!!onSplitClick} + showDelete={!!onDeleteClick} labels={labels} onRowClick={onRowClick} onFieldChange={onFieldChange} @@ -781,6 +818,7 @@ const SubtitleList: React.FC = ({ onTargetKeyDown={onTargetKeyDown} onAiOptimize={onAiOptimize} onSplit={onSplit} + onDelete={onDelete} onTimeCommit={onTimeCommit} /> diff --git a/renderer/hooks/useStandaloneSubtitles.ts b/renderer/hooks/useStandaloneSubtitles.ts index 38a44188..25793374 100644 --- a/renderer/hooks/useStandaloneSubtitles.ts +++ b/renderer/hooks/useStandaloneSubtitles.ts @@ -728,6 +728,34 @@ export const useStandaloneSubtitles = ( [applySubtitles, flushPendingEdit, history.push, t], ); + // 删除字幕(区间命令:1 行 → 0 行;支持撤销恢复) + const handleDeleteSubtitle = useCallback( + (index: number) => { + const current = subtitlesRef.current; + if (index < 0 || index >= current.length) return; + + flushPendingEdit(); + + const removed = current[index]; + history.push({ start: index, removed: [removed], inserted: [] }); + + const next = current.slice(); + next.splice(index, 1); + const normalized = renormalizeIds(next); + applySubtitles(normalized); + + setCurrentSubtitleIndex((currentIndex) => { + if (normalized.length === 0) return -1; + if (currentIndex > index) return currentIndex - 1; + if (currentIndex >= normalized.length) return normalized.length - 1; + return currentIndex; + }); + setIsDirty(true); + toast.success(t('deleteSubtitleSuccess')); + }, + [applySubtitles, flushPendingEdit, history.push, t], + ); + // 更新光标位置 const handleCursorPositionChange = useCallback((position: number) => { cursorPositionRef.current = position; @@ -767,6 +795,7 @@ export const useStandaloneSubtitles = ( canRedo, handleMergeSubtitles, handleSplitSubtitle, + handleDeleteSubtitle, handleTimeChange, // 光标位置 handleCursorPositionChange, diff --git a/renderer/public/locales/en/home.json b/renderer/public/locales/en/home.json index 53615d95..e090dbcc 100644 --- a/renderer/public/locales/en/home.json +++ b/renderer/public/locales/en/home.json @@ -215,8 +215,10 @@ "invalidMergeRange": "Invalid merge range", "splitSubtitle": "Split Subtitle", "split": "Split", + "deleteSubtitle": "Delete subtitle", "mergeSuccess": "Subtitles merged", "splitSuccess": "Subtitle split", + "deleteSubtitleSuccess": "Subtitle deleted", "splitSubtitleDesc": "Choose text split position and time allocation", "textSplitPosition": "Text split position", "sourcePreview": "Source preview", diff --git a/renderer/public/locales/zh/home.json b/renderer/public/locales/zh/home.json index 03c745ba..64a9a417 100644 --- a/renderer/public/locales/zh/home.json +++ b/renderer/public/locales/zh/home.json @@ -215,8 +215,10 @@ "invalidMergeRange": "无效的合并范围", "splitSubtitle": "拆分字幕", "split": "拆分", + "deleteSubtitle": "删除字幕", "mergeSuccess": "字幕已合并", "splitSuccess": "字幕已拆分", + "deleteSubtitleSuccess": "字幕已删除", "splitSubtitleDesc": "选择文字拆分位置和时间分配", "textSplitPosition": "文字拆分位置", "sourcePreview": "原文预览",