mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 03:33:14 +01:00
start to tsify
This commit is contained in:
parent
6fddf72a2d
commit
8b6f0cc593
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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,13 +1073,14 @@ function App() {
|
||||
} catch (err) {
|
||||
if (err instanceof DirectoryAccessDeclinedError) return;
|
||||
|
||||
if (err.killed === true) {
|
||||
if (err instanceof Error) {
|
||||
if ('killed' in err && err.killed === true) {
|
||||
// assume execa killed (aborted by user)
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('stdout:', err.stdout);
|
||||
console.error('stderr:', err.stderr);
|
||||
if ('stdout' in err) console.error('stdout:', err.stdout);
|
||||
if ('stderr' in err) console.error('stderr:', err.stderr);
|
||||
|
||||
if (isExecaFailure(err)) {
|
||||
if (isOutOfSpaceError(err)) {
|
||||
@ -1087,11 +1091,12 @@ function App() {
|
||||
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,13 +1291,14 @@ function App() {
|
||||
|
||||
resetMergedOutFileName();
|
||||
} catch (err) {
|
||||
if (err.killed === true) {
|
||||
if (err instanceof Error) {
|
||||
if ('killed' in err && err.killed === true) {
|
||||
// assume execa killed (aborted by user)
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('stdout:', err.stdout);
|
||||
console.error('stderr:', err.stderr);
|
||||
if ('stdout' in err) console.error('stdout:', err.stdout);
|
||||
if ('stderr' in err) console.error('stderr:', err.stderr);
|
||||
|
||||
if (isExecaFailure(err)) {
|
||||
if (isOutOfSpaceError(err)) {
|
||||
@ -1302,11 +1308,12 @@ function App() {
|
||||
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>
|
@ -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 (
|
@ -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>
|
@ -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';
|
||||
|
@ -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();
|
@ -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 (
|
@ -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 (
|
||||
<>
|
@ -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();
|
@ -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]);
|
@ -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) => {
|
@ -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]);
|
@ -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} />
|
||||
));
|
@ -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 }} />
|
||||
);
|
||||
|
@ -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);
|
@ -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 (
|
@ -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();
|
||||
|
@ -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>
|
@ -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);
|
@ -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();
|
||||
|
@ -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]);
|
||||
|
@ -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' }}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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.');
|
||||
},
|
||||
});
|
@ -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;
|
@ -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') {
|
@ -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;
|
||||
|
@ -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
|
@ -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');
|
@ -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 }) {
|
@ -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',
|
@ -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
30
src/types.ts
Normal 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 }[];
|
@ -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 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);
|
@ -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 ? '-' : '';
|
@ -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();
|
||||
|
@ -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);
|
@ -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
458
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user