diff --git a/src/App.jsx b/src/App.jsx index faf9eb66..f301a473 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -79,7 +79,7 @@ import { askForHtml5ifySpeed } from './dialogs/html5ify'; import { askForOutDir, askForImportChapters, promptTimeOffset, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog } from './dialogs'; import { openSendReportDialog } from './reporting'; import { fallbackLng } from './i18n'; -import { createSegment, getCleanCutSegments, findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment } from './segments'; +import { createSegment, getCleanCutSegments, findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments'; import { generateOutSegFileNames as generateOutSegFileNamesRaw, defaultOutSegTemplate } from './util/outputNameTemplate'; import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants'; import BigWaveform from './components/BigWaveform'; @@ -156,6 +156,8 @@ const App = memo(() => { const [keyboardShortcutsVisible, setKeyboardShortcutsVisible] = useState(false); const [mifiLink, setMifiLink] = useState(); const [alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles] = useState(false); + const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState(); + const [editingSegmentTags, setEditingSegmentTags] = useState(); // Batch state / concat files const [batchFiles, setBatchFiles] = useState([]); @@ -1933,6 +1935,15 @@ const App = memo(() => { } }, []); + const onEditSegmentTags = useCallback((index) => { + setEditingSegmentTagsSegmentIndex(index); + setEditingSegmentTags(getSegmentTags(apparentCutSegments[index])); + }, [apparentCutSegments]); + + const editCurrentSegmentTags = useCallback(() => { + onEditSegmentTags(currentSegIndexSafe); + }, [currentSegIndexSafe, onEditSegmentTags]); + const mainActions = useMemo(() => { async function exportYouTube() { if (!checkFileOpened()) return; @@ -2036,6 +2047,7 @@ const App = memo(() => { deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, + editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, @@ -2064,7 +2076,7 @@ const App = memo(() => { showIncludeExternalStreamsDialog, toggleFullscreenVideo, }; - }, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); + }, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); const getKeyboardAction = useCallback((action) => mainActions[action], [mainActions]); @@ -2455,6 +2467,11 @@ const App = memo(() => { onSelectSegmentsByTag={onSelectSegmentsByTag} onLabelSelectedSegments={onLabelSelectedSegments} updateSegAtIndex={updateSegAtIndex} + editingSegmentTags={editingSegmentTags} + editingSegmentTagsSegmentIndex={editingSegmentTagsSegmentIndex} + setEditingSegmentTags={setEditingSegmentTags} + setEditingSegmentTagsSegmentIndex={setEditingSegmentTagsSegmentIndex} + onEditSegmentTags={onEditSegmentTags} /> )} diff --git a/src/SegmentList.jsx b/src/SegmentList.jsx index aa228fb8..bb11d753 100644 --- a/src/SegmentList.jsx +++ b/src/SegmentList.jsx @@ -162,6 +162,7 @@ const SegmentList = memo(({ onLabelSegment, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment, selectedSegments, isSegmentSelected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, onDuplicateSegmentClick, jumpSegStart, jumpSegEnd, updateSegAtIndex, + editingSegmentTags, editingSegmentTagsSegmentIndex, setEditingSegmentTags, setEditingSegmentTagsSegmentIndex, onEditSegmentTags, }) => { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -271,26 +272,19 @@ const SegmentList = memo(({ ); } - const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState(); - const [editingSegmentTags, setEditingSegmentTags] = useState(); const [editingTag, setEditingTag] = useState(); const onTagChange = useCallback((tag, value) => setEditingSegmentTags((existingTags) => ({ ...existingTags, [tag]: value, - })), []); + })), [setEditingSegmentTags]); - const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), []); - - const onEditSegmentTags = useCallback((index) => { - setEditingSegmentTagsSegmentIndex(index); - setEditingSegmentTags(getSegmentTags(apparentCutSegments[index])); - }, [apparentCutSegments]); + const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]); const onSegmentTagsCloseComplete = useCallback(() => { setEditingSegmentTagsSegmentIndex(); setEditingSegmentTags(); - }, []); + }, [setEditingSegmentTags, setEditingSegmentTagsSegmentIndex]); const onSegmentTagsConfirm = useCallback(() => { updateSegAtIndex(editingSegmentTagsSegmentIndex, { tags: editingSegmentTags }); diff --git a/src/components/KeyboardShortcuts.jsx b/src/components/KeyboardShortcuts.jsx index 8da13d27..07029c1d 100644 --- a/src/components/KeyboardShortcuts.jsx +++ b/src/components/KeyboardShortcuts.jsx @@ -7,6 +7,7 @@ import groupBy from 'lodash/groupBy'; import orderBy from 'lodash/orderBy'; import uniq from 'lodash/uniq'; +import Swal from '../swal'; import SetCutpointButton from './SetCutpointButton'; import SegmentCutpointButton from './SegmentCutpointButton'; @@ -295,6 +296,10 @@ const KeyboardShortcuts = memo(({ name: t('Label current segment'), category: segmentsAndCutpointsCategory, }, + editCurrentSegmentTags: { + name: t('Edit current segment tags'), + category: segmentsAndCutpointsCategory, + }, splitCurrentSegment: { name: t('Split segment at cursor'), category: segmentsAndCutpointsCategory, @@ -622,8 +627,9 @@ const KeyboardShortcuts = memo(({ console.log('new key binding', action, keysStr); setKeyBindings((existingBindings) => { - const haveDuplicate = existingBindings.some((existingBinding) => existingBinding.keys === keysStr); - if (haveDuplicate) { + const duplicate = existingBindings.find((existingBinding) => existingBinding.keys === keysStr); + if (duplicate) { + Swal.fire({ icon: 'error', title: t('Duplicate keyboard combination'), text: t('Combination is already bound to "{{alreadyBoundKey}}"', { alreadyBoundKey: actionsMap[duplicate.action]?.name }) }); console.log('trying to add duplicate'); return existingBindings; } @@ -632,7 +638,7 @@ const KeyboardShortcuts = memo(({ setCreatingBinding(); return [...existingBindings, { action, keys: keysStr }]; }); - }, [setKeyBindings]); + }, [actionsMap, setKeyBindings, t]); const missingActions = Object.keys(mainActions).filter((key) => actionsMap[key] == null); if (missingActions.length > 0) throw new Error(`Action(s) missing: ${missingActions.join(',')}`); diff --git a/src/components/TagEditor.jsx b/src/components/TagEditor.jsx index de7df1dd..4e7780fc 100644 --- a/src/components/TagEditor.jsx +++ b/src/components/TagEditor.jsx @@ -24,7 +24,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi setNewTag(); }, [editingTag, onTagReset, setEditingTag]); - function onEditClick(tag) { + const onEditClick = useCallback((tag) => { if (newTag) { onTagChange(editingTag, editingTagVal); setEditingTag(); @@ -40,7 +40,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi setEditingTag(tag); setEditingTagVal(mergedTags[tag]); } - } + }, [editingTag, editingTagVal, existingTags, mergedTags, newTag, onResetClick, onTagChange, setEditingTag]); function onSubmit(e) { e.preventDefault(); @@ -50,12 +50,18 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi const onAddPress = useCallback(async (e) => { e.preventDefault(); e.target.blur(); + + if (newTag || editingTag != null) { + // save any unsaved edit + onEditClick(); + } + const tag = await askForMetadataKey({ title: addTagTitle, text: addTagText }); if (!tag || Object.keys(mergedTags).includes(tag)) return; setEditingTag(tag); setEditingTagVal(''); setNewTag(tag); - }, [addTagText, addTagTitle, mergedTags, setEditingTag]); + }, [addTagText, addTagTitle, editingTag, mergedTags, newTag, onEditClick, setEditingTag]); useEffect(() => { ref.current?.focus(); @@ -92,7 +98,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi - + ); }