mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +01:00
parent
88fbcc0129
commit
de9e927d26
@ -70,10 +70,10 @@ import {
|
||||
isStoreBuild, dragPreventer,
|
||||
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
|
||||
deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType,
|
||||
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities,
|
||||
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, getFrameDuration,
|
||||
} from './util';
|
||||
import { toast, errorToast } from './swal';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { formatDuration, parseDuration } from './util/duration';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
||||
import { askForHtml5ifySpeed } from './dialogs/html5ify';
|
||||
@ -86,7 +86,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
|
||||
import BigWaveform from './components/BigWaveform';
|
||||
|
||||
import isDev from './isDev';
|
||||
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
|
||||
|
||||
@ -368,9 +368,46 @@ function App() {
|
||||
return false;
|
||||
}, [isFileOpened]);
|
||||
|
||||
const getFrameCount = useCallback((sec: number) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
|
||||
const frameCountToDuration = useCallback((frames: number) => getFrameDuration(detectedFps) * frames, [detectedFps]);
|
||||
|
||||
const formatTimecode = useCallback<FormatTimecode>(({ seconds, shorten, fileNameFriendly }) => {
|
||||
if (timecodeFormat === 'frameCount') {
|
||||
const frameCount = getFrameCount(seconds);
|
||||
return frameCount != null ? String(frameCount) : '';
|
||||
}
|
||||
if (timecodeFormat === 'timecodeWithFramesFraction') {
|
||||
return formatDuration({ seconds, shorten, fileNameFriendly, fps: detectedFps });
|
||||
}
|
||||
return formatDuration({ seconds, shorten, fileNameFriendly });
|
||||
}, [detectedFps, timecodeFormat, getFrameCount]);
|
||||
|
||||
const timecodePlaceholder = useMemo(() => formatTimecode({ seconds: 0, shorten: false }), [formatTimecode]);
|
||||
|
||||
const parseTimecode = useCallback<ParseTimecode>((val: string) => {
|
||||
if (timecodeFormat === 'frameCount') {
|
||||
const parsed = parseInt(val, 10);
|
||||
return frameCountToDuration(parsed);
|
||||
}
|
||||
if (timecodeFormat === 'timecodeWithFramesFraction') {
|
||||
return parseDuration(val, detectedFps);
|
||||
}
|
||||
return parseDuration(val);
|
||||
}, [detectedFps, frameCountToDuration, timecodeFormat]);
|
||||
|
||||
const formatTimeAndFrames = useCallback((seconds: number) => {
|
||||
const frameCount = getFrameCount(seconds);
|
||||
|
||||
const timeStr = timecodeFormat === 'timecodeWithFramesFraction'
|
||||
? formatDuration({ seconds, fps: detectedFps })
|
||||
: formatDuration({ seconds });
|
||||
|
||||
return `${timeStr} (${frameCount ?? '0'})`;
|
||||
}, [detectedFps, timecodeFormat, getFrameCount]);
|
||||
|
||||
const {
|
||||
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByTag, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
|
||||
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly });
|
||||
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode });
|
||||
|
||||
|
||||
const segmentAtCursor = useMemo(() => {
|
||||
@ -416,30 +453,6 @@ function App() {
|
||||
const jumpTimelineStart = useCallback(() => userSeekAbs(0), [userSeekAbs]);
|
||||
const jumpTimelineEnd = useCallback(() => userSeekAbs(durationSafe), [durationSafe, userSeekAbs]);
|
||||
|
||||
|
||||
const getFrameCount = useCallback((sec: number) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
|
||||
|
||||
const formatTimecode = useCallback<FormatTimecode>(({ seconds, shorten, fileNameFriendly }) => {
|
||||
if (timecodeFormat === 'frameCount') {
|
||||
const frameCount = getFrameCount(seconds);
|
||||
return frameCount != null ? String(frameCount) : '';
|
||||
}
|
||||
if (timecodeFormat === 'timecodeWithFramesFraction') {
|
||||
return formatDuration({ seconds, fps: detectedFps, shorten, fileNameFriendly });
|
||||
}
|
||||
return formatDuration({ seconds, shorten, fileNameFriendly });
|
||||
}, [detectedFps, timecodeFormat, getFrameCount]);
|
||||
|
||||
const formatTimeAndFrames = useCallback((seconds: number) => {
|
||||
const frameCount = getFrameCount(seconds);
|
||||
|
||||
const timeStr = timecodeFormat === 'timecodeWithFramesFraction'
|
||||
? formatDuration({ seconds, fps: detectedFps })
|
||||
: formatDuration({ seconds });
|
||||
|
||||
return `${timeStr} (${frameCount ?? '0'})`;
|
||||
}, [detectedFps, timecodeFormat, getFrameCount]);
|
||||
|
||||
const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ formatTimecode, treatOutputFileModifiedTimeAsStart });
|
||||
|
||||
// const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]);
|
||||
@ -1703,14 +1716,16 @@ function App() {
|
||||
const goToTimecode = useCallback(async () => {
|
||||
if (!filePath) return;
|
||||
const timeCode = await promptTimeOffset({
|
||||
initialValue: formatDuration({ seconds: commandedTimeRef.current }),
|
||||
initialValue: formatTimecode({ seconds: commandedTimeRef.current }),
|
||||
title: i18n.t('Seek to timecode'),
|
||||
inputPlaceholder: timecodePlaceholder,
|
||||
parseTimecode,
|
||||
});
|
||||
|
||||
if (timeCode === undefined) return;
|
||||
|
||||
userSeekAbs(timeCode);
|
||||
}, [filePath, userSeekAbs]);
|
||||
}, [filePath, formatTimecode, parseTimecode, timecodePlaceholder, userSeekAbs]);
|
||||
|
||||
const toggleStreamsSelector = useCallback(() => setStreamsSelectorShown((v) => !v), []);
|
||||
|
||||
@ -1776,15 +1791,17 @@ function App() {
|
||||
|
||||
const askStartTimeOffset = useCallback(async () => {
|
||||
const newStartTimeOffset = await promptTimeOffset({
|
||||
initialValue: startTimeOffset !== undefined ? formatDuration({ seconds: startTimeOffset }) : undefined,
|
||||
initialValue: startTimeOffset !== undefined ? formatTimecode({ seconds: startTimeOffset }) : undefined,
|
||||
title: i18n.t('Set custom start time offset'),
|
||||
text: i18n.t('Instead of video apparently starting at 0, you can offset by a specified value. This only applies to the preview inside LosslessCut and does not modify the file in any way. (Useful for viewing/cutting videos according to timecodes)'),
|
||||
inputPlaceholder: timecodePlaceholder,
|
||||
parseTimecode,
|
||||
});
|
||||
|
||||
if (newStartTimeOffset === undefined) return;
|
||||
|
||||
setStartTimeOffset(newStartTimeOffset);
|
||||
}, [startTimeOffset]);
|
||||
}, [formatTimecode, parseTimecode, startTimeOffset, timecodePlaceholder]);
|
||||
|
||||
const toggleKeyboardShortcuts = useCallback(() => setKeyboardShortcutsVisible((v) => !v), []);
|
||||
|
||||
@ -2701,6 +2718,8 @@ function App() {
|
||||
setDarkMode={setDarkMode}
|
||||
outputPlaybackRate={outputPlaybackRate}
|
||||
setOutputPlaybackRate={setOutputPlaybackRate}
|
||||
formatTimecode={formatTimecode}
|
||||
parseTimecode={parseTimecode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -2731,6 +2750,7 @@ function App() {
|
||||
setCustomTagsByFile={setCustomTagsByFile}
|
||||
paramsByStreamId={paramsByStreamId}
|
||||
updateStreamParams={updateStreamParams}
|
||||
formatTimecode={formatTimecode}
|
||||
/>
|
||||
)}
|
||||
</Sheet>
|
||||
|
@ -20,7 +20,7 @@ import { withBlur, mirrorTransform, checkAppPath } from './util';
|
||||
import { toast } from './swal';
|
||||
import { getSegColor as getSegColorRaw } from './util/colors';
|
||||
import { useSegColors } from './contexts';
|
||||
import { formatDuration, parseDuration, isExactDurationMatch } from './util/duration';
|
||||
import { isExactDurationMatch } from './util/duration';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
import { askForPlaybackRate } from './dialogs';
|
||||
|
||||
@ -63,7 +63,7 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
|
||||
});
|
||||
|
||||
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => {
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
@ -102,19 +102,19 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
||||
e.preventDefault();
|
||||
|
||||
// Don't proceed if not a valid time value
|
||||
const timeWithOffset = parseDuration(cutTimeManual);
|
||||
const timeWithOffset = parseTimecode(cutTimeManual);
|
||||
if (timeWithOffset === undefined) return;
|
||||
|
||||
trySetTime(timeWithOffset);
|
||||
}, [cutTimeManual, trySetTime]);
|
||||
}, [cutTimeManual, parseTimecode, trySetTime]);
|
||||
|
||||
const parseAndSetCutTime = useCallback((text) => {
|
||||
// Don't proceed if not a valid time value
|
||||
const timeWithOffset = parseDuration(text);
|
||||
const timeWithOffset = parseTimecode(text);
|
||||
if (timeWithOffset === undefined) return;
|
||||
|
||||
trySetTime(timeWithOffset);
|
||||
}, [trySetTime]);
|
||||
}, [parseTimecode, trySetTime]);
|
||||
|
||||
function handleCutTimeInput(text) {
|
||||
setCutTimeManual(text);
|
||||
@ -160,7 +160,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
||||
onContextMenu={handleContextMenu}
|
||||
value={isCutTimeManualSet()
|
||||
? cutTimeManual
|
||||
: formatDuration({ seconds: cutTime + startTimeOffset })}
|
||||
: formatTimecode({ seconds: cutTime + startTimeOffset })}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
@ -178,6 +178,7 @@ const BottomBar = memo(({
|
||||
darkMode, setDarkMode,
|
||||
toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
|
||||
outputPlaybackRate, setOutputPlaybackRate,
|
||||
formatTimecode, parseTimecode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
@ -308,7 +309,7 @@ const BottomBar = memo(({
|
||||
|
||||
<SetCutpointButton currentCutSeg={currentCutSeg} side="start" onClick={setCutStart} title={t('Start current segment at current time')} style={{ marginRight: 5 }} />
|
||||
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.start} setCutTime={setCutTime} isStart />}
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.start} setCutTime={setCutTime} isStart formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
|
||||
|
||||
<IoMdKey
|
||||
size={25}
|
||||
@ -353,7 +354,7 @@ const BottomBar = memo(({
|
||||
onClick={() => seekClosestKeyframe(1)}
|
||||
/>
|
||||
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.end} setCutTime={setCutTime} />}
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.end} setCutTime={setCutTime} formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
|
||||
|
||||
<SetCutpointButton currentCutSeg={currentCutSeg} side="end" onClick={setCutEnd} title={t('End current segment at current time')} style={{ marginLeft: 5 }} />
|
||||
|
||||
|
@ -10,7 +10,6 @@ import prettyBytes from 'pretty-bytes';
|
||||
import AutoExportToggler from './components/AutoExportToggler';
|
||||
import Select from './components/Select';
|
||||
import { showJson5Dialog } from './dialogs';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { getStreamFps } from './ffmpeg';
|
||||
import { deleteDispositionValue } from './util';
|
||||
import { getActiveDisposition, attachedPicDisposition } from './util/streams';
|
||||
@ -131,7 +130,7 @@ function onInfoClick(json, title) {
|
||||
showJson5Dialog({ title, json });
|
||||
}
|
||||
|
||||
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams }) => {
|
||||
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const effectiveDisposition = useMemo(() => getStreamEffectiveDisposition(paramsByStreamId, filePath, stream), [filePath, paramsByStreamId, stream]);
|
||||
@ -192,7 +191,7 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
|
||||
</td>
|
||||
<td style={{ maxWidth: '3em', overflow: 'hidden' }} title={stream.codec_name}>{stream.codec_name} {codecTag}</td>
|
||||
<td>
|
||||
{!Number.isNaN(duration) && `${formatDuration({ seconds: duration, shorten: true })}`}
|
||||
{!Number.isNaN(duration) && `${formatTimecode({ seconds: duration, shorten: true })}`}
|
||||
{stream.nb_frames != null ? <div>{stream.nb_frames}f</div> : null}
|
||||
</td>
|
||||
<td>{!Number.isNaN(bitrate) && (stream.codec_type === 'audio' ? `${Math.round(bitrate / 1000)} kbps` : prettyBytes(bitrate, { bits: true }))}</td>
|
||||
@ -297,6 +296,7 @@ const StreamsSelector = memo(({
|
||||
setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta,
|
||||
showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams,
|
||||
customTagsByFile, setCustomTagsByFile, paramsByStreamId, updateStreamParams,
|
||||
formatTimecode,
|
||||
}) => {
|
||||
const [editingFile, setEditingFile] = useState();
|
||||
const [editingStream, setEditingStream] = useState();
|
||||
@ -360,6 +360,7 @@ const StreamsSelector = memo(({
|
||||
onExtractStreamPress={() => onExtractStreamPress(stream.index)}
|
||||
paramsByStreamId={paramsByStreamId}
|
||||
updateStreamParams={updateStreamParams}
|
||||
formatTimecode={formatTimecode}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
@ -385,6 +386,7 @@ const StreamsSelector = memo(({
|
||||
fileDuration={getFormatDuration(formatData)}
|
||||
paramsByStreamId={paramsByStreamId}
|
||||
updateStreamParams={updateStreamParams}
|
||||
formatTimecode={formatTimecode}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -8,18 +8,18 @@ import { tomorrow as syntaxStyle } from 'react-syntax-highlighter/dist/esm/style
|
||||
import JSON5 from 'json5';
|
||||
import { SweetAlertOptions } from 'sweetalert2';
|
||||
|
||||
import { parseDuration, formatDuration } from '../util/duration';
|
||||
import { formatDuration } from '../util/duration';
|
||||
import Swal, { swalToastOptions, toast } from '../swal';
|
||||
import { parseYouTube } from '../edlFormats';
|
||||
import CopyClipboardButton from '../components/CopyClipboardButton';
|
||||
import { isWindows, showItemInFolder } from '../util';
|
||||
import { SegmentBase } from '../types';
|
||||
import { ParseTimecode, SegmentBase } from '../types';
|
||||
|
||||
const { dialog } = window.require('@electron/remote');
|
||||
|
||||
const ReactSwal = withReactContent(Swal);
|
||||
|
||||
export async function promptTimeOffset({ initialValue, title, text }: { initialValue?: string | undefined, title: string, text?: string | undefined }) {
|
||||
export async function promptTimeOffset({ initialValue, title, text, inputPlaceholder, parseTimecode }: { initialValue?: string | undefined, title: string, text?: string | undefined, inputPlaceholder: string, parseTimecode: ParseTimecode }) {
|
||||
// @ts-expect-error todo
|
||||
const { value } = await Swal.fire({
|
||||
title,
|
||||
@ -27,16 +27,16 @@ export async function promptTimeOffset({ initialValue, title, text }: { initialV
|
||||
input: 'text',
|
||||
inputValue: initialValue || '',
|
||||
showCancelButton: true,
|
||||
inputPlaceholder: '00:00:00.000',
|
||||
inputPlaceholder,
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const duration = parseDuration(value);
|
||||
const duration = parseTimecode(value);
|
||||
// Invalid, try again
|
||||
if (duration === undefined) return promptTimeOffset({ initialValue: value, title, text });
|
||||
if (duration === undefined) return promptTimeOffset({ initialValue: value, title, text, inputPlaceholder, parseTimecode });
|
||||
|
||||
return duration;
|
||||
}
|
||||
@ -200,25 +200,27 @@ export async function createNumSegments(fileDuration) {
|
||||
|
||||
const exampleDuration = '00:00:05.123';
|
||||
|
||||
async function askForSegmentDuration(fileDuration) {
|
||||
async function askForSegmentDuration({ fileDuration, inputPlaceholder, parseTimecode }: {
|
||||
fileDuration: number, inputPlaceholder: string, parseTimecode: ParseTimecode,
|
||||
}) {
|
||||
const { value } = await Swal.fire({
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
inputValue: '00:00:00.000',
|
||||
inputValue: inputPlaceholder,
|
||||
text: i18n.t('Divide timeline into a number of segments with the specified length'),
|
||||
inputValidator: (v) => {
|
||||
const duration = parseDuration(v);
|
||||
const duration = parseTimecode(v);
|
||||
if (duration != null) {
|
||||
const numSegments = Math.ceil(fileDuration / duration);
|
||||
if (duration > 0 && duration < fileDuration && numSegments <= maxSegments) return null;
|
||||
}
|
||||
return i18n.t('Please input a valid duration. Example: {{example}}', { example: exampleDuration });
|
||||
return i18n.t('Please input a valid duration. Example: {{example}}', { example: inputPlaceholder });
|
||||
},
|
||||
});
|
||||
|
||||
if (value == null) return undefined;
|
||||
|
||||
return parseDuration(value);
|
||||
return parseTimecode(value);
|
||||
}
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/1153
|
||||
@ -273,15 +275,15 @@ async function askForSegmentsStartOrEnd(text) {
|
||||
return value === 'both' ? ['start', 'end'] : [value];
|
||||
}
|
||||
|
||||
export async function askForShiftSegments() {
|
||||
function parseValue(value) {
|
||||
export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: { inputPlaceholder: string, parseTimecode: ParseTimecode }) {
|
||||
function parseValue(value: string) {
|
||||
let parseableValue = value;
|
||||
let sign = 1;
|
||||
if (parseableValue[0] === '-') {
|
||||
parseableValue = parseableValue.slice(1);
|
||||
sign = -1;
|
||||
}
|
||||
const duration = parseDuration(parseableValue);
|
||||
const duration = parseTimecode(parseableValue);
|
||||
if (duration != null && duration > 0) {
|
||||
return duration * sign;
|
||||
}
|
||||
@ -291,7 +293,7 @@ export async function askForShiftSegments() {
|
||||
const { value } = await Swal.fire({
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
inputValue: '00:00:00.000',
|
||||
inputValue: inputPlaceholder,
|
||||
text: i18n.t('Shift all segments on the timeline by this amount. Negative values will be shifted back, while positive value will be shifted forward in time.'),
|
||||
inputValidator: (v) => {
|
||||
const parsed = parseValue(v);
|
||||
@ -417,8 +419,10 @@ export async function showCleanupFilesDialog(cleanupChoicesIn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function createFixedDurationSegments(fileDuration) {
|
||||
const segmentDuration = await askForSegmentDuration(fileDuration);
|
||||
export async function createFixedDurationSegments({ fileDuration, inputPlaceholder, parseTimecode }: {
|
||||
fileDuration: number, inputPlaceholder: string, parseTimecode: ParseTimecode,
|
||||
}) {
|
||||
const segmentDuration = await askForSegmentDuration({ fileDuration, inputPlaceholder, parseTimecode });
|
||||
if (segmentDuration == null) return undefined;
|
||||
const edl: SegmentBase[] = [];
|
||||
for (let start = 0; start < fileDuration; start += segmentDuration) {
|
||||
|
@ -14,13 +14,13 @@ import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegmen
|
||||
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
|
||||
import * as ffmpegParameters from '../ffmpeg-parameters';
|
||||
import { maxSegmentsAllowed } from '../util/constants';
|
||||
import { SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
|
||||
import { ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
|
||||
|
||||
const { ffmpeg: { blackDetect, silenceDetect } } = window.require('@electron/remote').require('./index.js');
|
||||
|
||||
|
||||
function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: {
|
||||
filePath?: string | undefined, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number | undefined, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean,
|
||||
function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode }: {
|
||||
filePath?: string | undefined, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number | undefined, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean, timecodePlaceholder: string, parseTimecode: ParseTimecode,
|
||||
}) {
|
||||
// Segment related state
|
||||
const segCounterRef = useRef(0);
|
||||
@ -259,7 +259,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
||||
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
|
||||
|
||||
const shiftAllSegmentTimes = useCallback(async () => {
|
||||
const shift = await askForShiftSegments();
|
||||
const shift = await askForShiftSegments({ inputPlaceholder: timecodePlaceholder, parseTimecode });
|
||||
if (shift == null) return;
|
||||
|
||||
const { shiftAmount, shiftKeys } = shift;
|
||||
@ -270,7 +270,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
||||
});
|
||||
return newSegment;
|
||||
});
|
||||
}, [modifySelectedSegmentTimes]);
|
||||
}, [modifySelectedSegmentTimes, parseTimecode, timecodePlaceholder]);
|
||||
|
||||
const alignSegmentTimesToKeyframes = useCallback(async () => {
|
||||
if (!videoStream || workingRef.current) return;
|
||||
@ -444,9 +444,9 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
||||
|
||||
const createFixedDurationSegments = useCallback(async () => {
|
||||
if (!checkFileOpened() || !isDurationValid(duration)) return;
|
||||
const segments = await createFixedDurationSegmentsDialog(duration);
|
||||
const segments = await createFixedDurationSegmentsDialog({ fileDuration: duration, inputPlaceholder: timecodePlaceholder, parseTimecode });
|
||||
if (segments) loadCutSegments(segments);
|
||||
}, [checkFileOpened, duration, loadCutSegments]);
|
||||
}, [checkFileOpened, duration, loadCutSegments, parseTimecode, timecodePlaceholder]);
|
||||
|
||||
const createRandomSegments = useCallback(async () => {
|
||||
if (!checkFileOpened() || !isDurationValid(duration)) return;
|
||||
|
@ -85,6 +85,7 @@ export interface Thumbnail {
|
||||
}
|
||||
|
||||
export type FormatTimecode = (a: { seconds: number, shorten?: boolean | undefined, fileNameFriendly?: boolean | undefined }) => string;
|
||||
export type ParseTimecode = (val: string) => number | undefined;
|
||||
|
||||
export type GetFrameCount = (sec: number) => number | undefined;
|
||||
|
||||
|
@ -41,6 +41,10 @@ it('should format and parse duration with correct rounding', () => {
|
||||
expect(formatDuration({ seconds: parseDuration('-1') })).toBe('-00:00:01.000');
|
||||
expect(formatDuration({ seconds: parseDuration('01') })).toBe('00:00:01.000');
|
||||
expect(formatDuration({ seconds: parseDuration('01:00:00.000') })).toBe('01:00:00.000');
|
||||
|
||||
expect(formatDuration({ seconds: parseDuration('00:00:00,00', 30), fps: 30 })).toBe('00:00:00.00');
|
||||
expect(formatDuration({ seconds: parseDuration('00:00:00,29', 30), fps: 30 })).toBe('00:00:00.29');
|
||||
expect(formatDuration({ seconds: parseDuration('01:00:01,01', 30), fps: 30 })).toBe('01:00:01.01');
|
||||
});
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/1603
|
||||
|
@ -39,22 +39,32 @@ export function formatDuration({ seconds: totalSecondsIn, fileNameFriendly, show
|
||||
return `${sign}${hoursPart}${minutesPadded}${delim}${secondsPadded}${fraction}`;
|
||||
}
|
||||
|
||||
// todo adapt also to frame counts and frame fractions?
|
||||
export const isExactDurationMatch = (str) => /^-?\d{2}:\d{2}:\d{2}.\d{3}$/.test(str);
|
||||
|
||||
// See also parseYoutube
|
||||
export function parseDuration(str) {
|
||||
export function parseDuration(str: string, fps?: number) {
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const match = str.replaceAll(/\s/g, '').match(/^(-?)(?:(?:(\d{1,}):)?(\d{1,2}):)?(\d{1,2}(?:[.,]\d{1,3})?)$/);
|
||||
|
||||
if (!match) return undefined;
|
||||
|
||||
const [, sign, hourStr, minStr, secStrRaw] = match;
|
||||
const secStr = secStrRaw.replace(',', '.');
|
||||
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
|
||||
const min = minStr != null ? parseInt(minStr, 10) : 0;
|
||||
const sec = parseFloat(secStr);
|
||||
|
||||
if (min > 59 || sec >= 60) return undefined;
|
||||
const secWithFraction = secStrRaw!.replace(',', '.');
|
||||
|
||||
let sec: number;
|
||||
if (fps == null) {
|
||||
sec = parseFloat(secWithFraction);
|
||||
} else {
|
||||
const [secStr, framesStr] = secWithFraction.split('.');
|
||||
sec = parseInt(secStr!, 10) + parseInt(framesStr!, 10) / fps;
|
||||
}
|
||||
|
||||
if (min > 59) return undefined;
|
||||
if (sec >= 60) return undefined;
|
||||
|
||||
let time = (((hour * 60) + min) * 60 + sec);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user