lyrics-transcriber 0.48.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.
@@ -35962,6 +35962,12 @@ const PauseIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35962
35962
  const PlayArrowIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35963
35963
  d: "M8 5v14l11-7z"
35964
35964
  }), "PlayArrow");
35965
+ const RedoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35966
+ d: "M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7z"
35967
+ }), "Redo");
35968
+ const UndoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35969
+ d: "M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8"
35970
+ }), "Undo");
35965
35971
  const normalizeWordForComparison = (word) => ({
35966
35972
  text: word.text,
35967
35973
  start_time: word.start_time ?? 0,
@@ -36546,7 +36552,11 @@ function Header({
36546
36552
  isUpdatingHandlers,
36547
36553
  onHandlerClick,
36548
36554
  onFindReplace,
36549
- onEditAll
36555
+ onEditAll,
36556
+ onUndo,
36557
+ onRedo,
36558
+ canUndo,
36559
+ canRedo
36550
36560
  }) {
36551
36561
  var _a, _b, _c;
36552
36562
  const theme2 = useTheme();
@@ -36711,6 +36721,40 @@ function Header({
36711
36721
  onChange: onModeChange
36712
36722
  }
36713
36723
  ),
36724
+ !isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", height: "32px" }, children: [
36725
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Undo (Cmd/Ctrl+Z)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
36726
+ IconButton,
36727
+ {
36728
+ size: "small",
36729
+ onClick: onUndo,
36730
+ disabled: !canUndo,
36731
+ sx: {
36732
+ border: `1px solid ${theme2.palette.divider}`,
36733
+ borderRadius: "4px",
36734
+ mx: 0.25,
36735
+ height: "32px",
36736
+ width: "32px"
36737
+ },
36738
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(UndoIcon, { fontSize: "small" })
36739
+ }
36740
+ ) }) }),
36741
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Redo (Cmd/Ctrl+Shift+Z)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
36742
+ IconButton,
36743
+ {
36744
+ size: "small",
36745
+ onClick: onRedo,
36746
+ disabled: !canRedo,
36747
+ sx: {
36748
+ border: `1px solid ${theme2.palette.divider}`,
36749
+ borderRadius: "4px",
36750
+ mx: 0.25,
36751
+ height: "32px",
36752
+ width: "32px"
36753
+ },
36754
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(RedoIcon, { fontSize: "small" })
36755
+ }
36756
+ ) }) })
36757
+ ] }),
36714
36758
  !isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
36715
36759
  Button,
36716
36760
  {
@@ -37253,7 +37297,11 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
37253
37297
  isUpdatingHandlers,
37254
37298
  onHandlerClick,
37255
37299
  onFindReplace,
37256
- onEditAll
37300
+ onEditAll,
37301
+ onUndo,
37302
+ onRedo,
37303
+ canUndo,
37304
+ canRedo
37257
37305
  }) {
37258
37306
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
37259
37307
  Header,
@@ -37271,7 +37319,11 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
37271
37319
  isUpdatingHandlers,
37272
37320
  onHandlerClick,
37273
37321
  onFindReplace,
37274
- onEditAll
37322
+ onEditAll,
37323
+ onUndo,
37324
+ onRedo,
37325
+ canUndo,
37326
+ canRedo
37275
37327
  }
37276
37328
  );
37277
37329
  });
@@ -37287,7 +37339,6 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37287
37339
  return availableSources.length > 0 ? availableSources[0] : "";
37288
37340
  });
37289
37341
  const [isReviewComplete, setIsReviewComplete] = reactExports.useState(false);
37290
- const [data, setData] = reactExports.useState(initialData);
37291
37342
  const [originalData] = reactExports.useState(() => JSON.parse(JSON.stringify(initialData)));
37292
37343
  const [interactionMode, setInteractionMode] = reactExports.useState("edit");
37293
37344
  const [isShiftPressed, setIsShiftPressed] = reactExports.useState(false);
@@ -37308,12 +37359,27 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37308
37359
  const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = reactExports.useState(false);
37309
37360
  const theme2 = useTheme();
37310
37361
  const isMobile = useMediaQuery(theme2.breakpoints.down("md"));
37362
+ const [history, setHistory] = reactExports.useState([initialData]);
37363
+ const [historyIndex, setHistoryIndex] = reactExports.useState(0);
37364
+ const data = history[historyIndex];
37365
+ const updateDataWithHistory = reactExports.useCallback((newData, actionDescription) => {
37366
+ const newHistory = history.slice(0, historyIndex + 1);
37367
+ const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
37368
+ newHistory.push(deepCopiedNewData);
37369
+ setHistory(newHistory);
37370
+ setHistoryIndex(newHistory.length - 1);
37371
+ }, [history, historyIndex]);
37372
+ reactExports.useEffect(() => {
37373
+ setHistory([initialData]);
37374
+ setHistoryIndex(0);
37375
+ }, [initialData]);
37311
37376
  reactExports.useEffect(() => {
37312
37377
  }, [initialData]);
37313
37378
  reactExports.useEffect(() => {
37314
37379
  const savedData = loadSavedData(initialData);
37315
37380
  if (savedData && window.confirm("Found saved progress for this song. Would you like to restore it?")) {
37316
- setData(savedData);
37381
+ setHistory([savedData]);
37382
+ setHistoryIndex(0);
37317
37383
  }
37318
37384
  }, [initialData]);
37319
37385
  reactExports.useEffect(() => {
@@ -37366,7 +37432,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37366
37432
  var _a, _b, _c, _d, _e, _f, _g;
37367
37433
  if (effectiveMode === "delete_word") {
37368
37434
  const newData = deleteWord(data, info.word_id);
37369
- setData(newData);
37435
+ updateDataWithHistory(newData, "delete word");
37370
37436
  handleFlash("word");
37371
37437
  return;
37372
37438
  }
@@ -37477,17 +37543,24 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37477
37543
  });
37478
37544
  }
37479
37545
  }
37480
- }, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
37546
+ }, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
37481
37547
  const handleUpdateSegment = reactExports.useCallback((updatedSegment) => {
37482
37548
  if (!editModalSegment) return;
37483
- const newData = updateSegment(data, editModalSegment.index, updatedSegment);
37484
- setData(newData);
37549
+ const currentData = history[historyIndex];
37550
+ const newSegments = currentData.corrected_segments.map(
37551
+ (segment, i) => i === editModalSegment.index ? updatedSegment : segment
37552
+ );
37553
+ const newDataImmutable = {
37554
+ ...currentData,
37555
+ corrected_segments: newSegments
37556
+ };
37557
+ updateDataWithHistory(newDataImmutable, "update segment");
37485
37558
  setEditModalSegment(null);
37486
- }, [data, editModalSegment]);
37559
+ }, [history, historyIndex, editModalSegment, updateDataWithHistory]);
37487
37560
  const handleDeleteSegment = reactExports.useCallback((segmentIndex) => {
37488
37561
  const newData = deleteSegment(data, segmentIndex);
37489
- setData(newData);
37490
- }, [data]);
37562
+ updateDataWithHistory(newData, "delete segment");
37563
+ }, [data, updateDataWithHistory]);
37491
37564
  const handleFinishReview = reactExports.useCallback(() => {
37492
37565
  setIsReviewModalOpen(true);
37493
37566
  }, []);
@@ -37512,7 +37585,8 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37512
37585
  const handleResetCorrections = reactExports.useCallback(() => {
37513
37586
  if (window.confirm("Are you sure you want to reset all corrections? This cannot be undone.")) {
37514
37587
  clearSavedData(initialData);
37515
- setData(JSON.parse(JSON.stringify(initialData)));
37588
+ setHistory([JSON.parse(JSON.stringify(initialData))]);
37589
+ setHistoryIndex(0);
37516
37590
  setModalContent(null);
37517
37591
  setFlashingType(null);
37518
37592
  setHighlightInfo(null);
@@ -37521,20 +37595,20 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37521
37595
  }, [initialData]);
37522
37596
  const handleAddSegment = reactExports.useCallback((beforeIndex) => {
37523
37597
  const newData = addSegmentBefore(data, beforeIndex);
37524
- setData(newData);
37525
- }, [data]);
37598
+ updateDataWithHistory(newData, "add segment");
37599
+ }, [data, updateDataWithHistory]);
37526
37600
  const handleSplitSegment = reactExports.useCallback((segmentIndex, afterWordIndex) => {
37527
37601
  const newData = splitSegment(data, segmentIndex, afterWordIndex);
37528
37602
  if (newData) {
37529
- setData(newData);
37603
+ updateDataWithHistory(newData, "split segment");
37530
37604
  setEditModalSegment(null);
37531
37605
  }
37532
- }, [data]);
37606
+ }, [data, updateDataWithHistory]);
37533
37607
  const handleMergeSegment = reactExports.useCallback((segmentIndex, mergeWithNext) => {
37534
37608
  const newData = mergeSegment(data, segmentIndex, mergeWithNext);
37535
- setData(newData);
37609
+ updateDataWithHistory(newData, "merge segment");
37536
37610
  setEditModalSegment(null);
37537
- }, [data]);
37611
+ }, [data, updateDataWithHistory]);
37538
37612
  const handleHandlerToggle = reactExports.useCallback(async (handler, enabled) => {
37539
37613
  if (!apiClient) return;
37540
37614
  try {
@@ -37546,7 +37620,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37546
37620
  currentEnabled.delete(handler);
37547
37621
  }
37548
37622
  const newData = await apiClient.updateHandlers(Array.from(currentEnabled));
37549
- setData(newData);
37623
+ updateDataWithHistory(newData, `toggle handler ${handler}`);
37550
37624
  setModalContent(null);
37551
37625
  setFlashingType(null);
37552
37626
  setHighlightInfo(null);
@@ -37557,7 +37631,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37557
37631
  } finally {
37558
37632
  setIsUpdatingHandlers(false);
37559
37633
  }
37560
- }, [apiClient, data.metadata.enabled_handlers, handleFlash]);
37634
+ }, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory]);
37561
37635
  const handleHandlerClick = reactExports.useCallback((handler) => {
37562
37636
  setFlashingHandler(handler);
37563
37637
  setFlashingType("handler");
@@ -37574,14 +37648,14 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37574
37648
  try {
37575
37649
  setIsAddingLyrics(true);
37576
37650
  const newData = await apiClient.addLyrics(source, lyrics);
37577
- setData(newData);
37651
+ updateDataWithHistory(newData, "add lyrics");
37578
37652
  } finally {
37579
37653
  setIsAddingLyrics(false);
37580
37654
  }
37581
- }, [apiClient]);
37655
+ }, [apiClient, updateDataWithHistory]);
37582
37656
  const handleFindReplace = (findText, replaceText, options) => {
37583
37657
  const newData = findAndReplace(data, findText, replaceText, options);
37584
- setData(newData);
37658
+ updateDataWithHistory(newData, "find/replace");
37585
37659
  };
37586
37660
  const handleEditAll = reactExports.useCallback(() => {
37587
37661
  console.log("EditAll - Starting process");
@@ -37714,19 +37788,70 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37714
37788
  totalWordCount: updatedSegments.reduce((count, segment) => count + segment.words.length, 0),
37715
37789
  originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
37716
37790
  });
37717
- setData({
37791
+ const newData = {
37718
37792
  ...data,
37719
37793
  corrected_segments: updatedSegments
37720
- });
37794
+ };
37795
+ updateDataWithHistory(newData, "edit all");
37721
37796
  setIsEditAllModalOpen(false);
37722
37797
  setGlobalEditSegment(null);
37723
- }, [data]);
37798
+ }, [data, updateDataWithHistory]);
37799
+ const handleUndo = reactExports.useCallback(() => {
37800
+ if (historyIndex > 0) {
37801
+ const newIndex = historyIndex - 1;
37802
+ setHistoryIndex(newIndex);
37803
+ }
37804
+ }, [historyIndex, history]);
37805
+ const handleRedo = reactExports.useCallback(() => {
37806
+ if (historyIndex < history.length - 1) {
37807
+ const newIndex = historyIndex + 1;
37808
+ setHistoryIndex(newIndex);
37809
+ }
37810
+ }, [historyIndex, history]);
37811
+ const canUndo = historyIndex > 0;
37812
+ const canRedo = historyIndex < history.length - 1;
37724
37813
  const metricClickHandlers = reactExports.useMemo(() => ({
37725
37814
  anchor: () => handleFlash("anchor"),
37726
37815
  corrected: () => handleFlash("corrected"),
37727
37816
  uncorrected: () => handleFlash("uncorrected")
37728
37817
  }), [handleFlash]);
37729
37818
  const isAnyModalOpenMemo = reactExports.useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
37819
+ reactExports.useEffect(() => {
37820
+ const { handleKeyDown: baseHandleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
37821
+ setIsShiftPressed,
37822
+ setIsCtrlPressed
37823
+ });
37824
+ const handleKeyDown = (e) => {
37825
+ const targetElement = e.target;
37826
+ const isInputFocused = targetElement.tagName === "INPUT" || targetElement.tagName === "TEXTAREA";
37827
+ if (!isAnyModalOpen && !isInputFocused) {
37828
+ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
37829
+ const modifierKey = isMac ? e.metaKey : e.ctrlKey;
37830
+ if (modifierKey && e.key.toLowerCase() === "z") {
37831
+ e.preventDefault();
37832
+ if (e.shiftKey) {
37833
+ if (canRedo) handleRedo();
37834
+ } else {
37835
+ if (canUndo) handleUndo();
37836
+ }
37837
+ return;
37838
+ }
37839
+ }
37840
+ baseHandleKeyDown(e);
37841
+ };
37842
+ window.addEventListener("keydown", handleKeyDown);
37843
+ window.addEventListener("keyup", handleKeyUp);
37844
+ if (isAnyModalOpen) {
37845
+ setIsShiftPressed(false);
37846
+ setIsCtrlPressed(false);
37847
+ }
37848
+ return () => {
37849
+ window.removeEventListener("keydown", handleKeyDown);
37850
+ window.removeEventListener("keyup", handleKeyUp);
37851
+ document.body.style.userSelect = "";
37852
+ cleanup();
37853
+ };
37854
+ }, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen, handleUndo, handleRedo, canUndo, canRedo]);
37730
37855
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
37731
37856
  p: 1,
37732
37857
  pb: 3,
@@ -37750,7 +37875,11 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37750
37875
  onHandlerClick: handleHandlerClick,
37751
37876
  onAddLyrics: () => setIsAddLyricsModalOpen(true),
37752
37877
  onFindReplace: () => setIsFindReplaceModalOpen(true),
37753
- onEditAll: handleEditAll
37878
+ onEditAll: handleEditAll,
37879
+ onUndo: handleUndo,
37880
+ onRedo: handleRedo,
37881
+ canUndo,
37882
+ canRedo
37754
37883
  }
37755
37884
  ),
37756
37885
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Grid, { container: true, direction: isMobile ? "column" : "row", children: [
@@ -37770,7 +37899,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37770
37899
  anchors: data.anchor_sequences,
37771
37900
  disableHighlighting: isAnyModalOpenMemo,
37772
37901
  onDataChange: (updatedData) => {
37773
- setData(updatedData);
37902
+ updateDataWithHistory(updatedData, "direct data change");
37774
37903
  }
37775
37904
  }
37776
37905
  ),
@@ -38253,4 +38382,4 @@ ReactDOM$1.createRoot(document.getElementById("root")).render(
38253
38382
  /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
38254
38383
  ] })
38255
38384
  );
38256
- //# sourceMappingURL=index-BvRLUQmZ.js.map
38385
+ //# sourceMappingURL=index-BpvPgWoc.js.map