kopia lustrzana https://github.com/mifi/lossless-cut
Porównaj commity
8 Commity
096db54f11
...
4892437b83
Autor | SHA1 | Data |
---|---|---|
Mikael Finstad | 4892437b83 | |
Mikael Finstad | 918277bd75 | |
Mikael Finstad | 281ff6e980 | |
Mikael Finstad | 799333f898 | |
Mikael Finstad | e64e0fb216 | |
Mikael Finstad | 0f8605f897 | |
Mikael Finstad | 8b86795f29 | |
Mikael Finstad | ec3e626693 |
|
@ -5,9 +5,6 @@ module.exports = {
|
|||
'jsx-a11y/click-events-have-key-events': 0,
|
||||
'jsx-a11y/interactive-supports-focus': 0,
|
||||
'jsx-a11y/control-has-associated-label': 0,
|
||||
'unicorn/prefer-node-protocol': 0, // todo
|
||||
'@typescript-eslint/no-var-requires': 0, // todo
|
||||
'react/display-name': 0, // todo
|
||||
},
|
||||
|
||||
overrides: [
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Expressions
|
||||
|
||||
## Select segments by expression
|
||||
|
||||
LosslessCut has support for normal JavaScript expressions. You will be given a variable `segment` and can create an expression that returns `true` or `false`. For example to select all segments with a duration of less than 5 seconds use this expression:
|
||||
|
||||
```js
|
||||
segment.duration < 5
|
||||
```
|
||||
|
||||
See more examples in-app.
|
|
@ -131,7 +131,6 @@
|
|||
"i18next-fs-backend": "^2.1.1",
|
||||
"json5": "^2.2.2",
|
||||
"lodash": "^4.17.19",
|
||||
"mathjs": "^12.4.2",
|
||||
"mime-types": "^2.1.14",
|
||||
"morgan": "^1.10.0",
|
||||
"semver": "^7.6.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
import { execa } from 'execa';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
// we need a wrapper script because altool tends to error out very often
|
||||
// https://developer.apple.com/forums/thread/698477
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import assert from 'assert';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import logger from './logger.js';
|
||||
import { createMediaSourceProcess, readOneJpegFrame as readOneJpegFrameRaw } from './ffmpeg.js';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Store from 'electron-store';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import electron from 'electron';
|
||||
import { join, dirname } from 'path';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { pathExists } from 'fs-extra';
|
||||
|
||||
import { KeyBinding, Config } from '../../types.js';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { join } from 'path';
|
||||
import readline from 'readline';
|
||||
import { join } from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import stringToStream from 'string-to-stream';
|
||||
import { BufferEncodingOption, execa, ExecaChildProcess, Options as ExecaOptions } from 'execa';
|
||||
import assert from 'assert';
|
||||
import { Readable } from 'stream';
|
||||
import assert from 'node:assert';
|
||||
import { Readable } from 'node:stream';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { app } from 'electron';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import http from 'http';
|
||||
import http from 'node:http';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import assert from 'assert';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { homepage } from './constants.js';
|
||||
import logger from './logger.js';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// const LanguageDetector = window.require('i18next-electron-language-detector');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { join } from 'node:path';
|
||||
import { InitOptions } from 'i18next';
|
||||
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import debounce from 'lodash/debounce';
|
|||
import yargsParser from 'yargs-parser';
|
||||
import JSON5 from 'json5';
|
||||
import remote from '@electron/remote/main';
|
||||
import { stat } from 'fs/promises';
|
||||
import assert from 'assert';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import logger from './logger.js';
|
||||
import menu from './menu.js';
|
||||
|
@ -374,7 +374,8 @@ const readyPromise = app.whenReady();
|
|||
|
||||
|
||||
if (isDev) {
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,global-require,import/no-extraneous-dependencies
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
|
||||
|
||||
installExtension(REACT_DEVELOPER_TOOLS)
|
||||
.then((name: string) => logger.info('Added Extension', name))
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import winston from 'winston';
|
||||
import util from 'util';
|
||||
import util from 'node:util';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { join } from 'node:path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import type { TransformableInfo } from 'logform';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import os from 'os';
|
||||
import os from 'node:os';
|
||||
|
||||
export const platform = os.platform();
|
||||
export const arch = os.arch();
|
||||
|
|
|
@ -59,8 +59,8 @@ import {
|
|||
readFileMeta, getSmarterOutFormat, renderThumbnails as ffmpegRenderThumbnails,
|
||||
extractStreams, setCustomFfPath as ffmpegSetCustomFfPath,
|
||||
isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl,
|
||||
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
|
||||
RefuseOverwriteError, abortFfmpegs,
|
||||
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrackVtt,
|
||||
RefuseOverwriteError, abortFfmpegs, extractSubtitleTrackToSegments,
|
||||
} from './ffmpeg';
|
||||
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback, isStreamThumbnail, getSubtitleStreams, getVideoTrackForStreamIndex, getAudioTrackForStreamIndex, enableVideoTrack, enableAudioTrack } from './util/streams';
|
||||
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
|
||||
|
@ -87,7 +87,7 @@ import BigWaveform from './components/BigWaveform';
|
|||
|
||||
import isDev from './isDev';
|
||||
import { Chapter, ChromiumHTMLVideoElement, CustomTagsByFile, EdlFileType, FfmpegCommandLog, FilesMeta, FormatTimecode, ParamsByStreamId, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
|
||||
import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode } from '../../../types';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
|
||||
|
||||
const electron = window.require('electron');
|
||||
|
@ -151,7 +151,7 @@ function App() {
|
|||
|
||||
// State per application launch
|
||||
const lastOpenedPathRef = useRef<string>();
|
||||
const [waveformMode, setWaveformMode] = useState<'big-waveform' | 'waveform'>();
|
||||
const [waveformMode, setWaveformMode] = useState<WaveformMode>();
|
||||
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
|
||||
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
|
||||
const [showRightBar, setShowRightBar] = useState(true);
|
||||
|
@ -175,10 +175,11 @@ function App() {
|
|||
|
||||
// Store "working" in a ref so we can avoid race conditions
|
||||
const workingRef = useRef(!!working);
|
||||
const setWorking = useCallback((val: { text: string, abortController?: AbortController } | undefined) => {
|
||||
workingRef.current = !!val;
|
||||
const setWorking = useCallback((valOrBool: { text: string, abortController?: AbortController } | true | undefined) => {
|
||||
workingRef.current = !!valOrBool;
|
||||
const val = valOrBool === true ? { text: t('Loading') } : valOrBool;
|
||||
setWorkingState(val ? { text: val.text, abortController: val.abortController } : undefined);
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const handleAbortWorkingClick = useCallback(() => {
|
||||
console.log('User clicked abort');
|
||||
|
@ -214,7 +215,7 @@ function App() {
|
|||
const videoRef = useRef<ChromiumHTMLVideoElement>(null);
|
||||
const videoContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setOutputPlaybackRate = useCallback((v) => {
|
||||
const setOutputPlaybackRate = useCallback((v: number) => {
|
||||
setOutputPlaybackRateState(v);
|
||||
if (videoRef.current) videoRef.current.playbackRate = v;
|
||||
}, []);
|
||||
|
@ -407,7 +408,7 @@ function App() {
|
|||
}, [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, onSelectSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
|
||||
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
|
||||
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode });
|
||||
|
||||
|
||||
|
@ -643,7 +644,7 @@ function App() {
|
|||
try {
|
||||
setWorking({ text: i18n.t('Loading subtitle') });
|
||||
invariant(filePath != null);
|
||||
const url = await extractSubtitleTrack(filePath, index);
|
||||
const url = await extractSubtitleTrackVtt(filePath, index);
|
||||
setSubtitlesByStreamId((old) => ({ ...old, [index]: { url, lang: subtitleStream.tags && subtitleStream.tags.language } }));
|
||||
setActiveSubtitleStreamIndex(index);
|
||||
} catch (err) {
|
||||
|
@ -1462,6 +1463,17 @@ function App() {
|
|||
loadCutSegments(await readEdlFile({ type, path }), append);
|
||||
}, [loadCutSegments]);
|
||||
|
||||
const loadSubtitleTrackToSegments = useCallback(async (streamId: number) => {
|
||||
invariant(filePath != null);
|
||||
setWorking(true);
|
||||
try {
|
||||
setStreamsSelectorShown(false);
|
||||
loadCutSegments(await extractSubtitleTrackToSegments(filePath, streamId), true);
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
}
|
||||
}, [filePath, loadCutSegments, setWorking]);
|
||||
|
||||
const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string }) => {
|
||||
async function tryOpenProjectPath(path, type) {
|
||||
if (!(await exists(path))) return false;
|
||||
|
@ -1622,7 +1634,7 @@ function App() {
|
|||
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
|
||||
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
|
||||
|
||||
const seekClosestKeyframe = useCallback((direction) => {
|
||||
const seekClosestKeyframe = useCallback((direction: number) => {
|
||||
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
|
||||
if (time == null) return;
|
||||
userSeekAbs(time);
|
||||
|
@ -2124,6 +2136,7 @@ function App() {
|
|||
setCutEnd,
|
||||
cleanupFilesDialog,
|
||||
splitCurrentSegment,
|
||||
focusSegmentAtCursor,
|
||||
increaseRotation,
|
||||
goToTimecode,
|
||||
seekBackwards: ({ keyup }) => seekRel2({ keyup, amount: -1 * keyboardNormalSeekSpeed }),
|
||||
|
@ -2226,7 +2239,7 @@ function App() {
|
|||
};
|
||||
|
||||
return ret;
|
||||
}, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegments.length, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardSeekSpeed2, keyboardSeekSpeed3, onExportPress, onLabelSegment, openDirDialog, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCurrentSegIndex, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleStripThumbnail, 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, cutSegments.length, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardSeekSpeed2, keyboardSeekSpeed3, onExportPress, onLabelSegment, openDirDialog, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCurrentSegIndex, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, focusSegmentAtCursor, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleStripThumbnail, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
|
||||
|
||||
const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]);
|
||||
|
||||
|
@ -2516,7 +2529,6 @@ function App() {
|
|||
<AnimatePresence>
|
||||
{showLeftBar && (
|
||||
<BatchFilesList
|
||||
// @ts-expect-error todo
|
||||
selectedBatchFiles={selectedBatchFiles}
|
||||
filePath={filePath}
|
||||
width={leftBarWidth}
|
||||
|
@ -2683,7 +2695,6 @@ function App() {
|
|||
/>
|
||||
|
||||
<BottomBar
|
||||
// @ts-expect-error todo
|
||||
zoom={zoom}
|
||||
setZoom={setZoom}
|
||||
timelineToggleComfortZoom={timelineToggleComfortZoom}
|
||||
|
@ -2760,6 +2771,7 @@ function App() {
|
|||
paramsByStreamId={paramsByStreamId}
|
||||
updateStreamParams={updateStreamParams}
|
||||
formatTimecode={formatTimecode}
|
||||
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
|
||||
/>
|
||||
)}
|
||||
</Sheet>
|
||||
|
|
|
@ -5,7 +5,8 @@ import { FaTrashAlt, FaSave } from 'react-icons/fa';
|
|||
import { mySpring } from './animations';
|
||||
import { saveColor } from './colors';
|
||||
|
||||
const BetweenSegments = memo(({ start, end, duration, invertCutSegments }: { start: number, end: number, duration: number, invertCutSegments: boolean }) => {
|
||||
|
||||
function BetweenSegments({ start, end, duration, invertCutSegments }: { start: number, end: number, duration: number, invertCutSegments: boolean }) {
|
||||
const left = `${(start / duration) * 100}%`;
|
||||
|
||||
return (
|
||||
|
@ -38,6 +39,6 @@ const BetweenSegments = memo(({ start, end, duration, invertCutSegments }: { sta
|
|||
<div style={{ flexGrow: 1, borderBottom: '1px dashed var(--gray10)', marginLeft: 5, marginRight: 5 }} />
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default BetweenSegments;
|
||||
export default memo(BetweenSegments);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { MdRotate90DegreesCcw } from 'react-icons/md';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -23,15 +23,18 @@ import { useSegColors } from './contexts';
|
|||
import { isExactDurationMatch } from './util/duration';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
import { askForPlaybackRate } from './dialogs';
|
||||
import { ApparentCutSegment, FormatTimecode, ParseTimecode, SegmentToExport, StateSegment } from './types';
|
||||
import { WaveformMode } from '../../../types';
|
||||
|
||||
const { clipboard } = window.require('electron');
|
||||
|
||||
|
||||
const zoomOptions = Array.from({ length: 13 }).fill().map((unused, z) => 2 ** z);
|
||||
const zoomOptions = Array.from({ length: 13 }).fill(undefined).map((_unused, z) => 2 ** z);
|
||||
|
||||
const leftRightWidth = 100;
|
||||
|
||||
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }: { invertCutSegments: boolean, setInvertCutSegments: Dispatch<SetStateAction<boolean>> }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onYinYangClick = useCallback(() => {
|
||||
|
@ -63,15 +66,27 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
|
|||
});
|
||||
|
||||
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }) => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }: {
|
||||
darkMode: boolean,
|
||||
cutTime: number,
|
||||
setCutTime: (type: 'start' | 'end', v: number) => void,
|
||||
startTimeOffset: number,
|
||||
seekAbs: (a: number) => void,
|
||||
currentCutSeg: StateSegment,
|
||||
currentApparentCutSeg: ApparentCutSegment,
|
||||
isStart?: boolean,
|
||||
formatTimecode: FormatTimecode,
|
||||
parseTimecode: ParseTimecode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
const [cutTimeManual, setCutTimeManual] = useState();
|
||||
const [cutTimeManual, setCutTimeManual] = useState<string>();
|
||||
|
||||
// Clear manual overrides if upstream cut time has changed
|
||||
useEffect(() => {
|
||||
setCutTimeManual();
|
||||
setCutTimeManual(undefined);
|
||||
}, [setCutTimeManual, currentApparentCutSeg.start, currentApparentCutSeg.end]);
|
||||
|
||||
const isCutTimeManualSet = () => cutTimeManual !== undefined;
|
||||
|
@ -81,7 +96,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
|||
return `.1em solid ${darkMode ? segColor.desaturate(0.4).lightness(50).string() : segColor.desaturate(0.2).lightness(60).string()}`;
|
||||
}, [currentCutSeg, darkMode, getSegColor]);
|
||||
|
||||
const cutTimeInputStyle = {
|
||||
const cutTimeInputStyle: CSSProperties = {
|
||||
border, borderRadius: 5, backgroundColor: 'var(--gray5)', transition: darkModeTransition, fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none',
|
||||
};
|
||||
|
||||
|
@ -90,7 +105,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
|||
try {
|
||||
setCutTime(isStart ? 'start' : 'end', timeWithoutOffset);
|
||||
seekAbs(timeWithoutOffset);
|
||||
setCutTimeManual();
|
||||
setCutTimeManual(undefined);
|
||||
} catch (err) {
|
||||
console.error('Cannot set cut time', err);
|
||||
// If we get an error from setCutTime, remain in the editing state (cutTimeManual)
|
||||
|
@ -102,7 +117,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
|||
e.preventDefault();
|
||||
|
||||
// Don't proceed if not a valid time value
|
||||
const timeWithOffset = parseTimecode(cutTimeManual);
|
||||
const timeWithOffset = cutTimeManual != null ? parseTimecode(cutTimeManual) : undefined;
|
||||
if (timeWithOffset === undefined) return;
|
||||
|
||||
trySetTime(timeWithOffset);
|
||||
|
@ -156,7 +171,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
|||
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
|
||||
onChange={(e) => handleCutTimeInput(e.target.value)}
|
||||
onPaste={handleCutTimePaste}
|
||||
onBlur={() => setCutTimeManual()}
|
||||
onBlur={() => setCutTimeManual(undefined)}
|
||||
onContextMenu={handleContextMenu}
|
||||
value={isCutTimeManualSet()
|
||||
? cutTimeManual
|
||||
|
@ -166,7 +181,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
|
|||
);
|
||||
});
|
||||
|
||||
const BottomBar = memo(({
|
||||
function BottomBar({
|
||||
zoom, setZoom, timelineToggleComfortZoom,
|
||||
isRotationSet, rotation, areWeCutting, increaseRotation, cleanupFilesDialog,
|
||||
captureSnapshot, onExportPress, segmentsToExport, hasVideo,
|
||||
|
@ -179,14 +194,62 @@ const BottomBar = memo(({
|
|||
toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
|
||||
outputPlaybackRate, setOutputPlaybackRate,
|
||||
formatTimecode, parseTimecode,
|
||||
}) => {
|
||||
}: {
|
||||
zoom: number,
|
||||
setZoom: Dispatch<SetStateAction<number>>,
|
||||
timelineToggleComfortZoom: () => void,
|
||||
isRotationSet: boolean,
|
||||
rotation: number,
|
||||
areWeCutting: boolean,
|
||||
increaseRotation: () => void,
|
||||
cleanupFilesDialog: () => void,
|
||||
captureSnapshot: () => void,
|
||||
onExportPress: () => void,
|
||||
segmentsToExport: SegmentToExport[],
|
||||
hasVideo: boolean,
|
||||
seekAbs: (a: number) => void,
|
||||
currentSegIndexSafe: number,
|
||||
cutSegments: StateSegment[],
|
||||
currentCutSeg: StateSegment,
|
||||
setCutStart: () => void,
|
||||
setCutEnd: () => void,
|
||||
setCurrentSegIndex: Dispatch<SetStateAction<number>>,
|
||||
jumpTimelineStart: () => void,
|
||||
jumpTimelineEnd: () => void,
|
||||
jumpCutEnd: () => void,
|
||||
jumpCutStart: () => void,
|
||||
startTimeOffset: number,
|
||||
setCutTime: (type: 'start' | 'end', v: number) => void,
|
||||
currentApparentCutSeg: ApparentCutSegment,
|
||||
playing: boolean,
|
||||
shortStep: (a: number) => void,
|
||||
togglePlay: () => void,
|
||||
toggleLoopSelectedSegments: () => void,
|
||||
hasAudio: boolean,
|
||||
keyframesEnabled: boolean,
|
||||
toggleShowKeyframes: () => void,
|
||||
seekClosestKeyframe: (a: number) => void,
|
||||
detectedFps: number | undefined,
|
||||
isFileOpened: boolean,
|
||||
selectedSegments: ApparentCutSegment[],
|
||||
darkMode: boolean,
|
||||
setDarkMode: Dispatch<SetStateAction<boolean>>,
|
||||
toggleShowThumbnails: () => void,
|
||||
toggleWaveformMode: () => void,
|
||||
waveformMode: WaveformMode | undefined,
|
||||
showThumbnails: boolean,
|
||||
outputPlaybackRate: number,
|
||||
setOutputPlaybackRate: (v: number) => void,
|
||||
formatTimecode: FormatTimecode,
|
||||
parseTimecode: ParseTimecode,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
// ok this is a bit over-engineered but what the hell!
|
||||
const loopSelectedSegmentsButtonStyle = useMemo(() => {
|
||||
// cannot have less than 1 gradient element:
|
||||
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0], selectedSegments[0]]).slice(0, 10);
|
||||
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0]!, selectedSegments[0]!]).slice(0, 10);
|
||||
|
||||
const gradientColors = selectedSegmentsSafe.map((seg, i) => {
|
||||
const segColor = getSegColorRaw(seg);
|
||||
|
@ -231,7 +294,7 @@ const BottomBar = memo(({
|
|||
const opacity = seg ? undefined : 0.5;
|
||||
const text = seg ? `${newIndex + 1}` : '-';
|
||||
const wide = text.length > 1;
|
||||
const segButtonStyle = {
|
||||
const segButtonStyle: CSSProperties = {
|
||||
backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: seg ? 'white' : undefined, fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px',
|
||||
};
|
||||
|
||||
|
@ -260,7 +323,7 @@ const BottomBar = memo(({
|
|||
{hasAudio && (
|
||||
<GiSoundWaves
|
||||
size={24}
|
||||
style={{ padding: '0 .1em', color: ['big-waveform', 'waveform'].includes(waveformMode) ? primaryTextColor : undefined }}
|
||||
style={{ padding: '0 .1em', color: waveformMode != null && ['big-waveform', 'waveform'].includes(waveformMode) ? primaryTextColor : undefined }}
|
||||
role="button"
|
||||
title={t('Show waveform')}
|
||||
onClick={() => toggleWaveformMode()}
|
||||
|
@ -458,6 +521,6 @@ const BottomBar = memo(({
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default BottomBar;
|
||||
export default memo(BottomBar);
|
|
@ -5,9 +5,9 @@ import CopyClipboardButton from './components/CopyClipboardButton';
|
|||
import Sheet from './components/Sheet';
|
||||
import { FfmpegCommandLog } from './types';
|
||||
|
||||
const LastCommandsSheet = memo(({ visible, onTogglePress, ffmpegCommandLog }: {
|
||||
function LastCommandsSheet({ visible, onTogglePress, ffmpegCommandLog }: {
|
||||
visible: boolean, onTogglePress: () => void, ffmpegCommandLog: FfmpegCommandLog,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -28,6 +28,6 @@ const LastCommandsSheet = memo(({ visible, onTogglePress, ffmpegCommandLog }: {
|
|||
)}
|
||||
</Sheet>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default LastCommandsSheet;
|
||||
export default memo(LastCommandsSheet);
|
||||
|
|
|
@ -8,9 +8,9 @@ import useUserSettings from './hooks/useUserSettings';
|
|||
|
||||
const electron = window.require('electron');
|
||||
|
||||
const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }: {
|
||||
function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode }: {
|
||||
mifiLink: unknown, currentCutSeg, onClick: () => void, darkMode?: boolean,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { simpleMode } = useUserSettings();
|
||||
|
||||
|
@ -48,6 +48,6 @@ const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }: {
|
|||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default NoFileLoaded;
|
||||
export default memo(NoFileLoaded);
|
||||
|
|
|
@ -26,6 +26,7 @@ const buttonBaseStyle = {
|
|||
|
||||
const neutralButtonColor = 'var(--gray8)';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Segment = memo(({
|
||||
seg,
|
||||
index,
|
||||
|
@ -218,7 +219,7 @@ const Segment = memo(({
|
|||
);
|
||||
});
|
||||
|
||||
const SegmentList = memo(({
|
||||
function SegmentList({
|
||||
width,
|
||||
formatTimecode,
|
||||
apparentCutSegments,
|
||||
|
@ -294,7 +295,7 @@ const SegmentList = memo(({
|
|||
setEditingSegmentTags: Dispatch<SetStateAction<SegmentTags | undefined>>,
|
||||
setEditingSegmentTagsSegmentIndex: Dispatch<SetStateAction<number | undefined>>,
|
||||
onEditSegmentTags: (index: number) => void,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
|
@ -502,6 +503,6 @@ const SegmentList = memo(({
|
|||
</motion.div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default SegmentList;
|
||||
export default memo(SegmentList);
|
||||
|
|
|
@ -28,6 +28,7 @@ interface EditingStream {
|
|||
path: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setCustomTagsByFile, editingTag, setEditingTag }: {
|
||||
editingFile: string, allFilesMeta: FilesMeta, customTagsByFile: CustomTagsByFile, setCustomTagsByFile: Dispatch<SetStateAction<CustomTagsByFile>>, editingTag: string | undefined, setEditingTag: (tag: string | undefined) => void
|
||||
}) => {
|
||||
|
@ -92,6 +93,7 @@ function StreamParametersEditor({ stream, streamParams, updateStreamParams }: {
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, path: editingFile }, setEditingStream, allFilesMeta, paramsByStreamId, updateStreamParams }: {
|
||||
editingStream: EditingStream, setEditingStream: Dispatch<SetStateAction<EditingStream | undefined>>, allFilesMeta: FilesMeta, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams,
|
||||
}) => {
|
||||
|
@ -149,8 +151,9 @@ function onInfoClick(json: unknown, title: string) {
|
|||
showJson5Dialog({ title, json });
|
||||
}
|
||||
|
||||
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode }: {
|
||||
filePath: string, stream: FFprobeStream, onToggle: (a: number) => void, batchSetCopyStreamIds: (filter: (a: FFprobeStream) => boolean, enabled: boolean) => void, copyStream: boolean, fileDuration: number | undefined, setEditingStream: (a: EditingStream) => void, onExtractStreamPress?: () => void, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams, formatTimecode: FormatTimecode,
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments }: {
|
||||
filePath: string, stream: FFprobeStream, onToggle: (a: number) => void, batchSetCopyStreamIds: (filter: (a: FFprobeStream) => boolean, enabled: boolean) => void, copyStream: boolean, fileDuration: number | undefined, setEditingStream: (a: EditingStream) => void, onExtractStreamPress?: () => void, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams, formatTimecode: FormatTimecode, loadSubtitleTrackToSegments?: (index: number) => void,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -205,6 +208,10 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
|
|||
});
|
||||
}, [filePath, updateStreamParams, stream.index]);
|
||||
|
||||
const onLoadSubtitleTrackToSegmentsClick = useCallback(() => {
|
||||
loadSubtitleTrackToSegments?.(stream.index);
|
||||
}, [loadSubtitleTrackToSegments, stream.index]);
|
||||
|
||||
const codecTag = stream.codec_tag !== '0x0000' && stream.codec_tag_string;
|
||||
|
||||
return (
|
||||
|
@ -251,6 +258,11 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
|
|||
{t('Extract this track as file')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{stream.codec_type === 'subtitle' && (
|
||||
<Menu.Item icon={<MdSubtitles color="black" />} onClick={onLoadSubtitleTrackToSegmentsClick}>
|
||||
{t('Create segments from subtitles')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
|
@ -319,7 +331,7 @@ const fileStyle: CSSProperties = { margin: '1.5em 1em 1.5em 1em', padding: 5, ov
|
|||
|
||||
|
||||
function StreamsSelector({
|
||||
mainFilePath, mainFileFormatData, mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId, setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta, showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams, customTagsByFile, setCustomTagsByFile, paramsByStreamId, updateStreamParams, formatTimecode,
|
||||
mainFilePath, mainFileFormatData, mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId, setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta, showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams, customTagsByFile, setCustomTagsByFile, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments,
|
||||
}: {
|
||||
mainFilePath: string,
|
||||
mainFileFormatData: FFprobeFormat | undefined,
|
||||
|
@ -342,6 +354,7 @@ function StreamsSelector({
|
|||
paramsByStreamId: ParamsByStreamId,
|
||||
updateStreamParams: UpdateStreamParams,
|
||||
formatTimecode: FormatTimecode,
|
||||
loadSubtitleTrackToSegments: (index: number) => void,
|
||||
}) {
|
||||
const [editingFile, setEditingFile] = useState<string>();
|
||||
const [editingStream, setEditingStream] = useState<EditingStream>();
|
||||
|
@ -406,27 +419,28 @@ function StreamsSelector({
|
|||
paramsByStreamId={paramsByStreamId}
|
||||
updateStreamParams={updateStreamParams}
|
||||
formatTimecode={formatTimecode}
|
||||
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{externalFilesEntries.map(([path, { streams, formatData }]) => (
|
||||
{externalFilesEntries.map(([path, { streams: externalFileStreams, formatData }]) => (
|
||||
<div key={path} style={fileStyle}>
|
||||
<FileHeading path={path} formatData={formatData} onTrashClick={() => removeFile(path)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(path, enabled)} />
|
||||
|
||||
<table style={tableStyle}>
|
||||
<Thead />
|
||||
<tbody>
|
||||
{streams.map((stream) => (
|
||||
{externalFileStreams.map((stream) => (
|
||||
<Stream
|
||||
key={stream.index}
|
||||
filePath={path}
|
||||
stream={stream}
|
||||
copyStream={isCopyingStreamId(path, stream.index)}
|
||||
onToggle={(streamId) => toggleCopyStreamId(path, streamId)}
|
||||
batchSetCopyStreamIds={(filter: (a: FFprobeStream) => boolean, enabled: boolean) => batchSetCopyStreamIdsForPath(path, streams, filter, enabled)}
|
||||
batchSetCopyStreamIds={(filter: (a: FFprobeStream) => boolean, enabled: boolean) => batchSetCopyStreamIdsForPath(path, externalFileStreams, filter, enabled)}
|
||||
setEditingStream={setEditingStream}
|
||||
fileDuration={getFormatDuration(formatData)}
|
||||
paramsByStreamId={paramsByStreamId}
|
||||
|
|
|
@ -20,6 +20,7 @@ type CalculateTimelinePercent = (time: number) => string | undefined;
|
|||
|
||||
const currentTimeWidth = 1;
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe }: {
|
||||
waveform: RenderableWaveform, calculateTimelinePercent: CalculateTimelinePercent, durationSafe: number,
|
||||
}) => {
|
||||
|
@ -43,6 +44,7 @@ const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe }: {
|
|||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, height }: {
|
||||
calculateTimelinePercent: CalculateTimelinePercent, durationSafe: number, waveforms: RenderableWaveform[], zoom: number, height: number,
|
||||
}) => (
|
||||
|
@ -53,6 +55,7 @@ const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoo
|
|||
</div>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CommandedTime = memo(({ commandedTimePercent }: { commandedTimePercent: string }) => {
|
||||
const color = 'var(--gray12)';
|
||||
const commonStyle: CSSProperties = { left: commandedTimePercent, position: 'absolute', pointerEvents: 'none' };
|
||||
|
@ -70,7 +73,7 @@ const timelineHeight = 36;
|
|||
const timeWrapperStyle: CSSProperties = { position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' };
|
||||
const timeStyle: CSSProperties = { background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' };
|
||||
|
||||
const Timeline = memo(({
|
||||
function Timeline({
|
||||
durationSafe,
|
||||
startTimeOffset,
|
||||
playerTime,
|
||||
|
@ -126,7 +129,7 @@ const Timeline = memo(({
|
|||
commandedTimeRef: MutableRefObject<number>,
|
||||
goToTimecode: () => void,
|
||||
isSegmentSelected: (a: { segId: string }) => boolean,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { invertCutSegments } = useUserSettings();
|
||||
|
@ -405,6 +408,6 @@ const Timeline = memo(({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default Timeline;
|
||||
export default memo(Timeline);
|
||||
|
|
|
@ -8,11 +8,11 @@ import { useSegColors } from './contexts';
|
|||
import { ApparentCutSegment, FormatTimecode } from './types';
|
||||
|
||||
|
||||
const TimelineSeg = memo(({
|
||||
function TimelineSeg({
|
||||
seg, duration, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
|
||||
} : {
|
||||
seg: ApparentCutSegment, duration: number, isActive: boolean, segNum: number, onSegClick: (a: number) => void, invertCutSegments: boolean, formatTimecode: FormatTimecode, selected: boolean,
|
||||
}) => {
|
||||
}) {
|
||||
const { darkMode } = useUserSettings();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
|
@ -103,6 +103,6 @@ const TimelineSeg = memo(({
|
|||
<div style={{ flexGrow: 1 }} />
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default TimelineSeg;
|
||||
export default memo(TimelineSeg);
|
||||
|
|
|
@ -16,7 +16,7 @@ import { InverseCutSegment } from './types';
|
|||
const outFmtStyle = { height: 20, maxWidth: 100 };
|
||||
const exportModeStyle = { flexGrow: 0, flexBasis: 140 };
|
||||
|
||||
const TopMenu = memo(({
|
||||
function TopMenu({
|
||||
filePath,
|
||||
fileFormat,
|
||||
copyAnyAudioTrack,
|
||||
|
@ -42,7 +42,7 @@ const TopMenu = memo(({
|
|||
selectedSegments: InverseCutSegment[],
|
||||
isCustomFormatSelected: boolean,
|
||||
clearOutDir: () => void,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { customOutDir, changeOutDir, simpleMode, outFormatLocked, setOutFormatLocked } = useUserSettings();
|
||||
|
||||
|
@ -113,6 +113,6 @@ const TopMenu = memo(({
|
|||
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default TopMenu;
|
||||
export default memo(TopMenu);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Button, ForkIcon, DisableIcon } from 'evergreen-ui';
|
|||
|
||||
import useUserSettings from '../hooks/useUserSettings';
|
||||
|
||||
const AutoExportToggler = memo(() => {
|
||||
function AutoExportToggler() {
|
||||
const { t } = useTranslation();
|
||||
const { autoExportExtraStreams, setAutoExportExtraStreams } = useUserSettings();
|
||||
|
||||
|
@ -13,6 +13,6 @@ const AutoExportToggler = memo(() => {
|
|||
{autoExportExtraStreams ? t('Extract') : t('Discard')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default AutoExportToggler;
|
||||
export default memo(AutoExportToggler);
|
||||
|
|
|
@ -5,9 +5,9 @@ import { FaAngleRight, FaFile } from 'react-icons/fa';
|
|||
import useContextMenu from '../hooks/useContextMenu';
|
||||
import { primaryTextColor } from '../colors';
|
||||
|
||||
const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }: {
|
||||
function BatchFile({ path, isOpen, isSelected, name, onSelect, onDelete }: {
|
||||
path: string, isOpen: boolean, isSelected: boolean, name: string, onSelect: (a: string) => void, onDelete: (a: string) => void
|
||||
}) => {
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
@ -26,6 +26,6 @@ const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }:
|
|||
{isOpen && <FaAngleRight size={14} style={{ color: 'var(--gray9)', marginRight: -5, flexShrink: 0 }} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default BatchFile;
|
||||
export default memo(BatchFile);
|
||||
|
|
|
@ -20,10 +20,10 @@ const iconStyle = {
|
|||
padding: '3px 5px',
|
||||
};
|
||||
|
||||
const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) => {
|
||||
function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [sortDesc, setSortDesc] = useState();
|
||||
const [sortDesc, setSortDesc] = useState<boolean>();
|
||||
|
||||
const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile }));
|
||||
|
||||
|
@ -70,6 +70,6 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
|
|||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default BatchFilesList;
|
||||
export default memo(BatchFilesList);
|
|
@ -5,9 +5,9 @@ import { ffmpegExtractWindow } from '../util/constants';
|
|||
import { RenderableWaveform } from '../types';
|
||||
|
||||
|
||||
const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }: {
|
||||
function BigWaveform({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }: {
|
||||
waveforms: RenderableWaveform[], relevantTime: number, playing: boolean, durationSafe: number, zoom: number, seekRel: (a: number) => void,
|
||||
}) => {
|
||||
}) {
|
||||
const windowSize = ffmpegExtractWindow * 2;
|
||||
const windowStart = Math.max(0, relevantTime - windowSize);
|
||||
const windowEnd = relevantTime + windowSize;
|
||||
|
@ -129,6 +129,6 @@ const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom
|
|||
<div style={{ pointerEvents: 'none', position: 'absolute', height: '100%', backgroundColor: 'var(--red11)', width: 1, left: '50%', top: 0 }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default BigWaveform;
|
||||
export default memo(BigWaveform);
|
||||
|
|
|
@ -2,9 +2,11 @@ import { ButtonHTMLAttributes, memo } from 'react';
|
|||
|
||||
import styles from './Button.module.css';
|
||||
|
||||
const Button = memo(({ type = 'button', ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type
|
||||
<button className={styles['button']} type={type} {...props} />
|
||||
));
|
||||
function Button({ type = 'button', ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type
|
||||
<button className={styles['button']} type={type} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
export default memo(Button);
|
||||
|
|
|
@ -6,7 +6,8 @@ import { FaImage } from 'react-icons/fa';
|
|||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import { withBlur } from '../util';
|
||||
|
||||
const CaptureFormatButton = memo(({ showIcon = false, ...props }: { showIcon?: boolean } & ButtonProps) => {
|
||||
|
||||
function CaptureFormatButton({ showIcon = false, ...props }: { showIcon?: boolean } & ButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { captureFormat, toggleCaptureFormat } = useUserSettings();
|
||||
return (
|
||||
|
@ -20,6 +21,6 @@ const CaptureFormatButton = memo(({ showIcon = false, ...props }: { showIcon?: b
|
|||
{captureFormat}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default CaptureFormatButton;
|
||||
export default memo(CaptureFormatButton);
|
||||
|
|
|
@ -26,9 +26,9 @@ const rowStyle: CSSProperties = {
|
|||
color: 'black', fontSize: 14, margin: '4px 0px', overflowY: 'auto', whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
|
||||
function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
|
||||
isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: FFprobeStream[], outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
|
||||
|
||||
|
@ -238,6 +238,6 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti
|
|||
</Dialog>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ConcatDialog;
|
||||
export default memo(ConcatDialog);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MotionStyle, motion, useAnimation } from 'framer-motion';
|
|||
const electron = window.require('electron');
|
||||
const { clipboard } = electron;
|
||||
|
||||
const CopyClipboardButton = memo(({ text, style }: { text: string, style?: MotionStyle }) => {
|
||||
function CopyClipboardButton({ text, style }: { text: string, style?: MotionStyle }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const animation = useAnimation();
|
||||
|
@ -24,6 +24,6 @@ const CopyClipboardButton = memo(({ text, style }: { text: string, style?: Motio
|
|||
<FaClipboard title={t('Copy to clipboard')} onClick={onClick} />
|
||||
</motion.span>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default CopyClipboardButton;
|
||||
export default memo(CopyClipboardButton);
|
||||
|
|
|
@ -8,9 +8,9 @@ import useUserSettings from '../hooks/useUserSettings';
|
|||
import { SegmentToExport } from '../types';
|
||||
|
||||
|
||||
const ExportButton = memo(({ segmentsToExport, areWeCutting, onClick, size = 1 }: {
|
||||
function ExportButton({ segmentsToExport, areWeCutting, onClick, size = 1 }: {
|
||||
segmentsToExport: SegmentToExport[], areWeCutting: boolean, onClick: () => void, size?: number | undefined,
|
||||
}) => {
|
||||
}) {
|
||||
const CutIcon = areWeCutting ? FiScissors : FaFileExport;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
@ -40,6 +40,6 @@ const ExportButton = memo(({ segmentsToExport, areWeCutting, onClick, size = 1 }
|
|||
{text}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ExportButton;
|
||||
export default memo(ExportButton);
|
||||
|
|
|
@ -35,11 +35,11 @@ const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 1
|
|||
|
||||
const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break-all', cursor: 'pointer' };
|
||||
|
||||
const warningStyle: CSSProperties = { color: 'var(--red11)', fontSize: '80%', marginBottom: '.5em' };
|
||||
const warningStyle: CSSProperties = { color: 'var(--orange8)', fontSize: '80%', marginBottom: '.5em' };
|
||||
|
||||
const HelpIcon = ({ onClick, style }: { onClick: () => void, style?: CSSProperties }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', color: primaryTextColor, verticalAlign: 'middle', ...style }} />;
|
||||
|
||||
const ExportConfirm = memo(({
|
||||
function ExportConfirm({
|
||||
areWeCutting,
|
||||
selectedSegments,
|
||||
segmentsToExport,
|
||||
|
@ -89,7 +89,7 @@ const ExportConfirm = memo(({
|
|||
setMergedOutFileName: (a: string) => void,
|
||||
smartCutBitrate: number | undefined,
|
||||
setSmartCutBitrate: Dispatch<SetStateAction<number | undefined>>,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { changeOutDir, keyframeCut, toggleKeyframeCut, preserveMovData, movFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, preserveMetadataOnMerge, togglePreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, ffmpegExperimental, setFfmpegExperimental, cutFromAdjustmentFrames, setCutFromAdjustmentFrames } = useUserSettings();
|
||||
|
@ -519,7 +519,7 @@ const ExportConfirm = memo(({
|
|||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 50 }}
|
||||
transition={{ duration: 0.4, easings: ['easeOut'] }}
|
||||
style={{ display: 'flex', alignItems: 'flex-end', background: 'rgba(0,0,0,0.5)' }}
|
||||
style={{ display: 'flex', alignItems: 'flex-end', background: 'var(--gray2)' }}
|
||||
>
|
||||
<ToggleExportConfirm size={25} />
|
||||
<div style={{ fontSize: 13, marginLeft: 3, marginRight: 7, maxWidth: 120, lineHeight: '100%', color: exportConfirmEnabled ? 'var(--gray12)' : 'var(--gray11)', cursor: 'pointer' }} role="button" onClick={toggleExportConfirmEnabled}>{t('Show this page before exporting?')}</div>
|
||||
|
@ -539,6 +539,6 @@ const ExportConfirm = memo(({
|
|||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ExportConfirm;
|
||||
export default memo(ExportConfirm);
|
||||
|
|
|
@ -7,7 +7,7 @@ import Select from './Select';
|
|||
import { ExportMode } from '../types';
|
||||
|
||||
|
||||
const ExportModeButton = memo(({ selectedSegments, style }: { selectedSegments: unknown[], style?: CSSProperties }) => {
|
||||
function ExportModeButton({ selectedSegments, style }: { selectedSegments: unknown[], style?: CSSProperties }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { effectiveExportMode, setAutoMerge, setAutoDeleteMergedSegments, setSegmentsToChaptersOnly } = useUserSettings();
|
||||
|
@ -75,6 +75,6 @@ const ExportModeButton = memo(({ selectedSegments, style }: { selectedSegments:
|
|||
})}
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ExportModeButton;
|
||||
export default memo(ExportModeButton);
|
||||
|
|
|
@ -44,6 +44,7 @@ function fixKeys(keys: string[]) {
|
|||
return orderBy(uniqed, [(key) => key !== 'shift', (key) => key !== 'ctrl', (key) => key !== 'alt', (key) => key !== 'meta', (key) => key]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CreateBinding = memo(({
|
||||
actionsMap, action, setCreatingBinding, onNewKeyBindingConfirmed,
|
||||
}: {
|
||||
|
@ -117,6 +118,7 @@ const CreateBinding = memo(({
|
|||
|
||||
const rowStyle = { display: 'flex', alignItems: 'center', margin: '.2em 0', borderBottom: '1px solid rgba(0,0,0,0.1)', paddingBottom: '.5em' };
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const KeyboardShortcuts = memo(({
|
||||
keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg,
|
||||
}: {
|
||||
|
@ -317,6 +319,10 @@ const KeyboardShortcuts = memo(({
|
|||
name: t('Split segment at cursor'),
|
||||
category: segmentsAndCutpointsCategory,
|
||||
},
|
||||
focusSegmentAtCursor: {
|
||||
name: t('Focus segment at cursor'),
|
||||
category: segmentsAndCutpointsCategory,
|
||||
},
|
||||
duplicateCurrentSegment: {
|
||||
name: t('Duplicate current segment'),
|
||||
category: segmentsAndCutpointsCategory,
|
||||
|
@ -771,11 +777,11 @@ const KeyboardShortcuts = memo(({
|
|||
);
|
||||
});
|
||||
|
||||
const KeyboardShortcutsDialog = memo(({
|
||||
function KeyboardShortcutsDialog({
|
||||
isShown, onHide, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg,
|
||||
}: {
|
||||
isShown: boolean, onHide: () => void, keyBindings: KeyBinding[], setKeyBindings: Dispatch<SetStateAction<KeyBinding[]>>, resetKeyBindings: () => void, currentCutSeg: StateSegment,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -791,6 +797,6 @@ const KeyboardShortcutsDialog = memo(({
|
|||
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> : <div />}
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default KeyboardShortcutsDialog;
|
||||
export default memo(KeyboardShortcutsDialog);
|
||||
|
|
|
@ -3,10 +3,12 @@ import { memo } from 'react';
|
|||
import TextInput from './TextInput';
|
||||
|
||||
|
||||
const MergedOutFileName = memo(({ mergedOutFileName, setMergedOutFileName }: { mergedOutFileName: string | undefined, setMergedOutFileName: (a: string) => void }) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||
<TextInput value={mergedOutFileName ?? ''} onChange={(e) => setMergedOutFileName(e.target.value)} style={{ textAlign: 'right' }} />
|
||||
</div>
|
||||
));
|
||||
function MergedOutFileName({ mergedOutFileName, setMergedOutFileName }: { mergedOutFileName: string | undefined, setMergedOutFileName: (a: string) => void }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||
<TextInput value={mergedOutFileName ?? ''} onChange={(e) => setMergedOutFileName(e.target.value)} style={{ textAlign: 'right' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MergedOutFileName;
|
||||
export default memo(MergedOutFileName);
|
||||
|
|
|
@ -5,12 +5,12 @@ import useUserSettings from '../hooks/useUserSettings';
|
|||
import Switch from './Switch';
|
||||
|
||||
|
||||
const MovFastStartButton = memo(() => {
|
||||
function MovFastStartButton() {
|
||||
const { movFastStart, toggleMovFastStart } = useUserSettings();
|
||||
|
||||
return (
|
||||
<Switch checked={movFastStart} onCheckedChange={withBlur(toggleMovFastStart)} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default MovFastStartButton;
|
||||
export default memo(MovFastStartButton);
|
|
@ -23,9 +23,9 @@ const formatVariable = (variable) => `\${${variable}}`;
|
|||
|
||||
const extVar = formatVariable('EXT');
|
||||
|
||||
const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }: {
|
||||
function OutSegTemplateEditor({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }: {
|
||||
outSegTemplate: string, setOutSegTemplate: (text: string) => void, generateOutSegFileNames: GenerateOutSegFileNames, currentSegIndexSafe: number,
|
||||
}) => {
|
||||
}) {
|
||||
const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings();
|
||||
|
||||
const [text, setText] = useState(outSegTemplate);
|
||||
|
@ -174,6 +174,6 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
|
|||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default OutSegTemplateEditor;
|
||||
export default memo(OutSegTemplateEditor);
|
||||
|
|
|
@ -9,15 +9,15 @@ const commonVideoAudioFormats = ['matroska', 'mov', 'mp4', 'mpegts', 'ogv', 'web
|
|||
const commonAudioFormats = ['flac', 'ipod', 'mp3', 'oga', 'ogg', 'opus', 'wav'];
|
||||
const commonSubtitleFormats = ['ass', 'srt', 'sup', 'webvtt'];
|
||||
|
||||
function renderFormatOptions(formats) {
|
||||
function renderFormatOptions(formats: string[]) {
|
||||
return formats.map((format) => (
|
||||
<option key={format} value={format}>{format} - {allOutFormats[format]}</option>
|
||||
));
|
||||
}
|
||||
|
||||
const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutputFormatUserChange }: {
|
||||
function OutputFormatSelect({ style, detectedFileFormat, fileFormat, onOutputFormatUserChange }: {
|
||||
style: CSSProperties, detectedFileFormat?: string | undefined, fileFormat?: string | undefined, onOutputFormatUserChange: (a: string) => void,
|
||||
}) => {
|
||||
}) {
|
||||
const commonVideoAudioFormatsExceptDetectedFormat = useMemo(() => commonVideoAudioFormats.filter((f) => f !== detectedFileFormat), [detectedFileFormat]);
|
||||
const commonAudioFormatsExceptDetectedFormat = useMemo(() => commonAudioFormats.filter((f) => f !== detectedFileFormat), [detectedFileFormat]);
|
||||
const commonSubtitleFormatsExceptDetectedFormat = useMemo(() => commonSubtitleFormats.filter((f) => f !== detectedFileFormat), [detectedFileFormat]);
|
||||
|
@ -49,6 +49,6 @@ const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutp
|
|||
{renderFormatOptions(otherFormats)}
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default OutputFormatSelect;
|
||||
export default memo(OutputFormatSelect);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MdSubtitles } from 'react-icons/md';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import Select from './Select';
|
||||
|
||||
const PlaybackStreamSelector = memo(({
|
||||
function PlaybackStreamSelector({
|
||||
subtitleStreams,
|
||||
videoStreams,
|
||||
audioStreams,
|
||||
|
@ -23,7 +23,7 @@ const PlaybackStreamSelector = memo(({
|
|||
onActiveSubtitleChange: (a?: number | undefined) => void,
|
||||
onActiveVideoStreamChange: (a?: number | undefined) => void,
|
||||
onActiveAudioStreamChange: (a?: number | undefined) => void,
|
||||
}) => {
|
||||
}) {
|
||||
const [controlVisible, setControlVisible] = useState(false);
|
||||
const timeoutRef = useRef<number>();
|
||||
|
||||
|
@ -105,6 +105,6 @@ const PlaybackStreamSelector = memo(({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default PlaybackStreamSelector;
|
||||
export default memo(PlaybackStreamSelector);
|
||||
|
|
|
@ -5,12 +5,12 @@ import useUserSettings from '../hooks/useUserSettings';
|
|||
import Switch from './Switch';
|
||||
|
||||
|
||||
const PreserveMovDataButton = memo(() => {
|
||||
function PreserveMovDataButton() {
|
||||
const { preserveMovData, togglePreserveMovData } = useUserSettings();
|
||||
|
||||
return (
|
||||
<Switch checked={preserveMovData} onCheckedChange={withBlur(togglePreserveMovData)} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default PreserveMovDataButton;
|
||||
export default memo(PreserveMovDataButton);
|
|
@ -2,9 +2,12 @@ import { SelectHTMLAttributes, memo } from 'react';
|
|||
|
||||
import styles from './Select.module.css';
|
||||
|
||||
const Select = memo((props: SelectHTMLAttributes<HTMLSelectElement>) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<select className={styles['select']} {...props} />
|
||||
));
|
||||
|
||||
export default Select;
|
||||
function Select(props: SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<select className={styles['select']} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Select);
|
||||
|
|
|
@ -41,7 +41,7 @@ const Header = ({ title }: { title: string }) => (
|
|||
|
||||
const detailsStyle: CSSProperties = { opacity: 0.75, fontSize: '.9em', marginTop: '.3em' };
|
||||
|
||||
const Settings = memo(({
|
||||
function Settings({
|
||||
onTunerRequested,
|
||||
onKeyboardShortcutsDialogRequested,
|
||||
askForCleanupChoices,
|
||||
|
@ -55,7 +55,7 @@ const Settings = memo(({
|
|||
toggleStoreProjectInWorkingDir: () => Promise<void>,
|
||||
simpleMode: boolean,
|
||||
clearOutDir: () => Promise<void>,
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);
|
||||
|
||||
|
@ -481,6 +481,6 @@ const Settings = memo(({
|
|||
</table>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
export default memo(Settings);
|
||||
|
|
|
@ -5,9 +5,10 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import styles from './Sheet.module.css';
|
||||
|
||||
const Sheet = memo(({ visible, onClosePress, children, maxWidth = 800, style }: {
|
||||
|
||||
function Sheet({ visible, onClosePress, children, maxWidth = 800, style }: {
|
||||
visible: boolean, onClosePress: () => void, children: ReactNode, maxWidth?: number, style?: CSSProperties
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -30,6 +31,6 @@ const Sheet = memo(({ visible, onClosePress, children, maxWidth = 800, style }:
|
|||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default Sheet;
|
||||
export default memo(Sheet);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { primaryTextColor } from '../colors';
|
|||
import useUserSettings from '../hooks/useUserSettings';
|
||||
|
||||
|
||||
const SimpleModeButton = memo(({ size = 20, style }: { size?: number, style: CSSProperties }) => {
|
||||
function SimpleModeButton({ size = 20, style }: { size?: number, style: CSSProperties }) {
|
||||
const { t } = useTranslation();
|
||||
const { simpleMode, toggleSimpleMode } = useUserSettings();
|
||||
|
||||
|
@ -18,6 +18,6 @@ const SimpleModeButton = memo(({ size = 20, style }: { size?: number, style: CSS
|
|||
onClick={toggleSimpleMode}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default SimpleModeButton;
|
||||
export default memo(SimpleModeButton);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
|
||||
const inputStyle: CSSProperties = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' };
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const TextInput = forwardRef<HTMLInputElement, JSX.IntrinsicElements['input']>(({ style, ...props }, forwardedRef) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<input type="text" ref={forwardedRef} style={{ ...inputStyle, ...style }} {...props} />
|
||||
|
|
|
@ -7,13 +7,13 @@ import { primaryTextColor } from '../colors';
|
|||
import useUserSettings from '../hooks/useUserSettings';
|
||||
|
||||
|
||||
const ToggleExportConfirm = memo(({ size = 23, style }: { size?: number | undefined, style?: CSSProperties }) => {
|
||||
function ToggleExportConfirm({ size = 23, style }: { size?: number | undefined, style?: CSSProperties }) {
|
||||
const { t } = useTranslation();
|
||||
const { exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings();
|
||||
|
||||
return (
|
||||
<MdEventNote style={{ color: exportConfirmEnabled ? primaryTextColor : 'var(--gray11)', ...style }} size={size} title={t('Show export options screen before exporting?')} role="button" onClick={toggleExportConfirmEnabled} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ToggleExportConfirm;
|
||||
export default memo(ToggleExportConfirm);
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
|||
import Switch from './Switch';
|
||||
|
||||
|
||||
const ValueTuner = memo(({ style, title, value, setValue, onFinished, resolution = 1000, min: minIn = 0, max: maxIn = 1, resetToDefault }: {
|
||||
function ValueTuner({ style, title, value, setValue, onFinished, resolution = 1000, min: minIn = 0, max: maxIn = 1, resetToDefault }: {
|
||||
style?: CSSProperties, title: string, value: number, setValue: (string) => void, onFinished: () => void, resolution?: number, min?: number, max?: number, resetToDefault: () => void
|
||||
}) => {
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [min, setMin] = useState(minIn);
|
||||
|
@ -49,6 +49,6 @@ const ValueTuner = memo(({ style, title, value, setValue, onFinished, resolution
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default ValueTuner;
|
||||
export default memo(ValueTuner);
|
||||
|
|
|
@ -5,7 +5,8 @@ import ValueTuner from './ValueTuner';
|
|||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import { TunerType } from '../types';
|
||||
|
||||
const ValueTuners = memo(({ type, onFinished }: { type: TunerType, onFinished: () => void }) => {
|
||||
|
||||
function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { wheelSensitivity, setWheelSensitivity, keyboardNormalSeekSpeed, keyboardSeekSpeed2, setKeyboardSeekSpeed2, keyboardSeekSpeed3, setKeyboardSeekSpeed3, setKeyboardNormalSeekSpeed, keyboardSeekAccFactor, setKeyboardSeekAccFactor } = useUserSettings();
|
||||
|
||||
|
@ -57,7 +58,7 @@ const ValueTuners = memo(({ type, onFinished }: { type: TunerType, onFinished: (
|
|||
const resetToDefault = () => setValue(defaultValue);
|
||||
|
||||
return <ValueTuner title={title} value={value} setValue={setValue} onFinished={onFinished} max={max} min={min} resetToDefault={resetToDefault} />;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default ValueTuners;
|
||||
export default memo(ValueTuners);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
const VolumeControl = memo(({ playbackVolume, setPlaybackVolume }: { playbackVolume: number, setPlaybackVolume: (a: number) => void }) => {
|
||||
function VolumeControl({ playbackVolume, setPlaybackVolume }: { playbackVolume: number, setPlaybackVolume: (a: number) => void }) {
|
||||
const [volumeControlVisible, setVolumeControlVisible] = useState(false);
|
||||
const timeoutRef = useRef<number>();
|
||||
const { t } = useTranslation();
|
||||
|
@ -52,6 +52,6 @@ const VolumeControl = memo(({ playbackVolume, setPlaybackVolume }: { playbackVol
|
|||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default VolumeControl;
|
||||
export default memo(VolumeControl);
|
||||
|
|
|
@ -8,40 +8,42 @@ import { primaryColor } from '../colors';
|
|||
import loadingLottie from '../7077-magic-flow.json';
|
||||
|
||||
|
||||
const Working = memo(({ text, cutProgress, onAbortClick }: {
|
||||
function Working({ text, cutProgress, onAbortClick }: {
|
||||
text: string, cutProgress?: number | undefined, onAbortClick: () => void
|
||||
}) => (
|
||||
<div style={{ position: 'absolute', bottom: 0, top: 0, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<motion.div
|
||||
style={{ background: primaryColor, boxShadow: `${primaryColor} 0px 0px 20px 25px`, borderRadius: 60, paddingBottom: 5, color: 'white', fontSize: 14, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
>
|
||||
<div style={{ width: 150, height: 80 }}>
|
||||
<Lottie
|
||||
loop
|
||||
animationData={loadingLottie}
|
||||
play
|
||||
style={{ width: '170%', height: '210%', marginLeft: '-40%', marginTop: '-35%', pointerEvents: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{text}...
|
||||
</div>
|
||||
|
||||
{(cutProgress != null) && (
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{`${(cutProgress * 100).toFixed(1)} %`}
|
||||
}) {
|
||||
return (
|
||||
<div style={{ position: 'absolute', bottom: 0, top: 0, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<motion.div
|
||||
style={{ background: primaryColor, boxShadow: `${primaryColor} 0px 0px 20px 25px`, borderRadius: 60, paddingBottom: 5, color: 'white', fontSize: 14, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
>
|
||||
<div style={{ width: 150, height: 80 }}>
|
||||
<Lottie
|
||||
loop
|
||||
animationData={loadingLottie}
|
||||
play
|
||||
style={{ width: '170%', height: '210%', marginLeft: '-40%', marginTop: '-35%', pointerEvents: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<Button intent="danger" onClick={onAbortClick} height={20}><Trans>Abort</Trans></Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
));
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{text}...
|
||||
</div>
|
||||
|
||||
export default Working;
|
||||
{(cutProgress != null) && (
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{`${(cutProgress * 100).toFixed(1)} %`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<Button intent="danger" onClick={onAbortClick} height={20}><Trans>Abort</Trans></Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Working);
|
||||
|
|
|
@ -4,8 +4,8 @@ import Swal from '../swal';
|
|||
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }) {
|
||||
const { value: captureChoice } = await Swal.fire({
|
||||
export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }: { segmentsNumFrames: number, plural: boolean, fps: number }) {
|
||||
const { value: captureChoice } = await Swal.fire<string>({
|
||||
text: i18n.t(plural ? 'Extract frames of the selected segments as images' : 'Extract frames of the current segment as images'),
|
||||
icon: 'question',
|
||||
input: 'radio',
|
||||
|
@ -23,7 +23,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps
|
|||
|
||||
if (!captureChoice) return undefined;
|
||||
|
||||
let filter;
|
||||
let filter: string | undefined;
|
||||
let estimatedMaxNumFiles = segmentsNumFrames;
|
||||
|
||||
if (captureChoice === 'thumbnailFilter') {
|
||||
|
@ -44,7 +44,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps
|
|||
}
|
||||
|
||||
if (captureChoice === 'selectNthSec' || captureChoice === 'selectNthFrame') {
|
||||
let nthFrame;
|
||||
let nthFrame: number;
|
||||
if (captureChoice === 'selectNthFrame') {
|
||||
const { value } = await Swal.fire({
|
||||
text: i18n.t('Capture exactly one image every nth frame'),
|
|
@ -15,7 +15,7 @@ import CopyClipboardButton from '../components/CopyClipboardButton';
|
|||
import { isWindows, showItemInFolder } from '../util';
|
||||
import { ParseTimecode, SegmentBase } from '../types';
|
||||
|
||||
const { dialog } = window.require('@electron/remote');
|
||||
const { dialog, shell } = window.require('@electron/remote');
|
||||
|
||||
const ReactSwal = withReactContent(Swal);
|
||||
|
||||
|
@ -538,12 +538,13 @@ export async function selectSegmentsByLabelDialog(currentName: string) {
|
|||
return value;
|
||||
}
|
||||
|
||||
export async function selectSegmentsByExprDialog(inputValidator: (v: string) => string | undefined) {
|
||||
export async function selectSegmentsByExprDialog(inputValidator: (v: string) => Promise<string | undefined>) {
|
||||
const examples = {
|
||||
duration: { name: i18n.t('Segment duration less than 5 seconds'), code: 'segment.duration < 5' },
|
||||
start: { name: i18n.t('Segment starts after 00:60'), code: 'segment.start > 60' },
|
||||
label: { name: i18n.t('Segment label'), code: "equalText(segment.label, 'My label')" },
|
||||
tag: { name: i18n.t('Segment tag value'), code: "equalText(segment.tags.myTag, 'tag value')" },
|
||||
label: { name: i18n.t('Segment label (exact)'), code: "segment.label === 'My label'" },
|
||||
regexp: { name: i18n.t('Segment label (regexp)'), code: '/^My label/.test(segment.label)' },
|
||||
tag: { name: i18n.t('Segment tag value'), code: "segment.tags.myTag === 'tag value'" },
|
||||
};
|
||||
|
||||
function addExample(type: string) {
|
||||
|
@ -557,14 +558,10 @@ export async function selectSegmentsByExprDialog(inputValidator: (v: string) =>
|
|||
html: (
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ marginBottom: '1em' }}>
|
||||
{i18n.t('Enter an expression which will be evaluated for each segment. Segments for which the expression evaluates to "true" will be selected. For available syntax, see {{url}}.', { url: 'https://mathjs.org/' })}
|
||||
<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to "true" will be selected. <button type="button" className="button-unstyled" style={{ fontWeight: 'bold' }} onClick={() => shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/expressions.md')}>View available syntax.</button></Trans>
|
||||
</div>
|
||||
|
||||
<div><b>{i18n.t('Variables')}:</b></div>
|
||||
|
||||
<div style={{ marginBottom: '1em' }}>
|
||||
segment.label, segment.start, segment.end, segment.duration
|
||||
</div>
|
||||
<div style={{ marginBottom: '1em' }}><b>{i18n.t('Variables')}:</b> segment.label, segment.start, segment.end, segment.duration, segment.tags.*</div>
|
||||
|
||||
<div><b>{i18n.t('Examples')}:</b></div>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import fs from 'fs/promises';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { it, describe, expect } from 'vitest';
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { pcmAudioCodecs, getMapStreamsArgs, isMov, LiteFFprobeStream } from './u
|
|||
import { getSuffixedOutPath, isExecaFailure } from './util';
|
||||
import { isDurationValid } from './segments';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
|
||||
import { parseSrt } from './edlFormats';
|
||||
|
||||
const FileType = window.require('file-type');
|
||||
const { pathExists } = window.require('fs-extra');
|
||||
|
@ -505,7 +506,20 @@ async function renderThumbnail(filePath: string, timestamp: number) {
|
|||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function extractSubtitleTrack(filePath: string, streamId: number) {
|
||||
export async function extractSubtitleTrackToSegments(filePath: string, streamId: number) {
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-i', filePath,
|
||||
'-map', `0:${streamId}`,
|
||||
'-f', 'srt',
|
||||
'-',
|
||||
];
|
||||
|
||||
const { stdout } = await runFfmpeg(args);
|
||||
return parseSrt(stdout.toString('utf8'));
|
||||
}
|
||||
|
||||
export async function extractSubtitleTrackVtt(filePath: string, streamId: number) {
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-i', filePath,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// This code is for future use (e.g. creating black video to fill in using same codec parameters)
|
||||
|
||||
export function parseLevel(videoStream) {
|
||||
import { FFprobeStream } from '../../../ffprobe';
|
||||
|
||||
export function parseLevel(videoStream: FFprobeStream) {
|
||||
const { level: levelNumeric, codec_name: videoCodec } = videoStream;
|
||||
|
||||
if (levelNumeric == null || Number.isNaN(levelNumeric)) return undefined;
|
||||
|
@ -9,7 +11,7 @@ export function parseLevel(videoStream) {
|
|||
if (levelNumeric === 9) return '1b'; // 13 is 1.3. That are all like that (20 is 2.0, etc) except 1b which is 9.
|
||||
|
||||
let level = (levelNumeric / 10).toFixed(1); // https://stackoverflow.com/questions/42619191/what-does-level-mean-in-ffprobe-output
|
||||
if (level >= 0) {
|
||||
if (parseFloat(level) >= 0) {
|
||||
if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0
|
||||
const validLevels = ['1', '1b', '1.1', '1.2', '1.3', '2', '2.1', '2.2', '3', '3.1', '3.2', '4', '4.1', '4.2', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (validLevels.includes(level)) return level;
|
||||
|
@ -17,7 +19,7 @@ export function parseLevel(videoStream) {
|
|||
} else if (videoCodec === 'hevc') {
|
||||
// Note that on MacOS we don't use x265, but videotoolbox
|
||||
let level = (levelNumeric / 30).toFixed(1); // https://stackoverflow.com/questions/69983131/whats-the-difference-between-ffprobe-level-and-h-264-level
|
||||
if (level >= 0) {
|
||||
if (parseFloat(level) >= 0) {
|
||||
if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0
|
||||
const validLevels = ['1', '2', '2.1', '3', '3.1', '4', '4.1', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
if (validLevels.includes(level)) return level;
|
|
@ -1,7 +1,7 @@
|
|||
import dataUriToBuffer from 'data-uri-to-buffer';
|
||||
import pMap from 'p-map';
|
||||
import { useCallback } from 'react';
|
||||
import type * as FsPromises from 'fs/promises';
|
||||
import type * as FsPromises from 'node:fs/promises';
|
||||
|
||||
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp, fsOperationWithRetry } from '../util';
|
||||
import { getNumDigits } from '../segments';
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
|
|||
import i18n from 'i18next';
|
||||
import pMap from 'p-map';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { evaluate } from 'mathjs';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg';
|
||||
|
@ -15,6 +14,8 @@ import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, comb
|
|||
import * as ffmpegParameters from '../ffmpeg-parameters';
|
||||
import { maxSegmentsAllowed } from '../util/constants';
|
||||
import { ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
|
||||
import safeishEval from '../worker/eval';
|
||||
import { ScopeSegment } from '../../../../types';
|
||||
|
||||
const { ffmpeg: { blackDetect, silenceDetect } } = window.require('@electron/remote').require('./index.js');
|
||||
|
||||
|
@ -229,7 +230,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|||
setCutSegments(cutSegmentsNew);
|
||||
}, [setCutSegments, cutSegments]);
|
||||
|
||||
const setCutTime = useCallback((type, time) => {
|
||||
const setCutTime = useCallback((type: 'start' | 'end', time: number) => {
|
||||
if (!isDurationValid(duration)) return;
|
||||
|
||||
const currentSeg = currentCutSeg;
|
||||
|
@ -412,6 +413,13 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|||
if (value != null) updateSegAtIndex(index, { name: value });
|
||||
}, [cutSegments, updateSegAtIndex, maxLabelLength]);
|
||||
|
||||
const focusSegmentAtCursor = useCallback(() => {
|
||||
const relevantTime = getRelevantTime();
|
||||
const [firstSegmentAtCursorIndex] = findSegmentsAtCursor(apparentCutSegments, relevantTime);
|
||||
if (firstSegmentAtCursorIndex == null) return;
|
||||
setCurrentSegIndex(firstSegmentAtCursorIndex);
|
||||
}, [apparentCutSegments, getRelevantTime]);
|
||||
|
||||
const splitCurrentSegment = useCallback(() => {
|
||||
const relevantTime = getRelevantTime();
|
||||
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, relevantTime);
|
||||
|
@ -472,40 +480,34 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|||
}, [currentCutSeg, cutSegments, enableSegments]);
|
||||
|
||||
const onSelectSegmentsByExpr = useCallback(async () => {
|
||||
function matchSegment(seg: StateSegment, expr: string) {
|
||||
async function matchSegment(seg: StateSegment, expr: string) {
|
||||
const start = getSegApparentStart(seg);
|
||||
const end = getSegApparentEnd(seg);
|
||||
// must clone tags because scope is mutable (editable by expression)
|
||||
const scopeSegment: { label: string, start: number, end: number, duration: number, tags: Record<string, string> } = { label: seg.name, start, end, duration: end - start, tags: { ...seg.tags } };
|
||||
return evaluate(expr, { segment: scopeSegment }) === true;
|
||||
const scopeSegment: ScopeSegment = { label: seg.name, start, end, duration: end - start, tags: { ...seg.tags } };
|
||||
return (await safeishEval(expr, { segment: scopeSegment })) === true;
|
||||
}
|
||||
|
||||
const getSegmentsToEnable = (expr: string) => cutSegments.filter((seg) => {
|
||||
try {
|
||||
return matchSegment(seg, expr);
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
const getSegmentsToEnable = async (expr: string) => (await pMap(cutSegments, async (seg) => (
|
||||
((await matchSegment(seg, expr)) ? [seg] : [])
|
||||
), { concurrency: 5 })).flat();
|
||||
|
||||
const value = await selectSegmentsByExprDialog((v: string) => {
|
||||
const value = await selectSegmentsByExprDialog(async (v: string) => {
|
||||
try {
|
||||
const segments = getSegmentsToEnable(v);
|
||||
if (segments.length === 0) return i18n.t('No segments matched');
|
||||
if (v.trim().length === 0) return i18n.t('Please enter a JavaScript expression.');
|
||||
const segments = await getSegmentsToEnable(v);
|
||||
if (segments.length === 0) return i18n.t('No segments match this expression.');
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
return i18n.t('Expression failed: {{errorMessage}}', { errorMessage: err.message });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
if (value == null) return;
|
||||
const segmentsToEnable = getSegmentsToEnable(value);
|
||||
const segmentsToEnable = await getSegmentsToEnable(value);
|
||||
enableSegments(segmentsToEnable);
|
||||
}, [cutSegments, enableSegments, getSegApparentEnd]);
|
||||
|
||||
|
@ -571,6 +573,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|||
setCutEnd,
|
||||
onLabelSegment,
|
||||
splitCurrentSegment,
|
||||
focusSegmentAtCursor,
|
||||
createNumSegments,
|
||||
createFixedDurationSegments,
|
||||
createRandomSegments,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { MotionConfig } from 'framer-motion';
|
|||
import { enableMapSet } from 'immer';
|
||||
import * as Electron from 'electron';
|
||||
import Remote from '@electron/remote';
|
||||
import type path from 'path';
|
||||
import type path from 'node:path';
|
||||
|
||||
import 'sweetalert2/dist/sweetalert2.css';
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ https://www.radix-ui.com/docs/colors/palette-composition/the-scales
|
|||
https://www.radix-ui.com/docs/colors/palette-composition/understanding-the-scale
|
||||
*/
|
||||
@import '@radix-ui/colors/red.css';
|
||||
@import '@radix-ui/colors/orange.css';
|
||||
@import '@radix-ui/colors/redDark.css';
|
||||
@import '@radix-ui/colors/amber.css';
|
||||
@import '@radix-ui/colors/amberDark.css';
|
||||
|
|
|
@ -17,11 +17,11 @@ export async function loadMifiLink() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function runStartupCheck({ ffmpeg }) {
|
||||
export async function runStartupCheck({ ffmpeg }: { ffmpeg: boolean }) {
|
||||
try {
|
||||
if (ffmpeg) await runFfmpegStartupCheck();
|
||||
} catch (err) {
|
||||
if (['EPERM', 'EACCES'].includes(err.code)) {
|
||||
if (err instanceof Error && 'code' in err && typeof err.code === 'string' && ['EPERM', 'EACCES'].includes(err.code)) {
|
||||
toast.fire({
|
||||
timer: 30000,
|
||||
icon: 'error',
|
|
@ -5,9 +5,9 @@ import prettyBytes from 'pretty-bytes';
|
|||
import sortBy from 'lodash/sortBy';
|
||||
import pRetry, { Options } from 'p-retry';
|
||||
import { ExecaError } from 'execa';
|
||||
import type * as FsPromises from 'fs/promises';
|
||||
import type * as FsPromises from 'node:fs/promises';
|
||||
import type * as FsExtra from 'fs-extra';
|
||||
import type { PlatformPath } from 'path';
|
||||
import type { PlatformPath } from 'node:path';
|
||||
|
||||
import isDev from './isDev';
|
||||
import Swal, { toast } from './swal';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import i18n from 'i18next';
|
||||
import lodashTemplate from 'lodash/template';
|
||||
import { PlatformPath } from 'path';
|
||||
import { PlatformPath } from 'node:path';
|
||||
|
||||
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
|
||||
import isDev from '../isDev';
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
const workerUrl = new URL('evalWorker.js', import.meta.url);
|
||||
|
||||
// https://v3.vitejs.dev/guide/features.html#web-workers
|
||||
// todo terminate() and recreate in case of error?
|
||||
const worker = new Worker(workerUrl);
|
||||
|
||||
let lastRequestId = 0;
|
||||
|
||||
export default async function safeishEval(code: string, context: unknown) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lastRequestId += 1;
|
||||
const id = lastRequestId;
|
||||
|
||||
// console.log({ lastRequestId, code, context })
|
||||
|
||||
function cleanup() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
worker.removeEventListener('message', onMessage);
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
worker.removeEventListener('messageerror', onMessageerror);
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
worker.removeEventListener('error', onError);
|
||||
}
|
||||
|
||||
function onMessage({ data: { id: responseId, error, data } }) {
|
||||
// console.log('message', { responseId, error, data })
|
||||
|
||||
if (responseId === id) {
|
||||
cleanup();
|
||||
if (error) reject(new Error(error));
|
||||
else resolve(data);
|
||||
}
|
||||
}
|
||||
|
||||
function onMessageerror() {
|
||||
cleanup();
|
||||
reject(new Error('safeishEval messageerror'));
|
||||
}
|
||||
|
||||
function onError(err: ErrorEvent) {
|
||||
cleanup();
|
||||
reject(new Error(`safeishEval error: ${err.message}`));
|
||||
}
|
||||
|
||||
worker.addEventListener('message', onMessage);
|
||||
worker.addEventListener('messageerror', onMessageerror);
|
||||
worker.addEventListener('error', onError);
|
||||
|
||||
worker.postMessage({ id, code, context: JSON.stringify(context) });
|
||||
});
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
|
||||
const myGlobal = this;
|
||||
|
||||
// https://stackoverflow.com/a/10796616/6519037
|
||||
// https://github.com/Zirak/SO-ChatBot/blob/master/source/eval.js
|
||||
// https://github.com/Zirak/SO-ChatBot/blob/master/source/codeWorker.js
|
||||
|
||||
const wl = {
|
||||
self: 1,
|
||||
onmessage: 1,
|
||||
postMessage: 1,
|
||||
global: 1,
|
||||
wl: 1,
|
||||
eval: 1,
|
||||
Array: 1,
|
||||
Boolean: 1,
|
||||
Date: 1,
|
||||
Function: 1,
|
||||
Number: 1,
|
||||
Object: 1,
|
||||
RegExp: 1,
|
||||
String: 1,
|
||||
Error: 1,
|
||||
EvalError: 1,
|
||||
RangeError: 1,
|
||||
ReferenceError: 1,
|
||||
SyntaxError: 1,
|
||||
TypeError: 1,
|
||||
URIError: 1,
|
||||
decodeURI: 1,
|
||||
decodeURIComponent: 1,
|
||||
encodeURI: 1,
|
||||
encodeURIComponent: 1,
|
||||
isFinite: 1,
|
||||
isNaN: 1,
|
||||
parseFloat: 1,
|
||||
parseInt: 1,
|
||||
Infinity: 1,
|
||||
JSON: 1,
|
||||
Math: 1,
|
||||
NaN: 1,
|
||||
undefined: 1,
|
||||
|
||||
// Chrome errors if you attempt to write over either of these properties, so put them in the whitelist
|
||||
// https://github.com/owl-factory/lantern/blob/addda28034d5d30a7ea720646aa56fefa8f05cf4/archive/src/nodes/sandbox/workers/sandboxed-code.worker.ts#L47
|
||||
TEMPORARY: 1,
|
||||
PERSISTENT: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line prefer-arrow-callback, func-names
|
||||
Object.getOwnPropertyNames(myGlobal).forEach(function (prop) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (!wl.hasOwnProperty(prop)) {
|
||||
Object.defineProperty(myGlobal, prop, {
|
||||
// eslint-disable-next-line func-names, object-shorthand
|
||||
get: function () {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw `Security Exception: cannot access ${prop}`;
|
||||
},
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-proto, prefer-arrow-callback, func-names
|
||||
Object.getOwnPropertyNames(myGlobal.__proto__).forEach(function (prop) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (!wl.hasOwnProperty(prop)) {
|
||||
// eslint-disable-next-line no-proto
|
||||
Object.defineProperty(myGlobal.__proto__, prop, {
|
||||
// eslint-disable-next-line func-names, object-shorthand
|
||||
get: function () {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw `Security Exception: cannot access ${prop}`;
|
||||
},
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Array(5000000000).join("adasdadadasd") instantly crashing some browser tabs
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Object.defineProperty(Array.prototype, 'join', {
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
// eslint-disable-next-line wrap-iife, func-names
|
||||
value: function (old) {
|
||||
// eslint-disable-next-line func-names
|
||||
return function (arg) {
|
||||
// @ts-expect-error dunno how to fix
|
||||
if (this.length > 500 || (arg && arg.length > 500)) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw 'Exception: too many items';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-reflect-apply, prefer-rest-params
|
||||
// @ts-expect-error dunno how to fix
|
||||
return old.apply(this, arg);
|
||||
};
|
||||
}(Array.prototype.join),
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
https://github.com/Zirak/SO-ChatBot/blob/accbfb4b8738781afaf4f080a6bb0337e13f7c25/source/codeWorker.js#L87
|
||||
|
||||
DOM specification doesn't define an enumerable `fetch` function object on
|
||||
the global object so we add the property here, and the following code will
|
||||
blacklist it. (`fetch` descends from `GlobalFetch`, and is thus present in
|
||||
worker code as well)
|
||||
Just in case someone runs the bot on some old browser where `fetch` is not
|
||||
defined anyways, this will have no effect.
|
||||
Reason for blacklisting fetch: well, same as XHR.
|
||||
*/
|
||||
// @ts-expect-error expected
|
||||
myGlobal.fetch = undefined;
|
||||
|
||||
|
||||
// eslint-disable-next-line wrap-iife, func-names
|
||||
(function () {
|
||||
onmessage = (event) => {
|
||||
// eslint-disable-next-line strict, lines-around-directive
|
||||
'use strict';
|
||||
|
||||
const { code, id, context: contextStr } = event.data;
|
||||
const context = { ...JSON.parse(contextStr) };
|
||||
|
||||
try {
|
||||
// https://stackoverflow.com/questions/8403108/calling-eval-in-particular-context
|
||||
// eslint-disable-next-line unicorn/new-for-builtins, no-new-func
|
||||
const result = Function(`\nwith (this) { return (${code}); }`).call(context);
|
||||
postMessage({ id, data: result });
|
||||
} catch (e) {
|
||||
postMessage({ id, error: `${e}` });
|
||||
}
|
||||
};
|
||||
})();
|
|
@ -5,8 +5,6 @@
|
|||
"noEmit": true,
|
||||
|
||||
"noImplicitAny": false, // todo
|
||||
"checkJs": false, // todo
|
||||
"allowJs": true, // todo
|
||||
},
|
||||
"references": [
|
||||
{ "path": "./tsconfig.main.json" },
|
||||
|
|
13
types.ts
13
types.ts
|
@ -1,4 +1,4 @@
|
|||
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit';
|
||||
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit';
|
||||
|
||||
export interface KeyBinding {
|
||||
keys: string,
|
||||
|
@ -111,3 +111,14 @@ export interface ApiKeyboardActionRequest {
|
|||
}
|
||||
|
||||
export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest';
|
||||
|
||||
export type WaveformMode = 'big-waveform' | 'waveform';
|
||||
|
||||
// This is the contract with the user, see https://github.com/mifi/lossless-cut/blob/master/expressions.md
|
||||
export interface ScopeSegment {
|
||||
label: string,
|
||||
start: number,
|
||||
end: number,
|
||||
duration: number,
|
||||
tags: Record<string, string>,
|
||||
}
|
||||
|
|
85
yarn.lock
85
yarn.lock
|
@ -545,15 +545,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.24.4":
|
||||
version: 7.24.5
|
||||
resolution: "@babel/runtime@npm:7.24.5"
|
||||
dependencies:
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: e0f4f4d4503f7338749d1dd92361ad132d683bde64e6b61d6c855e100dcd01592295fcfdcc960c946b85ef7908dc2f501080da58447c05812cf3cd80c599bb62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.20.7":
|
||||
version: 7.20.7
|
||||
resolution: "@babel/template@npm:7.20.7"
|
||||
|
@ -3907,13 +3898,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"complex.js@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "complex.js@npm:2.1.1"
|
||||
checksum: 1905d5204dd8a4d6f591182aca2045986f1ff3c5373e455ccd10c6ee2905bf1d3811a313d38c68f8a8507523202f91e25177387e3adc386c1b5b5ec2f13a6dbb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compute-scroll-into-view@npm:^1.0.14, compute-scroll-into-view@npm:^1.0.17":
|
||||
version: 1.0.17
|
||||
resolution: "compute-scroll-into-view@npm:1.0.17"
|
||||
|
@ -4254,13 +4238,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decimal.js@npm:^10.4.3":
|
||||
version: 10.4.3
|
||||
resolution: "decimal.js@npm:10.4.3"
|
||||
checksum: de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-response@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "decompress-response@npm:6.0.0"
|
||||
|
@ -5218,13 +5195,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-latex@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "escape-latex@npm:1.2.0"
|
||||
checksum: 73a787319f0965ecb8244bb38bf3a3cba872f0b9a5d3da8821140e9f39fe977045dc953a62b1a2bed4d12bfccbe75a7d8ec786412bf00739eaa2f627d0a8e0d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-string-regexp@npm:5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "escape-string-regexp@npm:5.0.0"
|
||||
|
@ -5975,13 +5945,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fraction.js@npm:4.3.4":
|
||||
version: 4.3.4
|
||||
resolution: "fraction.js@npm:4.3.4"
|
||||
checksum: 3a1e6b268038ffdea625fab6a8d155d7ab644d35d0c99bc59084bfd29fbc714f3a38381b0627751ddb5f188bcde0b3f48c27e80eeb2ecd440825a7d2cd2bf9f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^9.0.3":
|
||||
version: 9.0.3
|
||||
resolution: "framer-motion@npm:9.0.3"
|
||||
|
@ -7535,13 +7498,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"javascript-natural-sort@npm:^0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "javascript-natural-sort@npm:0.7.1"
|
||||
checksum: 7bf6eab67871865d347f09a95aa770f9206c1ab0226bcda6fdd9edec340bf41111a7f82abac30556aa16a21cfa3b2b1ca4a362c8b73dd5ce15220e5d31f49d79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-cookie@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "js-cookie@npm:2.2.1"
|
||||
|
@ -7993,7 +7949,6 @@ __metadata:
|
|||
ky: "npm:^0.33.1"
|
||||
lodash: "npm:^4.17.19"
|
||||
luxon: "npm:^3.3.0"
|
||||
mathjs: "npm:^12.4.2"
|
||||
mime-types: "npm:^2.1.14"
|
||||
mkdirp: "npm:^1.0.3"
|
||||
morgan: "npm:^1.10.0"
|
||||
|
@ -8185,25 +8140,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mathjs@npm:^12.4.2":
|
||||
version: 12.4.2
|
||||
resolution: "mathjs@npm:12.4.2"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.24.4"
|
||||
complex.js: "npm:^2.1.1"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
escape-latex: "npm:^1.2.0"
|
||||
fraction.js: "npm:4.3.4"
|
||||
javascript-natural-sort: "npm:^0.7.1"
|
||||
seedrandom: "npm:^3.0.5"
|
||||
tiny-emitter: "npm:^2.1.0"
|
||||
typed-function: "npm:^4.1.1"
|
||||
bin:
|
||||
mathjs: bin/cli.js
|
||||
checksum: 4b88ac1b137d00b8f3d66f4d1662d3670399390b59623ecf3ab7d587ba18be7b97ce9c5b07e953029ac75f48567d675c99323889ae231eb071ddd84db5dd699c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdn-data@npm:2.0.14":
|
||||
version: 2.0.14
|
||||
resolution: "mdn-data@npm:2.0.14"
|
||||
|
@ -10362,13 +10298,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"seedrandom@npm:^3.0.5":
|
||||
version: 3.0.5
|
||||
resolution: "seedrandom@npm:3.0.5"
|
||||
checksum: acad5e516c04289f61c2fb9848f449b95f58362b75406b79ec51e101ec885293fc57e3675d2f39f49716336559d7190f7273415d185fead8cd27b171ebf7d8fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver-compare@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "semver-compare@npm:1.0.0"
|
||||
|
@ -11280,13 +11209,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-emitter@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "tiny-emitter@npm:2.1.0"
|
||||
checksum: 75633f4de4f47f43af56aff6162f25b87be7efc6f669fda256658f3c3f4a216f23dc0d13200c6fafaaf1b0c7142f0201352fb06aec0b77f68aea96be898f4516
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-invariant@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "tiny-invariant@npm:1.2.0"
|
||||
|
@ -11614,13 +11536,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typed-function@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "typed-function@npm:4.1.1"
|
||||
checksum: 0ef538d5f02e5c40659cccc14b5f2727f0e4181f11d91bb7897327c33cc2893de7e92343b6b32e1bb15e44a215a1e92e27ab2aa1353b100a9a2697abf2989a0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typedarray-to-buffer@npm:^3.1.5":
|
||||
version: 3.1.5
|
||||
resolution: "typedarray-to-buffer@npm:3.1.5"
|
||||
|
|
Ładowanie…
Reference in New Issue