From b3082ef03354cdb92a9e1bb875af8d8328f7e7d4 Mon Sep 17 00:00:00 2001
From: Nightt <87569709+nightt5879@users.noreply.github.com>
Date: Mon, 6 Jul 2026 15:49:35 +0800
Subject: [PATCH] feat: add subtitle line deletion in proofread editor
---
.../components/proofread/ProofreadEditor.tsx | 9 +++++
renderer/components/subtitle/SubtitleList.tsx | 38 +++++++++++++++++++
renderer/hooks/useStandaloneSubtitles.ts | 29 ++++++++++++++
renderer/public/locales/en/home.json | 2 +
renderer/public/locales/zh/home.json | 2 +
5 files changed, 80 insertions(+)
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": "原文预览",