1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

start to tsify

This commit is contained in:
Mikael Finstad 2024-02-12 14:11:36 +08:00
parent 6fddf72a2d
commit 8b6f0cc593
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
41 changed files with 945 additions and 356 deletions

View File

@ -39,6 +39,7 @@ module.exports = {
'@typescript-eslint/no-unused-vars': 0, // todo
'import/extensions': 0, // doesn't work with TS https://github.com/import-js/eslint-plugin-import/issues/2111
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/require-default-props': 0,
},
parserOptions: {
ecmaVersion: 2022,

View File

@ -6,6 +6,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@ -97,6 +97,7 @@
"sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7",
"typescript": "^5.3.3",
"typescript-plugin-css-modules": "^5.1.0",
"use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0",
"uuid": "^8.3.2",

View File

@ -1,4 +1,4 @@
import React, { memo, useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { memo, useEffect, useState, useCallback, useRef, useMemo, CSSProperties } from 'react';
import { FaAngleLeft, FaWindowClose } from 'react-icons/fa';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { AnimatePresence } from 'framer-motion';
@ -85,6 +85,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { EdlFileType, FfmpegCommandLog, Html5ifyMode, TunerType } from './types';
const electron = window.require('electron');
const { exists } = window.require('fs-extra');
@ -96,8 +97,8 @@ const remote = window.require('@electron/remote');
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
const videoStyle = { width: '100%', height: '100%', objectFit: 'contain' };
const bottomStyle = { background: controlsBackground, transition: darkModeTransition };
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition };
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
hevcPlaybackSupportedPromise.catch((err) => console.error(err));
@ -106,66 +107,66 @@ hevcPlaybackSupportedPromise.catch((err) => console.error(err));
function App() {
// Per project state
const [commandedTime, setCommandedTime] = useState(0);
const [ffmpegCommandLog, setFfmpegCommandLog] = useState([]);
const [ffmpegCommandLog, setFfmpegCommandLog] = useState<FfmpegCommandLog>([]);
const [previewFilePath, setPreviewFilePath] = useState();
const [working, setWorkingState] = useState();
const [previewFilePath, setPreviewFilePath] = useState<string>();
const [working, setWorkingState] = useState<{ text: string, abortController: AbortController }>();
const [usingDummyVideo, setUsingDummyVideo] = useState(false);
const [playing, setPlaying] = useState(false);
const [compatPlayerEventId, setCompatPlayerEventId] = useState(0);
const playbackModeRef = useRef();
const [playerTime, setPlayerTime] = useState();
const [duration, setDuration] = useState();
const playbackModeRef = useRef<{ playbackMode: 'loop-selected-segments', segId: string }>();
const [playerTime, setPlayerTime] = useState<number>();
const [duration, setDuration] = useState<number>();
const [rotation, setRotation] = useState(360);
const [cutProgress, setCutProgress] = useState();
const [cutProgress, setCutProgress] = useState<number>();
const [startTimeOffset, setStartTimeOffset] = useState(0);
const [filePath, setFilePath] = useState('');
const [externalFilesMeta, setExternalFilesMeta] = useState({});
const [customTagsByFile, setCustomTagsByFile] = useState({});
const [paramsByStreamId, setParamsByStreamId] = useState(new Map());
const [detectedFps, setDetectedFps] = useState();
const [mainFileMeta, setMainFileMeta] = useState({ streams: [], formatData: {} });
const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState({});
const [detectedFps, setDetectedFps] = useState<number>();
const [mainFileMeta, setMainFileMeta] = useState<{ streams: any[], formatData: any, chapters?: any }>({ streams: [], formatData: {} });
const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState<Record<string, Record<string, boolean>>>({});
const [streamsSelectorShown, setStreamsSelectorShown] = useState(false);
const [concatDialogVisible, setConcatDialogVisible] = useState(false);
const [zoomUnrounded, setZoom] = useState(1);
const [thumbnails, setThumbnails] = useState([]);
const [thumbnails, setThumbnails] = useState<{ from: number, url: string }[]>([]);
const [shortestFlag, setShortestFlag] = useState(false);
const [zoomWindowStartTime, setZoomWindowStartTime] = useState(0);
const [subtitlesByStreamId, setSubtitlesByStreamId] = useState({});
const [activeVideoStreamIndex, setActiveVideoStreamIndex] = useState();
const [activeAudioStreamIndex, setActiveAudioStreamIndex] = useState();
const [activeSubtitleStreamIndex, setActiveSubtitleStreamIndex] = useState();
const [subtitlesByStreamId, setSubtitlesByStreamId] = useState<Record<string, { url: string, lang?: string }>>({});
const [activeVideoStreamIndex, setActiveVideoStreamIndex] = useState<number>();
const [activeAudioStreamIndex, setActiveAudioStreamIndex] = useState<number>();
const [activeSubtitleStreamIndex, setActiveSubtitleStreamIndex] = useState<number>();
const [hideMediaSourcePlayer, setHideMediaSourcePlayer] = useState(false);
const [exportConfirmVisible, setExportConfirmVisible] = useState(false);
const [cacheBuster, setCacheBuster] = useState(0);
const [mergedOutFileName, setMergedOutFileName] = useState();
const [mergedOutFileName, setMergedOutFileName] = useState<string>();
const [outputPlaybackRate, setOutputPlaybackRateState] = useState(1);
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
// State per application launch
const lastOpenedPathRef = useRef();
const [waveformMode, setWaveformMode] = useState();
const [waveformMode, setWaveformMode] = useState<'big-waveform' | 'waveform'>();
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
const [showRightBar, setShowRightBar] = useState(true);
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState();
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState<Html5ifyMode>();
const [lastCommandsVisible, setLastCommandsVisible] = useState(false);
const [settingsVisible, setSettingsVisible] = useState(false);
const [tunerVisible, setTunerVisible] = useState();
const [tunerVisible, setTunerVisible] = useState<TunerType>();
const [keyboardShortcutsVisible, setKeyboardShortcutsVisible] = useState(false);
const [mifiLink, setMifiLink] = useState();
const [mifiLink, setMifiLink] = useState<unknown>();
const [alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles] = useState(false);
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState();
const [editingSegmentTags, setEditingSegmentTags] = useState();
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState<number>();
const [editingSegmentTags, setEditingSegmentTags] = useState<Record<string, unknown>>();
const [mediaSourceQuality, setMediaSourceQuality] = useState(0);
const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []);
// Batch state / concat files
const [batchFiles, setBatchFiles] = useState([]);
const [selectedBatchFiles, setSelectedBatchFiles] = useState([]);
const [batchFiles, setBatchFiles] = useState<{ path: string }[]>([]);
const [selectedBatchFiles, setSelectedBatchFiles] = useState<string[]>([]);
// Store "working" in a ref so we can avoid race conditions
const workingRef = useRef(!!working);
@ -205,8 +206,8 @@ function App() {
electron.ipcRenderer.send('setLanguage', l);
}, [language]);
const videoRef = useRef();
const videoContainerRef = useRef();
const videoRef = useRef<HTMLVideoElement>(null);
const videoContainerRef = useRef<HTMLDivElement>(null);
const setOutputPlaybackRate = useCallback((v) => {
setOutputPlaybackRateState(v);
@ -244,8 +245,8 @@ function App() {
});
}, [zoomedDuration]);
function appendFfmpegCommandLog(command) {
setFfmpegCommandLog(old => [...old, { command, time: new Date() }]);
function appendFfmpegCommandLog(command: string) {
setFfmpegCommandLog((old) => [...old, { command, time: new Date() }]);
}
const setCopyStreamIdsForPath = useCallback((path, cb) => {
@ -267,7 +268,7 @@ function App() {
if (waveformMode === 'waveform') {
setWaveformMode('big-waveform');
} else if (waveformMode === 'big-waveform') {
setWaveformMode();
setWaveformMode(undefined);
} else {
if (!hideAllNotifications) toast.fire({ text: i18n.t('Mini-waveform has been enabled. Click again to enable full-screen waveform') });
setWaveformMode('waveform');
@ -285,7 +286,7 @@ function App() {
const seekAbs = useCallback((val) => {
const video = videoRef.current;
if (val == null || Number.isNaN(val)) return;
if (video == null || val == null || Number.isNaN(val)) return;
let outVal = val;
if (outVal < 0) outVal = 0;
if (outVal > video.duration) outVal = video.duration;
@ -295,7 +296,7 @@ function App() {
setCompatPlayerEventId((id) => id + 1); // To make sure that we can seek even to the same commanded time that we are already add (e.g. loop current segment)
}, []);
const userSeekAbs = useCallback((val) => {
const userSeekAbs = useCallback((val: number) => {
playbackModeRef.current = undefined; // If the user seeks, we clear any custom playback mode
return seekAbs(val);
}, [seekAbs]);
@ -305,8 +306,8 @@ function App() {
commandedTimeRef.current = commandedTime;
}, [commandedTime]);
const seekRel = useCallback((val) => {
userSeekAbs(videoRef.current.currentTime + val);
const seekRel = useCallback((val: number) => {
userSeekAbs(videoRef.current!.currentTime + val);
}, [userSeekAbs]);
const seekRelPercent = useCallback((val) => {
@ -319,7 +320,7 @@ function App() {
const fps = detectedFps || 30;
// try to align with frame
const currentTimeNearestFrameNumber = getFrameCountRaw(fps, videoRef.current.currentTime);
const currentTimeNearestFrameNumber = getFrameCountRaw(fps, videoRef.current!.currentTime);
const nextFrame = currentTimeNearestFrameNumber + direction;
userSeekAbs(nextFrame / fps);
}, [detectedFps, userSeekAbs]);
@ -344,7 +345,7 @@ function App() {
const activeVideoStream = useMemo(() => (activeVideoStreamIndex != null ? videoStreams.find((stream) => stream.index === activeVideoStreamIndex) : undefined) ?? mainVideoStream, [activeVideoStreamIndex, mainVideoStream, videoStreams]);
const activeAudioStream = useMemo(() => (activeAudioStreamIndex != null ? audioStreams.find((stream) => stream.index === activeAudioStreamIndex) : undefined) ?? mainAudioStream, [activeAudioStreamIndex, audioStreams, mainAudioStream]);
const activeSubtitle = useMemo(() => subtitlesByStreamId[activeSubtitleStreamIndex], [activeSubtitleStreamIndex, subtitlesByStreamId]);
const activeSubtitle = useMemo(() => activeSubtitleStreamIndex != null ? subtitlesByStreamId[activeSubtitleStreamIndex] : undefined, [activeSubtitleStreamIndex, subtitlesByStreamId]);
// 360 means we don't modify rotation gtrgt
const isRotationSet = rotation !== 360;
@ -377,7 +378,7 @@ function App() {
// Relevant time is the player's playback position if we're currently playing - if not, it's the user's commanded time.
const relevantTime = useMemo(() => (playing ? playerTime : commandedTime) || 0, [commandedTime, playerTime, playing]);
// The reason why we also have a getter is because it can be used when we need to get the time, but don't want to re-render for every time update (which can be heavy!)
const getRelevantTime = useCallback(() => (playing ? videoRef.current.currentTime : commandedTimeRef.current) || 0, [playing]);
const getRelevantTime = useCallback(() => (playing ? videoRef.current!.currentTime : commandedTimeRef.current) || 0, [playing]);
const maxLabelLength = safeOutputFileName ? 100 : 500;
@ -399,12 +400,12 @@ function App() {
const jumpTimelineEnd = useCallback(() => userSeekAbs(durationSafe), [durationSafe, userSeekAbs]);
const getFrameCount = useCallback((sec) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
const getFrameCount = useCallback((sec: number) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
const formatTimecode = useCallback(({ seconds, shorten, fileNameFriendly }) => {
if (timecodeFormat === 'frameCount') {
const frameCount = getFrameCount(seconds);
return frameCount != null ? frameCount : '';
return frameCount != null ? String(frameCount) : '';
}
if (timecodeFormat === 'timecodeWithFramesFraction') {
return formatDuration({ seconds, fps: detectedFps, shorten, fileNameFriendly });
@ -436,7 +437,7 @@ function App() {
// https://github.com/mifi/lossless-cut/issues/1674
if (cacheBuster !== 0) {
const qs = new URLSearchParams();
qs.set('t', cacheBuster);
qs.set('t', String(cacheBuster));
return `${uri}?${qs.toString()}`;
}
return uri;
@ -445,7 +446,7 @@ function App() {
const projectSuffix = 'proj.llc';
const oldProjectSuffix = 'llc-edl.csv';
// New LLC format can be stored along with input file or in working dir (customOutDir)
const getEdlFilePath = useCallback((fp, cod) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []);
const getEdlFilePath = useCallback((fp: string, cod?: string) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []);
// Old versions of LosslessCut used CSV files and stored them always in customOutDir:
const getEdlFilePathOld = useCallback((fp, cod) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: oldProjectSuffix }), []);
const getProjectFileSavePath = useCallback((storeProjectInWorkingDirIn) => getEdlFilePath(filePath, storeProjectInWorkingDirIn ? customOutDir : undefined), [getEdlFilePath, filePath, customOutDir]);
@ -458,7 +459,7 @@ function App() {
const [debouncedSaveOperation] = useDebounce(currentSaveOperation, isDev ? 2000 : 500);
const lastSaveOperation = useRef();
const lastSaveOperation = useRef<typeof debouncedSaveOperation>();
useEffect(() => {
async function save() {
// NOTE: Could lose a save if user closes too fast, but not a big issue I think
@ -486,7 +487,7 @@ function App() {
function onPlayingChange(val) {
setPlaying(val);
if (!val) {
setCommandedTime(videoRef.current.currentTime);
setCommandedTime(videoRef.current!.currentTime);
}
}
@ -513,7 +514,7 @@ function App() {
setRotation((r) => (r + 90) % 450);
setHideMediaSourcePlayer(false);
// Matroska is known not to work, so we warn user. See https://github.com/mifi/lossless-cut/discussions/661
const supportsRotation = !['matroska', 'webm'].includes(fileFormat);
const supportsRotation = !(fileFormat != null && ['matroska', 'webm'].includes(fileFormat));
if (!supportsRotation && !hideAllNotifications) toast.fire({ text: i18n.t('Lossless rotation might not work with this file format. You may try changing to MP4') });
}, [hideAllNotifications, fileFormat]);
@ -563,7 +564,7 @@ function App() {
const clearOutDir = useCallback(async () => {
try {
await ensureWritableOutDir({ inputPath: filePath, outDir: undefined });
setCustomOutDir();
setCustomOutDir(undefined);
} catch (err) {
if (err instanceof DirectoryAccessDeclinedError) return;
throw err;
@ -595,9 +596,9 @@ function App() {
},
}), [preferStrongColors]);
const onActiveSubtitleChange = useCallback(async (index) => {
const onActiveSubtitleChange = useCallback(async (index?: number) => {
if (index == null) {
setActiveSubtitleStreamIndex();
setActiveSubtitleStreamIndex(undefined);
return;
}
if (subtitlesByStreamId[index]) { // Already loaded
@ -612,18 +613,20 @@ function App() {
setSubtitlesByStreamId((old) => ({ ...old, [index]: { url, lang: subtitleStream.tags && subtitleStream.tags.language } }));
setActiveSubtitleStreamIndex(index);
} catch (err) {
handleError(`Failed to extract subtitles for stream ${index}`, err.message);
handleError(`Failed to extract subtitles for stream ${index}`, err instanceof Error && err.message);
} finally {
setWorking();
setWorking(undefined);
}
}, [setWorking, subtitleStreams, subtitlesByStreamId, filePath]);
const onActiveVideoStreamChange = useCallback((index) => {
const onActiveVideoStreamChange = useCallback((index?: number) => {
if (!videoRef.current) throw new Error();
setHideMediaSourcePlayer(index == null || getVideoTrackForStreamIndex(videoRef.current, index) != null);
enableVideoTrack(videoRef.current, index);
setActiveVideoStreamIndex(index);
}, []);
const onActiveAudioStreamChange = useCallback((index) => {
const onActiveAudioStreamChange = useCallback((index?: number) => {
if (!videoRef.current) throw new Error();
setHideMediaSourcePlayer(index == null || getAudioTrackForStreamIndex(videoRef.current, index) != null);
enableAudioTrack(videoRef.current, index);
setActiveAudioStreamIndex(index);
@ -670,8 +673,8 @@ function App() {
const toggleStripAudio = useCallback(() => toggleStripStream((stream) => stream.codec_type === 'audio'), [toggleStripStream]);
const toggleStripThumbnail = useCallback(() => toggleStripStream(isStreamThumbnail), [toggleStripStream]);
const thumnailsRef = useRef([]);
const thumnailsRenderingPromiseRef = useRef();
const thumnailsRef = useRef<{ from: number, url: string }[]>([]);
const thumnailsRenderingPromiseRef = useRef<Promise<void>>();
function addThumbnail(thumbnail) {
// console.log('Rendered thumbnail', thumbnail.url);
@ -681,7 +684,7 @@ function App() {
const hasAudio = !!activeAudioStream;
const hasVideo = !!activeVideoStream;
const waveformEnabled = hasAudio && ['waveform', 'big-waveform'].includes(waveformMode);
const waveformEnabled = hasAudio && waveformMode != null && ['waveform', 'big-waveform'].includes(waveformMode);
const bigWaveformEnabled = waveformEnabled && waveformMode === 'big-waveform';
const showThumbnails = thumbnailsEnabled && hasVideo;
@ -739,28 +742,28 @@ function App() {
console.log('State reset');
const video = videoRef.current;
setCommandedTime(0);
video.currentTime = 0;
video.playbackRate = 1;
video!.currentTime = 0;
video!.playbackRate = 1;
// setWorking();
setPreviewFilePath();
setPreviewFilePath(undefined);
setUsingDummyVideo(false);
setPlaying(false);
playbackModeRef.current = undefined;
setCompatPlayerEventId(0);
setDuration();
setDuration(undefined);
cutSegmentsHistory.go(0);
clearSegments();
setFileFormat();
setDetectedFileFormat();
setFileFormat(undefined);
setDetectedFileFormat(undefined);
setRotation(360);
setCutProgress();
setCutProgress(undefined);
setStartTimeOffset(0);
setFilePath(''); // Setting video src="" prevents memory leak in chromium
setExternalFilesMeta({});
setCustomTagsByFile({});
setParamsByStreamId(new Map());
setDetectedFps();
setDetectedFps(undefined);
setMainFileMeta({ streams: [], formatData: [] });
setCopyStreamIdsByFile({});
setStreamsSelectorShown(false);
@ -770,9 +773,9 @@ function App() {
setZoomWindowStartTime(0);
setDeselectedSegmentIds({});
setSubtitlesByStreamId({});
setActiveAudioStreamIndex();
setActiveVideoStreamIndex();
setActiveSubtitleStreamIndex();
setActiveAudioStreamIndex(undefined);
setActiveVideoStreamIndex(undefined);
setActiveSubtitleStreamIndex(undefined);
setHideMediaSourcePlayer(false);
setExportConfirmVisible(false);
resetMergedOutFileName();
@ -809,7 +812,7 @@ function App() {
setCutProgress(0);
await html5ifyDummy({ filePath: fp, outPath: path, onProgress: setCutProgress });
} finally {
setCutProgress();
setCutProgress(undefined);
}
return path;
}
@ -818,7 +821,7 @@ function App() {
const shouldIncludeVideo = !usesDummyVideo && hv;
return await html5ify({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: shouldIncludeVideo, onProgress: setCutProgress });
} finally {
setCutProgress();
setCutProgress(undefined);
}
}
@ -833,7 +836,7 @@ function App() {
if (batchFiles.length < 1) return;
const filePaths = batchFiles.map((f) => f.path);
const failedFiles = [];
const failedFiles: string[] = [];
let i = 0;
const setTotalProgress = (fileProgress = 0) => setCutProgress((i + fileProgress) / filePaths.length);
@ -864,13 +867,13 @@ function App() {
setTotalProgress();
}
if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}`, timer: null, showConfirmButton: true });
if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}`, timer: null as any as undefined, showConfirmButton: true });
} catch (err) {
errorToast(i18n.t('Failed to batch convert to supported format'));
console.error('Failed to html5ify', err);
} finally {
setWorking();
setCutProgress();
setWorking(undefined);
setCutProgress(undefined);
}
}, [batchFiles, customOutDir, ensureWritableOutDir, html5ify, setWorking]);
@ -889,10 +892,10 @@ function App() {
const pause = useCallback(() => {
if (!filePath || !playing) return;
videoRef.current.pause();
videoRef.current!.pause();
}, [filePath, playing]);
const play = useCallback((resetPlaybackRate) => {
const play = useCallback((resetPlaybackRate?: boolean) => {
if (!filePath || playing) return;
const video = videoRef.current;
@ -900,8 +903,8 @@ function App() {
// This was added to re-sync time if file gets reloaded #1674 - but I had to remove this because it broke loop-selected-segments https://github.com/mifi/lossless-cut/discussions/1785#discussioncomment-7852134
// if (Math.abs(commandedTimeRef.current - video.currentTime) > 1) video.currentTime = commandedTimeRef.current;
if (resetPlaybackRate) video.playbackRate = outputPlaybackRate;
video.play().catch((err) => {
if (resetPlaybackRate) video!.playbackRate = outputPlaybackRate;
video?.play().catch((err) => {
showPlaybackFailedMessage();
console.error(err);
});
@ -982,7 +985,7 @@ function App() {
newBatch.splice(index, 1);
const newItemAtIndex = newBatch[index];
if (newItemAtIndex != null) setSelectedBatchFiles([newItemAtIndex.path]);
else if (newBatch.length > 0) setSelectedBatchFiles([newBatch[0].path]);
else if (newBatch.length > 0) setSelectedBatchFiles([newBatch[0]!.path]);
else setSelectedBatchFiles([]);
return newBatch;
});
@ -995,7 +998,7 @@ function App() {
preserveMetadataOnMerge,
}), [ffmpegExperimental, movFastStart, preserveMetadataOnMerge, preserveMovData]);
const openSendReportDialogWithState = useCallback(async (err) => {
const openSendReportDialogWithState = useCallback(async (err?: unknown) => {
const state = {
...commonSettings,
@ -1057,8 +1060,8 @@ function App() {
const metadataFromPath = paths[0];
const { haveExcludedStreams } = await concatFiles({ paths, outPath, outDir, outFormat, metadataFromPath, includeAllStreams, streams, ffmpegExperimental, onProgress: setCutProgress, preserveMovData, movFastStart, preserveMetadataOnMerge, chapters: chaptersFromSegments, appendFfmpegCommandLog });
const warnings = [];
const notices = [];
const warnings: string[] = [];
const notices: string[] = [];
const outputSize = await readFileSize(outPath); // * 1.06; // testing:)
const sizeCheckResult = checkFileSizes(inputSize, outputSize);
@ -1070,28 +1073,30 @@ function App() {
} catch (err) {
if (err instanceof DirectoryAccessDeclinedError) return;
if (err.killed === true) {
// assume execa killed (aborted by user)
return;
}
console.error('stdout:', err.stdout);
console.error('stderr:', err.stderr);
if (isExecaFailure(err)) {
if (isOutOfSpaceError(err)) {
showDiskFull();
if (err instanceof Error) {
if ('killed' in err && err.killed === true) {
// assume execa killed (aborted by user)
return;
}
if ('stdout' in err) console.error('stdout:', err.stdout);
if ('stderr' in err) console.error('stderr:', err.stderr);
if (isExecaFailure(err)) {
if (isOutOfSpaceError(err)) {
showDiskFull();
return;
}
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
handleConcatFailed(err, reportState);
return;
}
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
handleConcatFailed(err, reportState);
return;
}
handleError(err);
} finally {
setWorking();
setCutProgress();
setWorking(undefined);
setCutProgress(undefined);
}
}, [setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, handleConcatFailed]);
@ -1111,14 +1116,14 @@ function App() {
setWorking({ text: i18n.t('Cleaning up'), abortController });
console.log('Cleaning up files', cleanupChoices2);
const pathsToDelete = [];
const pathsToDelete: string[] = [];
if (cleanupChoices2.trashTmpFiles && savedPaths.previewFilePath) pathsToDelete.push(savedPaths.previewFilePath);
if (cleanupChoices2.trashProjectFile && savedPaths.projectFilePath) pathsToDelete.push(savedPaths.projectFilePath);
if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath);
await deleteFiles({ paths: pathsToDelete, deleteIfTrashFails: cleanupChoices2.deleteIfTrashFails, signal: abortController.signal });
} catch (err) {
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err.message }));
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err instanceof Error ? err.message : String(err) }));
console.error(err);
}
}, [batchListRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]);
@ -1148,12 +1153,12 @@ function App() {
try {
await cleanupFilesWithDialog();
} finally {
setWorking();
setWorking(undefined);
}
}, [cleanupFilesWithDialog, isFileOpened, setWorking]);
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template, forceSafeOutputFileName }) => (
generateOutSegFileNamesRaw({ segments, template, forceSafeOutputFileName, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding })
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }) => (
generateOutSegFileNamesRaw({ segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding })
), [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]);
const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []);
@ -1267,7 +1272,7 @@ function App() {
if (exportExtraStreams) {
try {
setCutProgress(); // If extracting extra streams takes a long time, prevent loader from being stuck at 100%
setCutProgress(undefined); // If extracting extra streams takes a long time, prevent loader from being stuck at 100%
setWorking({ text: i18n.t('Extracting {{count}} unprocessable tracks', { count: nonCopiedExtraStreams.length }) });
await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams, enableOverwriteOutput });
notices.push(i18n.t('Unprocessable streams were exported as separate files.'));
@ -1286,27 +1291,29 @@ function App() {
resetMergedOutFileName();
} catch (err) {
if (err.killed === true) {
// assume execa killed (aborted by user)
return;
}
console.error('stdout:', err.stdout);
console.error('stderr:', err.stderr);
if (isExecaFailure(err)) {
if (isOutOfSpaceError(err)) {
showDiskFull();
if (err instanceof Error) {
if ('killed' in err && err.killed === true) {
// assume execa killed (aborted by user)
return;
}
if ('stdout' in err) console.error('stdout:', err.stdout);
if ('stderr' in err) console.error('stderr:', err.stderr);
if (isExecaFailure(err)) {
if (isOutOfSpaceError(err)) {
showDiskFull();
return;
}
handleExportFailed(err);
return;
}
handleExportFailed(err);
return;
}
handleError(err);
} finally {
setWorking();
setCutProgress();
setWorking(undefined);
setCutProgress(undefined);
}
}, [numStreamsToCopy, segmentsToExport, haveInvalidSegs, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, mergedOutFilePath, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, resetMergedOutFileName, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, filePath, handleExportFailed]);
@ -1370,15 +1377,15 @@ function App() {
} catch (err) {
handleError(err);
} finally {
setWorking();
setCutProgress();
setWorking(undefined);
setCutProgress(undefined);
}
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, captureFramesRange, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages([currentCutSeg?.segId]), [currentCutSeg?.segId, extractSegmentFramesAsImages]);
const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentFramesAsImages(selectedSegments.map((seg) => seg.segId)), [extractSegmentFramesAsImages, selectedSegments]);
const changePlaybackRate = useCallback((dir, rateMultiplier) => {
const changePlaybackRate = useCallback((dir: number, rateMultiplier?: number) => {
if (compatPlayerEnabled) {
toast.fire({ title: i18n.t('Unable to change playback rate right now'), timer: 1000 });
return;
@ -1386,11 +1393,11 @@ function App() {
const video = videoRef.current;
if (!playing) {
video.play();
video!.play();
} else {
const newRate = adjustRate(video.playbackRate, dir, rateMultiplier);
const newRate = adjustRate(video!.playbackRate, dir, rateMultiplier);
toast.fire({ title: `${i18n.t('Playback rate:')} ${Math.round(newRate * 100)}%`, timer: 1000 });
video.playbackRate = newRate;
video!.playbackRate = newRate;
}
}, [playing, compatPlayerEnabled]);
@ -1398,10 +1405,11 @@ function App() {
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, commandedTime);
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
if (firstSegmentAtCursorIndex == null) return undefined;
return cutSegments[firstSegmentAtCursorIndex];
}, [apparentCutSegments, commandedTime, cutSegments]);
const loadEdlFile = useCallback(async ({ path, type, append }) => {
const loadEdlFile = useCallback(async ({ path, type, append }: { path: string, type: EdlFileType, append?: boolean }) => {
console.log('Loading EDL file', type, path, append);
loadCutSegments(await readEdlFile({ type, path }), append);
}, [loadCutSegments]);
@ -1443,7 +1451,7 @@ function App() {
} catch (err) {
if (err instanceof DirectoryAccessDeclinedError) throw err;
console.error('EDL load failed, but continuing', err);
errorToast(`${i18n.t('Failed to load segments')} (${err.message})`);
errorToast(`${i18n.t('Failed to load segments')} (${err instanceof Error && err.message})`);
}
}
@ -1622,14 +1630,14 @@ function App() {
} catch (err) {
handleError(err);
} finally {
setWorking();
setWorking(undefined);
}
}, [userOpenSingleFile, setWorking, filePath]);
const batchFileJump = useCallback((direction) => {
if (batchFiles.length === 0) return;
if (selectedBatchFiles.length === 0) {
setSelectedBatchFiles([batchFiles[0].path]);
setSelectedBatchFiles([batchFiles[0]!.path]);
return;
}
const selectedFilePath = selectedBatchFiles[direction > 0 ? selectedBatchFiles.length - 1 : 0];
@ -1645,7 +1653,7 @@ function App() {
batchOpenSingleFile(selectedBatchFiles[0]);
}, [batchOpenSingleFile, selectedBatchFiles]);
const onBatchFileSelect = useCallback((path) => {
const onBatchFileSelect = useCallback((path: string) => {
if (selectedBatchFiles.includes(path)) batchOpenSingleFile(path);
else setSelectedBatchFiles([path]);
}, [batchOpenSingleFile, selectedBatchFiles]);
@ -1687,7 +1695,7 @@ function App() {
errorToast(i18n.t('Failed to extract all streams'));
console.error('Failed to extract all streams', err);
} finally {
setWorking();
setWorking(undefined);
}
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking]);
@ -1697,7 +1705,7 @@ function App() {
let selectedOption = rememberConvertToSupportedFormat;
if (selectedOption == null || ignoreRememberedValue) {
let allowedOptions = [];
let allowedOptions: Html5ifyMode[] = [];
if (hasAudio && hasVideo) allowedOptions = ['fastest', 'fast-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest'];
else if (hasAudio) allowedOptions = ['fast-audio-remux', 'slow-audio', 'slowest'];
else if (hasVideo) allowedOptions = ['fastest', 'fast', 'slow', 'slowest'];
@ -1720,7 +1728,7 @@ function App() {
errorToast(i18n.t('Failed to convert file. Try a different conversion'));
console.error('Failed to html5ify file', err);
} finally {
setWorking();
setWorking(undefined);
}
}, [customOutDir, filePath, html5ifyAndLoad, hasVideo, hasAudio, rememberConvertToSupportedFormat, setWorking]);
@ -1751,12 +1759,12 @@ function App() {
errorToast(i18n.t('Failed to fix file duration'));
console.error('Failed to fix file duration', err);
} finally {
setWorking();
setCutProgress();
setWorking(undefined);
setCutProgress(undefined);
}
}, [checkFileOpened, customOutDir, duration, fileFormat, fixInvalidDuration, hideAllNotifications, loadMedia, setWorking]);
const addStreamSourceFile = useCallback(async (path) => {
const addStreamSourceFile = useCallback(async (path: string) => {
if (allFilesMeta[path]) return undefined; // Already added?
const fileMeta = await readFileMeta(path);
// console.log('streams', fileMeta.streams);
@ -1794,15 +1802,19 @@ function App() {
}
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, hideAllNotifications]);
const batchLoadPaths = useCallback((newPaths, append) => {
const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => {
setBatchFiles((existingFiles) => {
const mapPathsToFiles = (paths) => paths.map((path) => ({ path, name: basename(path) }));
if (append) {
const newUniquePaths = newPaths.filter((newPath) => !existingFiles.some(({ path: existingPath }) => newPath === existingPath));
setSelectedBatchFiles([newUniquePaths[0]]);
const [firstNewUniquePath] = newUniquePaths;
if (firstNewUniquePath == null) throw new Error();
setSelectedBatchFiles([firstNewUniquePath]);
return [...existingFiles, ...mapPathsToFiles(newUniquePaths)];
}
setSelectedBatchFiles([newPaths[0]]);
const [firstNewPath] = newPaths;
if (firstNewPath == null) throw new Error();
setSelectedBatchFiles([firstNewPath]);
return mapPathsToFiles(newPaths);
});
}, []);
@ -1874,7 +1886,7 @@ function App() {
const isLlcProject = filePathLowerCase.endsWith('.llc');
// Need to ask the user what to do if more than one option
const inputOptions = {
const inputOptions: { open: string, project?: string, tracks?: string, subtitles?: string, addToBatch?: string, mergeWithCurrentFile?: string } = {
open: isFileOpened ? i18n.t('Open the file instead of the current one') : i18n.t('Open the file'),
};
@ -1912,7 +1924,7 @@ function App() {
return;
}
if (openFileResponse === 'mergeWithCurrentFile') {
const batchPaths = new Set();
const batchPaths = new Set<string>();
if (filePath) batchPaths.add(filePath);
filePaths.forEach((path) => batchPaths.add(path));
batchLoadPaths([...batchPaths]);
@ -1927,13 +1939,13 @@ function App() {
await userOpenSingleFile({ path: firstFilePath, isLlcProject });
} catch (err) {
console.error('userOpenFiles', err);
if (err.code === 'LLC_FFPROBE_UNSUPPORTED_FILE') {
if (err instanceof Error && 'code' in err && err.code === 'LLC_FFPROBE_UNSUPPORTED_FILE') {
errorToast(i18n.t('Unsupported file'));
} else {
handleError(i18n.t('Failed to open file'), err);
}
} finally {
setWorking();
setWorking(undefined);
}
}, [alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]);
@ -1979,13 +1991,14 @@ function App() {
console.warn('No video tag to full screen');
return;
}
if (videoContainerRef.current == null) throw new Error('videoContainerRef.current == null');
await screenfull.toggle(videoContainerRef.current, { navigationUI: 'hide' });
} catch (err) {
console.error('Failed to toggle fullscreen', err);
}
}, []);
const onEditSegmentTags = useCallback((index) => {
const onEditSegmentTags = useCallback((index: number) => {
setEditingSegmentTagsSegmentIndex(index);
setEditingSegmentTags(getSegmentTags(apparentCutSegments[index]));
}, [apparentCutSegments]);
@ -1994,7 +2007,7 @@ function App() {
onEditSegmentTags(currentSegIndexSafe);
}, [currentSegIndexSafe, onEditSegmentTags]);
const mainActions = useMemo(() => {
const mainActions: Record<string, (a: { keyup: boolean }) => void> = useMemo(() => {
async function exportYouTube() {
if (!checkFileOpened()) return;
@ -2129,9 +2142,9 @@ function App() {
};
}, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleStripThumbnail, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
const getKeyboardAction = useCallback((action) => mainActions[action], [mainActions]);
const getKeyboardAction = useCallback((action: string) => mainActions[action], [mainActions]);
const onKeyPress = useCallback(({ action, keyup }) => {
const onKeyPress = useCallback(({ action, keyup }: { action: string, keyup: boolean }) => {
function tryMainActions() {
const fn = getKeyboardAction(action);
if (!fn) return { match: false };
@ -2204,14 +2217,14 @@ function App() {
errorToast(i18n.t('Failed to extract track'));
console.error('Failed to extract track', err);
} finally {
setWorking();
setWorking(undefined);
}
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking]);
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);
const onVideoError = useCallback(async () => {
const { error } = videoRef.current;
const error = videoRef.current?.error;
if (!error) return;
if (!fileUri) return; // Probably MEDIA_ELEMENT_ERROR: Empty src attribute
@ -2242,7 +2255,7 @@ function App() {
console.error(err);
showPlaybackFailedMessage();
} finally {
setWorking();
setWorking(undefined);
}
} catch (err) {
handleError(err);
@ -2292,14 +2305,14 @@ function App() {
}
}
const actionsWithArgs = {
openFiles: (filePaths) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); },
const actionsWithArgs: Record<string, (...args: any[]) => void> = {
openFiles: (filePaths: string[]) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); },
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
importEdlFile,
exportEdlFile: tryExportEdlFile,
};
async function actionWithCatch(fn) {
async function actionWithCatch(fn: () => void) {
try {
await fn();
} catch (err) {
@ -2307,17 +2320,17 @@ function App() {
}
}
const actionsWithCatch = [
const actionsWithCatch: Readonly<[string, (event: unknown, ...a: any) => Promise<void>]>[] = [
// actions with arguments:
...Object.entries(actionsWithArgs).map(([key, fn]) => [
key,
async (event, ...args) => actionWithCatch(() => fn(...args)),
]),
async (_event: unknown, ...args: unknown[]) => actionWithCatch(() => fn(...args)),
] as const),
// all main actions (no arguments, except keyup which we don't support):
...Object.entries(mainActions).map(([key, fn]) => [
key,
async () => actionWithCatch(() => fn({ keyup: false })),
]),
] as const),
];
actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.on(key, action));
@ -2330,8 +2343,9 @@ function App() {
}, [checkFileOpened, customOutDir, detectedFps, filePath, getFrameCount, getKeyboardAction, loadCutSegments, mainActions, selectedSegments, userOpenFiles]);
useEffect(() => {
async function onDrop(ev) {
async function onDrop(ev: DragEvent) {
ev.preventDefault();
if (!ev.dataTransfer) return;
const { files } = ev.dataTransfer;
const filePaths = Array.from(files).map(f => f.path);
@ -2343,11 +2357,11 @@ function App() {
return () => document.body.removeEventListener('drop', onDrop);
}, [userOpenFiles]);
const renderOutFmt = useCallback((style) => (
const renderOutFmt = useCallback((style: CSSProperties) => (
<OutputFormatSelect style={style} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
), [detectedFileFormat, fileFormat, onOutputFormatUserChange]);
const onTunerRequested = useCallback((type) => {
const onTunerRequested = useCallback((type: TunerType) => {
setSettingsVisible(false);
setTunerVisible(type);
}, []);
@ -2392,6 +2406,7 @@ function App() {
<ThemeProvider value={theme}>
<div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}>
<TopMenu
// @ts-expect-error todo
filePath={filePath}
fileFormat={fileFormat}
copyAnyAudioTrack={copyAnyAudioTrack}
@ -2410,6 +2425,7 @@ function App() {
<AnimatePresence>
{showLeftBar && (
<BatchFilesList
// @ts-expect-error todo
selectedBatchFiles={selectedBatchFiles}
filePath={filePath}
width={leftBarWidth}
@ -2496,12 +2512,13 @@ function App() {
{working && <Working text={working.text} cutProgress={cutProgress} onAbortClick={handleAbortWorkingClick} />}
</AnimatePresence>
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible()} />}
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible(undefined)} />}
</div>
<AnimatePresence>
{showRightBar && isFileOpened && (
<SegmentList
// @ts-expect-error todo
width={rightBarWidth}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
@ -2546,6 +2563,7 @@ function App() {
<div className="no-user-select" style={bottomStyle}>
<Timeline
// @ts-expect-error todo
shouldShowKeyframes={shouldShowKeyframes}
waveforms={waveforms}
shouldShowWaveform={shouldShowWaveform}
@ -2577,6 +2595,7 @@ function App() {
/>
<BottomBar
// @ts-expect-error todo
zoom={zoom}
setZoom={setZoom}
timelineToggleComfortZoom={timelineToggleComfortZoom}
@ -2625,11 +2644,13 @@ function App() {
/>
</div>
{/* @ts-expect-error todo */}
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
{mainStreams && (
<StreamsSelector
// @ts-expect-error todo
mainFilePath={filePath}
mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters}
@ -2673,6 +2694,7 @@ function App() {
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
{/* @ts-expect-error todo */}
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} mainActions={mainActions} />
</div>
</ThemeProvider>

View File

@ -1,10 +1,13 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import CopyClipboardButton from './components/CopyClipboardButton';
import Sheet from './components/Sheet';
import { FfmpegCommandLog } from './types';
const LastCommandsSheet = memo(({ visible, onTogglePress, ffmpegCommandLog }) => {
const LastCommandsSheet = memo(({ visible, onTogglePress, ffmpegCommandLog }: {
visible: boolean, onTogglePress: () => void, ffmpegCommandLog: FfmpegCommandLog,
}) => {
const { t } = useTranslation();
return (

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { useTranslation, Trans } from 'react-i18next';
@ -8,7 +8,9 @@ import useUserSettings from './hooks/useUserSettings';
const electron = window.require('electron');
const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }) => {
const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }: {
mifiLink: unknown, currentCutSeg, onClick: () => void, darkMode?: boolean,
}) => {
const { t } = useTranslation();
const { simpleMode } = useUserSettings();
@ -37,12 +39,11 @@ const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }) => {
)}
</div>
{mifiLink && mifiLink.loadUrl && (
{mifiLink && typeof mifiLink === 'object' && 'loadUrl' in mifiLink && typeof mifiLink.loadUrl === 'string' && mifiLink.loadUrl && (
<div style={{ position: 'relative', margin: '3vmin', width: '60vmin', height: '20vmin' }}>
<iframe src={`${mifiLink.loadUrl}#dark=${darkMode ? 'true' : 'false'}`} title="iframe" style={{ background: 'rgba(0,0,0,0)', border: 'none', pointerEvents: 'none', width: '100%', height: '100%', position: 'absolute' }} />
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
<div style={{ width: '100%', height: '100%', position: 'absolute', cursor: 'pointer' }} role="button" onClick={(e) => { e.stopPropagation(); electron.shell.openExternal(mifiLink.targetUrl); }} />
<div style={{ width: '100%', height: '100%', position: 'absolute', cursor: 'pointer' }} role="button" onClick={(e) => { e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
</div>
)}
</div>

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, ForkIcon, DisableIcon } from 'evergreen-ui';

View File

@ -1,8 +1,11 @@
import React, { memo, useEffect, useState, useCallback, useRef } from 'react';
import { memo, useEffect, useState, useCallback, useRef } from 'react';
import { ffmpegExtractWindow } from '../util/constants';
import { Waveform } from '../types';
const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }) => {
const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }: {
waveforms: Waveform[], 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;
@ -10,14 +13,14 @@ const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom
const scaleFactor = zoom;
const [smoothTimeRaw, setSmoothTime] = useState(relevantTime);
const [smoothTimeRaw, setSmoothTime] = useState<number | undefined>(relevantTime);
const smoothTime = smoothTimeRaw ?? relevantTime;
const mouseDownRef = useRef();
const containerRef = useRef();
const mouseDownRef = useRef<{ relevantTime: number, x }>();
const containerRef = useRef<HTMLDivElement>(null);
const getRect = useCallback(() => containerRef.current.getBoundingClientRect(), []);
const getRect = useCallback(() => containerRef.current!.getBoundingClientRect(), []);
const handleMouseDown = useCallback((e) => {
const rect = e.target.getBoundingClientRect();

View File

@ -1,12 +1,12 @@
import React, { memo } from 'react';
import { Button } from 'evergreen-ui';
import { memo } from 'react';
import { Button, ButtonProps } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { FaImage } from 'react-icons/fa';
import useUserSettings from '../hooks/useUserSettings';
import { withBlur } from '../util';
const CaptureFormatButton = memo(({ showIcon = false, ...props }) => {
const CaptureFormatButton = memo(({ showIcon = false, ...props }: { showIcon?: boolean } & ButtonProps) => {
const { t } = useTranslation();
const { captureFormat, toggleCaptureFormat } = useUserSettings();
return (

View File

@ -1,4 +1,4 @@
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { memo, useState, useCallback, useEffect, useMemo, CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { TextInput, IconButton, Alert, Checkbox, Dialog, Button, Paragraph, CogIcon } from 'evergreen-ui';
import { AiOutlineMergeCells } from 'react-icons/ai';
@ -18,27 +18,26 @@ const { basename } = window.require('path');
const ReactSwal = withReactContent(Swal);
const containerStyle = { color: 'black' };
const containerStyle: CSSProperties = { color: 'black' };
const rowStyle = {
const rowStyle: CSSProperties = {
color: 'black', fontSize: 14, margin: '4px 0px', overflowY: 'auto', whiteSpace: 'nowrap',
};
const ConcatDialog = memo(({
isShown, onHide, paths, onConcat,
alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles,
const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: any, 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();
const [includeAllStreams, setIncludeAllStreams] = useState(false);
const [fileMeta, setFileMeta] = useState();
const [fileMeta, setFileMeta] = useState<{ format: any, streams: any, chapters: any }>();
const [allFilesMetaCache, setAllFilesMetaCache] = useState({});
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
const [settingsVisible, setSettingsVisible] = useState(false);
const [enableReadFileMeta, setEnableReadFileMeta] = useState(false);
const [outFileName, setOutFileName] = useState();
const [uniqueSuffix, setUniqueSuffix] = useState();
const [outFileName, setOutFileName] = useState<string>();
const [uniqueSuffix, setUniqueSuffix] = useState<number>();
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
@ -53,10 +52,10 @@ const ConcatDialog = memo(({
let aborted = false;
(async () => {
setFileMeta();
setFileFormat();
setDetectedFileFormat();
setOutFileName();
setFileMeta(undefined);
setFileFormat(undefined);
setDetectedFileFormat(undefined);
setOutFileName(undefined);
const fileMetaNew = await readFileMeta(firstPath);
const fileFormatNew = await getSmarterOutFormat({ filePath: firstPath, fileMeta: fileMetaNew });
if (aborted) return;
@ -73,7 +72,7 @@ const ConcatDialog = memo(({
useEffect(() => {
if (fileFormat == null || firstPath == null) {
setOutFileName();
setOutFileName(undefined);
return;
}
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath: firstPath });
@ -94,7 +93,7 @@ const ConcatDialog = memo(({
const problemsByFile = useMemo(() => {
if (!allFilesMeta) return [];
const allFilesMetaExceptFirstFile = allFilesMeta.slice(1);
const [, firstFileMeta] = allFilesMeta[0];
const [, firstFileMeta] = allFilesMeta[0]!;
const errors = {};
function addError(path, error) {
if (!errors[path]) errors[path] = [];
@ -155,7 +154,11 @@ const ConcatDialog = memo(({
const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]);
const onConcatClick = useCallback(() => onConcat({ paths, includeAllStreams, streams: fileMeta.streams, outFileName, fileFormat, clearBatchFilesAfterConcat }), [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, onConcat, outFileName, paths]);
const onConcatClick = useCallback(() => {
if (outFileName == null) throw new Error();
if (fileFormat == null) throw new Error();
onConcat({ paths, includeAllStreams, streams: fileMeta!.streams, outFileName, fileFormat, clearBatchFilesAfterConcat });
}, [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, onConcat, outFileName, paths]);
return (
<>

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback } from 'react';
import { CSSProperties, memo, useCallback } from 'react';
import { FaClipboard } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { motion, useAnimation } from 'framer-motion';
@ -6,7 +6,7 @@ import { motion, useAnimation } from 'framer-motion';
const electron = window.require('electron');
const { clipboard } = electron;
const CopyClipboardButton = memo(({ text, style }) => {
const CopyClipboardButton = memo(({ text, style }: { text: string, style?: CSSProperties }) => {
const { t } = useTranslation();
const animation = useAnimation();

View File

@ -1,4 +1,4 @@
import React, { memo, useMemo } from 'react';
import { CSSProperties, memo, useMemo } from 'react';
import i18n from 'i18next';
import allOutFormats from '../outFormats';
@ -15,7 +15,9 @@ function renderFormatOptions(formats) {
));
}
const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutputFormatUserChange }) => {
const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutputFormatUserChange }: {
style: CSSProperties, detectedFileFormat?: string, fileFormat?: string, 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]);

View File

@ -1,4 +1,4 @@
import React, { memo, useState, useCallback, useRef, useEffect } from 'react';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { MdSubtitles } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import Select from './Select';
@ -13,15 +13,25 @@ const PlaybackStreamSelector = memo(({
onActiveSubtitleChange,
onActiveVideoStreamChange,
onActiveAudioStreamChange,
}: {
subtitleStreams,
videoStreams,
audioStreams,
activeSubtitleStreamIndex?: number,
activeVideoStreamIndex?: number,
activeAudioStreamIndex?: number,
onActiveSubtitleChange: (a?: number) => void,
onActiveVideoStreamChange: (a?: number) => void,
onActiveAudioStreamChange: (a?: number) => void,
}) => {
const [controlVisible, setControlVisible] = useState(false);
const timeoutRef = useRef();
const timeoutRef = useRef<number>();
const { t } = useTranslation();
const resetTimer = useCallback(() => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setControlVisible(false), 7000);
timeoutRef.current = window.setTimeout(() => setControlVisible(false), 7000);
}, []);
const onChange = useCallback((e, fn) => {

View File

@ -1,9 +1,12 @@
import React, { useMemo } from 'react';
import { CSSProperties, useMemo } from 'react';
import { useSegColors } from '../contexts';
import useUserSettings from '../hooks/useUserSettings';
import { SegmentBase } from '../types';
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }) => {
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: {
currentCutSeg: SegmentBase, side: 'start' | 'end', Icon, onClick?: () => void, title?: string, style?: CSSProperties
}) => {
const { darkMode } = useUserSettings();
const { getSegColor } = useSegColors();
const segColor = useMemo(() => getSegColor(currentCutSeg), [currentCutSeg, getSegColor]);

View File

@ -1,8 +1,8 @@
import React, { memo } from 'react';
import { SelectHTMLAttributes, memo } from 'react';
import styles from './Select.module.css';
const Select = memo((props) => (
const Select = memo((props: SelectHTMLAttributes<HTMLSelectElement>) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<select className={styles.select} {...props} />
));

View File

@ -1,11 +1,14 @@
import React from 'react';
import { CSSProperties } from 'react';
import { FaHandPointUp } from 'react-icons/fa';
import SegmentCutpointButton from './SegmentCutpointButton';
import { mirrorTransform } from '../util';
import { SegmentBase } from '../types';
// constant side because we are mirroring
const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }) => (
const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: {
currentCutSeg: SegmentBase, side: 'start' | 'end', title?: string, onClick?: () => void, style?: CSSProperties
}) => (
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="end" Icon={FaHandPointUp} onClick={onClick} title={title} style={{ transform: side === 'start' ? mirrorTransform : undefined, ...style }} />
);

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { FaYinYang, FaKeyboard } from 'react-icons/fa';
import { GlobeIcon, CleanIcon, CogIcon, Button, NumericalIcon, FolderCloseIcon, DocumentIcon, TimeIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
@ -15,6 +15,7 @@ import styles from './Settings.module.css';
import Select from './Select';
import { getModifierKeyNames } from '../hooks/useTimelineScroll';
import { TunerType } from '../types';
const Row = (props) => (
@ -45,6 +46,12 @@ const Settings = memo(({
askForCleanupChoices,
toggleStoreProjectInWorkingDir,
simpleMode,
}: {
onTunerRequested: (type: TunerType) => void,
onKeyboardShortcutsDialogRequested: () => void,
askForCleanupChoices: () => Promise<void>,
toggleStoreProjectInWorkingDir: () => Promise<void>,
simpleMode: boolean,
}) => {
const { t } = useTranslation();
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);

View File

@ -1,11 +1,13 @@
import React, { memo } from 'react';
import { CSSProperties, ReactNode, memo } from 'react';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import styles from './Sheet.module.css';
const Sheet = memo(({ visible, onClosePress, children, maxWidth = 800, style }) => {
const Sheet = memo(({ visible, onClosePress, children, maxWidth = 800, style }: {
visible: boolean, onClosePress: () => void, children: ReactNode, maxWidth?: number, style?: CSSProperties
}) => {
const { t } = useTranslation();
return (

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { CSSProperties, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBaby } from 'react-icons/fa';
@ -6,7 +6,7 @@ import { primaryTextColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
const SimpleModeButton = memo(({ size = 20, style }) => {
const SimpleModeButton = memo(({ size = 20, style }: { size?: number, style: CSSProperties }) => {
const { t } = useTranslation();
const { simpleMode, toggleSimpleMode } = useUserSettings();

View File

@ -1,9 +1,11 @@
import React from 'react';
import { CSSProperties } from 'react';
import * as RadixSwitch from '@radix-ui/react-switch';
import classes from './Switch.module.css';
const Switch = ({ checked, disabled, onCheckedChange, title, style }) => (
const Switch = ({ checked, disabled, onCheckedChange, title, style }: {
checked: boolean, disabled?: boolean, onCheckedChange: (v: boolean) => void, title?: string, style?: CSSProperties,
}) => (
<RadixSwitch.Root disabled={disabled} className={classes.SwitchRoot} checked={checked} onCheckedChange={onCheckedChange} style={style} title={title}>
<RadixSwitch.Thumb className={classes.SwitchThumb} />
</RadixSwitch.Root>

View File

@ -1,11 +1,13 @@
import React, { memo, useState, useCallback } from 'react';
import { memo, useState, useCallback, CSSProperties } from 'react';
import { Button } from 'evergreen-ui';
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 }) => {
const ValueTuner = memo(({ 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);
@ -13,7 +15,7 @@ const ValueTuner = memo(({ style, title, value, setValue, onFinished, resolution
function onChange(e) {
e.target.blur();
setValue(Math.min(Math.max(min, ((e.target.value / resolution) * (max - min)) + min)), max);
setValue(Math.min(Math.max(min, ((e.target.value / resolution) * (max - min)) + min), max));
}
const isZoomed = !(min === minIn && max === maxIn);

View File

@ -1,10 +1,11 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import ValueTuner from './ValueTuner';
import useUserSettings from '../hooks/useUserSettings';
import { TunerType } from '../types';
const ValueTuners = memo(({ type, onFinished }) => {
const ValueTuners = memo(({ type, onFinished }: { type: TunerType, onFinished: () => void }) => {
const { t } = useTranslation();
const { wheelSensitivity, setWheelSensitivity, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, keyboardSeekAccFactor, setKeyboardSeekAccFactor } = useUserSettings();

View File

@ -1,17 +1,17 @@
import React, { memo, useState, useCallback, useRef, useEffect } from 'react';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
const VolumeControl = memo(({ playbackVolume, setPlaybackVolume }) => {
const VolumeControl = memo(({ playbackVolume, setPlaybackVolume }: { playbackVolume: number, setPlaybackVolume: (a: number) => void }) => {
const [volumeControlVisible, setVolumeControlVisible] = useState(false);
const timeoutRef = useRef();
const timeoutRef = useRef<number>();
const { t } = useTranslation();
useEffect(() => {
const clear = () => clearTimeout(timeoutRef.current);
clear();
timeoutRef.current = setTimeout(() => setVolumeControlVisible(false), 4000);
timeoutRef.current = window.setTimeout(() => setVolumeControlVisible(false), 4000);
return () => clear();
}, [playbackVolume, volumeControlVisible]);

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { motion } from 'framer-motion';
import Lottie from 'react-lottie-player/dist/LottiePlayerLight';
import { Button } from 'evergreen-ui';
@ -8,7 +8,9 @@ import { primaryColor } from '../colors';
import loadingLottie from '../7077-magic-flow.json';
const Working = memo(({ text, cutProgress, onAbortClick }) => (
const Working = memo(({ text, cutProgress, onAbortClick }: {
text: string, cutProgress?: number, 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' }}

View File

@ -1,17 +1,20 @@
import React, { useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { Checkbox, RadioGroup, Paragraph } from 'evergreen-ui';
import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content';
import Swal from '../swal';
import { Html5ifyMode } from '../types';
const ReactSwal = withReactContent(Swal);
// eslint-disable-next-line import/prefer-default-export
export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initialOption }) {
const availOptions = {
export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initialOption }: {
allowedOptions: Html5ifyMode[], showRemember?: boolean, initialOption?: Html5ifyMode
}) {
const availOptions: Record<Html5ifyMode, string> = {
fastest: i18n.t('Fastest: FFmpeg-assisted playback'),
fast: i18n.t('Fast: Full quality remux (no audio), likely to fail'),
'fast-audio-remux': i18n.t('Fast: Full quality remux, likely to fail'),
@ -20,12 +23,12 @@ export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initia
'slow-audio': i18n.t('Slow: Low quality encode'),
slowest: i18n.t('Slowest: High quality encode'),
};
const inputOptions = {};
const inputOptions: Partial<Record<Html5ifyMode, string>> = {};
allowedOptions.forEach((allowedOption) => {
inputOptions[allowedOption] = availOptions[allowedOption];
});
let selectedOption = inputOptions[initialOption] ? initialOption : Object.keys(inputOptions)[0];
let selectedOption: Html5ifyMode = initialOption != null && inputOptions[initialOption] ? initialOption : Object.keys(inputOptions)[0]! as Html5ifyMode;
let rememberChoice = !!initialOption;
const Html = () => {
@ -59,7 +62,7 @@ export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initia
});
return {
selectedOption: response && selectedOption,
selectedOption: response != null ? selectedOption : undefined,
remember: rememberChoice,
};
}

View File

@ -1,23 +1,25 @@
import React, { useState } from 'react';
import { ArrowRightIcon, HelpIcon, TickCircleIcon, WarningSignIcon, InfoSignIcon, Checkbox } from 'evergreen-ui';
import { CSSProperties, ReactNode, useState } from 'react';
import { ArrowRightIcon, HelpIcon, TickCircleIcon, WarningSignIcon, InfoSignIcon, Checkbox, IconComponent } from 'evergreen-ui';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
import withReactContent from 'sweetalert2-react-content';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow as style } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import { tomorrow as syntaxStyle } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import JSON5 from 'json5';
import { SweetAlertOptions } from 'sweetalert2';
import { parseDuration, formatDuration } from '../util/duration';
import Swal, { swalToastOptions, toast } from '../swal';
import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton';
import { isWindows, showItemInFolder } from '../util';
import { SegmentBase } from '../types';
const { dialog } = window.require('@electron/remote');
const ReactSwal = withReactContent(Swal);
export async function promptTimeOffset({ initialValue, title, text }) {
export async function promptTimeOffset({ initialValue, title, text }: { initialValue?: string, title: string, text?: string }) {
const { value } = await Swal.fire({
title,
text,
@ -55,7 +57,7 @@ export async function askForYouTubeInput() {
inputValidator: (v) => {
if (v) {
const edl = parseYouTube(v);
if (edl.length > 0) return undefined;
if (edl.length > 0) return null;
}
return i18n.t('Please input a valid format.');
},
@ -99,7 +101,7 @@ export async function askForFfPath(defaultPath) {
export async function askForFileOpenAction(inputOptions) {
let value;
function onClick(key) {
function onClick(key?: string) {
value = key;
Swal.close();
}
@ -163,15 +165,17 @@ async function askForNumSegments() {
const { value } = await Swal.fire({
input: 'number',
inputAttributes: {
min: 0,
max: maxSegments,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
min: 0 as any as string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
max: maxSegments as any as string,
},
showCancelButton: true,
inputValue: '2',
text: i18n.t('Divide timeline into a number of equal length segments'),
inputValidator: (v) => {
const parsed = parseInt(v, 10);
if (!Number.isNaN(parsed) && parsed >= 2 && parsed <= maxSegments) return undefined;
if (!Number.isNaN(parsed) && parsed >= 2 && parsed <= maxSegments) return null;
return i18n.t('Please input a valid number of segments');
},
});
@ -184,7 +188,7 @@ async function askForNumSegments() {
export async function createNumSegments(fileDuration) {
const numSegments = await askForNumSegments();
if (numSegments == null) return undefined;
const edl = [];
const edl: SegmentBase[] = [];
const segDuration = fileDuration / numSegments;
for (let i = 0; i < numSegments; i += 1) {
edl.push({ start: i * segDuration, end: i === numSegments - 1 ? undefined : (i + 1) * segDuration });
@ -204,7 +208,7 @@ async function askForSegmentDuration(fileDuration) {
const duration = parseDuration(v);
if (duration != null) {
const numSegments = Math.ceil(fileDuration / duration);
if (duration > 0 && duration < fileDuration && numSegments <= maxSegments) return undefined;
if (duration > 0 && duration < fileDuration && numSegments <= maxSegments) return null;
}
return i18n.t('Please input a valid duration. Example: {{example}}', { example: exampleDuration });
},
@ -240,7 +244,7 @@ async function askForSegmentsRandomDurationRange() {
inputValidator: (v) => {
const parsed = parse(v);
if (!parsed) return i18n.t('Invalid input');
return undefined;
return null;
},
});
@ -289,7 +293,7 @@ export async function askForShiftSegments() {
inputValidator: (v) => {
const parsed = parseValue(v);
if (parsed == null) return i18n.t('Please input a valid duration. Example: {{example}}', { example: exampleDuration });
return undefined;
return null;
},
});
@ -337,7 +341,7 @@ export async function askForMetadataKey({ title, text }) {
input: 'text',
showCancelButton: true,
inputPlaceholder: 'key',
inputValidator: (v) => v.includes('=') && i18n.t('Invalid character(s) found in key'),
inputValidator: (v) => (v.includes('=') ? i18n.t('Invalid character(s) found in key') : null),
});
return value;
}
@ -413,7 +417,7 @@ export async function showCleanupFilesDialog(cleanupChoicesIn) {
export async function createFixedDurationSegments(fileDuration) {
const segmentDuration = await askForSegmentDuration(fileDuration);
if (segmentDuration == null) return undefined;
const edl = [];
const edl: SegmentBase[] = [];
for (let start = 0; start < fileDuration; start += segmentDuration) {
const end = start + segmentDuration;
edl.push({ start, end: end >= fileDuration ? undefined : end });
@ -429,7 +433,7 @@ export async function createRandomSegments(fileDuration) {
const randomInRange = (min, max) => min + Math.random() * (max - min);
const edl = [];
const edl: SegmentBase[] = [];
for (let start = randomInRange(gapMin, gapMax); start < fileDuration && edl.length < maxSegments; start += randomInRange(gapMin, gapMax)) {
const end = Math.min(fileDuration, start + randomInRange(durationMin, durationMax));
edl.push({ start, end });
@ -438,7 +442,7 @@ export async function createRandomSegments(fileDuration) {
return edl;
}
const MovSuggestion = ({ fileFormat }) => fileFormat === 'mp4' && <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li>;
const MovSuggestion = ({ fileFormat }) => (fileFormat === 'mp4' ? <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li> : null);
const OutputFormatSuggestion = () => <li><Trans>Select a different output <b>Format</b> (<b>matroska</b> and <b>mp4</b> support most codecs)</Trans></li>;
const WorkingDirectorySuggestion = () => <li><Trans>Set a different <b>Working directory</b></Trans></li>;
const DifferentFileSuggestion = () => <li><Trans>Try with a <b>Different file</b></Trans></li>;
@ -463,7 +467,8 @@ export async function showExportFailedDialog({ fileFormat, safeOutputFileName })
</div>
);
const { value } = await ReactSwal.fire({ title: i18n.t('Unable to export this file'), html, timer: null, showConfirmButton: true, showCancelButton: true, cancelButtonText: i18n.t('OK'), confirmButtonText: i18n.t('Report'), reverseButtons: true, focusCancel: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { value } = await ReactSwal.fire({ title: i18n.t('Unable to export this file'), html, timer: null as any as undefined, showConfirmButton: true, showCancelButton: true, cancelButtonText: i18n.t('OK'), confirmButtonText: i18n.t('Report'), reverseButtons: true, focusCancel: true });
return value;
}
@ -483,11 +488,12 @@ export async function showConcatFailedDialog({ fileFormat }) {
</div>
);
const { value } = await ReactSwal.fire({ title: i18n.t('Unable to merge files'), html, timer: null, showConfirmButton: true, showCancelButton: true, cancelButtonText: i18n.t('OK'), confirmButtonText: i18n.t('Report'), reverseButtons: true, focusCancel: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { value } = await ReactSwal.fire({ title: i18n.t('Unable to merge files'), html, timer: null as any as undefined, showConfirmButton: true, showCancelButton: true, cancelButtonText: i18n.t('OK'), confirmButtonText: i18n.t('Report'), reverseButtons: true, focusCancel: true });
return value;
}
export function openYouTubeChaptersDialog(text) {
export function openYouTubeChaptersDialog(text: string) {
ReactSwal.fire({
showCloseButton: true,
title: i18n.t('YouTube Chapters'),
@ -510,7 +516,7 @@ export async function labelSegmentDialog({ currentName, maxLength }) {
title: i18n.t('Label current segment'),
inputValue: currentName,
input: currentName.includes('\n') ? 'textarea' : 'text',
inputValidator: (v) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : undefined),
inputValidator: (v) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : null),
});
return value;
}
@ -547,7 +553,7 @@ export async function selectSegmentsByTagDialog() {
export function showJson5Dialog({ title, json }) {
const html = (
<SyntaxHighlighter language="javascript" style={style} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
<SyntaxHighlighter language="javascript" style={syntaxStyle} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
{JSON5.stringify(json, null, 2)}
</SyntaxHighlighter>
);
@ -559,7 +565,7 @@ export function showJson5Dialog({ title, json }) {
});
}
export async function openDirToast({ filePath, text, html, ...props }) {
export async function openDirToast({ filePath, text, html, ...props }: SweetAlertOptions & { filePath: string }) {
const swal = text ? toast : ReactSwal;
const { value } = await swal.fire({
@ -576,7 +582,7 @@ export async function openDirToast({ filePath, text, html, ...props }) {
}
const UnorderedList = ({ children }) => <ul style={{ paddingLeft: '1em' }}>{children}</ul>;
const ListItem = ({ icon: Icon, iconColor, children }) => <li style={{ listStyle: 'none' }}>{Icon && <Icon color={iconColor} size={14} marginRight=".3em" />} {children}</li>;
const ListItem = ({ icon: Icon, iconColor, children, style }: { icon: IconComponent, iconColor?: string, children: ReactNode, style?: CSSProperties }) => <li style={{ listStyle: 'none', ...style }}>{Icon && <Icon color={iconColor} size={14} marginRight=".3em" />} {children}</li>;
const Notices = ({ notices }) => notices.map((msg) => <ListItem key={msg} icon={InfoSignIcon} iconColor="info">{msg}</ListItem>);
const Warnings = ({ warnings }) => warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>);
@ -586,7 +592,7 @@ export async function openExportFinishedToast({ filePath, warnings, notices }) {
const hasWarnings = warnings.length > 0;
const html = (
<UnorderedList>
<ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'warning' : 'success'} fontWeight="bold">{hasWarnings ? i18n.t('Export finished with warning(s)', { count: warnings.length }) : i18n.t('Export is done!')}</ListItem>
<ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Export finished with warning(s)', { count: warnings.length }) : i18n.t('Export is done!')}</ListItem>
<ListItem icon={InfoSignIcon}>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
@ -601,7 +607,7 @@ export async function openConcatFinishedToast({ filePath, warnings, notices }) {
const hasWarnings = warnings.length > 0;
const html = (
<UnorderedList>
<ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'warning' : 'success'} fontWeight="bold">{hasWarnings ? i18n.t('Files merged with warning(s)', { count: warnings.length }) : i18n.t('Files merged!')}</ListItem>
<ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Files merged with warning(s)', { count: warnings.length }) : i18n.t('Files merged!')}</ListItem>
<ListItem icon={InfoSignIcon}>{i18n.t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
@ -632,7 +638,7 @@ export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
showCancelButton: true,
inputValidator: (v) => {
const parsed = parseValue(v);
if (parsed != null) return undefined;
if (parsed != null) return null;
return i18n.t('Please enter a valid number.');
},
});

View File

@ -8,18 +8,19 @@ import sortBy from 'lodash/sortBy';
import { formatDuration } from './util/duration';
import { invertSegments, sortSegments } from './segments';
import { Segment } from './types';
const csvParseAsync = pify(csvParse);
const csvStringifyAsync = pify(csvStringify);
export const getTimeFromFrameNum = (detectedFps, frameNum) => frameNum / detectedFps;
export function getFrameCountRaw(detectedFps, sec) {
export function getFrameCountRaw(detectedFps: number | undefined, sec: number) {
if (detectedFps == null) return undefined;
return Math.round(sec * detectedFps);
}
function parseTime(str) {
function parseTime(str: string) {
const timeMatch = str.match(/^[^0-9]*(?:(?:([0-9]{1,}):)?([0-9]{1,2}):)?([0-9]{1,})(?:\.([0-9]{1,3}))?:?/);
if (!timeMatch) return undefined;
@ -28,7 +29,7 @@ function parseTime(str) {
const [, hourStr, minStr, secStr, msStr] = timeMatch;
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
const min = minStr != null ? parseInt(minStr, 10) : 0;
const sec = parseFloat(msStr != null ? `${secStr}.${msStr}` : secStr);
const sec = parseFloat(msStr != null ? `${secStr}.${msStr}` : secStr!);
const time = (((hour * 60) + min) * 60 + sec);
return { time, rest };
@ -124,18 +125,18 @@ export function parseCuesheet(cuesheet) {
}
// See https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037090403
export function parsePbf(buf) {
export function parsePbf(buf: Buffer) {
const text = buf.toString('utf16le');
const bookmarks = text.split('\n').map((line) => {
const match = line.match(/^[0-9]+=([0-9]+)\*([^*]+)*([^*]+)?/);
if (match) return { time: parseInt(match[1], 10) / 1000, name: match[2] };
if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] };
return undefined;
}).filter((it) => it);
const out = [];
const out: Segment[] = [];
for (let i = 0; i < bookmarks.length;) {
const bookmark = bookmarks[i];
const bookmark = bookmarks[i]!;
const nextBookmark = bookmarks[i + 1];
if (!nextBookmark) {
out.push({ start: bookmark.time, end: undefined, name: bookmark.name });
@ -265,24 +266,24 @@ export async function formatTsv(cutSegments) {
return csvStringifyAsync(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
}
export function parseDvAnalyzerSummaryTxt(txt) {
export function parseDvAnalyzerSummaryTxt(txt: string) {
const lines = txt.split(/\r?\n/);
let headerFound = false;
const times = [];
const times: { time: number, name: string, tags: Record<string, string> }[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const line of lines) {
if (headerFound) {
const match = line.match(/^(\d{2}):(\d{2}):(\d{2}).(\d{3})\s+([^\s]+)\s+-\s+([^\s]+)\s+([^\s]+\s+[^\s]+)\s+-\s+([^\s]+\s+[^\s]+)/);
if (!match) break;
const h = parseInt(match[1], 10);
const m = parseInt(match[2], 10);
const s = parseInt(match[3], 10);
const ms = parseInt(match[4], 10);
const h = parseInt(match[1]!, 10);
const m = parseInt(match[2]!, 10);
const s = parseInt(match[3]!, 10);
const ms = parseInt(match[4]!, 10);
const total = s + ((m + (h * 60)) * 60) + (ms / 1000);
const recordedStart = match[7];
const recordedEnd = match[8];
const recordedStart = match[7]!;
const recordedEnd = match[8]!;
times.push({ time: total, name: recordedStart, tags: { recordedStart, recordedEnd } });
}
if (/^Absolute time\s+DV timecode range\s+Recorded date\/time range\s+Frame range\s*$/.test(line)) headerFound = true;
@ -297,18 +298,18 @@ export function parseDvAnalyzerSummaryTxt(txt) {
}
// http://www.textfiles.com/uploads/kds-srt.txt
export function parseSrt(text) {
const ret = [];
export function parseSrt(text: string) {
const ret: { start?: number, end?: number, name: string, tags: Record<string, string> }[] = [];
// working state
let subtitleIndexAt;
let start;
let end;
let lines = [];
let subtitleIndexAt: number | undefined;
let start: number | undefined;
let end: number | undefined;
let lines: string[] = [];
const flush = () => {
if (start != null && end != null && lines.length > 0) {
ret.push({ start, end, name: lines.join('\r\n'), tags: { index: subtitleIndexAt } });
ret.push({ start, end, name: lines.join('\r\n'), tags: { index: String(subtitleIndexAt) } });
}
start = undefined;
end = undefined;

View File

@ -4,6 +4,7 @@ import i18n from 'i18next';
import { parseSrt, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
import { askForYouTubeInput, showOpenDialog } from './dialogs';
import { getOutPath } from './util';
import { EdlExportType, EdlFileType, EdlImportType, Segment } from './types';
const { readFile, writeFile } = window.require('fs/promises');
const cueParser = window.require('cue-parser');
@ -81,8 +82,7 @@ export async function loadLlcProject(path) {
return JSON5.parse(await readFile(path));
}
export async function readEdlFile({ type, path, fps }) {
export async function readEdlFile({ type, path, fps }: { type: EdlFileType, path: string, fps?: number }) {
if (type === 'csv') return loadCsvSeconds(path);
if (type === 'csv-frames') return loadCsvFrames(path, fps);
if (type === 'xmeml') return loadXmeml(path);
@ -99,7 +99,7 @@ export async function readEdlFile({ type, path, fps }) {
throw new Error('Invalid EDL type');
}
export async function askForEdlImport({ type, fps }) {
export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?: number }) {
if (type === 'youtube') return askForYouTubeInput();
let filters;
@ -118,7 +118,9 @@ export async function askForEdlImport({ type, fps }) {
return readEdlFile({ type, path: filePaths[0], fps });
}
export async function exportEdlFile({ type, cutSegments, customOutDir, filePath, getFrameCount }) {
export async function exportEdlFile({ type, cutSegments, customOutDir, filePath, getFrameCount }: {
type: EdlExportType, cutSegments: Segment, customOutDir?: string, filePath?: string, getFrameCount: (a: number) => number | undefined,
}) {
let filters;
let ext;
if (type === 'csv') {

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
export default () => {
const [detectedFileFormat, setDetectedFileFormat] = useState();
const [fileFormat, setFileFormat] = useState();
const [detectedFileFormat, setDetectedFileFormat] = useState<string>();
const [fileFormat, setFileFormat] = useState<string>();
const isCustomFormatSelected = fileFormat !== detectedFileFormat;

View File

@ -7,6 +7,13 @@ const remote = window.require('@electron/remote');
const { commonI18nOptions, fallbackLng, loadPath, addPath } = remote.require('./i18n-common');
// https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
// todo This should not be necessary anymore since v23.0.0
declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false;
}
}
export { fallbackLng };
// https://github.com/i18next/react-i18next/blob/master/example/react/src/i18n.js

View File

@ -1,7 +1,8 @@
import React, { Suspense, StrictMode } from 'react';
import { Suspense, StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { MotionConfig } from 'framer-motion';
import { enableMapSet } from 'immer';
import * as Electron from 'electron';
import 'sweetalert2/dist/sweetalert2.css';
@ -25,6 +26,13 @@ import './i18n';
import './main.css';
declare global {
interface Window {
require: (module: 'electron') => typeof Electron;
}
}
enableMapSet();
const { app } = window.require('@electron/remote');

View File

@ -2,21 +2,22 @@ import { v4 as uuidv4 } from 'uuid';
import sortBy from 'lodash/sortBy';
import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import { InverseSegment } from './types';
export const isDurationValid = (duration) => Number.isFinite(duration) && duration > 0;
export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0;
export const createSegment = ({ start, end, name, tags, segColorIndex } = {}) => ({
start,
end,
name: name || '',
export const createSegment = (props?: { start?: number, end?: number, name?: string, tags?: unknown, segColorIndex?: number }) => ({
start: props?.start,
end: props?.end,
name: props?.name || '',
segId: uuidv4(),
segColorIndex,
segColorIndex: props?.segColorIndex,
// `tags` is an optional object (key-value). Values must always be string
// See https://github.com/mifi/lossless-cut/issues/879
tags: tags != null && typeof tags === 'object'
? Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, String(value)]))
tags: props?.tags != null && typeof props.tags === 'object'
? Object.fromEntries(Object.entries(props.tags).map(([key, value]) => [key, String(value)]))
: undefined,
});
@ -42,7 +43,7 @@ export const getCleanCutSegments = (cs) => cs.map((seg) => ({
}));
export function findSegmentsAtCursor(apparentSegments, currentTime) {
const indexes = [];
const indexes: number[] = [];
apparentSegments.forEach((segment, index) => {
if (segment.start < currentTime && segment.end > currentTime) indexes.push(index);
});
@ -65,13 +66,13 @@ export function partitionIntoOverlappingRanges(array, getSegmentStart = (seg) =>
return getSegmentEnd(array2[0]);
}
const ret = [];
const ret: number[][] = [];
let g = 0;
ret[g] = [array[0]];
for (let i = 1; i < array.length; i += 1) {
if (getSegmentStart(array[i]) >= getSegmentStart(array[i - 1]) && getSegmentStart(array[i]) < getMaxEnd(ret[g])) {
ret[g].push(array[i]);
ret[g]!.push(array[i]);
} else {
g += 1;
ret[g] = [array[i]];
@ -126,29 +127,28 @@ export function hasAnySegmentOverlap(sortedSegments) {
return overlappingGroups.length > 0;
}
export function invertSegments(sortedCutSegments, includeFirstSegment, includeLastSegment, duration) {
export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
if (sortedCutSegments.length < 1) return undefined;
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
const ret = [];
const ret: InverseSegment[] = [];
if (includeFirstSegment) {
const firstSeg = sortedCutSegments[0];
if (firstSeg.start > 0) {
const inverted = {
ret.push({
start: 0,
end: firstSeg.start,
};
if (firstSeg.segId != null) inverted.segId = `start-${firstSeg.segId}`;
ret.push(inverted);
...(firstSeg.segId != null ? { segId: `start-${firstSeg.segId}` } : {}),
});
}
}
sortedCutSegments.forEach((cutSegment, i) => {
if (i === 0) return;
const previousSeg = sortedCutSegments[i - 1];
const inverted = {
const inverted: InverseSegment = {
start: previousSeg.end,
end: cutSegment.start,
};
@ -158,8 +158,8 @@ export function invertSegments(sortedCutSegments, includeFirstSegment, includeLa
if (includeLastSegment) {
const lastSeg = sortedCutSegments[sortedCutSegments.length - 1];
if (lastSeg.end < duration || duration == null) {
const inverted = {
if (duration == null || lastSeg.end < duration) {
const inverted: InverseSegment = {
start: lastSeg.end,
end: duration,
};
@ -182,7 +182,7 @@ export function convertSegmentsToChapters(sortedSegments) {
const invertedSegments = invertSegments(sortedSegments, true, false);
// inverted segments will be "gap" segments. Merge together with normal segments
return sortSegments([...sortedSegments, ...invertedSegments]);
return sortSegments([...sortedSegments, ...(invertedSegments ?? [])]);
}
export function playOnlyCurrentSegment({ playbackMode, currentTime, playingSegment }) {

View File

@ -1,4 +1,4 @@
import SwalRaw from 'sweetalert2';
import SwalRaw, { SweetAlertOptions } from 'sweetalert2';
import { primaryColor } from './colors';
@ -7,7 +7,7 @@ const { systemPreferences } = window.require('@electron/remote');
const animationSettings = systemPreferences.getAnimationSettings();
let commonSwalOptions = {
let commonSwalOptions: SweetAlertOptions = {
confirmButtonColor: primaryColor,
};
@ -33,7 +33,7 @@ const Swal = SwalRaw.mixin({
export default Swal;
export const swalToastOptions = {
export const swalToastOptions: SweetAlertOptions = {
...commonSwalOptions,
toast: true,
position: 'top',

View File

@ -1,19 +1,20 @@
import { defaultTheme } from 'evergreen-ui';
import { DefaultTheme, IntentTypes, defaultTheme } from 'evergreen-ui';
import { ProviderProps } from 'react';
function colorKeyForIntent(intent) {
function colorKeyForIntent(intent: IntentTypes) {
if (intent === 'danger') return 'var(--red12)';
if (intent === 'success') return 'var(--green12)';
return 'var(--gray12)';
}
function borderColorForIntent(intent, isHover) {
function borderColorForIntent(intent: IntentTypes, isHover?: boolean) {
if (intent === 'danger') return isHover ? 'var(--red8)' : 'var(--red7)';
if (intent === 'success') return isHover ? 'var(--green8)' : 'var(--green7)';
return 'var(--gray8)';
}
export default {
const customTheme: ProviderProps<DefaultTheme>['value'] = {
...defaultTheme,
colors: {
...defaultTheme.colors,
@ -35,8 +36,10 @@ export default {
backgroundColor: 'var(--gray3)',
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
border: (theme, props) => `1px solid ${borderColorForIntent(props.intent)}`,
color: (theme, props) => props.color || colorKeyForIntent(props.intent),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
border: ((_theme, props) => `1px solid ${borderColorForIntent(props.intent)}`) as any as string, // todo types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
_hover: {
backgroundColor: 'var(--gray4)',
@ -50,13 +53,15 @@ export default {
},
disabled: {
opacity: 0.5,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any as boolean, // todo types
},
minimal: {
...defaultTheme.components.Button.appearances.minimal,
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
color: (theme, props) => props.color || colorKeyForIntent(props.intent),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
_hover: {
backgroundColor: 'var(--gray4)',
@ -66,9 +71,12 @@ export default {
},
disabled: {
opacity: 0.5,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any as boolean, // todo types,
},
},
},
},
};
export default customTheme;

30
src/types.ts Normal file
View File

@ -0,0 +1,30 @@
export interface SegmentBase {
start?: number,
end?: number,
}
export interface Segment extends SegmentBase {
name?: string,
}
export interface InverseSegment extends SegmentBase {
segId?: string,
}
export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest';
export type EdlFileType = 'csv' | 'csv-frames' | 'xmeml' | 'fcpxml' | 'dv-analyzer-summary-txt' | 'cue' | 'pbf' | 'mplayer' | 'srt' | 'llc';
export type EdlImportType = 'youtube' | EdlFileType;
export type EdlExportType = 'csv' | 'tsv-human' | 'csv-human' | 'csv-frames' | 'srt' | 'llc';
export type TunerType = 'wheelSensitivity' | 'keyboardNormalSeekSpeed' | 'keyboardSeekAccFactor';
export interface Waveform {
from: number,
to: number,
url: string,
}
export type FfmpegCommandLog = { command: string, time: Date }[];

View File

@ -22,11 +22,11 @@ const trashFile = async (path) => ipcRenderer.invoke('tryTrashItem', path);
export const showItemInFolder = async (path) => ipcRenderer.invoke('showItemInFolder', path);
export function getFileDir(filePath) {
export function getFileDir(filePath?: string) {
return filePath ? dirname(filePath) : undefined;
}
export function getOutDir(customOutDir, filePath) {
export function getOutDir(customOutDir?: string, filePath?: string) {
if (customOutDir) return customOutDir;
if (filePath) return getFileDir(filePath);
return undefined;
@ -59,7 +59,7 @@ export async function havePermissionToReadFile(filePath) {
console.error('Failed to close fd', err);
}
} catch (err) {
if (['EPERM', 'EACCES'].includes(err.code)) return false;
if (err instanceof Error && 'code' in err && ['EPERM', 'EACCES'].includes(err.code as string)) return false;
console.error(err);
}
return true;
@ -69,8 +69,10 @@ export async function checkDirWriteAccess(dirPath) {
try {
await fsExtra.access(dirPath, fsExtra.constants.W_OK);
} catch (err) {
if (err.code === 'EPERM') return false; // Thrown on Mac (MAS build) when user has not yet allowed access
if (err.code === 'EACCES') return false; // Thrown on Linux when user doesn't have access to output dir
if (err instanceof Error && 'code' in err) {
if (err.code === 'EPERM') return false; // Thrown on Mac (MAS build) when user has not yet allowed access
if (err.code === 'EACCES') return false; // Thrown on Linux when user doesn't have access to output dir
}
console.error(err);
}
return true;
@ -80,12 +82,12 @@ export async function pathExists(pathIn) {
return fsExtra.pathExists(pathIn);
}
export async function getPathReadAccessError(pathIn) {
export async function getPathReadAccessError(pathIn: string) {
try {
await fsExtra.access(pathIn, fsExtra.constants.R_OK);
return undefined;
} catch (err) {
return err.code;
return err instanceof Error && 'code' in err && typeof err.code === 'string' ? err.code : undefined;
}
}
@ -107,7 +109,9 @@ export async function fsOperationWithRetry(operation, { signal, retries = 10, mi
minTimeout,
maxTimeout,
// mimic fs.rm `maxRetries` https://nodejs.org/api/fs.html#fspromisesrmpath-options
shouldRetry: (err) => err instanceof Error && 'code' in err && ['EBUSY', 'EMFILE', 'ENFILE', 'EPERM'].includes(err.code),
// todo
// @ts-expect-error I think error in the types
shouldRetry: (err) => err instanceof Error && 'code' in err && typeof err.code === 'string' && ['EBUSY', 'EMFILE', 'ENFILE', 'EPERM'].includes(err.code),
...opts,
});
}
@ -115,7 +119,8 @@ export async function fsOperationWithRetry(operation, { signal, retries = 10, mi
// example error: index-18074aaf.js:166 Failed to delete C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4 Error: EPERM: operation not permitted, unlink 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4'
export const unlinkWithRetry = async (path, options) => fsOperationWithRetry(async () => unlink(path), { ...options, onFailedAttempt: (error) => console.warn('Retrying delete', path, error.attemptNumber) });
// example error: index-18074aaf.js:160 Error: EPERM: operation not permitted, utime 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-cut-merged-1703933070237.mp4'
export const utimesWithRetry = async (path, atime, mtime, options) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const utimesWithRetry = async (path: string, atime: number, mtime: number, options?: any) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) });
export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo = 0, duration = 0, treatInputFileModifiedTimeAsStart = true, treatOutputFileModifiedTimeAsStart }) {
if (treatOutputFileModifiedTimeAsStart == null) return; // null means disabled;
@ -143,7 +148,7 @@ export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo =
}
}
export function handleError(arg1, arg2) {
export function handleError(arg1: unknown, arg2?: unknown) {
console.error('handleError', arg1, arg2);
let msg;
@ -215,7 +220,7 @@ export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePat
export const hasDuplicates = (arr) => new Set(arr).size !== arr.length;
// Need to resolve relative paths from the command line https://github.com/mifi/lossless-cut/issues/639
export const resolvePathIfNeeded = (inPath) => (isAbsolute(inPath) ? inPath : resolve(inPath));
export const resolvePathIfNeeded = (inPath: string) => (isAbsolute(inPath) ? inPath : resolve(inPath));
export const html5ifiedPrefix = 'html5ified-';
export const html5dummySuffix = 'dummy';
@ -230,7 +235,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
const html5ifiedDirEntries = dirEntries.filter((entry) => entry.startsWith(prefix));
let matches = [];
let matches: { entry: string, suffix: string }[] = [];
suffixes.forEach((suffix) => {
const entryWithSuffix = html5ifiedDirEntries.find((entry) => new RegExp(`${suffix}\\..*$`).test(entry.replace(prefix, '')));
if (entryWithSuffix) matches = [...matches, { entry: entryWithSuffix, suffix }];
@ -244,7 +249,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
// console.log(matches);
if (matches.length < 1) return undefined;
const { suffix, entry } = matches[0];
const { suffix, entry } = matches[0]!;
return {
path: join(outDir, entry),
@ -258,8 +263,8 @@ export function getHtml5ifiedPath(cod, fp, type) {
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
}
export async function deleteFiles({ paths, deleteIfTrashFails, signal }) {
const failedToTrashFiles = [];
export async function deleteFiles({ paths, deleteIfTrashFails, signal }: { paths: string[], deleteIfTrashFails?: boolean, signal: AbortSignal }) {
const failedToTrashFiles: string[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const path of paths) {
@ -323,10 +328,10 @@ export async function checkAppPath() {
// this will report the path and may return a msg
const url = `https://losslesscut-analytics.mifi.no/${pathSeg.length}/${encodeURIComponent(btoa(pathSeg))}`;
// console.log('Reporting app', pathSeg, url);
const response = await ky(url).json();
const response = await ky(url).json<{ invalid?: boolean, title: string, text: string }>();
if (response.invalid) toast.fire({ timer: 60000, icon: 'error', title: response.title, text: response.text });
} catch (err) {
if (isDev) console.warn(err.message);
if (isDev) console.warn(err instanceof Error && err.message);
}
}
@ -375,8 +380,8 @@ function setDocumentExtraTitle(extra) {
else document.title = baseTitle;
}
export function setDocumentTitle({ filePath, working, cutProgress }) {
const parts = [];
export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) {
const parts: string[] = [];
if (filePath) parts.push(basename(filePath));
if (working) {
parts.push('-', working);

View File

@ -1,6 +1,8 @@
import padStart from 'lodash/padStart';
export function formatDuration({ seconds: totalSecondsIn, fileNameFriendly, showFraction = true, shorten = false, fps }) {
export function formatDuration({ seconds: totalSecondsIn, fileNameFriendly, showFraction = true, shorten = false, fps }: {
seconds?: number, fileNameFriendly?: boolean, showFraction?: boolean, shorten?: boolean, fps?: number,
}) {
const totalSeconds = totalSecondsIn || 0;
const totalSecondsAbs = Math.abs(totalSeconds);
const sign = totalSeconds < 0 ? '-' : '';

View File

@ -4,6 +4,7 @@ import lodashTemplate from 'lodash/template';
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
import isDev from '../isDev';
import { getSegmentTags, formatSegNum } from '../segments';
import { Segment } from '../types';
export const segNumVariable = 'SEG_NUM';
@ -113,7 +114,9 @@ function interpolateSegmentFileName({ template, epochMs, inputFileNameWithoutExt
return compiled(data);
}
export function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }) {
export function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }: {
segments: Segment[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat?: string, filePath: string, outputDir: string, safeOutputFileName: string, maxLabelLength: number, outputFileNameMinZeroPadding: number,
}) {
function generate({ template, forceSafeOutputFileName }) {
const epochMs = Date.now();

View File

@ -108,8 +108,10 @@ export function getActiveDisposition(disposition) {
export const isMov = (format) => ['ismv', 'ipod', 'mp4', 'mov'].includes(format);
function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined }) {
let args = [];
function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined }: {
stream, outputIndex: number, outFormat: string, manuallyCopyDisposition?: boolean, getVideoArgs?: (a: { streamIndex: number, outputIndex: number }) => string[] | undefined
}) {
let args: string[] = [];
function addArgs(...newArgs) {
args.push(...newArgs);
@ -189,7 +191,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
}
export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs }) {
let args = [];
let args: string[] = [];
let outputIndex = startIndex;
copyFileStreams.forEach(({ streamIds, path }, fileIndex) => {
@ -224,13 +226,13 @@ export const getRealVideoStreams = (streams) => streams.filter(stream => stream.
export const getSubtitleStreams = (streams) => streams.filter(stream => stream.codec_type === 'subtitle');
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
const getHtml5TrackId = (ffmpegTrackIndex) => String(ffmpegTrackIndex + 1);
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1);
const getHtml5VideoTracks = (video) => [...(video.videoTracks ?? [])];
const getHtml5AudioTracks = (video) => [...(video.audioTracks ?? [])];
export const getVideoTrackForStreamIndex = (video, index) => getHtml5VideoTracks(video).find((videoTrack) => videoTrack.id === getHtml5TrackId(index));
export const getAudioTrackForStreamIndex = (video, index) => getHtml5AudioTracks(video).find((audioTrack) => audioTrack.id === getHtml5TrackId(index));
export const getVideoTrackForStreamIndex = (video: HTMLVideoElement, index) => getHtml5VideoTracks(video).find((videoTrack) => videoTrack.id === getHtml5TrackId(index));
export const getAudioTrackForStreamIndex = (video: HTMLVideoElement, index) => getHtml5AudioTracks(video).find((audioTrack) => audioTrack.id === getHtml5TrackId(index));
function resetVideoTrack(video) {
console.log('Resetting video track');
@ -285,7 +287,7 @@ export function getStreamIdsToCopy({ streams, includeAllStreams }) {
// We cannot use the ffmpeg's automatic stream selection or else ffmpeg might use the metadata source input (index 1)
// instead of the concat input (index 0)
// https://ffmpeg.org/ffmpeg.html#Automatic-stream-selection
const streamIdsToCopy = [];
const streamIdsToCopy: number[] = [];
// TODO try to mimic ffmpeg default mapping https://ffmpeg.org/ffmpeg.html#Automatic-stream-selection
const videoStreams = getRealVideoStreams(streams);
const audioStreams = getAudioStreams(streams);

View File

@ -1,6 +1,8 @@
{
"extends": ["@tsconfig/strictest", "@tsconfig/vite-react/tsconfig.json"],
"compilerOptions": {
"plugins": [{ "name": "typescript-plugin-css-modules" }],
"exactOptionalPropertyTypes": false, // todo
"noUncheckedIndexedAccess": true,
"noEmit": true,

458
yarn.lock
View File

@ -19,6 +19,13 @@ __metadata:
languageName: node
linkType: hard
"@adobe/css-tools@npm:~4.3.1":
version: 4.3.3
resolution: "@adobe/css-tools@npm:4.3.3"
checksum: 0e77057efb4e18182560855503066b75edca98671be327d3f8a7ae89ec3da6821e693114b55225909fca00d7e7ed8422f3d79d71fe95dd4d5df1f2026a9fda02
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.1.0":
version: 2.2.0
resolution: "@ampproject/remapping@npm:2.2.0"
@ -1677,6 +1684,24 @@ __metadata:
languageName: node
linkType: hard
"@types/postcss-modules-local-by-default@npm:^4.0.2":
version: 4.0.2
resolution: "@types/postcss-modules-local-by-default@npm:4.0.2"
dependencies:
postcss: "npm:^8.0.0"
checksum: c4a50f0fab1bacbf2968a05156f0acf10225a605b021dcfb4e39892429507089a91919609111c79d1ed5902c55f9b4ee35c00aa75d98bb18d5415b3cd1223239
languageName: node
linkType: hard
"@types/postcss-modules-scope@npm:^3.0.4":
version: 3.0.4
resolution: "@types/postcss-modules-scope@npm:3.0.4"
dependencies:
postcss: "npm:^8.0.0"
checksum: 4249ace34023dc797b47a1041c844d6a772d6339a96e7a45fdacc70d03db8fb2917ac90728c390a743ecf2da821f921761ad2bdb57f4d6936ad4690bc572ad5c
languageName: node
linkType: hard
"@types/prop-types@npm:*":
version: 15.7.4
resolution: "@types/prop-types@npm:15.7.4"
@ -2160,6 +2185,16 @@ __metadata:
languageName: node
linkType: hard
"anymatch@npm:~3.1.2":
version: 3.1.3
resolution: "anymatch@npm:3.1.3"
dependencies:
normalize-path: "npm:^3.0.0"
picomatch: "npm:^2.0.4"
checksum: 3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2
languageName: node
linkType: hard
"app-builder-bin@npm:4.0.0":
version: 4.0.0
resolution: "app-builder-bin@npm:4.0.0"
@ -2481,6 +2516,13 @@ __metadata:
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0":
version: 2.2.0
resolution: "binary-extensions@npm:2.2.0"
checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8
languageName: node
linkType: hard
"bl@npm:^4.0.3":
version: 4.1.0
resolution: "bl@npm:4.1.0"
@ -2568,7 +2610,7 @@ __metadata:
languageName: node
linkType: hard
"braces@npm:^3.0.1":
"braces@npm:^3.0.1, braces@npm:~3.0.2":
version: 3.0.2
resolution: "braces@npm:3.0.2"
dependencies:
@ -2882,6 +2924,25 @@ __metadata:
languageName: node
linkType: hard
"chokidar@npm:>=3.0.0 <4.0.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
anymatch: "npm:~3.1.2"
braces: "npm:~3.0.2"
fsevents: "npm:~2.3.2"
glob-parent: "npm:~5.1.2"
is-binary-path: "npm:~2.1.0"
is-glob: "npm:~4.0.1"
normalize-path: "npm:~3.0.0"
readdirp: "npm:~3.6.0"
dependenciesMeta:
fsevents:
optional: true
checksum: c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df
languageName: node
linkType: hard
"chownr@npm:^1.1.1":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
@ -3287,6 +3348,15 @@ __metadata:
languageName: node
linkType: hard
"copy-anything@npm:^2.0.1":
version: 2.0.6
resolution: "copy-anything@npm:2.0.6"
dependencies:
is-what: "npm:^3.14.1"
checksum: 3b41be8f6322c2c13e93cde62a64d532f138f31d44ab85a3405d88601134afccc068be06534c162ed5c06b209788c423d7aaa50f1c34a92db81a1f8560d199eb
languageName: node
linkType: hard
"copy-to-clipboard@npm:^3.3.1":
version: 3.3.1
resolution: "copy-to-clipboard@npm:3.3.1"
@ -3396,6 +3466,15 @@ __metadata:
languageName: node
linkType: hard
"cssesc@npm:^3.0.0":
version: 3.0.0
resolution: "cssesc@npm:3.0.0"
bin:
cssesc: bin/cssesc
checksum: 0e161912c1306861d8f46e1883be1cbc8b1b2879f0f509287c0db71796e4ddfb97ac96bdfca38f77f452e2c10554e1bb5678c99b07a5cf947a12778f73e47e12
languageName: node
linkType: hard
"csstype@npm:^3.0.2, csstype@npm:^3.0.6":
version: 3.0.10
resolution: "csstype@npm:3.0.10"
@ -3778,6 +3857,13 @@ __metadata:
languageName: node
linkType: hard
"dotenv@npm:^16.4.2":
version: 16.4.2
resolution: "dotenv@npm:16.4.2"
checksum: a6069f3bed960f9bdb5c2e55df8b4d121e7f151441b1ce129600597d7717f7bfda7fa250706b1fbe06bc05b2e764d6649ecedb46dd95455f490882bd324a3ac1
languageName: node
linkType: hard
"dotenv@npm:^9.0.2":
version: 9.0.2
resolution: "dotenv@npm:9.0.2"
@ -4033,6 +4119,17 @@ __metadata:
languageName: node
linkType: hard
"errno@npm:^0.1.1":
version: 0.1.8
resolution: "errno@npm:0.1.8"
dependencies:
prr: "npm:~1.0.1"
bin:
errno: cli.js
checksum: 93076ed11bedb8f0389cbefcbdd3445f66443159439dccbaac89a053428ad92147676736235d275612dc0296d3f9a7e6b7177ed78a566b6cd15dacd4fa0d5888
languageName: node
linkType: hard
"error-stack-parser@npm:^2.0.6":
version: 2.0.6
resolution: "error-stack-parser@npm:2.0.6"
@ -5376,7 +5473,7 @@ __metadata:
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2":
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
@ -5518,6 +5615,13 @@ __metadata:
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.2":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2
languageName: node
linkType: hard
"graphemer@npm:^1.4.0":
version: 1.4.0
resolution: "graphemer@npm:1.4.0"
@ -5860,7 +5964,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@ -5869,6 +5973,15 @@ __metadata:
languageName: node
linkType: hard
"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0":
version: 5.1.0
resolution: "icss-utils@npm:5.1.0"
peerDependencies:
postcss: ^8.1.0
checksum: 5c324d283552b1269cfc13a503aaaa172a280f914e5b81544f3803bc6f06a3b585fb79f66f7c771a2c052db7982c18bf92d001e3b47282e3abbbb4c4cc488d68
languageName: node
linkType: hard
"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
@ -5890,6 +6003,15 @@ __metadata:
languageName: node
linkType: hard
"image-size@npm:~0.5.0":
version: 0.5.5
resolution: "image-size@npm:0.5.5"
bin:
image-size: bin/image-size.js
checksum: f41ec6cfccfa6471980e83568033a66ec53f84d1bcb70033e946a7db9c1b6bbf5645ec90fa5a8bdcdc84d86af0032014eff6fa078a60c2398dfce6676c46bdb7
languageName: node
linkType: hard
"immediate@npm:~3.0.5":
version: 3.0.6
resolution: "immediate@npm:3.0.6"
@ -5904,6 +6026,13 @@ __metadata:
languageName: node
linkType: hard
"immutable@npm:^4.0.0":
version: 4.3.5
resolution: "immutable@npm:4.3.5"
checksum: dbc1b8c808b9aa18bfce2e0c7bc23714a47267bc311f082145cc9220b2005e9b9cd2ae78330f164a19266a2b0f78846c60f4f74893853ac16fd68b5ae57092d2
languageName: node
linkType: hard
"import-fresh@npm:^3.2.1":
version: 3.3.0
resolution: "import-fresh@npm:3.3.0"
@ -6062,6 +6191,15 @@ __metadata:
languageName: node
linkType: hard
"is-binary-path@npm:~2.1.0":
version: 2.1.0
resolution: "is-binary-path@npm:2.1.0"
dependencies:
binary-extensions: "npm:^2.0.0"
checksum: 078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e
languageName: node
linkType: hard
"is-boolean-object@npm:^1.1.0":
version: 1.1.2
resolution: "is-boolean-object@npm:1.1.2"
@ -6179,7 +6317,7 @@ __metadata:
languageName: node
linkType: hard
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3":
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1":
version: 4.0.3
resolution: "is-glob@npm:4.0.3"
dependencies:
@ -6364,6 +6502,13 @@ __metadata:
languageName: node
linkType: hard
"is-what@npm:^3.14.1":
version: 3.14.1
resolution: "is-what@npm:3.14.1"
checksum: 249beb4a8c1729c80ed24fa8527835301c8c70d2fa99706a301224576e0650df61edd7a0a8853999bf5fbe2c551f07148d2c3535260772e05a4c373d3d5362e1
languageName: node
linkType: hard
"is-windows@npm:^1.0.1":
version: 1.0.2
resolution: "is-windows@npm:1.0.2"
@ -6668,6 +6813,41 @@ __metadata:
languageName: node
linkType: hard
"less@npm:^4.2.0":
version: 4.2.0
resolution: "less@npm:4.2.0"
dependencies:
copy-anything: "npm:^2.0.1"
errno: "npm:^0.1.1"
graceful-fs: "npm:^4.1.2"
image-size: "npm:~0.5.0"
make-dir: "npm:^2.1.0"
mime: "npm:^1.4.1"
needle: "npm:^3.1.0"
parse-node-version: "npm:^1.0.1"
source-map: "npm:~0.6.0"
tslib: "npm:^2.3.0"
dependenciesMeta:
errno:
optional: true
graceful-fs:
optional: true
image-size:
optional: true
make-dir:
optional: true
mime:
optional: true
needle:
optional: true
source-map:
optional: true
bin:
lessc: bin/lessc
checksum: 98200dce570cdc396e03cafc95fb7bbbecdbe3ae28e456a6dcf7a1ac75c3b1979aa56749ac7581ace1814f8a03c9d3456b272280cc098a6e1e24295c4b7caddb
languageName: node
linkType: hard
"levn@npm:^0.4.1":
version: 0.4.1
resolution: "levn@npm:0.4.1"
@ -6687,6 +6867,13 @@ __metadata:
languageName: node
linkType: hard
"lilconfig@npm:^2.0.5":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
checksum: b1314a2e55319013d5e7d7d08be39015829d2764a1eaee130129545d40388499d81b1c31b0f9b3417d4db12775a88008b72ec33dd06e0184cf7503b32ca7cc0b
languageName: node
linkType: hard
"lilconfig@npm:^2.0.6":
version: 2.0.6
resolution: "lilconfig@npm:2.0.6"
@ -6730,6 +6917,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
checksum: c301cc379310441dc73cd6cebeb91fb254bea74e6ad3027f9346fc43b4174385153df420ffa521654e502fd34c40ef69ca4e7d40ee7129a99e06f306032bfc65
languageName: node
linkType: hard
"lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
@ -6883,6 +7077,7 @@ __metadata:
sweetalert2: "npm:^11.0.0"
sweetalert2-react-content: "npm:^5.0.7"
typescript: "npm:^5.3.3"
typescript-plugin-css-modules: "npm:^5.1.0"
use-debounce: "npm:^5.1.0"
use-trace-update: "npm:^1.3.0"
uuid: "npm:^8.3.2"
@ -6968,6 +7163,16 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^2.1.0":
version: 2.1.0
resolution: "make-dir@npm:2.1.0"
dependencies:
pify: "npm:^4.0.1"
semver: "npm:^5.6.0"
checksum: 043548886bfaf1820323c6a2997e6d2fa51ccc2586ac14e6f14634f7458b4db2daf15f8c310e2a0abd3e0cddc64df1890d8fc7263033602c47bb12cbfcf86aab
languageName: node
linkType: hard
"make-dir@npm:^3.0.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
@ -7104,7 +7309,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:1.6.0":
"mime@npm:1.6.0, mime@npm:^1.4.1":
version: 1.6.0
resolution: "mime@npm:1.6.0"
bin:
@ -7424,6 +7629,18 @@ __metadata:
languageName: node
linkType: hard
"needle@npm:^3.1.0":
version: 3.3.1
resolution: "needle@npm:3.3.1"
dependencies:
iconv-lite: "npm:^0.6.3"
sax: "npm:^1.2.4"
bin:
needle: bin/needle
checksum: 31925ec72b93ffd1f5614a4f381878e7c31f1838cd36055aa4148c49a3a9d16429987fc64b509538f61fccbb49aac9ec2e91b1ed028aafb16f943f1993097d96
languageName: node
linkType: hard
"negotiator@npm:0.6.3, negotiator@npm:^0.6.3":
version: 0.6.3
resolution: "negotiator@npm:0.6.3"
@ -7524,6 +7741,13 @@ __metadata:
languageName: node
linkType: hard
"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
version: 3.0.0
resolution: "normalize-path@npm:3.0.0"
checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20
languageName: node
linkType: hard
"normalize-url@npm:^6.0.1":
version: 6.1.0
resolution: "normalize-url@npm:6.1.0"
@ -7883,6 +8107,13 @@ __metadata:
languageName: node
linkType: hard
"parse-node-version@npm:^1.0.1":
version: 1.0.1
resolution: "parse-node-version@npm:1.0.1"
checksum: ac9b40c6473035ec2dd0afe793b226743055f8119b50853be2022c817053c3377d02b4bb42e0735d9dcb6c32d16478086934b0a8de570a5f5eebacbfc1514ccd
languageName: node
linkType: hard
"parse5-htmlparser2-tree-adapter@npm:^7.0.0":
version: 7.0.0
resolution: "parse5-htmlparser2-tree-adapter@npm:7.0.0"
@ -8007,13 +8238,20 @@ __metadata:
languageName: node
linkType: hard
"picomatch@npm:^2.2.3":
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3":
version: 2.3.1
resolution: "picomatch@npm:2.3.1"
checksum: 60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc
languageName: node
linkType: hard
"pify@npm:^4.0.1":
version: 4.0.1
resolution: "pify@npm:4.0.1"
checksum: 8b97cbf9dc6d4c1320cc238a2db0fc67547f9dc77011729ff353faf34f1936ea1a4d7f3c63b2f4980b253be77bcc72ea1e9e76ee3fd53cce2aafb6a8854d07ec
languageName: node
linkType: hard
"pify@npm:^5.0.0":
version: 5.0.0
resolution: "pify@npm:5.0.0"
@ -8069,6 +8307,85 @@ __metadata:
languageName: node
linkType: hard
"postcss-load-config@npm:^3.1.4":
version: 3.1.4
resolution: "postcss-load-config@npm:3.1.4"
dependencies:
lilconfig: "npm:^2.0.5"
yaml: "npm:^1.10.2"
peerDependencies:
postcss: ">=8.0.9"
ts-node: ">=9.0.0"
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
checksum: 75fa409d77b96e6f53e99f680c550f25ca8922c1150d3d368ded1f6bd8e0d4d67a615fe1f1c5d409aefb6e66fb4b5e48e86856d581329913de84578def078b19
languageName: node
linkType: hard
"postcss-modules-extract-imports@npm:^3.0.0":
version: 3.0.0
resolution: "postcss-modules-extract-imports@npm:3.0.0"
peerDependencies:
postcss: ^8.1.0
checksum: 8d68bb735cef4d43f9cdc1053581e6c1c864860b77fcfb670372b39c5feeee018dc5ddb2be4b07fef9bcd601edded4262418bbaeaf1bd4af744446300cebe358
languageName: node
linkType: hard
"postcss-modules-local-by-default@npm:^4.0.4":
version: 4.0.4
resolution: "postcss-modules-local-by-default@npm:4.0.4"
dependencies:
icss-utils: "npm:^5.0.0"
postcss-selector-parser: "npm:^6.0.2"
postcss-value-parser: "npm:^4.1.0"
peerDependencies:
postcss: ^8.1.0
checksum: 45790af417b2ed6ed26e9922724cf3502569995833a2489abcfc2bb44166096762825cc02f6132cc6a2fb235165e76b859f9d90e8a057bc188a1b2c17f2d7af0
languageName: node
linkType: hard
"postcss-modules-scope@npm:^3.1.1":
version: 3.1.1
resolution: "postcss-modules-scope@npm:3.1.1"
dependencies:
postcss-selector-parser: "npm:^6.0.4"
peerDependencies:
postcss: ^8.1.0
checksum: ca035969eba62cf126864b10d7722e49c0d4f050cbd4618b6e9714d81b879cf4c53a5682501e00f9622e8f4ea6d7d7d53af295ae935fa833e0cc0bda416a287b
languageName: node
linkType: hard
"postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
version: 6.0.15
resolution: "postcss-selector-parser@npm:6.0.15"
dependencies:
cssesc: "npm:^3.0.0"
util-deprecate: "npm:^1.0.2"
checksum: cea591e1d9bce60eea724428863187228e27ddaebd98e5ecb4ee6d4c9a4b68e8157fd44c916b3fef1691d19ad16aa416bb7279b5eab260c32340ae630a34e200
languageName: node
linkType: hard
"postcss-value-parser@npm:^4.1.0":
version: 4.2.0
resolution: "postcss-value-parser@npm:4.2.0"
checksum: e4e4486f33b3163a606a6ed94f9c196ab49a37a7a7163abfcd469e5f113210120d70b8dd5e33d64636f41ad52316a3725655421eb9a1094f1bcab1db2f555c62
languageName: node
linkType: hard
"postcss@npm:^8.0.0, postcss@npm:^8.4.35":
version: 8.4.35
resolution: "postcss@npm:8.4.35"
dependencies:
nanoid: "npm:^3.3.7"
picocolors: "npm:^1.0.0"
source-map-js: "npm:^1.0.2"
checksum: 93a7ce50cd6188f5f486a9ca98950ad27c19dfed996c45c414fa242944497e4d084a8760d3537f078630226f2bd3c6ab84b813b488740f4432e7c7039cd73a20
languageName: node
linkType: hard
"postcss@npm:^8.4.21":
version: 8.4.31
resolution: "postcss@npm:8.4.31"
@ -8252,6 +8569,13 @@ __metadata:
languageName: node
linkType: hard
"prr@npm:~1.0.1":
version: 1.0.1
resolution: "prr@npm:1.0.1"
checksum: 3bca2db0479fd38f8c4c9439139b0c42dcaadcc2fbb7bb8e0e6afaa1383457f1d19aea9e5f961d5b080f1cfc05bfa1fe9e45c97a1d3fd6d421950a73d3108381
languageName: node
linkType: hard
"pump@npm:^2.0.0":
version: 2.0.1
resolution: "pump@npm:2.0.1"
@ -8582,6 +8906,15 @@ __metadata:
languageName: node
linkType: hard
"readdirp@npm:~3.6.0":
version: 3.6.0
resolution: "readdirp@npm:3.6.0"
dependencies:
picomatch: "npm:^2.2.1"
checksum: 196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7
languageName: node
linkType: hard
"refractor@npm:^3.2.0":
version: 3.5.0
resolution: "refractor@npm:3.5.0"
@ -8666,6 +8999,13 @@ __metadata:
languageName: node
linkType: hard
"reserved-words@npm:^0.1.2":
version: 0.1.2
resolution: "reserved-words@npm:0.1.2"
checksum: 72e80f71dcde1e2d697e102473ad6d597e1659118836092c63cc4db68a64857f07f509176d239c8675b24f7f03574336bf202a780cc1adb39574e2884d1fd1fa
languageName: node
linkType: hard
"resize-observer-polyfill@npm:^1.5.1":
version: 1.5.1
resolution: "resize-observer-polyfill@npm:1.5.1"
@ -8963,6 +9303,19 @@ __metadata:
languageName: node
linkType: hard
"sass@npm:^1.70.0":
version: 1.70.0
resolution: "sass@npm:1.70.0"
dependencies:
chokidar: "npm:>=3.0.0 <4.0.0"
immutable: "npm:^4.0.0"
source-map-js: "npm:>=0.6.2 <2.0.0"
bin:
sass: sass.js
checksum: f933545d72a932f4a82322dd4ca9f3ea7d3e9d08852d695f76d419939cbdf7f8db3dd894b059ed77bf76811b07319b75b3ef8bb077bf9f52f8fbdfd8cee162f6
languageName: node
linkType: hard
"sax@npm:^1.2.4":
version: 1.2.4
resolution: "sax@npm:1.2.4"
@ -8970,6 +9323,13 @@ __metadata:
languageName: node
linkType: hard
"sax@npm:~1.3.0":
version: 1.3.0
resolution: "sax@npm:1.3.0"
checksum: bb571b31d30ecb0353c2ff5f87b117a03e5fb9eb4c1519141854c1a8fbee0a77ddbe8045f413259e711833aa03da210887df8527d19cdc55f299822dbf4b34de
languageName: node
linkType: hard
"scheduler@npm:^0.23.0":
version: 0.23.0
resolution: "scheduler@npm:0.23.0"
@ -9009,6 +9369,15 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^5.6.0":
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
semver: bin/semver
checksum: fca14418a174d4b4ef1fecb32c5941e3412d52a4d3d85165924ce3a47fbc7073372c26faf7484ceb4bbc2bde25880c6b97e492473dc7e9708fdfb1c6a02d546e
languageName: node
linkType: hard
"semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.0":
version: 6.3.0
resolution: "semver@npm:6.3.0"
@ -9327,7 +9696,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.0.2":
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2":
version: 1.0.2
resolution: "source-map-js@npm:1.0.2"
checksum: 38e2d2dd18d2e331522001fc51b54127ef4a5d473f53b1349c5cca2123562400e0986648b52e9407e348eaaed53bce49248b6e2641e6d793ca57cb2c360d6d51
@ -9351,13 +9720,20 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:^0.6.0, source-map@npm:^0.6.1":
"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff
languageName: node
linkType: hard
"source-map@npm:^0.7.3":
version: 0.7.4
resolution: "source-map@npm:0.7.4"
checksum: a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc
languageName: node
linkType: hard
"sourcemap-codec@npm:^1.4.8":
version: 1.4.8
resolution: "sourcemap-codec@npm:1.4.8"
@ -9679,6 +10055,21 @@ __metadata:
languageName: node
linkType: hard
"stylus@npm:^0.62.0":
version: 0.62.0
resolution: "stylus@npm:0.62.0"
dependencies:
"@adobe/css-tools": "npm:~4.3.1"
debug: "npm:^4.3.2"
glob: "npm:^7.1.6"
sax: "npm:~1.3.0"
source-map: "npm:^0.7.3"
bin:
stylus: bin/stylus
checksum: a2d975e619c622a6646fec43489f4a7d0fe824e5dab6343295bca381dd9f1ae9f9d32710c0ca28219eebeb1609448112ba99a246c215824369aec3dc4652b6cf
languageName: node
linkType: hard
"sumchecker@npm:^3.0.1":
version: 3.0.1
resolution: "sumchecker@npm:3.0.1"
@ -10084,6 +10475,17 @@ __metadata:
languageName: node
linkType: hard
"tsconfig-paths@npm:^4.2.0":
version: 4.2.0
resolution: "tsconfig-paths@npm:4.2.0"
dependencies:
json5: "npm:^2.2.2"
minimist: "npm:^1.2.6"
strip-bom: "npm:^3.0.0"
checksum: 5e55cc2fb6b800eb72011522e10edefccb45b1f9af055681a51354c9b597d1390c6fa9cc356b8c7529f195ac8a90a78190d563159f3a1eed10e01bbd4d01a8ab
languageName: node
linkType: hard
"tslib@npm:^1.9.0":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
@ -10098,6 +10500,13 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.3.0":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
languageName: node
linkType: hard
"tslib@npm:^2.3.1, tslib@npm:^2.4.0":
version: 2.5.0
resolution: "tslib@npm:2.5.0"
@ -10177,6 +10586,32 @@ __metadata:
languageName: node
linkType: hard
"typescript-plugin-css-modules@npm:^5.1.0":
version: 5.1.0
resolution: "typescript-plugin-css-modules@npm:5.1.0"
dependencies:
"@types/postcss-modules-local-by-default": "npm:^4.0.2"
"@types/postcss-modules-scope": "npm:^3.0.4"
dotenv: "npm:^16.4.2"
icss-utils: "npm:^5.1.0"
less: "npm:^4.2.0"
lodash.camelcase: "npm:^4.3.0"
postcss: "npm:^8.4.35"
postcss-load-config: "npm:^3.1.4"
postcss-modules-extract-imports: "npm:^3.0.0"
postcss-modules-local-by-default: "npm:^4.0.4"
postcss-modules-scope: "npm:^3.1.1"
reserved-words: "npm:^0.1.2"
sass: "npm:^1.70.0"
source-map-js: "npm:^1.0.2"
stylus: "npm:^0.62.0"
tsconfig-paths: "npm:^4.2.0"
peerDependencies:
typescript: ">=4.0.0"
checksum: a87487f88262ea9b21108a00a68ccdc1f86f59c73c5faed432e834593679d973a4c7a72479b3a7556913ede1a1527e2f47cada282236194de0a44ee1581c4e81
languageName: node
linkType: hard
"typescript@npm:^4.0.2, typescript@npm:^4.2.4":
version: 4.9.5
resolution: "typescript@npm:4.9.5"
@ -10852,6 +11287,13 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^1.10.2":
version: 1.10.2
resolution: "yaml@npm:1.10.2"
checksum: e088b37b4d4885b70b50c9fa1b7e54bd2e27f5c87205f9deaffd1fb293ab263d9c964feadb9817a7b129a5bf30a06582cb08750f810568ecc14f3cdbabb79cb3
languageName: node
linkType: hard
"yargs-parser@npm:^20.2.2":
version: 20.2.9
resolution: "yargs-parser@npm:20.2.9"