From 814866fd13d398b28da8e56e9ccad826e0b65621 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 13 Mar 2025 20:56:25 +0900 Subject: [PATCH] usability improvements --- src/renderer/src/BottomBar.tsx | 4 +- src/renderer/src/NoFileLoaded.tsx | 6 ++- .../src/components/SegmentCutpointButton.tsx | 4 +- .../src/components/SetCutpointButton.tsx | 4 +- src/renderer/src/hooks/useSegments.ts | 37 ++++++++++++------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/renderer/src/BottomBar.tsx b/src/renderer/src/BottomBar.tsx index 287d2d24..c198d6d4 100644 --- a/src/renderer/src/BottomBar.tsx +++ b/src/renderer/src/BottomBar.tsx @@ -346,6 +346,8 @@ function BottomBar({ const PlayPause = playing ? FaPause : FaPlay; + const currentCutSegOrDefault = useMemo(() => currentCutSeg ?? { segColorIndex: 0 }, [currentCutSeg]); + return ( <>
@@ -404,7 +406,7 @@ function BottomBar({ )} - + {!simpleMode && } diff --git a/src/renderer/src/NoFileLoaded.tsx b/src/renderer/src/NoFileLoaded.tsx index 643ddbd5..7c229a68 100644 --- a/src/renderer/src/NoFileLoaded.tsx +++ b/src/renderer/src/NoFileLoaded.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation, Trans } from 'react-i18next'; @@ -18,6 +18,8 @@ function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode }: { const { t } = useTranslation(); const { simpleMode } = useUserSettings(); + const currentCutSegOrDefault = useMemo(() => currentCutSeg ?? { segColorIndex: 0 }, [currentCutSeg]); + return (
- or I O to set cutpoints + or I O to set cutpoints
e.stopPropagation()}> diff --git a/src/renderer/src/components/SegmentCutpointButton.tsx b/src/renderer/src/components/SegmentCutpointButton.tsx index 90a0d804..f808d0e3 100644 --- a/src/renderer/src/components/SegmentCutpointButton.tsx +++ b/src/renderer/src/components/SegmentCutpointButton.tsx @@ -3,10 +3,10 @@ import { FaStepForward } from 'react-icons/fa'; import { useSegColors } from '../contexts'; import useUserSettings from '../hooks/useUserSettings'; -import { SegmentBase, SegmentColorIndex } from '../types'; +import { SegmentColorIndex } from '../types'; const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: { - currentCutSeg: (SegmentBase & SegmentColorIndex) | undefined, + currentCutSeg: SegmentColorIndex | undefined, side: 'start' | 'end', Icon: typeof FaStepForward, onClick?: (() => void) | undefined, diff --git a/src/renderer/src/components/SetCutpointButton.tsx b/src/renderer/src/components/SetCutpointButton.tsx index c0363233..5f310bd4 100644 --- a/src/renderer/src/components/SetCutpointButton.tsx +++ b/src/renderer/src/components/SetCutpointButton.tsx @@ -3,11 +3,11 @@ import { FaHandPointUp } from 'react-icons/fa'; import SegmentCutpointButton from './SegmentCutpointButton'; import { mirrorTransform } from '../util'; -import { SegmentBase, SegmentColorIndex } from '../types'; +import { SegmentColorIndex } from '../types'; // constant side because we are mirroring const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: { - currentCutSeg: (SegmentBase & SegmentColorIndex) | undefined, + currentCutSeg: SegmentColorIndex | undefined, side: 'start' | 'end', title?: string, onClick?: () => void, diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index 64baf968..255d2792 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -163,8 +163,14 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea const haveInvalidSegs = useMemo(() => cutSegments.some((cutSegment) => cutSegment.end != null && cutSegment.start >= cutSegment.end), [cutSegments]); const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1); + const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]); + const currentCutSegOrWholeTimeline = useMemo(() => { + const { start = 0, end = fileDurationNonZero } = currentCutSeg ?? {}; + return { start, end }; + }, [currentCutSeg, fileDurationNonZero]); + const selectedSegmentsRaw = useMemo(() => cutSegments.filter((segment) => isSegmentSelected(segment)), [cutSegments, isSegmentSelected]); const getFfmpegParameters = useCallback((key: FfmpegDialog) => { @@ -174,7 +180,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }, [ffmpegParameters]); const detectBlackScenes = useCallback(async () => { - const { start = 0, end = fileDurationNonZero } = currentCutSeg ?? {}; + const { start, end } = currentCutSegOrWholeTimeline; const dialogType = 'blackdetect'; const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' }); if (parameters == null) return; @@ -183,10 +189,10 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea invariant(mode === '1' || mode === '2'); invariant(filePath != null); await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async (onSegmentDetected) => blackDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) }); - }, [fileDurationNonZero, currentCutSeg, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress]); + }, [currentCutSegOrWholeTimeline, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress]); const detectSilentScenes = useCallback(async () => { - const { start = 0, end = fileDurationNonZero } = currentCutSeg ?? {}; + const { start, end } = currentCutSegOrWholeTimeline; const dialogType = 'silencedetect'; const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#silencedetect' }); if (parameters == null) return; @@ -195,10 +201,10 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea invariant(mode === '1' || mode === '2'); invariant(filePath != null); await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async (onSegmentDetected) => silenceDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) }); - }, [currentCutSeg, detectSegments, fileDurationNonZero, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]); + }, [currentCutSegOrWholeTimeline, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]); const detectSceneChanges = useCallback(async () => { - const { start = 0, end = fileDurationNonZero } = currentCutSeg ?? {}; + const { start, end } = currentCutSegOrWholeTimeline; const dialogType = 'sceneChange'; const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType) }); if (parameters == null) return; @@ -208,16 +214,16 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea const minChange = parameters['minChange']; invariant(minChange != null); await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async (onSegmentDetected) => ffmpegDetectSceneChanges({ filePath, minChange, onProgress: setProgress, onSegmentDetected, from: start, to: end }) }); - }, [currentCutSeg, detectSegments, fileDurationNonZero, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]); + }, [currentCutSegOrWholeTimeline, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]); const createSegmentsFromKeyframes = useCallback(async () => { - const { start = 0, end = fileDurationNonZero } = currentCutSeg ?? {}; + const { start, end } = currentCutSegOrWholeTimeline; if (!videoStream) return; invariant(filePath != null); const keyframes = (await readFrames({ filePath, from: start, to: end, streamIndex: videoStream.index })).filter((frame) => frame.keyframe); const newSegments = mapTimesToSegments(keyframes.map((keyframe) => keyframe.time), true); loadCutSegments(newSegments, true); - }, [currentCutSeg, fileDurationNonZero, filePath, loadCutSegments, videoStream]); + }, [currentCutSegOrWholeTimeline, filePath, loadCutSegments, videoStream]); const removeSegments = useCallback((removeSegmentIds: string[]) => { safeSetCutSegments((existingSegments) => { @@ -408,10 +414,12 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea if (fileDuration == null || suggestedStart >= fileDuration) return; - const newSegment = createIndexedSegment({ segment: { start: suggestedStart }, incrementCount: true }); + const initial = isInitialSegment(cutSegments); + + const newSegment = createIndexedSegment({ segment: { start: suggestedStart }, incrementCount: !initial }); // if initial segment, replace it instead - const cutSegmentsNew = isInitialSegment(cutSegments) + const cutSegmentsNew = initial ? [ newSegment, ] : [ @@ -449,13 +457,13 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }, [currentCutSeg, duplicateSegment]); const setCutStart = useCallback(() => { - if (!checkFileOpened() || currentCutSeg == null) return; + if (!checkFileOpened()) return; const relevantTime = getRelevantTime(); // https://github.com/mifi/lossless-cut/issues/168 - // If current time is after the end of the current segment in the timeline, - // add a new segment that starts at playerTime - if (currentCutSeg.end != null && relevantTime >= currentCutSeg.end) { + // If current time is after the end of the current segment in the timeline, or there is no segment, + // conveniently add a new segment that starts at playerTime + if (currentCutSeg == null || (currentCutSeg.end != null && relevantTime >= currentCutSeg.end)) { addSegment(); } else { try { @@ -717,6 +725,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea if (currentCutSeg == null) return; selectOnlySegment(currentCutSeg); }, [currentCutSeg, selectOnlySegment]); + const toggleCurrentSegmentSelected = useCallback(() => { if (currentCutSeg == null) return; toggleSegmentSelected(currentCutSeg);