Porównaj commity

...

8 Commity

Autor SHA1 Wiadomość Data
Mikael Finstad 4892437b83
tsify 2024-05-20 12:17:53 +02:00
Mikael Finstad 918277bd75
lint 2024-05-20 11:54:37 +02:00
Mikael Finstad 281ff6e980
lint 2024-05-20 11:50:55 +02:00
Mikael Finstad 799333f898
lint 2024-05-20 11:49:37 +02:00
Mikael Finstad e64e0fb216
improve color #2007 2024-05-20 11:24:46 +02:00
Mikael Finstad 0f8605f897
add Focus segment at cursor
closes #2004
2024-05-19 23:32:14 +02:00
Mikael Finstad 8b86795f29
allow converting subtitle to segments #2002 2024-05-19 23:31:49 +02:00
Mikael Finstad ec3e626693
use js expressions instead of mathjs #2002 2024-05-19 23:16:59 +02:00
73 zmienionych plików z 607 dodań i 355 usunięć

Wyświetl plik

@ -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: [

11
expressions.md 100644
Wyświetl plik

@ -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.

Wyświetl plik

@ -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",

Wyświetl plik

@ -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';

Wyświetl plik

@ -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

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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))

Wyświetl plik

@ -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';

Wyświetl plik

@ -1,4 +1,4 @@
import os from 'os';
import os from 'node:os';
export const platform = os.platform();
export const arch = os.arch();

Wyświetl plik

@ -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>

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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'),

Wyświetl plik

@ -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 &quot;true&quot; 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>

Wyświetl plik

@ -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';

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;

Wyświetl plik

@ -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';

Wyświetl plik

@ -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,

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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',

Wyświetl plik

@ -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';

Wyświetl plik

@ -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';

Wyświetl plik

@ -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) });
});
}

Wyświetl plik

@ -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}` });
}
};
})();

Wyświetl plik

@ -5,8 +5,6 @@
"noEmit": true,
"noImplicitAny": false, // todo
"checkJs": false, // todo
"allowJs": true, // todo
},
"references": [
{ "path": "./tsconfig.main.json" },

Wyświetl plik

@ -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>,
}

Wyświetl plik

@ -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"