lyrics-transcriber 0.47.0__py3-none-any.whl → 0.49.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-2vK-qVJS.js → index-BpvPgWoc.js} +603 -442
- lyrics_transcriber/frontend/dist/assets/index-BpvPgWoc.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +55 -5
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +232 -47
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +26 -1
- lyrics_transcriber/frontend/src/components/shared/types.ts +1 -0
- {lyrics_transcriber-0.47.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.47.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/RECORD +12 -12
- {lyrics_transcriber-0.47.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-2vK-qVJS.js.map +0 -1
- {lyrics_transcriber-0.47.0.dist-info → lyrics_transcriber-0.49.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.47.0.dist-info → lyrics_transcriber-0.49.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-BpvPgWoc.js"></script>
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="root"></div>
|
@@ -1,8 +1,10 @@
|
|
1
|
-
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper } from '@mui/material'
|
1
|
+
import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper, IconButton } from '@mui/material'
|
2
2
|
import LockIcon from '@mui/icons-material/Lock'
|
3
3
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
4
4
|
import FindReplaceIcon from '@mui/icons-material/FindReplace'
|
5
5
|
import EditIcon from '@mui/icons-material/Edit'
|
6
|
+
import UndoIcon from '@mui/icons-material/Undo'
|
7
|
+
import RedoIcon from '@mui/icons-material/Redo'
|
6
8
|
import { CorrectionData, InteractionMode } from '../types'
|
7
9
|
import CorrectionMetrics from './CorrectionMetrics'
|
8
10
|
import ModeSelector from './ModeSelector'
|
@@ -29,6 +31,10 @@ interface HeaderProps {
|
|
29
31
|
onHandlerClick?: (handler: string) => void
|
30
32
|
onFindReplace?: () => void
|
31
33
|
onEditAll?: () => void
|
34
|
+
onUndo: () => void
|
35
|
+
onRedo: () => void
|
36
|
+
canUndo: boolean
|
37
|
+
canRedo: boolean
|
32
38
|
}
|
33
39
|
|
34
40
|
export default function Header({
|
@@ -46,6 +52,10 @@ export default function Header({
|
|
46
52
|
onHandlerClick,
|
47
53
|
onFindReplace,
|
48
54
|
onEditAll,
|
55
|
+
onUndo,
|
56
|
+
onRedo,
|
57
|
+
canUndo,
|
58
|
+
canRedo,
|
49
59
|
}: HeaderProps) {
|
50
60
|
const theme = useTheme()
|
51
61
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
@@ -102,8 +112,8 @@ export default function Header({
|
|
102
112
|
</Box>
|
103
113
|
)}
|
104
114
|
|
105
|
-
<Box sx={{
|
106
|
-
display: 'flex',
|
115
|
+
<Box sx={{
|
116
|
+
display: 'flex',
|
107
117
|
flexDirection: isMobile ? 'column' : 'row',
|
108
118
|
gap: 1,
|
109
119
|
justifyContent: 'space-between',
|
@@ -220,8 +230,8 @@ export default function Header({
|
|
220
230
|
</Box>
|
221
231
|
|
222
232
|
<Paper sx={{ p: 0.8, mb: 1 }}>
|
223
|
-
<Box sx={{
|
224
|
-
display: 'flex',
|
233
|
+
<Box sx={{
|
234
|
+
display: 'flex',
|
225
235
|
flexDirection: isMobile ? 'column' : 'row',
|
226
236
|
gap: 1,
|
227
237
|
alignItems: isMobile ? 'flex-start' : 'center',
|
@@ -239,6 +249,46 @@ export default function Header({
|
|
239
249
|
effectiveMode={effectiveMode}
|
240
250
|
onChange={onModeChange}
|
241
251
|
/>
|
252
|
+
{!isReadOnly && (
|
253
|
+
<Box sx={{ display: 'flex', height: '32px' }}>
|
254
|
+
<Tooltip title="Undo (Cmd/Ctrl+Z)">
|
255
|
+
<span>
|
256
|
+
<IconButton
|
257
|
+
size="small"
|
258
|
+
onClick={onUndo}
|
259
|
+
disabled={!canUndo}
|
260
|
+
sx={{
|
261
|
+
border: `1px solid ${theme.palette.divider}`,
|
262
|
+
borderRadius: '4px',
|
263
|
+
mx: 0.25,
|
264
|
+
height: '32px',
|
265
|
+
width: '32px'
|
266
|
+
}}
|
267
|
+
>
|
268
|
+
<UndoIcon fontSize="small" />
|
269
|
+
</IconButton>
|
270
|
+
</span>
|
271
|
+
</Tooltip>
|
272
|
+
<Tooltip title="Redo (Cmd/Ctrl+Shift+Z)">
|
273
|
+
<span>
|
274
|
+
<IconButton
|
275
|
+
size="small"
|
276
|
+
onClick={onRedo}
|
277
|
+
disabled={!canRedo}
|
278
|
+
sx={{
|
279
|
+
border: `1px solid ${theme.palette.divider}`,
|
280
|
+
borderRadius: '4px',
|
281
|
+
mx: 0.25,
|
282
|
+
height: '32px',
|
283
|
+
width: '32px'
|
284
|
+
}}
|
285
|
+
>
|
286
|
+
<RedoIcon fontSize="small" />
|
287
|
+
</IconButton>
|
288
|
+
</span>
|
289
|
+
</Tooltip>
|
290
|
+
</Box>
|
291
|
+
)}
|
242
292
|
{!isReadOnly && (
|
243
293
|
<Button
|
244
294
|
variant="outlined"
|
@@ -20,7 +20,6 @@ import {
|
|
20
20
|
addSegmentBefore,
|
21
21
|
splitSegment,
|
22
22
|
deleteSegment,
|
23
|
-
updateSegment,
|
24
23
|
mergeSegment,
|
25
24
|
findAndReplace,
|
26
25
|
deleteWord
|
@@ -80,6 +79,7 @@ interface MemoizedTranscriptionViewProps {
|
|
80
79
|
currentTime: number
|
81
80
|
anchors: AnchorSequence[]
|
82
81
|
disableHighlighting: boolean
|
82
|
+
onDataChange?: (updatedData: CorrectionData) => void
|
83
83
|
}
|
84
84
|
|
85
85
|
// Create a memoized TranscriptionView component
|
@@ -94,7 +94,8 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
94
94
|
onPlaySegment,
|
95
95
|
currentTime,
|
96
96
|
anchors,
|
97
|
-
disableHighlighting
|
97
|
+
disableHighlighting,
|
98
|
+
onDataChange
|
98
99
|
}: MemoizedTranscriptionViewProps) {
|
99
100
|
return (
|
100
101
|
<TranscriptionView
|
@@ -108,6 +109,7 @@ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
108
109
|
onPlaySegment={onPlaySegment}
|
109
110
|
currentTime={disableHighlighting ? undefined : currentTime}
|
110
111
|
anchors={anchors}
|
112
|
+
onDataChange={onDataChange}
|
111
113
|
/>
|
112
114
|
);
|
113
115
|
});
|
@@ -183,6 +185,10 @@ interface MemoizedHeaderProps {
|
|
183
185
|
onAddLyrics?: () => void
|
184
186
|
onFindReplace?: () => void
|
185
187
|
onEditAll?: () => void
|
188
|
+
onUndo: () => void
|
189
|
+
onRedo: () => void
|
190
|
+
canUndo: boolean
|
191
|
+
canRedo: boolean
|
186
192
|
}
|
187
193
|
|
188
194
|
// Create a memoized Header component
|
@@ -200,7 +206,11 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
200
206
|
isUpdatingHandlers,
|
201
207
|
onHandlerClick,
|
202
208
|
onFindReplace,
|
203
|
-
onEditAll
|
209
|
+
onEditAll,
|
210
|
+
onUndo,
|
211
|
+
onRedo,
|
212
|
+
canUndo,
|
213
|
+
canRedo
|
204
214
|
}: MemoizedHeaderProps) {
|
205
215
|
return (
|
206
216
|
<Header
|
@@ -218,6 +228,10 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
218
228
|
onHandlerClick={onHandlerClick}
|
219
229
|
onFindReplace={onFindReplace}
|
220
230
|
onEditAll={onEditAll}
|
231
|
+
onUndo={onUndo}
|
232
|
+
onRedo={onRedo}
|
233
|
+
canUndo={canUndo}
|
234
|
+
canRedo={canRedo}
|
221
235
|
/>
|
222
236
|
);
|
223
237
|
});
|
@@ -234,7 +248,6 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
234
248
|
return availableSources.length > 0 ? availableSources[0] : ''
|
235
249
|
})
|
236
250
|
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
237
|
-
const [data, setData] = useState(initialData)
|
238
251
|
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
239
252
|
const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
|
240
253
|
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
@@ -260,6 +273,35 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
260
273
|
const theme = useTheme()
|
261
274
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
262
275
|
|
276
|
+
// State history for Undo/Redo
|
277
|
+
const [history, setHistory] = useState<CorrectionData[]>([initialData])
|
278
|
+
const [historyIndex, setHistoryIndex] = useState(0)
|
279
|
+
|
280
|
+
// Derived state: the current data based on history index
|
281
|
+
const data = history[historyIndex];
|
282
|
+
|
283
|
+
// Function to update data and manage history
|
284
|
+
const updateDataWithHistory = useCallback((newData: CorrectionData, actionDescription?: string) => {
|
285
|
+
if (debugLog) {
|
286
|
+
console.log(`[DEBUG] updateDataWithHistory: Action - ${actionDescription || 'Unknown'}. Current index: ${historyIndex}, History length: ${history.length}`);
|
287
|
+
}
|
288
|
+
const newHistory = history.slice(0, historyIndex + 1)
|
289
|
+
const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
|
290
|
+
|
291
|
+
newHistory.push(deepCopiedNewData)
|
292
|
+
setHistory(newHistory)
|
293
|
+
setHistoryIndex(newHistory.length - 1)
|
294
|
+
if (debugLog) {
|
295
|
+
console.log(`[DEBUG] updateDataWithHistory: History updated. New index: ${newHistory.length - 1}, New length: ${newHistory.length}`);
|
296
|
+
}
|
297
|
+
}, [history, historyIndex])
|
298
|
+
|
299
|
+
// Reset history when initial data changes (e.g., new file loaded)
|
300
|
+
useEffect(() => {
|
301
|
+
setHistory([initialData])
|
302
|
+
setHistoryIndex(0)
|
303
|
+
}, [initialData])
|
304
|
+
|
263
305
|
// Update debug logging to use new ID-based structure
|
264
306
|
useEffect(() => {
|
265
307
|
if (debugLog) {
|
@@ -282,16 +324,18 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
282
324
|
useEffect(() => {
|
283
325
|
const savedData = loadSavedData(initialData)
|
284
326
|
if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
|
285
|
-
|
327
|
+
// Replace history with saved data as the initial state
|
328
|
+
setHistory([savedData])
|
329
|
+
setHistoryIndex(0)
|
286
330
|
}
|
287
|
-
}, [initialData])
|
331
|
+
}, [initialData]) // Keep dependency only on initialData
|
288
332
|
|
289
|
-
// Save data
|
333
|
+
// Save data - This should save the *current* state, not affect history
|
290
334
|
useEffect(() => {
|
291
335
|
if (!isReadOnly) {
|
292
|
-
saveData(data, initialData)
|
336
|
+
saveData(data, initialData) // Use 'data' derived from history and the initialData prop
|
293
337
|
}
|
294
|
-
}, [data, isReadOnly, initialData])
|
338
|
+
}, [data, isReadOnly, initialData]) // Correct dependencies
|
295
339
|
|
296
340
|
// Keyboard handlers
|
297
341
|
useEffect(() => {
|
@@ -377,8 +421,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
377
421
|
if (effectiveMode === 'delete_word') {
|
378
422
|
// Use the shared deleteWord utility function
|
379
423
|
const newData = deleteWord(data, info.word_id);
|
380
|
-
|
381
|
-
|
424
|
+
updateDataWithHistory(newData, 'delete word'); // Update history
|
425
|
+
|
382
426
|
// Flash to indicate the word was deleted
|
383
427
|
handleFlash('word');
|
384
428
|
return;
|
@@ -508,19 +552,47 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
508
552
|
});
|
509
553
|
}
|
510
554
|
}
|
511
|
-
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
|
555
|
+
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
|
512
556
|
|
513
557
|
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
514
558
|
if (!editModalSegment) return
|
515
|
-
|
516
|
-
|
559
|
+
|
560
|
+
if (debugLog) {
|
561
|
+
console.log('[DEBUG] handleUpdateSegment: Updating history from modal save', {
|
562
|
+
segmentIndex: editModalSegment.index,
|
563
|
+
currentHistoryIndex: historyIndex,
|
564
|
+
currentHistoryLength: history.length,
|
565
|
+
currentSegmentText: history[historyIndex]?.corrected_segments[editModalSegment.index]?.text,
|
566
|
+
updatedSegmentText: updatedSegment.text
|
567
|
+
});
|
568
|
+
}
|
569
|
+
|
570
|
+
// --- Ensure Immutability Here ---
|
571
|
+
const currentData = history[historyIndex];
|
572
|
+
const newSegments = currentData.corrected_segments.map((segment, i) =>
|
573
|
+
i === editModalSegment.index ? updatedSegment : segment
|
574
|
+
);
|
575
|
+
const newDataImmutable: CorrectionData = {
|
576
|
+
...currentData,
|
577
|
+
corrected_segments: newSegments,
|
578
|
+
};
|
579
|
+
// --- End Immutability Ensure ---
|
580
|
+
|
581
|
+
updateDataWithHistory(newDataImmutable, 'update segment');
|
582
|
+
|
583
|
+
if (debugLog) {
|
584
|
+
console.log('[DEBUG] handleUpdateSegment: History updated (async)', {
|
585
|
+
newHistoryIndex: historyIndex + 1,
|
586
|
+
newHistoryLength: history.length - historyIndex === 1 ? history.length + 1 : historyIndex + 2
|
587
|
+
});
|
588
|
+
}
|
517
589
|
setEditModalSegment(null)
|
518
|
-
}, [
|
590
|
+
}, [history, historyIndex, editModalSegment, updateDataWithHistory])
|
519
591
|
|
520
592
|
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
521
593
|
const newData = deleteSegment(data, segmentIndex)
|
522
|
-
|
523
|
-
}, [data])
|
594
|
+
updateDataWithHistory(newData, 'delete segment')
|
595
|
+
}, [data, updateDataWithHistory])
|
524
596
|
|
525
597
|
const handleFinishReview = useCallback(() => {
|
526
598
|
setIsReviewModalOpen(true)
|
@@ -556,7 +628,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
556
628
|
const handleResetCorrections = useCallback(() => {
|
557
629
|
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
558
630
|
clearSavedData(initialData)
|
559
|
-
|
631
|
+
// Reset history to the original initial data
|
632
|
+
setHistory([JSON.parse(JSON.stringify(initialData))])
|
633
|
+
setHistoryIndex(0)
|
560
634
|
setModalContent(null)
|
561
635
|
setFlashingType(null)
|
562
636
|
setHighlightInfo(null)
|
@@ -566,22 +640,22 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
566
640
|
|
567
641
|
const handleAddSegment = useCallback((beforeIndex: number) => {
|
568
642
|
const newData = addSegmentBefore(data, beforeIndex)
|
569
|
-
|
570
|
-
}, [data])
|
643
|
+
updateDataWithHistory(newData, 'add segment')
|
644
|
+
}, [data, updateDataWithHistory])
|
571
645
|
|
572
646
|
const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
|
573
647
|
const newData = splitSegment(data, segmentIndex, afterWordIndex)
|
574
648
|
if (newData) {
|
575
|
-
|
649
|
+
updateDataWithHistory(newData, 'split segment')
|
576
650
|
setEditModalSegment(null)
|
577
651
|
}
|
578
|
-
}, [data])
|
652
|
+
}, [data, updateDataWithHistory])
|
579
653
|
|
580
654
|
const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
|
581
655
|
const newData = mergeSegment(data, segmentIndex, mergeWithNext)
|
582
|
-
|
656
|
+
updateDataWithHistory(newData, 'merge segment')
|
583
657
|
setEditModalSegment(null)
|
584
|
-
}, [data])
|
658
|
+
}, [data, updateDataWithHistory])
|
585
659
|
|
586
660
|
const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
|
587
661
|
if (!apiClient) return
|
@@ -603,7 +677,8 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
603
677
|
const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
|
604
678
|
|
605
679
|
// Update local state with new correction data
|
606
|
-
|
680
|
+
// This API call returns the *entire* new state, so treat it as a single history step
|
681
|
+
updateDataWithHistory(newData, `toggle handler ${handler}`); // Update history
|
607
682
|
|
608
683
|
// Clear any existing modals or highlights
|
609
684
|
setModalContent(null)
|
@@ -618,7 +693,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
618
693
|
} finally {
|
619
694
|
setIsUpdatingHandlers(false);
|
620
695
|
}
|
621
|
-
}, [apiClient, data.metadata.enabled_handlers, handleFlash])
|
696
|
+
}, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory])
|
622
697
|
|
623
698
|
const handleHandlerClick = useCallback((handler: string) => {
|
624
699
|
if (debugLog) {
|
@@ -658,21 +733,22 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
658
733
|
try {
|
659
734
|
setIsAddingLyrics(true)
|
660
735
|
const newData = await apiClient.addLyrics(source, lyrics)
|
661
|
-
|
736
|
+
// This API call returns the *entire* new state
|
737
|
+
updateDataWithHistory(newData, 'add lyrics'); // Update history
|
662
738
|
} finally {
|
663
739
|
setIsAddingLyrics(false)
|
664
740
|
}
|
665
|
-
}, [apiClient])
|
741
|
+
}, [apiClient, updateDataWithHistory])
|
666
742
|
|
667
743
|
const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
|
668
744
|
const newData = findAndReplace(data, findText, replaceText, options)
|
669
|
-
|
745
|
+
updateDataWithHistory(newData, 'find/replace'); // Update history
|
670
746
|
}
|
671
747
|
|
672
748
|
// Add handler for Edit All functionality
|
673
749
|
const handleEditAll = useCallback(() => {
|
674
750
|
console.log('EditAll - Starting process');
|
675
|
-
|
751
|
+
|
676
752
|
// Create empty placeholder segments to prevent the modal from closing
|
677
753
|
const placeholderSegment: LyricsSegment = {
|
678
754
|
id: 'loading-placeholder',
|
@@ -681,31 +757,31 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
681
757
|
start_time: 0,
|
682
758
|
end_time: 1
|
683
759
|
};
|
684
|
-
|
760
|
+
|
685
761
|
// Set placeholder segments first
|
686
762
|
setGlobalEditSegment(placeholderSegment);
|
687
763
|
setOriginalGlobalSegment(placeholderSegment);
|
688
|
-
|
764
|
+
|
689
765
|
// Show loading state
|
690
766
|
setIsLoadingGlobalEdit(true);
|
691
767
|
console.log('EditAll - Set loading state to true');
|
692
|
-
|
768
|
+
|
693
769
|
// Open the modal with placeholder data
|
694
770
|
setIsEditAllModalOpen(true);
|
695
771
|
console.log('EditAll - Set modal open to true');
|
696
|
-
|
772
|
+
|
697
773
|
// Use requestAnimationFrame to ensure the modal with loading state is rendered
|
698
774
|
// before doing the expensive operation
|
699
775
|
requestAnimationFrame(() => {
|
700
776
|
console.log('EditAll - Inside requestAnimationFrame');
|
701
|
-
|
777
|
+
|
702
778
|
// Use setTimeout to allow the modal to render before doing the expensive operation
|
703
779
|
setTimeout(() => {
|
704
780
|
console.log('EditAll - Inside setTimeout, starting data processing');
|
705
|
-
|
781
|
+
|
706
782
|
try {
|
707
783
|
console.time('EditAll - Data processing');
|
708
|
-
|
784
|
+
|
709
785
|
// Create a combined segment with all words from all segments
|
710
786
|
const allWords = data.corrected_segments.flatMap(segment => segment.words)
|
711
787
|
console.log(`EditAll - Collected ${allWords.length} words from all segments`);
|
@@ -731,18 +807,18 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
731
807
|
// Store the original global segment for reset functionality
|
732
808
|
setGlobalEditSegment(globalSegment)
|
733
809
|
console.log('EditAll - Set global edit segment');
|
734
|
-
|
810
|
+
|
735
811
|
setOriginalGlobalSegment(JSON.parse(JSON.stringify(globalSegment)))
|
736
812
|
console.log('EditAll - Set original global segment');
|
737
|
-
|
813
|
+
|
738
814
|
// Create the original transcribed global segment for Un-Correct functionality
|
739
815
|
if (originalData.original_segments) {
|
740
816
|
console.log('EditAll - Processing original segments for Un-Correct functionality');
|
741
|
-
|
817
|
+
|
742
818
|
// Get all words from original segments
|
743
819
|
const originalWords = originalData.original_segments.flatMap((segment: LyricsSegment) => segment.words)
|
744
820
|
console.log(`EditAll - Collected ${originalWords.length} words from original segments`);
|
745
|
-
|
821
|
+
|
746
822
|
// Sort words by start time
|
747
823
|
const sortedOriginalWords = [...originalWords].sort((a, b) => {
|
748
824
|
const aTime = a.start_time ?? 0
|
@@ -750,7 +826,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
750
826
|
return aTime - bTime
|
751
827
|
})
|
752
828
|
console.log('EditAll - Sorted original words by start time');
|
753
|
-
|
829
|
+
|
754
830
|
// Create the original transcribed global segment
|
755
831
|
const originalTranscribedGlobal: LyricsSegment = {
|
756
832
|
id: 'original-transcribed-global',
|
@@ -760,14 +836,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
760
836
|
end_time: sortedOriginalWords[sortedOriginalWords.length - 1]?.end_time ?? null
|
761
837
|
}
|
762
838
|
console.log('EditAll - Created original transcribed global segment');
|
763
|
-
|
839
|
+
|
764
840
|
setOriginalTranscribedGlobalSegment(originalTranscribedGlobal)
|
765
841
|
console.log('EditAll - Set original transcribed global segment');
|
766
842
|
} else {
|
767
843
|
setOriginalTranscribedGlobalSegment(null)
|
768
844
|
console.log('EditAll - No original segments found, set original transcribed global segment to null');
|
769
845
|
}
|
770
|
-
|
846
|
+
|
771
847
|
console.timeEnd('EditAll - Data processing');
|
772
848
|
} catch (error) {
|
773
849
|
console.error('Error preparing global edit data:', error);
|
@@ -864,15 +940,49 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
864
940
|
})
|
865
941
|
|
866
942
|
// Update the data with the new segments
|
867
|
-
|
943
|
+
const newData = {
|
868
944
|
...data,
|
869
945
|
corrected_segments: updatedSegments
|
870
|
-
}
|
946
|
+
};
|
947
|
+
updateDataWithHistory(newData, 'edit all'); // Update history
|
871
948
|
|
872
949
|
// Close the modal
|
873
950
|
setIsEditAllModalOpen(false)
|
874
951
|
setGlobalEditSegment(null)
|
875
|
-
}, [data])
|
952
|
+
}, [data, updateDataWithHistory])
|
953
|
+
|
954
|
+
// Undo/Redo handlers
|
955
|
+
const handleUndo = useCallback(() => {
|
956
|
+
if (historyIndex > 0) {
|
957
|
+
const newIndex = historyIndex - 1;
|
958
|
+
if (debugLog) {
|
959
|
+
console.log(`[DEBUG] Undo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
960
|
+
}
|
961
|
+
setHistoryIndex(newIndex);
|
962
|
+
} else {
|
963
|
+
if (debugLog) {
|
964
|
+
console.log(`[DEBUG] Undo: already at the beginning (index ${historyIndex})`);
|
965
|
+
}
|
966
|
+
}
|
967
|
+
}, [historyIndex, history])
|
968
|
+
|
969
|
+
const handleRedo = useCallback(() => {
|
970
|
+
if (historyIndex < history.length - 1) {
|
971
|
+
const newIndex = historyIndex + 1;
|
972
|
+
if (debugLog) {
|
973
|
+
console.log(`[DEBUG] Redo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
974
|
+
}
|
975
|
+
setHistoryIndex(newIndex);
|
976
|
+
} else {
|
977
|
+
if (debugLog) {
|
978
|
+
console.log(`[DEBUG] Redo: already at the end (index ${historyIndex}, history length ${history.length})`);
|
979
|
+
}
|
980
|
+
}
|
981
|
+
}, [historyIndex, history])
|
982
|
+
|
983
|
+
// Determine if Undo/Redo is possible
|
984
|
+
const canUndo = historyIndex > 0
|
985
|
+
const canRedo = historyIndex < history.length - 1
|
876
986
|
|
877
987
|
// Memoize the metric click handlers
|
878
988
|
const metricClickHandlers = useMemo(() => ({
|
@@ -884,6 +994,72 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
884
994
|
// Determine if any modal is open to disable highlighting
|
885
995
|
const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
|
886
996
|
|
997
|
+
// Update keyboard handlers to include Undo/Redo shortcuts (Cmd/Ctrl + Z, Cmd/Ctrl + Shift + Z)
|
998
|
+
useEffect(() => {
|
999
|
+
const { currentModalHandler } = getModalState()
|
1000
|
+
|
1001
|
+
if (debugLog) {
|
1002
|
+
console.log('LyricsAnalyzer - Setting up keyboard effect (incl. Undo/Redo)', {
|
1003
|
+
isAnyModalOpen,
|
1004
|
+
hasSpacebarHandler: !!currentModalHandler
|
1005
|
+
})
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
const { handleKeyDown: baseHandleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
|
1009
|
+
setIsShiftPressed,
|
1010
|
+
setIsCtrlPressed
|
1011
|
+
})
|
1012
|
+
|
1013
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
1014
|
+
// Prevent Undo/Redo if a modal is open or input/textarea has focus
|
1015
|
+
const targetElement = e.target as HTMLElement;
|
1016
|
+
const isInputFocused = targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA';
|
1017
|
+
|
1018
|
+
if (!isAnyModalOpen && !isInputFocused) {
|
1019
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
1020
|
+
const modifierKey = isMac ? e.metaKey : e.ctrlKey;
|
1021
|
+
|
1022
|
+
if (modifierKey && e.key.toLowerCase() === 'z') {
|
1023
|
+
e.preventDefault();
|
1024
|
+
if (e.shiftKey) {
|
1025
|
+
if (canRedo) handleRedo();
|
1026
|
+
} else {
|
1027
|
+
if (canUndo) handleUndo();
|
1028
|
+
}
|
1029
|
+
return; // Prevent base handler if we handled undo/redo
|
1030
|
+
}
|
1031
|
+
}
|
1032
|
+
|
1033
|
+
// Call original handler for other keys or when conditions not met
|
1034
|
+
baseHandleKeyDown(e);
|
1035
|
+
};
|
1036
|
+
|
1037
|
+
// Always add keyboard listeners
|
1038
|
+
if (debugLog) {
|
1039
|
+
console.log('LyricsAnalyzer - Adding keyboard event listeners (incl. Undo/Redo)')
|
1040
|
+
}
|
1041
|
+
window.addEventListener('keydown', handleKeyDown)
|
1042
|
+
window.addEventListener('keyup', handleKeyUp)
|
1043
|
+
|
1044
|
+
// Reset modifier states when a modal opens
|
1045
|
+
if (isAnyModalOpen) {
|
1046
|
+
setIsShiftPressed(false)
|
1047
|
+
setIsCtrlPressed(false)
|
1048
|
+
}
|
1049
|
+
|
1050
|
+
// Cleanup function
|
1051
|
+
return () => {
|
1052
|
+
if (debugLog) {
|
1053
|
+
console.log('LyricsAnalyzer - Cleanup effect running (incl. Undo/Redo)')
|
1054
|
+
}
|
1055
|
+
window.removeEventListener('keydown', handleKeyDown)
|
1056
|
+
window.removeEventListener('keyup', handleKeyUp)
|
1057
|
+
document.body.style.userSelect = ''
|
1058
|
+
// Call the cleanup function to remove window blur/focus listeners
|
1059
|
+
cleanup()
|
1060
|
+
}
|
1061
|
+
}, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen, handleUndo, handleRedo, canUndo, canRedo]);
|
1062
|
+
|
887
1063
|
return (
|
888
1064
|
<Box sx={{
|
889
1065
|
p: 1,
|
@@ -907,6 +1083,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
907
1083
|
onAddLyrics={() => setIsAddLyricsModalOpen(true)}
|
908
1084
|
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
909
1085
|
onEditAll={handleEditAll}
|
1086
|
+
onUndo={handleUndo}
|
1087
|
+
onRedo={handleRedo}
|
1088
|
+
canUndo={canUndo}
|
1089
|
+
canRedo={canRedo}
|
910
1090
|
/>
|
911
1091
|
|
912
1092
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
@@ -923,6 +1103,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
923
1103
|
currentTime={currentAudioTime}
|
924
1104
|
anchors={data.anchor_sequences}
|
925
1105
|
disableHighlighting={isAnyModalOpenMemo}
|
1106
|
+
onDataChange={(updatedData) => {
|
1107
|
+
// Direct data change from TranscriptionView (e.g., drag-and-drop)
|
1108
|
+
// needs to update history
|
1109
|
+
updateDataWithHistory(updatedData, 'direct data change');
|
1110
|
+
}}
|
926
1111
|
/>
|
927
1112
|
{!isReadOnly && apiClient && (
|
928
1113
|
<Box sx={{
|