lyrics-transcriber 0.46.0__py3-none-any.whl → 0.48.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lyrics_transcriber/frontend/dist/assets/{index-BXOpmKq-.js → index-BvRLUQmZ.js} +374 -279
- lyrics_transcriber/frontend/dist/assets/index-BvRLUQmZ.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/EditModal.tsx +36 -36
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +73 -2
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +28 -6
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +35 -16
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +26 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +3 -2
- lyrics_transcriber/frontend/src/components/shared/types.ts +1 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +45 -8
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +47 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- {lyrics_transcriber-0.46.0.dist-info → lyrics_transcriber-0.48.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.46.0.dist-info → lyrics_transcriber-0.48.0.dist-info}/RECORD +18 -18
- lyrics_transcriber/frontend/dist/assets/index-BXOpmKq-.js.map +0 -1
- {lyrics_transcriber-0.46.0.dist-info → lyrics_transcriber-0.48.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.46.0.dist-info → lyrics_transcriber-0.48.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.46.0.dist-info → lyrics_transcriber-0.48.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
7
|
<title>Lyrics Transcriber Analyzer</title>
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
8
|
+
<script type="module" crossorigin src="/assets/index-BvRLUQmZ.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -185,15 +185,15 @@ export default function EditModal({
|
|
185
185
|
isGlobal = false,
|
186
186
|
isLoading = false
|
187
187
|
}: EditModalProps) {
|
188
|
-
console.log('EditModal - Render', {
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
});
|
188
|
+
// console.log('EditModal - Render', {
|
189
|
+
// open,
|
190
|
+
// isGlobal,
|
191
|
+
// isLoading,
|
192
|
+
// hasSegment: !!segment,
|
193
|
+
// segmentIndex,
|
194
|
+
// hasOriginalSegment: !!originalSegment,
|
195
|
+
// hasOriginalTranscribedSegment: !!originalTranscribedSegment
|
196
|
+
// });
|
197
197
|
|
198
198
|
const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
|
199
199
|
const [isPlaying, setIsPlaying] = useState(false)
|
@@ -233,7 +233,7 @@ export default function EditModal({
|
|
233
233
|
})
|
234
234
|
|
235
235
|
const handleClose = useCallback(() => {
|
236
|
-
console.log('EditModal - handleClose called');
|
236
|
+
// console.log('EditModal - handleClose called');
|
237
237
|
cleanupManualSync()
|
238
238
|
onClose()
|
239
239
|
}, [onClose, cleanupManualSync])
|
@@ -243,12 +243,12 @@ export default function EditModal({
|
|
243
243
|
const spacebarHandler = handleSpacebar // Capture the current handler
|
244
244
|
|
245
245
|
if (open) {
|
246
|
-
console.log('EditModal - Setting up modal spacebar handler', {
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
})
|
246
|
+
// console.log('EditModal - Setting up modal spacebar handler', {
|
247
|
+
// hasPlaySegment: !!onPlaySegment,
|
248
|
+
// editedSegmentId: editedSegment?.id,
|
249
|
+
// handlerFunction: spacebarHandler.toString().slice(0, 100),
|
250
|
+
// isLoading
|
251
|
+
// })
|
252
252
|
|
253
253
|
// Create a function that will be called by the global event listeners
|
254
254
|
const handleKeyEvent = (e: KeyboardEvent) => {
|
@@ -262,7 +262,7 @@ export default function EditModal({
|
|
262
262
|
// Only cleanup when the effect is re-run or the modal is closed
|
263
263
|
return () => {
|
264
264
|
if (!open) {
|
265
|
-
console.log('EditModal - Cleanup: clearing modal spacebar handler')
|
265
|
+
// console.log('EditModal - Cleanup: clearing modal spacebar handler')
|
266
266
|
setModalSpacebarHandler(undefined)
|
267
267
|
}
|
268
268
|
}
|
@@ -289,11 +289,11 @@ export default function EditModal({
|
|
289
289
|
|
290
290
|
// All useEffect hooks
|
291
291
|
useEffect(() => {
|
292
|
-
console.log('EditModal - segment changed', {
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
});
|
292
|
+
// console.log('EditModal - segment changed', {
|
293
|
+
// hasSegment: !!segment,
|
294
|
+
// segmentId: segment?.id,
|
295
|
+
// wordCount: segment?.words.length
|
296
|
+
// });
|
297
297
|
setEditedSegment(segment)
|
298
298
|
}, [segment])
|
299
299
|
|
@@ -304,7 +304,7 @@ export default function EditModal({
|
|
304
304
|
const endTime = editedSegment.end_time ?? 0
|
305
305
|
|
306
306
|
if (window.isAudioPlaying && currentTime > endTime) {
|
307
|
-
console.log('Stopping playback: current time exceeded end time')
|
307
|
+
// console.log('Stopping playback: current time exceeded end time')
|
308
308
|
window.toggleAudioPlayback?.()
|
309
309
|
cleanupManualSync()
|
310
310
|
}
|
@@ -519,7 +519,7 @@ export default function EditModal({
|
|
519
519
|
|
520
520
|
// Memoize the dialog title to prevent re-renders
|
521
521
|
const dialogTitle = useMemo(() => {
|
522
|
-
console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
|
522
|
+
// console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
|
523
523
|
|
524
524
|
if (isLoading) {
|
525
525
|
return (
|
@@ -563,24 +563,24 @@ export default function EditModal({
|
|
563
563
|
|
564
564
|
// Early return after all hooks and function definitions
|
565
565
|
if (!isLoading && (!segment || !editedSegment || !originalSegment)) {
|
566
|
-
console.log('EditModal - Early return: missing required data', {
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
});
|
566
|
+
// console.log('EditModal - Early return: missing required data', {
|
567
|
+
// hasSegment: !!segment,
|
568
|
+
// hasEditedSegment: !!editedSegment,
|
569
|
+
// hasOriginalSegment: !!originalSegment,
|
570
|
+
// isLoading
|
571
|
+
// });
|
572
572
|
return null;
|
573
573
|
}
|
574
574
|
if (!isLoading && !isGlobal && segmentIndex === null) {
|
575
|
-
console.log('EditModal - Early return: non-global mode with null segmentIndex');
|
575
|
+
// console.log('EditModal - Early return: non-global mode with null segmentIndex');
|
576
576
|
return null;
|
577
577
|
}
|
578
578
|
|
579
|
-
console.log('EditModal - Rendering dialog content', {
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
});
|
579
|
+
// console.log('EditModal - Rendering dialog content', {
|
580
|
+
// isLoading,
|
581
|
+
// hasEditedSegment: !!editedSegment,
|
582
|
+
// hasOriginalSegment: !!originalSegment
|
583
|
+
// });
|
584
584
|
|
585
585
|
return (
|
586
586
|
<Dialog
|
@@ -33,7 +33,8 @@ const WordRow = memo(function WordRow({
|
|
33
33
|
onWordUpdate,
|
34
34
|
onSplitWord,
|
35
35
|
onRemoveWord,
|
36
|
-
wordsLength
|
36
|
+
wordsLength,
|
37
|
+
onTabNavigation
|
37
38
|
}: {
|
38
39
|
word: Word
|
39
40
|
index: number
|
@@ -41,7 +42,17 @@ const WordRow = memo(function WordRow({
|
|
41
42
|
onSplitWord: (index: number) => void
|
42
43
|
onRemoveWord: (index: number) => void
|
43
44
|
wordsLength: number
|
45
|
+
onTabNavigation: (currentIndex: number) => void
|
44
46
|
}) {
|
47
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
48
|
+
// console.log('KeyDown event:', e.key, 'Shift:', e.shiftKey, 'Index:', index);
|
49
|
+
if (e.key === 'Tab' && !e.shiftKey) {
|
50
|
+
// console.log('Tab key detected, preventing default and navigating');
|
51
|
+
e.preventDefault();
|
52
|
+
onTabNavigation(index);
|
53
|
+
}
|
54
|
+
};
|
55
|
+
|
45
56
|
return (
|
46
57
|
<Box sx={{
|
47
58
|
display: 'flex',
|
@@ -53,8 +64,10 @@ const WordRow = memo(function WordRow({
|
|
53
64
|
label={`Word ${index}`}
|
54
65
|
value={word.text}
|
55
66
|
onChange={(e) => onWordUpdate(index, { text: e.target.value })}
|
67
|
+
onKeyDown={handleKeyDown}
|
56
68
|
fullWidth
|
57
69
|
size="small"
|
70
|
+
id={`word-text-${index}`}
|
58
71
|
/>
|
59
72
|
<TextField
|
60
73
|
label="Start Time"
|
@@ -108,7 +121,8 @@ const WordItem = memo(function WordItem({
|
|
108
121
|
onAddSegment,
|
109
122
|
onMergeSegment,
|
110
123
|
wordsLength,
|
111
|
-
isGlobal
|
124
|
+
isGlobal,
|
125
|
+
onTabNavigation
|
112
126
|
}: {
|
113
127
|
word: Word
|
114
128
|
index: number
|
@@ -122,6 +136,7 @@ const WordItem = memo(function WordItem({
|
|
122
136
|
onMergeSegment?: (mergeWithNext: boolean) => void
|
123
137
|
wordsLength: number
|
124
138
|
isGlobal: boolean
|
139
|
+
onTabNavigation: (currentIndex: number) => void
|
125
140
|
}) {
|
126
141
|
return (
|
127
142
|
<Box key={word.id}>
|
@@ -132,6 +147,7 @@ const WordItem = memo(function WordItem({
|
|
132
147
|
onSplitWord={onSplitWord}
|
133
148
|
onRemoveWord={onRemoveWord}
|
134
149
|
wordsLength={wordsLength}
|
150
|
+
onTabNavigation={onTabNavigation}
|
135
151
|
/>
|
136
152
|
|
137
153
|
{/* Word divider with merge/split functionality */}
|
@@ -210,6 +226,60 @@ export default function EditWordList({
|
|
210
226
|
setPage(value);
|
211
227
|
};
|
212
228
|
|
229
|
+
// Handle tab navigation between word text fields
|
230
|
+
const handleTabNavigation = (currentIndex: number) => {
|
231
|
+
// console.log('handleTabNavigation called with index:', currentIndex);
|
232
|
+
const nextIndex = (currentIndex + 1) % words.length;
|
233
|
+
// console.log('Next index calculated:', nextIndex, 'Total words:', words.length);
|
234
|
+
|
235
|
+
// If the next word is on a different page, change the page
|
236
|
+
if (isGlobal && (nextIndex < startIndex || nextIndex >= endIndex)) {
|
237
|
+
// console.log('Next word is on different page. Current page:', page, 'startIndex:', startIndex, 'endIndex:', endIndex);
|
238
|
+
const nextPage = Math.floor(nextIndex / pageSize) + 1;
|
239
|
+
// console.log('Changing to page:', nextPage);
|
240
|
+
setPage(nextPage);
|
241
|
+
|
242
|
+
// Use setTimeout to allow the page change to render before focusing
|
243
|
+
setTimeout(() => {
|
244
|
+
// console.log('Timeout callback executing, trying to focus element with ID:', `word-text-${nextIndex}`);
|
245
|
+
focusWordTextField(nextIndex);
|
246
|
+
}, 50);
|
247
|
+
} else {
|
248
|
+
// console.log('Next word is on same page, trying to focus element with ID:', `word-text-${nextIndex}`);
|
249
|
+
focusWordTextField(nextIndex);
|
250
|
+
}
|
251
|
+
};
|
252
|
+
|
253
|
+
// Helper function to focus a word text field by index
|
254
|
+
const focusWordTextField = (index: number) => {
|
255
|
+
// Material-UI TextField uses a more complex structure
|
256
|
+
// The actual input is inside the TextField component
|
257
|
+
const element = document.getElementById(`word-text-${index}`);
|
258
|
+
// console.log('Element found:', !!element);
|
259
|
+
|
260
|
+
if (element) {
|
261
|
+
// Try different selectors to find the input element
|
262
|
+
// First try the standard input selector
|
263
|
+
let input = element.querySelector('input');
|
264
|
+
|
265
|
+
// If that doesn't work, try the MUI-specific selector
|
266
|
+
if (!input) {
|
267
|
+
input = element.querySelector('.MuiInputBase-input');
|
268
|
+
}
|
269
|
+
|
270
|
+
// console.log('Input element found:', !!input);
|
271
|
+
if (input) {
|
272
|
+
input.focus();
|
273
|
+
input.select();
|
274
|
+
// console.log('Focus and select called on input');
|
275
|
+
} else {
|
276
|
+
// As a fallback, try to focus the TextField itself
|
277
|
+
// console.log('Trying to focus the TextField itself');
|
278
|
+
element.focus();
|
279
|
+
}
|
280
|
+
}
|
281
|
+
};
|
282
|
+
|
213
283
|
return (
|
214
284
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flexGrow: 1, minHeight: 0 }}>
|
215
285
|
{/* Initial divider with Add Segment Before button */}
|
@@ -265,6 +335,7 @@ export default function EditWordList({
|
|
265
335
|
onMergeSegment={onMergeSegment}
|
266
336
|
wordsLength={words.length}
|
267
337
|
isGlobal={isGlobal}
|
338
|
+
onTabNavigation={handleTabNavigation}
|
268
339
|
/>
|
269
340
|
);
|
270
341
|
})}
|
@@ -22,7 +22,8 @@ import {
|
|
22
22
|
deleteSegment,
|
23
23
|
updateSegment,
|
24
24
|
mergeSegment,
|
25
|
-
findAndReplace
|
25
|
+
findAndReplace,
|
26
|
+
deleteWord
|
26
27
|
} from './shared/utils/segmentOperations'
|
27
28
|
import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
|
28
29
|
import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
|
@@ -79,6 +80,7 @@ interface MemoizedTranscriptionViewProps {
|
|
79
80
|
currentTime: number
|
80
81
|
anchors: AnchorSequence[]
|
81
82
|
disableHighlighting: boolean
|
83
|
+
onDataChange?: (updatedData: CorrectionData) => void
|
82
84
|
}
|
83
85
|
|
84
86
|
// Create a memoized TranscriptionView component
|
@@ -93,7 +95,8 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
93
95
|
onPlaySegment,
|
94
96
|
currentTime,
|
95
97
|
anchors,
|
96
|
-
disableHighlighting
|
98
|
+
disableHighlighting,
|
99
|
+
onDataChange
|
97
100
|
}: MemoizedTranscriptionViewProps) {
|
98
101
|
return (
|
99
102
|
<TranscriptionView
|
@@ -107,6 +110,7 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
107
110
|
onPlaySegment={onPlaySegment}
|
108
111
|
currentTime={disableHighlighting ? undefined : currentTime}
|
109
112
|
anchors={anchors}
|
113
|
+
onDataChange={onDataChange}
|
110
114
|
/>
|
111
115
|
);
|
112
116
|
});
|
@@ -237,6 +241,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
237
241
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
238
242
|
const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
|
239
243
|
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
244
|
+
const [isCtrlPressed, setIsCtrlPressed] = useState(false)
|
240
245
|
const [editModalSegment, setEditModalSegment] = useState<{
|
241
246
|
segment: LyricsSegment
|
242
247
|
index: number
|
@@ -302,8 +307,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
302
307
|
})
|
303
308
|
}
|
304
309
|
|
305
|
-
const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
|
310
|
+
const { handleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
|
306
311
|
setIsShiftPressed,
|
312
|
+
setIsCtrlPressed
|
307
313
|
})
|
308
314
|
|
309
315
|
// Always add keyboard listeners
|
@@ -316,6 +322,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
316
322
|
// Reset modifier states when a modal opens
|
317
323
|
if (isAnyModalOpen) {
|
318
324
|
setIsShiftPressed(false)
|
325
|
+
setIsCtrlPressed(false)
|
319
326
|
}
|
320
327
|
|
321
328
|
// Cleanup function
|
@@ -326,8 +333,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
326
333
|
window.removeEventListener('keydown', handleKeyDown)
|
327
334
|
window.removeEventListener('keyup', handleKeyUp)
|
328
335
|
document.body.style.userSelect = ''
|
336
|
+
// Call the cleanup function to remove window blur/focus listeners
|
337
|
+
cleanup()
|
329
338
|
}
|
330
|
-
}, [setIsShiftPressed, isAnyModalOpen])
|
339
|
+
}, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen])
|
331
340
|
|
332
341
|
// Update modal state tracking
|
333
342
|
useEffect(() => {
|
@@ -343,7 +352,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
343
352
|
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
|
344
353
|
|
345
354
|
// Calculate effective mode based on modifier key states
|
346
|
-
const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
|
355
|
+
const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
|
347
356
|
|
348
357
|
const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
|
349
358
|
setFlashingType(null)
|
@@ -368,6 +377,16 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
368
377
|
console.log('LyricsAnalyzer handleWordClick:', { info });
|
369
378
|
}
|
370
379
|
|
380
|
+
if (effectiveMode === 'delete_word') {
|
381
|
+
// Use the shared deleteWord utility function
|
382
|
+
const newData = deleteWord(data, info.word_id);
|
383
|
+
setData(newData);
|
384
|
+
|
385
|
+
// Flash to indicate the word was deleted
|
386
|
+
handleFlash('word');
|
387
|
+
return;
|
388
|
+
}
|
389
|
+
|
371
390
|
if (effectiveMode === 'highlight') {
|
372
391
|
// Find if this word is part of a correction
|
373
392
|
const correction = data.corrections?.find(c =>
|
@@ -492,7 +511,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
492
511
|
});
|
493
512
|
}
|
494
513
|
}
|
495
|
-
}, [data, effectiveMode, setModalContent]);
|
514
|
+
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
|
496
515
|
|
497
516
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
498
517
|
if (!editModalSegment) return
|
@@ -907,6 +926,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
907
926
|
currentTime={currentAudioTime}
|
908
927
|
anchors={data.anchor_sequences}
|
909
928
|
disableHighlighting={isAnyModalOpenMemo}
|
929
|
+
onDataChange={(updatedData) => {
|
930
|
+
setData(updatedData)
|
931
|
+
}}
|
910
932
|
/>
|
911
933
|
{!isReadOnly && apiClient && (
|
912
934
|
<Box sx={{
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { ToggleButton, ToggleButtonGroup, Box, Typography } from '@mui/material';
|
1
|
+
import { ToggleButton, ToggleButtonGroup, Box, Typography, Tooltip } from '@mui/material';
|
2
2
|
import HighlightIcon from '@mui/icons-material/Highlight';
|
3
3
|
import EditIcon from '@mui/icons-material/Edit';
|
4
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
4
5
|
import { InteractionMode } from '../types';
|
5
6
|
|
6
7
|
interface ModeSelectorProps {
|
@@ -17,7 +18,7 @@ export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorPr
|
|
17
18
|
<ToggleButtonGroup
|
18
19
|
value={effectiveMode}
|
19
20
|
exclusive
|
20
|
-
onChange={(_, newMode) => newMode && onChange(newMode)}
|
21
|
+
onChange={(_, newMode) => newMode === 'edit' && onChange(newMode)}
|
21
22
|
size="small"
|
22
23
|
sx={{
|
23
24
|
height: '32px',
|
@@ -28,20 +29,38 @@ export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorPr
|
|
28
29
|
}
|
29
30
|
}}
|
30
31
|
>
|
31
|
-
<
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
32
|
+
<Tooltip title="Default mode; click words to edit that lyrics segment">
|
33
|
+
<ToggleButton
|
34
|
+
value="edit"
|
35
|
+
>
|
36
|
+
<EditIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
|
37
|
+
Edit
|
38
|
+
</ToggleButton>
|
39
|
+
</Tooltip>
|
40
|
+
|
41
|
+
<Tooltip title="Hold SHIFT and click words to highlight the matching anchor sequence in the reference lyrics">
|
42
|
+
<span>
|
43
|
+
<ToggleButton
|
44
|
+
value="highlight"
|
45
|
+
disabled
|
46
|
+
>
|
47
|
+
<HighlightIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
|
48
|
+
Highlight
|
49
|
+
</ToggleButton>
|
50
|
+
</span>
|
51
|
+
</Tooltip>
|
52
|
+
|
53
|
+
<Tooltip title="Hold CTRL and click words to delete them">
|
54
|
+
<span>
|
55
|
+
<ToggleButton
|
56
|
+
value="delete_word"
|
57
|
+
disabled
|
58
|
+
>
|
59
|
+
<DeleteIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
|
60
|
+
Delete
|
61
|
+
</ToggleButton>
|
62
|
+
</span>
|
63
|
+
</Tooltip>
|
45
64
|
</ToggleButtonGroup>
|
46
65
|
</Box>
|
47
66
|
);
|
@@ -6,6 +6,8 @@ import { styled } from '@mui/material/styles'
|
|
6
6
|
import SegmentDetailsModal from './SegmentDetailsModal'
|
7
7
|
import { TranscriptionWordPosition } from './shared/types'
|
8
8
|
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
|
9
|
+
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
|
10
|
+
import { deleteSegment } from './shared/utils/segmentOperations'
|
9
11
|
|
10
12
|
const SegmentIndex = styled(Typography)(({ theme }) => ({
|
11
13
|
color: theme.palette.text.secondary,
|
@@ -48,10 +50,18 @@ export default function TranscriptionView({
|
|
48
50
|
mode,
|
49
51
|
onPlaySegment,
|
50
52
|
currentTime = 0,
|
51
|
-
anchors = []
|
53
|
+
anchors = [],
|
54
|
+
onDataChange
|
52
55
|
}: TranscriptionViewProps) {
|
53
56
|
const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
|
54
57
|
|
58
|
+
const handleDeleteSegment = (segmentIndex: number) => {
|
59
|
+
if (onDataChange) {
|
60
|
+
const updatedData = deleteSegment(data, segmentIndex)
|
61
|
+
onDataChange(updatedData)
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
55
65
|
return (
|
56
66
|
<Paper sx={{ p: 0.8 }}>
|
57
67
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
@@ -125,6 +135,20 @@ export default function TranscriptionView({
|
|
125
135
|
>
|
126
136
|
{segmentIndex}
|
127
137
|
</SegmentIndex>
|
138
|
+
<IconButton
|
139
|
+
size="small"
|
140
|
+
onClick={() => handleDeleteSegment(segmentIndex)}
|
141
|
+
sx={{
|
142
|
+
padding: '1px',
|
143
|
+
height: '18px',
|
144
|
+
width: '18px',
|
145
|
+
minHeight: '18px',
|
146
|
+
minWidth: '18px'
|
147
|
+
}}
|
148
|
+
title="Delete segment"
|
149
|
+
>
|
150
|
+
<DeleteOutlineIcon sx={{ fontSize: '0.9rem', color: 'error.main' }} />
|
151
|
+
</IconButton>
|
128
152
|
{segment.start_time !== null && (
|
129
153
|
<IconButton
|
130
154
|
size="small"
|
@@ -136,6 +160,7 @@ export default function TranscriptionView({
|
|
136
160
|
minHeight: '18px',
|
137
161
|
minWidth: '18px'
|
138
162
|
}}
|
163
|
+
title="Play segment"
|
139
164
|
>
|
140
165
|
<PlayCircleOutlineIcon sx={{ fontSize: '0.9rem' }} />
|
141
166
|
</IconButton>
|
@@ -97,7 +97,7 @@ export function useWordClick({
|
|
97
97
|
}
|
98
98
|
}
|
99
99
|
|
100
|
-
if (mode === 'highlight' || mode === 'edit') {
|
100
|
+
if (mode === 'highlight' || mode === 'edit' || mode === 'delete_word') {
|
101
101
|
if (belongsToAnchor && anchor) {
|
102
102
|
onWordClick?.({
|
103
103
|
word_id: wordId,
|
@@ -131,7 +131,8 @@ export function useWordClick({
|
|
131
131
|
gap: undefined
|
132
132
|
})
|
133
133
|
}
|
134
|
-
} else
|
134
|
+
} else {
|
135
|
+
// This is a fallback for any future modes
|
135
136
|
if (belongsToAnchor && anchor) {
|
136
137
|
onElementClick({
|
137
138
|
type: 'anchor',
|
@@ -83,6 +83,7 @@ export interface TranscriptionViewProps {
|
|
83
83
|
currentTime?: number
|
84
84
|
anchors?: AnchorSequence[]
|
85
85
|
flashingHandler?: string | null
|
86
|
+
onDataChange?: (updatedData: CorrectionData) => void
|
86
87
|
}
|
87
88
|
|
88
89
|
// Add LinePosition type here since it's used in multiple places
|
@@ -35,6 +35,16 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
35
35
|
console.log(`Setting up keyboard handlers [${handlerId}]`)
|
36
36
|
}
|
37
37
|
|
38
|
+
// Function to reset modifier key states
|
39
|
+
const resetModifierStates = () => {
|
40
|
+
if (debugLog) {
|
41
|
+
console.log(`Resetting modifier states [${handlerId}]`)
|
42
|
+
}
|
43
|
+
state.setIsShiftPressed(false)
|
44
|
+
state.setIsCtrlPressed?.(false)
|
45
|
+
document.body.style.userSelect = ''
|
46
|
+
}
|
47
|
+
|
38
48
|
const handleKeyDown = (e: KeyboardEvent) => {
|
39
49
|
if (debugLog) {
|
40
50
|
console.log(`Keyboard event captured [${handlerId}]`, {
|
@@ -59,7 +69,7 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
59
69
|
if (e.key === 'Shift') {
|
60
70
|
state.setIsShiftPressed(true)
|
61
71
|
document.body.style.userSelect = 'none'
|
62
|
-
} else if (e.key === 'Meta') {
|
72
|
+
} else if (e.key === 'Control' || e.key === 'Ctrl' || e.key === 'Meta') {
|
63
73
|
state.setIsCtrlPressed?.(true)
|
64
74
|
} else if (e.key === ' ' || e.code === 'Space') {
|
65
75
|
if (debugLog) {
|
@@ -102,12 +112,11 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
102
112
|
})
|
103
113
|
}
|
104
114
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
} else if (e.key === ' ' || e.code === 'Space') {
|
115
|
+
// Always reset the modifier states regardless of the key which was released
|
116
|
+
// to help prevent accidentally getting stuck in a mode or accidentally deleting words
|
117
|
+
resetModifierStates()
|
118
|
+
|
119
|
+
if (e.key === ' ' || e.code === 'Space') {
|
111
120
|
if (debugLog) {
|
112
121
|
console.log('Keyboard handler - Spacebar released', {
|
113
122
|
modalOpen: isModalOpen,
|
@@ -128,7 +137,35 @@ export const setupKeyboardHandlers = (state: KeyboardState) => {
|
|
128
137
|
}
|
129
138
|
}
|
130
139
|
|
131
|
-
|
140
|
+
// Handle window blur event (user switches tabs or apps)
|
141
|
+
const handleWindowBlur = () => {
|
142
|
+
if (debugLog) {
|
143
|
+
console.log(`Window blur detected [${handlerId}], resetting modifier states`)
|
144
|
+
}
|
145
|
+
resetModifierStates()
|
146
|
+
}
|
147
|
+
|
148
|
+
// Handle window focus event (user returns to the app)
|
149
|
+
const handleWindowFocus = () => {
|
150
|
+
if (debugLog) {
|
151
|
+
console.log(`Window focus detected [${handlerId}], ensuring modifier states are reset`)
|
152
|
+
}
|
153
|
+
resetModifierStates()
|
154
|
+
}
|
155
|
+
|
156
|
+
// Add window event listeners
|
157
|
+
window.addEventListener('blur', handleWindowBlur)
|
158
|
+
window.addEventListener('focus', handleWindowFocus)
|
159
|
+
|
160
|
+
// Return a cleanup function that includes removing the window event listeners
|
161
|
+
return {
|
162
|
+
handleKeyDown,
|
163
|
+
handleKeyUp,
|
164
|
+
cleanup: () => {
|
165
|
+
window.removeEventListener('blur', handleWindowBlur)
|
166
|
+
window.removeEventListener('focus', handleWindowFocus)
|
167
|
+
}
|
168
|
+
}
|
132
169
|
}
|
133
170
|
|
134
171
|
// Export these for external use
|