pull/1925/head
Mikael Finstad 2024-03-15 21:45:33 +08:00
rodzic 96baa9a931
commit 27df6c20e6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
48 zmienionych plików z 691 dodań i 675 usunięć

Wyświetl plik

@ -1,2 +1,3 @@
/dist
/vite-dist
/ts-dist

Wyświetl plik

@ -18,10 +18,7 @@ module.exports = {
browser: true,
},
rules: {
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
optionalDependencies: false,
}],
'import/no-extraneous-dependencies': 0,
},
},
{

1
.gitignore vendored
Wyświetl plik

@ -17,3 +17,4 @@ node_modules
/doc
/ffmpeg
/app.log
/ts-dist

Wyświetl plik

@ -47,9 +47,13 @@
"@radix-ui/react-switch": "^1.0.1",
"@tsconfig/strictest": "^2.0.2",
"@tsconfig/vite-react": "^3.0.0",
"@types/color": "^3.0.6",
"@types/css-modules": "^1.0.5",
"@types/eslint": "^8",
"@types/lodash": "^4.14.202",
"@types/node": "18",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/sortablejs": "^1.15.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
@ -100,8 +104,8 @@
"sortablejs": "^1.13.0",
"sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7",
"tiny-invariant": "^1.3.3",
"typescript": "~5.2.0",
"typescript-plugin-css-modules": "^5.1.0",
"use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0",
"vite": "^4.5.2",

Wyświetl plik

@ -63,7 +63,9 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
return;
}
if (!err.killed) {
// @ts-expect-error todo
if (!(err.killed)) {
// @ts-expect-error todo
console.warn(err.message);
console.warn(stderr.toString('utf8'));
}
@ -89,6 +91,7 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
const { stdout } = await process;
return stdout;
} catch (err) {
// @ts-expect-error todo
logger.error('renderOneJpegFrame', err.shortMessage);
throw new Error('Failed to render JPEG frame');
}

Wyświetl plik

@ -10,6 +10,7 @@ const logger = require('./logger');
const { app } = electron;
/** @type {import('../types').KeyBinding[]} */
const defaultKeyBindings = [
{ keys: 'plus', action: 'addSegment' },
{ keys: 'space', action: 'togglePlayResetSpeed' },
@ -80,6 +81,7 @@ const defaultKeyBindings = [
{ keys: 'alt+down', action: 'decreaseVolume' },
];
/** @type {import('../types').Config} */
const defaults = {
captureFormat: 'jpeg',
customOutDir: undefined,
@ -128,13 +130,14 @@ const defaults = {
enableNativeHevc: true,
enableUpdateCheck: true,
cleanupChoices: {
trashTmpFiles: true, askForCleanup: true, closeFile: true,
trashTmpFiles: true, askForCleanup: true, closeFile: true, cleanupAfterExport: false,
},
allowMultipleInstances: false,
darkMode: true,
preferStrongColors: false,
outputFileNameMinZeroPadding: 1,
cutFromAdjustmentFrames: 0,
invertTimelineScroll: undefined,
};
// For portable app: https://github.com/mifi/lossless-cut/issues/645
@ -146,7 +149,7 @@ async function getCustomStoragePath() {
// https://github.com/mifi/lossless-cut/issues/645#issuecomment-1001363314
// https://stackoverflow.com/questions/46307797/how-to-get-the-original-path-of-a-portable-electron-app
// https://github.com/electron-userland/electron-builder/blob/master/docs/configuration/nsis.md
const customStorageDir = process.env.PORTABLE_EXECUTABLE_DIR || dirname(app.getPath('exe'));
const customStorageDir = process.env['PORTABLE_EXECUTABLE_DIR'] || dirname(app.getPath('exe'));
const customConfigPath = join(customStorageDir, 'config.json');
if (await pathExists(customConfigPath)) return customStorageDir;
return undefined;
@ -158,15 +161,18 @@ async function getCustomStoragePath() {
let store;
/** @type {import('../types').StoreGetConfig} */
function get(key) {
return store.get(key);
}
/** @type {import('../types').StoreSetConfig} */
function set(key, val) {
if (val === undefined) store.delete(key);
else store.set(key, val);
}
/** @type {import('../types').StoreResetConfig} */
function reset(key) {
set(key, defaults[key]);
}

Wyświetl plik

@ -6,7 +6,7 @@ module.exports = (window) => {
const selectionMenu = Menu.buildFromTemplate([
{ role: 'copy' },
{ type: 'separator' },
{ role: 'selectall' },
{ role: 'selectAll' },
]);
const inputMenu = Menu.buildFromTemplate([
@ -17,10 +17,10 @@ module.exports = (window) => {
{ role: 'copy' },
{ role: 'paste' },
{ type: 'separator' },
{ role: 'selectall' },
{ role: 'selectAll' },
]);
window.webContents.on('context-menu', (e, props) => {
window.webContents.on('context-menu', (_e, props) => {
const { selectionText, isEditable } = props;
if (isEditable) {
inputMenu.popup(window);

Wyświetl plik

@ -133,7 +133,6 @@ function createWindow() {
...getSizeOptions(),
darkTheme: true,
webPreferences: {
enableRemoteModule: true,
contextIsolation: false,
nodeIntegration: true,
// https://github.com/electron/electron/issues/5107
@ -226,15 +225,17 @@ function initApp() {
// However when users start your app in command line, the system's single instance mechanism will be bypassed, and you have to use this method to ensure single instance.
// This can be tested with one terminal: npx electron .
// and another terminal: npx electron . path/to/file.mp4
app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => {
app.on('second-instance', (_event, _commandLine, _workingDirectory, additionalData) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
// @ts-expect-error todo
if (!Array.isArray(additionalData?.argv)) return;
// @ts-expect-error todo
const argv2 = parseCliArgs(additionalData.argv);
logger.info('second-instance', argv2);
@ -268,26 +269,27 @@ function initApp() {
event.preventDefault(); // recommended in docs https://www.electronjs.org/docs/latest/api/app#event-open-file-macos
});
ipcMain.on('setAskBeforeClose', (e, val) => {
ipcMain.on('setAskBeforeClose', (_e, val) => {
askBeforeClose = val;
});
ipcMain.on('setLanguage', (e, language) => {
ipcMain.on('setLanguage', (_e, language) => {
i18n.changeLanguage(language).then(() => updateMenu()).catch((err) => logger.error('Failed to set language', err));
});
ipcMain.handle('tryTrashItem', async (e, path) => {
ipcMain.handle('tryTrashItem', async (_e, path) => {
try {
await stat(path);
} catch (err) {
// @ts-expect-error todo
if (err.code === 'ENOENT') return;
}
await shell.trashItem(path);
});
ipcMain.handle('showItemInFolder', (e, path) => shell.showItemInFolder(path));
ipcMain.handle('showItemInFolder', (_e, path) => shell.showItemInFolder(path));
ipcMain.on('apiKeyboardActionResponse', (e, { id }) => {
ipcMain.on('apiKeyboardActionResponse', (_e, { id }) => {
apiKeyboardActionRequests.get(id)?.();
});
}

Wyświetl plik

@ -61,6 +61,7 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
// eslint-disable-next-line unicorn/better-regex
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
if (!match) {
// @ts-expect-error todo
customMatcher(line);
return;
}
@ -86,11 +87,13 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
const progress = duration ? Math.min(progressTime / duration, 1) : 0; // sometimes progressTime will be greater than cutDuration
onProgress(progress);
} catch (err) {
// @ts-expect-error todo
console.log('Failed to parse ffmpeg progress line:', err.message);
}
});
}
// @ts-expect-error todo
function getExecaOptions({ env, ...customExecaOptions } = {}) {
const execaOptions = { ...customExecaOptions, env: { ...env } };
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489
@ -251,6 +254,7 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
if (!match) return;
const time = parseFloat(match[1]);
// @ts-expect-error todo
if (Number.isNaN(time) || time <= times.at(-1)) return;
times.push(time);
});
@ -280,6 +284,7 @@ async function detectIntervals({ filePath, customArgs, onProgress, from, to, mat
if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) return;
segments.push({ start, end });
}
// @ts-expect-error todo
handleProgress(process, to - from, onProgress, customMatcher);
await process;

Wyświetl plik

@ -20,7 +20,7 @@ module.exports = ({ port, onKeyboardAction }) => {
const apiRouter = express.Router();
app.get('/', (req, res) => res.send(`See ${homepage}`));
app.get('/', (_req, res) => res.send(`See ${homepage}`));
app.use('/api', apiRouter);
@ -38,6 +38,7 @@ module.exports = ({ port, onKeyboardAction }) => {
const host = '127.0.0.1';
server.listen(port, host, () => {
logger.info('HTTP API listening on', `http://${host}:${port}/`);
// @ts-expect-error tod
resolve();
});

Wyświetl plik

@ -442,5 +442,6 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
});
}
// @ts-expect-error todo
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
};

Wyświetl plik

@ -29,6 +29,7 @@ async function checkNewVersion() {
if (semver.lt(currentVersion, newestVersion)) return newestVersion;
return undefined;
} catch (err) {
// @ts-expect-error todo
logger.error('Failed to check github version', err.message);
return undefined;
}

Wyświetl plik

@ -15,6 +15,7 @@ import sortBy from 'lodash/sortBy';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sum from 'lodash/sum';
import invariant from 'tiny-invariant';
import theme from './theme';
import useTimelineScroll from './hooks/useTimelineScroll';
@ -85,7 +86,8 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, Html5ifyMode, PlaybackMode, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction } from '../types';
const electron = window.require('electron');
const { exists } = window.require('fs-extra');
@ -160,7 +162,7 @@ function App() {
const [mifiLink, setMifiLink] = useState<unknown>();
const [alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles] = useState(false);
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState<number>();
const [editingSegmentTags, setEditingSegmentTags] = useState<Record<string, unknown>>();
const [editingSegmentTags, setEditingSegmentTags] = useState<SegmentTags>();
const [mediaSourceQuality, setMediaSourceQuality] = useState(0);
const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []);
@ -404,8 +406,8 @@ function App() {
userSeekAbs(nextFrame / fps);
}, [detectedFps, userSeekAbs]);
const jumpSegStart = useCallback((index) => userSeekAbs(apparentCutSegments[index]!.start), [apparentCutSegments, userSeekAbs]);
const jumpSegEnd = useCallback((index) => userSeekAbs(apparentCutSegments[index]!.end), [apparentCutSegments, userSeekAbs]);
const jumpSegStart = useCallback((index: number) => userSeekAbs(apparentCutSegments[index]!.start), [apparentCutSegments, userSeekAbs]);
const jumpSegEnd = useCallback((index: number) => userSeekAbs(apparentCutSegments[index]!.end), [apparentCutSegments, userSeekAbs]);
const jumpCutStart = useCallback(() => jumpSegStart(currentSegIndexSafe), [currentSegIndexSafe, jumpSegStart]);
const jumpCutEnd = useCallback(() => jumpSegEnd(currentSegIndexSafe), [currentSegIndexSafe, jumpSegEnd]);
const jumpTimelineStart = useCallback(() => userSeekAbs(0), [userSeekAbs]);
@ -414,7 +416,7 @@ function App() {
const getFrameCount = useCallback((sec: number) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
const formatTimecode = useCallback(({ seconds, shorten, fileNameFriendly }) => {
const formatTimecode = useCallback<FormatTimecode>(({ seconds, shorten, fileNameFriendly }) => {
if (timecodeFormat === 'frameCount') {
const frameCount = getFrameCount(seconds);
return frameCount != null ? String(frameCount) : '';
@ -532,15 +534,17 @@ function App() {
const { ensureWritableOutDir, ensureAccessToSourceDir } = useDirectoryAccess({ setCustomOutDir });
const toggleCaptureFormat = useCallback(() => setCaptureFormat((f) => {
const captureFormats = ['jpeg', 'png', 'webp'];
const captureFormats: CaptureFormat[] = ['jpeg', 'png', 'webp'];
let index = captureFormats.indexOf(f);
if (index === -1) index = 0;
index += 1;
if (index >= captureFormats.length) index = 0;
return captureFormats[index];
const newCaptureFormat = captureFormats[index];
if (newCaptureFormat == null) throw new Error();
return newCaptureFormat;
}), [setCaptureFormat]);
const toggleKeyframeCut = useCallback((showMessage) => setKeyframeCut((val) => {
const toggleKeyframeCut = useCallback((showMessage?: boolean) => setKeyframeCut((val) => {
const newVal = !val;
if (showMessage && !hideAllNotifications) {
if (newVal) toast.fire({ title: i18n.t('Keyframe cut enabled'), text: i18n.t('Will now cut at the nearest keyframe before the desired start cutpoint. This is recommended for most files.') });
@ -601,7 +605,7 @@ function App() {
}), [allUserSettings, changeOutDir, effectiveExportMode, toggleCaptureFormat, toggleExportConfirmEnabled, toggleKeyframeCut, toggleMovFastStart, togglePreserveMetadataOnMerge, togglePreserveMovData, toggleSafeOutputFileName, toggleSegmentsToChapters, toggleSimpleMode]);
const segColorsContext = useMemo(() => ({
getSegColor: (seg) => {
getSegColor: (seg: SegmentColorIndex) => {
const color = getSegColor(seg);
return preferStrongColors ? color.desaturate(0.2) : color.desaturate(0.6);
},
@ -1051,7 +1055,9 @@ function App() {
if (sendErrorReport) openSendConcatReportDialogWithState(err, reportState);
}, [fileFormat, openSendConcatReportDialogWithState]);
const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, outFileName, clearBatchFilesAfterConcat }) => {
const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, outFileName, clearBatchFilesAfterConcat }: {
paths: string[], includeAllStreams: boolean, streams, fileFormat: string, outFileName: string, clearBatchFilesAfterConcat: boolean,
}) => {
if (workingRef.current) return;
try {
setConcatDialogVisible(false);
@ -1076,6 +1082,7 @@ function App() {
// console.log('merge', paths);
const metadataFromPath = paths[0];
invariant(metadataFromPath != null);
const { haveExcludedStreams } = await concatFiles({ paths, outPath, outDir, outFormat, metadataFromPath, includeAllStreams, streams, ffmpegExperimental, onProgress: setCutProgress, preserveMovData, movFastStart, preserveMetadataOnMerge, chapters: chaptersFromSegments, appendFfmpegCommandLog });
const warnings: string[] = [];
@ -1258,7 +1265,6 @@ function App() {
setCutProgress(0);
setWorking({ text: i18n.t('Merging') });
// @ts-expect-error name only exists for invertCutSegments = false
const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined;
await autoConcatCutSegments({
@ -1367,7 +1373,7 @@ function App() {
}
}, [filePath, getRelevantTime, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);
const extractSegmentFramesAsImages = useCallback(async (segIds) => {
const extractSegmentFramesAsImages = useCallback(async (segIds: string[]) => {
if (!filePath || detectedFps == null || workingRef.current) return;
const segments = apparentCutSegments.filter((seg) => segIds.includes(seg.segId));
const segmentsNumFrames = segments.reduce((acc, { start, end }) => acc + (getFrameCount(end - start) ?? 0), 0);
@ -1428,7 +1434,7 @@ function App() {
loadCutSegments(await readEdlFile({ type, path }), append);
}, [loadCutSegments]);
const loadMedia = useCallback(async ({ filePath: fp, projectPath }) => {
const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string }) => {
async function tryOpenProjectPath(path, type) {
if (!(await exists(path))) return false;
await loadEdlFile({ path, type });
@ -1596,7 +1602,7 @@ function App() {
const seekAccelerationRef = useRef(1);
const userOpenSingleFile = useCallback(async ({ path: pathIn, isLlcProject }) => {
const userOpenSingleFile = useCallback(async ({ path: pathIn, isLlcProject }: { path: string, isLlcProject?: boolean }) => {
let path = pathIn;
let projectPath;
@ -1713,7 +1719,7 @@ function App() {
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking]);
const userHtml5ifyCurrentFile = useCallback(async ({ ignoreRememberedValue } = {}) => {
const userHtml5ifyCurrentFile = useCallback(async ({ ignoreRememberedValue }: { ignoreRememberedValue?: boolean } = {}) => {
if (!filePath) return;
let selectedOption = rememberConvertToSupportedFormat;
@ -1764,6 +1770,7 @@ function App() {
try {
setWorking({ text: i18n.t('Fixing file duration') });
setCutProgress(0);
invariant(fileFormat != null);
const path = await fixInvalidDuration({ fileFormat, customOutDir, duration, onProgress: setCutProgress });
if (!hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('Duration has been fixed') });
@ -2023,7 +2030,9 @@ function App() {
onEditSegmentTags(currentSegIndexSafe);
}, [currentSegIndexSafe, onEditSegmentTags]);
const mainActions: Record<string, (a: { keyup: boolean }) => void> = useMemo(() => {
type MainKeyboardAction = Exclude<KeyboardAction, 'closeActiveScreen' | 'toggleKeyboardShortcuts'>;
const mainActions = useMemo<Record<MainKeyboardAction, ((a: { keyup?: boolean | undefined }) => boolean) | ((a: { keyup?: boolean | undefined }) => void)>>(() => {
async function exportYouTube() {
if (!checkFileOpened()) return;
@ -2145,7 +2154,6 @@ function App() {
showStreamsSelector: handleShowStreamsSelectorClick,
html5ify: () => userHtml5ifyCurrentFile({ ignoreRememberedValue: true }),
openFilesDialog,
toggleKeyboardShortcuts,
toggleSettings,
openSendReportDialog: () => { openSendReportDialogWithState(); },
detectBlackScenes: ({ keyup }) => {
@ -2164,15 +2172,16 @@ function App() {
showIncludeExternalStreamsDialog,
toggleFullscreenVideo,
};
}, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegments.length, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCurrentSegIndex, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleStripThumbnail, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
}, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegments.length, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, editCurrentSegmentTags, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCurrentSegIndex, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, showIncludeExternalStreamsDialog, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleFullscreenVideo, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleShowKeyframes, toggleShowThumbnails, toggleStreamsSelector, toggleStripAudio, toggleStripThumbnail, toggleWaveformMode, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
const getKeyboardAction = useCallback((action: string) => mainActions[action], [mainActions]);
const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]);
const onKeyPress = useCallback(({ action, keyup }: { action: string, keyup: boolean }) => {
function tryMainActions() {
const fn = getKeyboardAction(action);
const onKeyPress = useCallback(({ action, keyup }: { action: KeyboardAction, keyup?: boolean | undefined }) => {
function tryMainActions(mainAction: MainKeyboardAction) {
const fn = getKeyboardAction(mainAction);
if (!fn) return { match: false };
const bubble = fn({ keyup });
if (bubble === undefined) return { match: true };
return { match: true, bubble };
}
@ -2205,7 +2214,7 @@ function App() {
}
// allow main actions
const { match, bubble } = tryMainActions();
const { match, bubble } = tryMainActions(action);
if (match) return bubble;
return true; // bubble the event
@ -2547,7 +2556,6 @@ function App() {
<AnimatePresence>
{showRightBar && isFileOpened && (
<SegmentList
// @ts-expect-error todo
width={rightBarWidth}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
@ -2723,8 +2731,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} />
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
</UserSettingsContext.Provider>

Wyświetl plik

@ -39,13 +39,13 @@ const NoFileLoaded = memo(({ mifiLink, currentCutSeg, onClick, darkMode }: {
)}
</div>
{mifiLink && typeof mifiLink === 'object' && 'loadUrl' in mifiLink && typeof mifiLink.loadUrl === 'string' && 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(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
</div>
)}
) : undefined}
</div>
);
});

Wyświetl plik

@ -1,7 +1,7 @@
import { memo, useMemo, useRef, useCallback, useState } from 'react';
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai';
import { motion } from 'framer-motion';
import { MotionStyle, motion } from 'framer-motion';
import { useTranslation, Trans } from 'react-i18next';
import { ReactSortable } from 'react-sortablejs';
import isEqual from 'lodash/isEqual';
@ -17,6 +17,8 @@ import { useSegColors } from './contexts';
import { mySpring } from './animations';
import { getSegmentTags } from './segments';
import TagEditor from './components/TagEditor';
import { ApparentCutSegment, ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentTags, StateSegment } from './types';
import { UseSegments } from './hooks/useSegments';
const buttonBaseStyle = {
margin: '0 3px', borderRadius: 3, color: 'white', cursor: 'pointer',
@ -24,16 +26,71 @@ const buttonBaseStyle = {
const neutralButtonColor = 'var(--gray8)';
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateSegOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
const Segment = memo(({
seg,
index,
currentSegIndex,
formatTimecode,
getFrameCount,
updateSegOrder,
onClick,
onRemovePress,
onRemoveSelected,
onLabelSelectedSegments,
onReorderPress,
onLabelPress,
selected,
onSelectSingleSegment,
onToggleSegmentSelected,
onDeselectAllSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
onSelectAllSegments,
jumpSegStart,
jumpSegEnd,
addSegment,
onEditSegmentTags,
onExtractSegmentFramesAsImages,
onInvertSelectedSegments,
onDuplicateSegmentClick,
}: {
seg: ApparentCutSegment | InverseCutSegment,
index: number,
currentSegIndex: number,
formatTimecode: FormatTimecode,
getFrameCount: GetFrameCount,
updateSegOrder: UseSegments['updateSegOrder'],
onClick: (i: number) => void,
onRemovePress: UseSegments['removeCutSegment'],
onRemoveSelected: UseSegments['removeSelectedSegments'],
onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'],
onReorderPress: (i: number) => Promise<void>,
onLabelPress: UseSegments['onLabelSegment'],
selected: boolean,
onSelectSingleSegment: UseSegments['selectOnlySegment'],
onToggleSegmentSelected: UseSegments['toggleSegmentSelected'],
onDeselectAllSegments: UseSegments['deselectAllSegments'],
onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'],
onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'],
onSelectAllSegments: UseSegments['selectAllSegments'],
jumpSegStart: (i: number) => void,
jumpSegEnd: (i: number) => void,
addSegment: UseSegments['addSegment'],
onEditSegmentTags: (i: number) => void,
onExtractSegmentFramesAsImages: (segIds: string[]) => Promise<void>,
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment']
}) => {
const { invertCutSegments, darkMode } = useUserSettings();
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const ref = useRef();
const ref = useRef<HTMLDivElement>(null);
const contextMenuTemplate = useMemo(() => {
const contextMenuTemplate = useMemo<ContextMenuTemplate>(() => {
if (invertCutSegments) return [];
const updateOrder = (dir) => updateSegOrder(index, index + dir);
const updateOrder = (dir: number) => updateSegOrder(index, index + dir);
return [
{ label: t('Jump to start time'), click: () => jumpSegStart(index) },
@ -51,8 +108,8 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
{ label: t('Select only this segment'), click: () => onSelectSingleSegment(seg) },
{ label: t('Select all segments'), click: () => onSelectAllSegments() },
{ label: t('Deselect all segments'), click: () => onDeselectAllSegments() },
{ label: t('Select segments by label'), click: () => onSelectSegmentsByLabel(seg) },
{ label: t('Select segments by tag'), click: () => onSelectSegmentsByTag(seg) },
{ label: t('Select segments by label'), click: () => onSelectSegmentsByLabel() },
{ label: t('Select segments by tag'), click: () => onSelectSegmentsByTag() },
{ label: t('Invert selected segments'), click: () => onInvertSelectedSegments() },
{ type: 'separator' },
@ -85,7 +142,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
}, 300, [isActive]);
function renderNumber() {
if (invertCutSegments) return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
if (invertCutSegments || !('segColorIndex' in seg)) return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
const segColor = getSegColor(seg);
@ -114,11 +171,11 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
const cursor = invertCutSegments ? undefined : 'grab';
const tags = useMemo(() => getSegmentTags(seg), [seg]);
const tags = useMemo(() => getSegmentTags('tags' in seg ? seg : {}), [seg]);
const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]);
const motionStyle = useMemo(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]);
const motionStyle = useMemo<MotionStyle>(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]);
return (
<motion.div
@ -139,7 +196,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
</div>
<div style={{ fontSize: 12 }}>
{seg.name && <span style={{ color: primaryTextColor, marginRight: '.3em' }}>{seg.name}</span>}
{'name' in seg && seg.name && <span style={{ color: primaryTextColor, marginRight: '.3em' }}>{seg.name}</span>}
{Object.entries(tags).map(([name, value]) => (
<span style={{ fontSize: 11, backgroundColor: 'var(--gray5)', color: 'var(--gray12)', borderRadius: '.4em', padding: '0 .2em', marginRight: '.1em' }} key={name}>{name}:<b>{value}</b></span>
))}
@ -162,20 +219,88 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
});
const SegmentList = memo(({
width, formatTimecode, apparentCutSegments, inverseCutSegments, getFrameCount, onSegClick,
width,
formatTimecode,
apparentCutSegments,
inverseCutSegments,
getFrameCount,
onSegClick,
currentSegIndex,
updateSegOrder, updateSegOrders, addSegment, removeCutSegment, onRemoveSelected,
onLabelSegment, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment,
selectedSegments, isSegmentSelected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, onDuplicateSegmentClick,
jumpSegStart, jumpSegEnd, updateSegAtIndex,
editingSegmentTags, editingSegmentTagsSegmentIndex, setEditingSegmentTags, setEditingSegmentTagsSegmentIndex, onEditSegmentTags,
updateSegOrder,
updateSegOrders,
addSegment,
removeCutSegment,
onRemoveSelected,
onLabelSegment,
currentCutSeg,
segmentAtCursor,
toggleSegmentsList,
splitCurrentSegment,
selectedSegments,
isSegmentSelected,
onSelectSingleSegment,
onToggleSegmentSelected,
onDeselectAllSegments,
onSelectAllSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
onExtractSegmentFramesAsImages,
onLabelSelectedSegments,
onInvertSelectedSegments,
onDuplicateSegmentClick,
jumpSegStart,
jumpSegEnd,
updateSegAtIndex,
editingSegmentTags,
editingSegmentTagsSegmentIndex,
setEditingSegmentTags,
setEditingSegmentTagsSegmentIndex,
onEditSegmentTags,
}: {
width: number,
formatTimecode: FormatTimecode,
apparentCutSegments: ApparentCutSegment[],
inverseCutSegments: InverseCutSegment[],
getFrameCount: GetFrameCount,
onSegClick: (index: number) => void,
currentSegIndex: number,
updateSegOrder: UseSegments['updateSegOrder'],
updateSegOrders: UseSegments['updateSegOrders'],
addSegment: UseSegments['addSegment'],
removeCutSegment: UseSegments['removeCutSegment'],
onRemoveSelected: UseSegments['removeSelectedSegments'],
onLabelSegment: UseSegments['onLabelSegment'],
currentCutSeg: UseSegments['currentCutSeg'],
segmentAtCursor: StateSegment | undefined,
toggleSegmentsList: () => void,
splitCurrentSegment: UseSegments['splitCurrentSegment'],
selectedSegments: UseSegments['selectedSegmentsOrInverse'],
isSegmentSelected: UseSegments['isSegmentSelected'],
onSelectSingleSegment: UseSegments['selectOnlySegment'],
onToggleSegmentSelected: UseSegments['toggleSegmentSelected'],
onDeselectAllSegments: UseSegments['deselectAllSegments'],
onSelectAllSegments: UseSegments['selectAllSegments'],
onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'],
onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'],
onExtractSegmentFramesAsImages: (segIds: string[]) => Promise<void>,
onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'],
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment'],
jumpSegStart: (index: number) => void,
jumpSegEnd: (index: number) => void,
updateSegAtIndex: UseSegments['updateSegAtIndex'],
editingSegmentTags: SegmentTags | undefined,
editingSegmentTagsSegmentIndex: number | undefined,
setEditingSegmentTags: Dispatch<SetStateAction<SegmentTags | undefined>>,
setEditingSegmentTagsSegmentIndex: Dispatch<SetStateAction<number | undefined>>,
onEditSegmentTags: (index: number) => void,
}) => {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const { invertCutSegments, simpleMode, darkMode } = useUserSettings();
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const segments: (InverseCutSegment | ApparentCutSegment)[] = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]);
@ -184,15 +309,16 @@ const SegmentList = memo(({
updateSegOrders(newList.map((list) => list.id));
}, [segments, updateSegOrders]);
let header = t('Segments to export:');
let header: ReactNode = t('Segments to export:');
if (segments.length === 0) {
header = invertCutSegments ? (
<Trans>You have enabled the &quot;invert segments&quot; mode <FaYinYang style={{ verticalAlign: 'middle' }} /> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <FaYinYang style={{ verticalAlign: 'middle', color: primaryTextColor }} /> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.</Trans>
) : t('No segments to export.');
}
const onReorderSegs = useCallback(async (index) => {
const onReorderSegs = useCallback(async (index: number) => {
if (apparentCutSegments.length < 2) return;
// @ts-expect-error todo
const { value } = await Swal.fire({
title: `${t('Change order of segment')} ${index + 1}`,
text: `Please enter a number from 1 to ${apparentCutSegments.length} to be the new order for the current segment`,
@ -276,20 +402,25 @@ const SegmentList = memo(({
const [editingTag, setEditingTag] = useState();
const onTagChange = useCallback((tag, value) => setEditingSegmentTags((existingTags) => ({
const onTagChange = useCallback((tag: string, value: string) => setEditingSegmentTags((existingTags) => ({
...existingTags,
[tag]: value,
})), [setEditingSegmentTags]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]);
const onTagReset = useCallback((tag: string) => setEditingSegmentTags((tags) => {
if (tags == null) throw new Error();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [tag]: deleted, ...rest } = tags;
return rest;
}), [setEditingSegmentTags]);
const onSegmentTagsCloseComplete = useCallback(() => {
setEditingSegmentTagsSegmentIndex();
setEditingSegmentTags();
setEditingSegmentTagsSegmentIndex(undefined);
setEditingSegmentTags(undefined);
}, [setEditingSegmentTags, setEditingSegmentTagsSegmentIndex]);
const onSegmentTagsConfirm = useCallback(() => {
if (editingSegmentTagsSegmentIndex == null) throw new Error();
updateSegAtIndex(editingSegmentTagsSegmentIndex, { tags: editingSegmentTags });
onSegmentTagsCloseComplete();
}, [editingSegmentTags, editingSegmentTagsSegmentIndex, onSegmentTagsCloseComplete, updateSegAtIndex]);
@ -336,7 +467,6 @@ const SegmentList = memo(({
return (
<Segment
key={id}
darkMode={darkMode}
seg={seg}
index={index}
selected={selected}
@ -352,7 +482,6 @@ const SegmentList = memo(({
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
currentSegIndex={currentSegIndex}
invertCutSegments={invertCutSegments}
onSelectSingleSegment={onSelectSingleSegment}
onToggleSegmentSelected={onToggleSegmentSelected}
onDeselectAllSegments={onDeselectAllSegments}

Wyświetl plik

@ -1,4 +1,4 @@
import { memo, Fragment, useEffect, useMemo, useCallback, useState } from 'react';
import { memo, Fragment, useEffect, useMemo, useCallback, useState, ReactNode, SetStateAction, Dispatch } from 'react';
import { useTranslation } from 'react-i18next';
import { SearchInput, PlusIcon, InlineAlert, UndoIcon, Paragraph, TakeActionIcon, IconButton, Button, DeleteIcon, AddIcon, Heading, Text, Dialog } from 'evergreen-ui';
import { FaMouse, FaPlus, FaStepForward, FaStepBackward } from 'react-icons/fa';
@ -12,9 +12,15 @@ import Swal from '../swal';
import SetCutpointButton from './SetCutpointButton';
import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction } from '../../types';
import { StateSegment } from '../types';
const renderKeys = (keys) => keys.map((key, i) => (
type Category = string;
type ActionsMap = Record<KeyboardAction, { name: string, category?: Category, before?: ReactNode }>;
const renderKeys = (keys: string[]) => keys.map((key, i) => (
<Fragment key={key}>
{i > 0 && <FaPlus size={8} style={{ marginLeft: 4, marginRight: 4, color: 'rgba(0,0,0,0.5)' }} />}
<kbd>{key.toUpperCase()}</kbd>
@ -25,7 +31,7 @@ const renderKeys = (keys) => keys.map((key, i) => (
// For modifier keys you can use shift, ctrl, alt, or meta.
// You can substitute option for alt and command for meta.
const allModifiers = new Set(['shift', 'ctrl', 'alt', 'meta']);
function fixKeys(keys) {
function fixKeys(keys: string[]) {
const replaced = keys.map((key) => {
if (key === 'option') return 'alt';
if (key === 'command') return 'meta';
@ -40,10 +46,12 @@ function fixKeys(keys) {
const CreateBinding = memo(({
actionsMap, action, setCreatingBinding, onNewKeyBindingConfirmed,
}: {
actionsMap: ActionsMap, action: KeyboardAction | undefined, setCreatingBinding: Dispatch<SetStateAction<KeyboardAction | undefined>>, onNewKeyBindingConfirmed: (a: KeyboardAction, keys: string[]) => void,
}) => {
const { t } = useTranslation();
const [keysDown, setKeysDown] = useState([]);
const [keysDown, setKeysDown] = useState<string[]>([]);
const validKeysDown = useMemo(() => fixKeys(keysDown), [keysDown]);
@ -55,13 +63,13 @@ const CreateBinding = memo(({
}
}, [isShown]);
const addKeyDown = useCallback((character) => setKeysDown((old) => [...new Set([...old, character])]), []);
const addKeyDown = useCallback((character: string) => setKeysDown((old) => [...new Set([...old, character])]), []);
useEffect(() => {
if (!isShown) return undefined;
const mousetrap = new Mousetrap();
function handleKey(character, modifiers, e) {
function handleKey(character: string, _modifiers: unknown, e: { type: string, preventDefault: () => void }) {
if (['keydown', 'keypress'].includes(e.type)) {
addKeyDown(character);
}
@ -83,9 +91,9 @@ const CreateBinding = memo(({
isShown={action != null}
confirmLabel={t('Save')}
cancelLabel={t('Cancel')}
onCloseComplete={() => setCreatingBinding()}
onConfirm={() => onNewKeyBindingConfirmed(action, keysDown)}
onCancel={() => setCreatingBinding()}
onCloseComplete={() => setCreatingBinding(undefined)}
onConfirm={() => action != null && onNewKeyBindingConfirmed(action, keysDown)}
onCancel={() => setCreatingBinding(undefined)}
>
{isShown ? (
<div style={{ color: 'black' }}>
@ -110,13 +118,15 @@ const CreateBinding = memo(({
const rowStyle = { display: 'flex', alignItems: 'center', margin: '.2em 0', borderBottom: '1px solid rgba(0,0,0,0.1)', paddingBottom: '.5em' };
const KeyboardShortcuts = memo(({
keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, mainActions,
keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg,
}: {
keyBindings: KeyBinding[], setKeyBindings: Dispatch<SetStateAction<KeyBinding[]>>, resetKeyBindings: () => void, currentCutSeg: StateSegment,
}) => {
const { t } = useTranslation();
const { mouseWheelZoomModifierKey } = useUserSettings();
const { actionsMap, extraLinesPerCategory } = useMemo(() => {
const { actionsMap, extraLinesPerCategory } = useMemo<{ actionsMap: ActionsMap, extraLinesPerCategory: Record<Category, ReactNode> }>(() => {
const playbackCategory = t('Playback');
const selectivePlaybackCategory = t('Playback/preview segments only');
const seekingCategory = t('Seeking');
@ -594,7 +604,7 @@ const KeyboardShortcuts = memo(({
name: t('Quit LosslessCut'),
category: otherCategory,
},
},
} satisfies ActionsMap,
};
}, [currentCutSeg, mouseWheelZoomModifierKey, t]);
@ -608,10 +618,11 @@ const KeyboardShortcuts = memo(({
}
}, [actionsMap, keyBindings, setKeyBindings]);
const [creatingBinding, setCreatingBinding] = useState();
const [creatingBinding, setCreatingBinding] = useState<KeyboardAction>();
const [searchQuery, setSearchQuery] = useState('');
const actionEntries = useMemo(() => Object.entries(actionsMap).filter(([, { name }]) => !searchQuery || name.toLowerCase().includes(searchQuery.toLowerCase())), [actionsMap, searchQuery]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actionEntries = useMemo(() => (Object.entries(actionsMap) as any as [keyof typeof actionsMap, typeof actionsMap[keyof typeof actionsMap]][]).filter(([, { name }]) => !searchQuery || name.toLowerCase().includes(searchQuery.toLowerCase())), [actionsMap, searchQuery]);
const categoriesWithActions = useMemo(() => Object.entries(groupBy(actionEntries, ([, { category }]) => category)), [actionEntries]);
@ -631,13 +642,13 @@ const KeyboardShortcuts = memo(({
resetKeyBindings();
}, [resetKeyBindings, t]);
const onAddBindingClick = useCallback((action) => {
const onAddBindingClick = useCallback((action: KeyboardAction) => {
setCreatingBinding(action);
}, []);
const stringifyKeys = (keys) => keys.join('+');
const onNewKeyBindingConfirmed = useCallback((action, keys) => {
const onNewKeyBindingConfirmed = useCallback((action: KeyboardAction, keys: string[]) => {
const fixedKeys = fixKeys(keys);
if (fixedKeys.length === 0) return;
const keysStr = stringifyKeys(fixedKeys);
@ -652,14 +663,11 @@ const KeyboardShortcuts = memo(({
}
console.log('saving key binding');
setCreatingBinding();
setCreatingBinding(undefined);
return [...existingBindings, { action, keys: keysStr }];
});
}, [actionsMap, setKeyBindings, t]);
const missingActions = Object.keys(mainActions).filter((key) => actionsMap[key] == null);
if (missingActions.length > 0) throw new Error(`Action(s) missing: ${missingActions.join(',')}`);
return (
<>
<div style={{ color: 'black', marginBottom: '1em' }}>
@ -717,7 +725,9 @@ const KeyboardShortcuts = memo(({
});
const KeyboardShortcutsDialog = memo(({
isShown, onHide, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, mainActions,
isShown, onHide, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg,
}: {
isShown: boolean, onHide: () => void, keyBindings: KeyBinding[], setKeyBindings: Dispatch<SetStateAction<KeyBinding[]>>, resetKeyBindings: () => void, currentCutSeg: StateSegment,
}) => {
const { t } = useTranslation();
@ -731,7 +741,7 @@ const KeyboardShortcutsDialog = memo(({
onConfirm={onHide}
topOffset="3vh"
>
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} mainActions={mainActions} /> : <div />}
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> : <div />}
</Dialog>
);
});

Wyświetl plik

@ -2,10 +2,10 @@ import { CSSProperties, useMemo } from 'react';
import { useSegColors } from '../contexts';
import useUserSettings from '../hooks/useUserSettings';
import { SegmentBase } from '../types';
import { SegmentBase, SegmentColorIndex } from '../types';
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: {
currentCutSeg: SegmentBase, side: 'start' | 'end', Icon, onClick?: (() => void) | undefined, title?: string | undefined, style?: CSSProperties | undefined
currentCutSeg: SegmentBase & SegmentColorIndex, side: 'start' | 'end', Icon, onClick?: (() => void) | undefined, title?: string | undefined, style?: CSSProperties | undefined
}) => {
const { darkMode } = useUserSettings();
const { getSegColor } = useSegColors();

Wyświetl plik

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

Wyświetl plik

@ -3,11 +3,11 @@ import { FaHandPointUp } from 'react-icons/fa';
import SegmentCutpointButton from './SegmentCutpointButton';
import { mirrorTransform } from '../util';
import { SegmentBase } from '../types';
import { SegmentBase, SegmentColorIndex } from '../types';
// constant side because we are mirroring
const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: {
currentCutSeg: SegmentBase, side: 'start' | 'end', title?: string, onClick?: () => void, style?: CSSProperties
currentCutSeg: SegmentBase & SegmentColorIndex, 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 }} />
);

Wyświetl plik

@ -1,8 +1,9 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { CSSProperties, ChangeEventHandler, 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';
import { motion } from 'framer-motion';
import { HTMLMotionProps, motion } from 'framer-motion';
import invariant from 'tiny-invariant';
import CaptureFormatButton from './CaptureFormatButton';
import AutoExportToggler from './AutoExportToggler';
@ -10,7 +11,7 @@ import Switch from './Switch';
import useUserSettings from '../hooks/useUserSettings';
import { askForFfPath } from '../dialogs';
import { isMasBuild, isStoreBuild } from '../util';
import { langNames } from '../util/constants';
import { LanguageKey, TimecodeFormat, langNames } from '../../types';
import styles from './Settings.module.css';
import Select from './Select';
@ -18,7 +19,7 @@ import { getModifierKeyNames } from '../hooks/useTimelineScroll';
import { TunerType } from '../types';
const Row = (props) => (
const Row = (props: HTMLMotionProps<'tr'>) => (
<motion.tr
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
@ -31,14 +32,14 @@ const Row = (props) => (
// eslint-disable-next-line react/jsx-props-no-spreading
const KeyCell = (props) => <td {...props} />;
const Header = ({ title }) => (
<Row className={styles.header}>
const Header = ({ title }: { title: string }) => (
<Row className={styles['header']}>
<th>{title}</th>
<th />
</Row>
);
const detailsStyle = { opacity: 0.75, fontSize: '.9em', marginTop: '.3em' };
const detailsStyle: CSSProperties = { opacity: 0.75, fontSize: '.9em', marginTop: '.3em' };
const Settings = memo(({
onTunerRequested,
@ -58,24 +59,26 @@ const Settings = memo(({
const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings();
const onLangChange = useCallback((e) => {
const onLangChange = useCallback<ChangeEventHandler<HTMLSelectElement>>((e) => {
const { value } = e.target;
const l = value !== '' ? value : undefined;
setLanguage(l);
setLanguage(l as LanguageKey | undefined);
}, [setLanguage]);
const timecodeFormatOptions = useMemo(() => ({
const timecodeFormatOptions = useMemo<Record<TimecodeFormat, string>>(() => ({
frameCount: t('Frame counts'),
timecodeWithDecimalFraction: t('Millisecond fractions'),
timecodeWithFramesFraction: t('Frame fractions'),
}), [t]);
const onTimecodeFormatClick = useCallback(() => {
const keys = Object.keys(timecodeFormatOptions);
const keys = Object.keys(timecodeFormatOptions) as TimecodeFormat[];
let index = keys.indexOf(timecodeFormat);
if (index === -1 || index >= keys.length - 1) index = 0;
else index += 1;
setTimecodeFormat(keys[index]);
const newKey = keys[index];
invariant(newKey != null);
setTimecodeFormat(newKey);
}, [setTimecodeFormat, timecodeFormat, timecodeFormatOptions]);
const changeCustomFfPath = useCallback(async () => {
@ -89,9 +92,9 @@ const Settings = memo(({
<div>{t('Hover mouse over buttons in the main interface to see which function they have')}</div>
</div>
<table className={styles.settings}>
<table className={styles['settings']}>
<thead>
<tr className={styles.header}>
<tr className={styles['header']}>
<th>{t('Settings')}</th>
<th style={{ width: 300 }}>{t('Current setting')}</th>
</tr>
@ -209,7 +212,7 @@ const Settings = memo(({
<Row>
<KeyCell>{t('Set file modification date/time of output files to:')}</KeyCell>
<td>
<Select value={treatOutputFileModifiedTimeAsStart ?? 'disabled'} onChange={(e) => setTreatOutputFileModifiedTimeAsStart(e.target.value === 'disabled' ? null : (e.target.value === 'true'))}>
<Select value={treatOutputFileModifiedTimeAsStart ? String(treatOutputFileModifiedTimeAsStart) : 'disabled'} onChange={(e) => setTreatOutputFileModifiedTimeAsStart(e.target.value === 'disabled' ? null : (e.target.value === 'true'))}>
<option value="disabled">{t('Current time')}</option>
<option value="true">{t('Source file\'s time plus segment start cut time')}</option>
<option value="false">{t('Source file\'s time minus segment end cut time')}</option>
@ -222,7 +225,7 @@ const Settings = memo(({
<Row>
<KeyCell>{t('Treat source file modification date/time as:')}</KeyCell>
<td>
<Select disabled={treatOutputFileModifiedTimeAsStart == null} value={treatInputFileModifiedTimeAsStart} onChange={(e) => setTreatInputFileModifiedTimeAsStart((e.target.value === 'true'))}>
<Select disabled={treatOutputFileModifiedTimeAsStart == null} value={String(treatInputFileModifiedTimeAsStart)} onChange={(e) => setTreatInputFileModifiedTimeAsStart((e.target.value === 'true'))}>
<option value="true">{t('Start of video')}</option>
<option value="false">{t('End of video')}</option>
</Select>
@ -300,7 +303,7 @@ const Settings = memo(({
<Row>
<KeyCell>{t('Snapshot capture quality')}</KeyCell>
<td>
<input type="range" min={1} max={1000} style={{ width: 200 }} value={Math.round(captureFrameQuality * 1000)} onChange={(e) => setCaptureFrameQuality(Math.max(Math.min(1, parseInt(e.target.value, 10) / 1000)), 0)} /><br />
<input type="range" min={1} max={1000} style={{ width: 200 }} value={Math.round(captureFrameQuality * 1000)} onChange={(e) => setCaptureFrameQuality(Math.max(Math.min(1, parseInt(e.target.value, 10) / 1000), 0))} /><br />
{Math.round(captureFrameQuality * 100)}%
</td>
</Row>
@ -359,7 +362,7 @@ const Settings = memo(({
<Row>
<KeyCell>{t('Invert timeline trackpad/wheel direction?')}</KeyCell>
<td>
<Switch checked={invertTimelineScroll} onCheckedChange={setInvertTimelineScroll} />
<Switch checked={invertTimelineScroll ?? false} onCheckedChange={setInvertTimelineScroll} />
</td>
</Row>

Wyświetl plik

@ -17,7 +17,7 @@ const Sheet = memo(({ visible, onClosePress, children, maxWidth = 800, style }:
initial={{ scale: 0, opacity: 0.5 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
className={styles.sheet}
className={styles['sheet']}
>
<div style={{ margin: 'auto', maxWidth, height: '100%', position: 'relative' }}>
<div style={{ overflowY: 'scroll', height: '100%', ...style }}>

Wyświetl plik

@ -6,8 +6,8 @@ import classes from './Switch.module.css';
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 disabled={disabled} className={classes['SwitchRoot']} checked={checked} onCheckedChange={onCheckedChange} style={style} title={title}>
<RadixSwitch.Thumb className={classes['SwitchThumb']} />
</RadixSwitch.Root>
);

Wyświetl plik

@ -1,6 +0,0 @@
import React, { useContext } from 'react';
export const UserSettingsContext = React.createContext();
export const SegColorsContext = React.createContext();
export const useSegColors = () => useContext(SegColorsContext);

32
src/contexts.ts 100644
Wyświetl plik

@ -0,0 +1,32 @@
import React, { useContext } from 'react';
import Color from 'color';
import useUserSettingsRoot from './hooks/useUserSettingsRoot';
import { SegmentColorIndex } from './types';
type UserSettingsContextType = ReturnType<typeof useUserSettingsRoot> & {
toggleCaptureFormat: () => void,
changeOutDir: () => Promise<void>,
toggleKeyframeCut: (showMessage?: boolean) => void,
togglePreserveMovData: () => void,
toggleMovFastStart: () => void,
toggleExportConfirmEnabled: () => void,
toggleSegmentsToChapters: () => void,
togglePreserveMetadataOnMerge: () => void,
toggleSimpleMode: () => void,
toggleSafeOutputFileName: () => void,
effectiveExportMode: string,
}
interface SegColorsContextType {
getSegColor: (seg: SegmentColorIndex) => Color
}
export const UserSettingsContext = React.createContext<UserSettingsContextType | undefined>(undefined);
export const SegColorsContext = React.createContext<SegColorsContextType | undefined>(undefined);
export const useSegColors = () => {
const context = useContext(SegColorsContext);
if (context == null) throw new Error('SegColorsContext nullish');
return context;
};

Wyświetl plik

@ -70,7 +70,7 @@ export async function askForYouTubeInput() {
return parseYouTube(value);
}
export async function askForInputDir(defaultPath) {
export async function askForInputDir(defaultPath?: string | undefined) {
const { filePaths } = await showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
defaultPath,
@ -81,7 +81,7 @@ export async function askForInputDir(defaultPath) {
return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined;
}
export async function askForOutDir(defaultPath) {
export async function askForOutDir(defaultPath?: string | undefined) {
const { filePaths } = await showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
defaultPath,
@ -92,7 +92,7 @@ export async function askForOutDir(defaultPath) {
return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined;
}
export async function askForFfPath(defaultPath) {
export async function askForFfPath(defaultPath?: string | undefined) {
const { filePaths } = await showOpenDialog({
properties: ['openDirectory'],
defaultPath,

Wyświetl plik

@ -207,7 +207,7 @@ export async function tryMapChaptersToEdl(chapters) {
}
}
export async function createChaptersFromSegments({ segmentPaths, chapterNames }) {
export async function createChaptersFromSegments({ segmentPaths, chapterNames }: { segmentPaths: string[], chapterNames?: string[] }) {
if (!chapterNames) return undefined;
try {
const durations = await pMap(segmentPaths, (segmentPath) => getDuration(segmentPath), { concurrency: 3 });
@ -640,7 +640,7 @@ export async function runFfmpegStartupCheck() {
// https://superuser.com/questions/543589/information-about-ffmpeg-command-line-options
export const getExperimentalArgs = (ffmpegExperimental: boolean) => (ffmpegExperimental ? ['-strict', 'experimental'] : []);
export const getVideoTimescaleArgs = (videoTimebase: number) => (videoTimebase != null ? ['-video_track_timescale', String(videoTimebase)] : []);
export const getVideoTimescaleArgs = (videoTimebase: number | undefined) => (videoTimebase != null ? ['-video_track_timescale', String(videoTimebase)] : []);
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
export async function cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: {

1
src/global.d.ts vendored
Wyświetl plik

@ -1 +0,0 @@
declare module '*.module.css';

Wyświetl plik

@ -1,12 +1,13 @@
import { RefObject, useEffect } from 'react';
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
import useNativeMenu from './useNativeMenu';
import { ContextMenuTemplate } from '../types';
// https://github.com/transflow/use-electron-context-menu
export default function useContextMenu(
ref: RefObject<HTMLElement>,
template: (MenuItemConstructorOptions | MenuItem)[],
template: ContextMenuTemplate,
) {
const { openMenu, closeMenu } = useNativeMenu(template);

Wyświetl plik

@ -2,12 +2,14 @@ import { useCallback } from 'react';
import flatMap from 'lodash/flatMap';
import sum from 'lodash/sum';
import pMap from 'p-map';
import invariant from 'tiny-invariant';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration } from '../util';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat } from '../ffmpeg';
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { getSmartCutParams } from '../smartcut';
import { isDurationValid } from '../segments';
import { FfprobeStream } from '../types';
const { join, resolve, dirname } = window.require('path');
const { pathExists } = window.require('fs-extra');
@ -65,7 +67,9 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const getOutputPlaybackRateArgs = useCallback(() => (outputPlaybackRate !== 1 ? ['-itsscale', 1 / outputPlaybackRate] : []), [outputPlaybackRate]);
const concatFiles = useCallback(async ({ paths, outDir, outPath, metadataFromPath, includeAllStreams, streams, outFormat, ffmpegExperimental, onProgress = () => undefined, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, appendFfmpegCommandLog }) => {
const concatFiles = useCallback(async ({ paths, outDir, outPath, metadataFromPath, includeAllStreams, streams, outFormat, ffmpegExperimental, onProgress = () => undefined, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, appendFfmpegCommandLog }: {
paths: string[], outDir: string | undefined, outPath: string, metadataFromPath: string, includeAllStreams: boolean, streams: FfprobeStream, outFormat: string, ffmpegExperimental: boolean, onProgress?: (a: number) => void, preserveMovData: boolean, movFastStart: boolean, chapters: { start: number, end: number, name: string | undefined }[] | undefined, preserveMetadataOnMerge: boolean, videoTimebase?: number | undefined, appendFfmpegCommandLog: (a: string) => void,
}) => {
if (await shouldSkipExistingFile(outPath)) return { haveExcludedStreams: false };
console.log('Merging files', { paths }, 'to', outPath);
@ -76,6 +80,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
let chaptersPath;
if (chapters) {
const chaptersWithNames = chapters.map((chapter, i) => ({ ...chapter, name: chapter.name || `Chapter ${i + 1}` }));
invariant(outDir != null);
chaptersPath = await writeChaptersFfmetadata(outDir, chaptersWithNames);
}
@ -356,6 +361,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
if (!needSmartCut) {
const outPath = await makeSegmentOutPath();
// @ts-expect-error todo
await losslessCutSingle({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath, copyFileStreams, keyframeCut, avoidNegativeTs, videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
});
@ -409,6 +415,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}
// for smart cut we need to use keyframe cut here, and no avoid_negative_ts
// @ts-expect-error todo
await losslessCutSingle({
cutFrom: losslessCutFrom, cutTo, chaptersPath, outPath: losslessPartOutPath, copyFileStreams: copyFileStreamsFiltered, keyframeCut: true, avoidNegativeTs: false, videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, onProgress: onCutProgress,
});
@ -466,6 +473,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) => {
const outPath = getHtml5ifiedPath(customOutDir, filePathArg, speed);
await ffmpegHtml5ify({ filePath: filePathArg, outPath, speed, hasAudio, hasVideo, onProgress });
invariant(outPath != null);
await transferTimestamps({ inPath: filePathArg, outPath, treatOutputFileModifiedTimeAsStart });
return outPath;
}, [treatOutputFileModifiedTimeAsStart]);
@ -494,9 +502,10 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}, [treatOutputFileModifiedTimeAsStart]);
// https://stackoverflow.com/questions/34118013/how-to-determine-webm-duration-using-ffprobe
const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir, duration, onProgress }) => {
const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir, duration, onProgress }: { fileFormat: string, customOutDir?: string | undefined, duration: number | undefined, onProgress }) => {
const ext = getOutFileExtension({ outFormat: fileFormat, filePath });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `reformatted${ext}` });
invariant(outPath != null);
const ffmpegArgs = [
'-hide_banner',

Wyświetl plik

@ -7,12 +7,14 @@ import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName,
import { getNumDigits } from '../segments';
import { captureFrame as ffmpegCaptureFrame, captureFrames as ffmpegCaptureFrames } from '../ffmpeg';
import { FormatTimecode } from '../types';
import { CaptureFormat } from '../../types';
const mime = window.require('mime-types');
const { rename, readdir, writeFile }: typeof FsPromises = window.require('fs/promises');
function getFrameFromVideo(video, format, quality) {
function getFrameFromVideo(video: HTMLVideoElement, format: CaptureFormat, quality: number) {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
@ -24,7 +26,7 @@ function getFrameFromVideo(video, format, quality) {
return dataUriToBuffer(dataUri);
}
export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }: { formatTimecode: FormatTimecode, treatOutputFileModifiedTimeAsStart?: boolean | undefined | null }) => {
const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }: {
customOutDir, filePath: string, fps: number, fromTime: number, toTime: number, estimatedMaxNumFiles: number, captureFormat: string, quality: number, filter?: string | undefined, onProgress: (a: number) => void, outputTimestamps: boolean
}) => {
@ -74,7 +76,7 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
}, [formatTimecode]);
const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }: {
customOutDir?: string, filePath?: string, fromTime: number, captureFormat: string, quality: number,
customOutDir?: string | undefined, filePath: string, fromTime: number, captureFormat: CaptureFormat, quality: number,
}) => {
const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true });
const nameSuffix = `${time}.${captureFormat}`;
@ -86,7 +88,7 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }: {
customOutDir?: string, filePath?: string, currentTime: number, captureFormat: string, video: HTMLVideoElement, quality: number,
customOutDir?: string | undefined, filePath: string, currentTime: number, captureFormat: CaptureFormat, video: HTMLVideoElement, quality: number,
}) => {
const buf = getFrameFromVideo(video, captureFormat, quality);

Wyświetl plik

@ -4,12 +4,20 @@ import { useEffect, useRef } from 'react';
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
import Mousetrap from 'mousetrap';
import { KeyBinding, KeyboardAction } from '../../types';
// for all dialog actions (e.g. detectSceneChanges) we must use keyup, or we risk having the button press inserted into the dialog's input element right after the dialog opens
// todo use keyup for most events?
const keyupActions = new Set(['seekBackwards', 'seekForwards', 'detectBlackScenes', 'detectSilentScenes', 'detectSceneChanges']);
const keyupActions = new Set<KeyboardAction>(['seekBackwards', 'seekForwards', 'detectBlackScenes', 'detectSilentScenes', 'detectSceneChanges']);
export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
const onKeyPressRef = useRef();
interface StoredAction { action: KeyboardAction, keyup?: boolean }
export default ({ keyBindings, onKeyPress: onKeyPressProp }: {
keyBindings: KeyBinding[],
onKeyPress: ((a: { action: KeyboardAction, keyup?: boolean | undefined }) => boolean) | ((a: { action: KeyboardAction, keyup?: boolean | undefined }) => void),
}) => {
const onKeyPressRef = useRef<(a: StoredAction) => void>();
// optimization to prevent re-binding all the time:
useEffect(() => {
@ -19,8 +27,8 @@ export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
useEffect(() => {
const mousetrap = new Mousetrap();
function onKeyPress(...args) {
if (onKeyPressRef.current) return onKeyPressRef.current(...args);
function onKeyPress(params: StoredAction) {
if (onKeyPressRef.current) return onKeyPressRef.current(params);
return true;
}

Wyświetl plik

@ -10,25 +10,25 @@ import { handleError, shuffleArray } from '../util';
import { errorToast } from '../swal';
import { showParametersDialog } from '../dialogs/parameters';
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2 } from '../segments';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
import * as ffmpegParameters from '../ffmpeg-parameters';
import { maxSegmentsAllowed } from '../util/constants';
import { ApparentSegmentBase, SegmentBase, StateSegment } from '../types';
import { SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
const remote = window.require('@electron/remote');
const { blackDetect, silenceDetect } = remote.require('./ffmpeg');
export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: {
function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: {
filePath?: string | undefined, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number | undefined, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean,
}) => {
}) {
// Segment related state
const segCounterRef = useRef(0);
const createIndexedSegment = useCallback(({ segment, incrementCount } = {}) => {
const createIndexedSegment = useCallback(({ segment, incrementCount }: { segment?: Parameters<typeof createSegment>[0], incrementCount?: boolean } = {}) => {
if (incrementCount) segCounterRef.current += 1;
const ret = createSegment({ segColorIndex: segCounterRef.current, ...segment });
const ret = addSegmentColorIndex(createSegment(segment), segCounterRef.current);
return ret;
}, []);
@ -40,7 +40,7 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
);
const [currentSegIndex, setCurrentSegIndex] = useState(0);
const [deselectedSegmentIds, setDeselectedSegmentIds] = useState({});
const [deselectedSegmentIds, setDeselectedSegmentIds] = useState<Record<string, boolean>>({});
const isSegmentSelected = useCallback(({ segId }: { segId: string }) => !deselectedSegmentIds[segId], [deselectedSegmentIds]);
@ -174,7 +174,15 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
const inverseCutSegments = useMemo(() => {
if (haveInvalidSegs || !isDurationValid(duration)) return [];
return invertSegments(sortSegments(apparentCutSegments), true, true, duration) as (ApparentSegmentBase & { segId?: string })[]; // todo i don't know how to improve these types
return invertSegments(sortSegments(apparentCutSegments), true, true, duration).map(({ segId, start, end }) => {
// this is workaround to please TS
if (segId == null || start == null || end == null) throw new Error(`Encountered inverted segment with nullish value ${JSON.stringify({ segId, start, end })}`);
return {
segId,
start,
end,
};
});
}, [apparentCutSegments, duration, haveInvalidSegs]);
const invertAllSegments = useCallback(() => {
@ -183,7 +191,7 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
return;
}
// don't reset segColorIndex (which represent colors) when inverting
const newInverseCutSegments = inverseCutSegments.map((inverseSegment, index) => createSegment({ ...inverseSegment, segColorIndex: index }));
const newInverseCutSegments = inverseCutSegments.map((inverseSegment, index) => addSegmentColorIndex(createSegment(inverseSegment), index));
setCutSegments(newInverseCutSegments);
}, [inverseCutSegments, setCutSegments]);
@ -204,10 +212,12 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
setCutSegments((existingSegments) => combineSelectedSegments2(existingSegments, getSegApparentEnd2, isSegmentSelected));
}, [isSegmentSelected, setCutSegments]);
const updateSegAtIndex = useCallback((index, newProps) => {
const updateSegAtIndex = useCallback<UpdateSegAtIndex>((index, newProps) => {
if (index < 0) return;
const cutSegmentsNew = [...cutSegments];
cutSegmentsNew.splice(index, 1, { ...cutSegments[index], ...newProps });
const existing = cutSegments[index];
if (existing == null) throw new Error();
cutSegmentsNew.splice(index, 1, { ...existing, ...newProps });
setCutSegments(cutSegmentsNew);
}, [setCutSegments, cutSegments]);
@ -479,7 +489,7 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
const selectedSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegments), [inverseCutSegments, invertCutSegments, selectedSegments]);
const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]);
const segmentsToExport = useMemo(() => {
const segmentsToExport = useMemo<SegmentToExport[]>(() => {
// segmentsToChaptersOnly is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595
// Chapters export mode: Emulate a single segment with no cuts (full timeline)
if (segmentsToChaptersOnly) return [{ start: 0, end: getSegApparentEnd({}) }];
@ -558,4 +568,8 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
setCutTime,
updateSegAtIndex,
};
};
}
export type UseSegments = ReturnType<typeof useSegments>;
export default useSegments;

Wyświetl plik

@ -1,5 +0,0 @@
import { useContext } from 'react';
import { UserSettingsContext } from '../contexts';
export default () => useContext(UserSettingsContext);

Wyświetl plik

@ -0,0 +1,10 @@
import { useContext } from 'react';
import { UserSettingsContext } from '../contexts';
export default () => {
const context = useContext(UserSettingsContext);
if (context == null) throw new Error('UserSettingsContext nullish');
return context;
};

Wyświetl plik

@ -1,18 +1,21 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import i18n from 'i18next';
import { StoreGetConfig, StoreResetConfig, StoreSetConfig, Config } from '../../types';
import { errorToast } from '../swal';
import isDev from '../isDev';
const remote = window.require('@electron/remote');
const configStore = remote.require('./configStore');
const configStore: { get: StoreGetConfig, set: StoreSetConfig, reset: StoreResetConfig } = remote.require('./configStore');
export default () => {
const firstUpdateRef = useRef(true);
function safeSetConfig(keyValue: Record<string, string>) {
const [key, value] = Object.entries(keyValue)[0]!;
function safeSetConfig<T extends keyof Config>(keyValue: Record<T, Config[T]>) {
const entry = Object.entries(keyValue)[0]!;
const key = entry[0] as T;
const value = entry[1] as Config[T];
// Prevent flood-saving all config during mount
if (firstUpdateRef.current) return;
@ -26,18 +29,19 @@ export default () => {
}
}
function safeGetConfig(key: string) {
function safeGetConfig<T extends keyof Config>(key: T) {
const rawVal = configStore.get(key);
if (rawVal === undefined) return undefined;
if (rawVal === undefined) return undefined as typeof rawVal;
// NOTE: Need to clone any non-primitive in renderer, or it will become very slow
// I think because Electron is proxying objects over the bridge
return JSON.parse(JSON.stringify(rawVal));
const cloned: typeof rawVal = JSON.parse(JSON.stringify(rawVal));
return cloned;
}
// From https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
// If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render
// Without this there was a huge performance issue https://github.com/mifi/lossless-cut/issues/1097
const safeGetConfigInitial = (key: string) => () => safeGetConfig(key);
const safeGetConfigInitial = <T extends keyof Config>(key: T) => () => safeGetConfig(key);
const [captureFormat, setCaptureFormat] = useState(safeGetConfigInitial('captureFormat'));
useEffect(() => safeSetConfig({ captureFormat }), [captureFormat]);

Wyświetl plik

@ -44,7 +44,7 @@ const { app } = window.require('@electron/remote');
console.log('Version', app.getVersion());
const container = document.getElementById('root');
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<StrictMode>

Wyświetl plik

@ -2,17 +2,16 @@ import { nanoid } from 'nanoid';
import sortBy from 'lodash/sortBy';
import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import { ApparentSegmentBase, PlaybackMode, SegmentBase, SegmentTags } from './types';
import { ApparentSegmentBase, PlaybackMode, SegmentBase, SegmentTags, StateSegment } from './types';
export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0;
export const createSegment = (props?: { start?: number, end?: number, name?: string, tags?: unknown, segColorIndex?: number }) => ({
export const createSegment = (props?: { start?: number | undefined, end?: number | undefined, name?: string | undefined, tags?: unknown | undefined }): Omit<StateSegment, 'segColorIndex'> => ({
start: props?.start,
end: props?.end,
name: props?.name || '',
segId: nanoid(),
segColorIndex: props?.segColorIndex,
// `tags` is an optional object (key-value). Values must always be string
// See https://github.com/mifi/lossless-cut/issues/879
@ -21,6 +20,11 @@ export const createSegment = (props?: { start?: number, end?: number, name?: str
: undefined,
});
export const addSegmentColorIndex = (segment: Omit<StateSegment, 'segColorIndex'>, segColorIndex: number): StateSegment => ({
...segment,
segColorIndex,
});
// Because segments could have undefined start / end
// (meaning extend to start of timeline or end duration)
export function getSegApparentStart(seg: SegmentBase) {
@ -35,7 +39,7 @@ export function getSegApparentEnd(seg: SegmentBase, duration?: number) {
return 0; // Haven't gotten duration yet - what do to ¯\_(ツ)_/¯
}
export const getCleanCutSegments = (cs) => cs.map((seg) => ({
export const getCleanCutSegments = (cs: Pick<StateSegment, 'start' | 'end' | 'name' | 'tags'>[]) => cs.map((seg) => ({
start: seg.start,
end: seg.end,
name: seg.name,

Wyświetl plik

@ -1,9 +1,16 @@
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
export interface SegmentBase {
start?: number | undefined,
end?: number | undefined,
}
export interface ApparentSegmentBase {
export interface SegmentColorIndex {
segColorIndex: number,
}
export interface ApparentSegmentBase extends SegmentColorIndex {
start: number,
end: number,
}
@ -11,10 +18,11 @@ export interface ApparentSegmentBase {
export type SegmentTags = Record<string, unknown>;
export interface StateSegment extends SegmentBase {
export type EditingSegmentTags = Record<string, SegmentTags>
export interface StateSegment extends SegmentBase, SegmentColorIndex {
name: string;
segId: string;
segColorIndex?: number | undefined;
tags?: SegmentTags | undefined;
}
@ -23,16 +31,26 @@ export interface Segment extends SegmentBase {
}
export interface ApparentCutSegment extends ApparentSegmentBase {
segId?: string | undefined,
name: string;
segId: string,
tags?: SegmentTags | undefined;
}
export interface SegmentToExport extends ApparentSegmentBase {
export interface SegmentToExport {
start: number,
end: number,
name?: string | undefined;
segId?: string | undefined;
tags?: SegmentTags | undefined;
}
export interface InverseCutSegment {
start: number,
end: number,
segId: string;
}
export type PlaybackMode = 'loop-segment-start-end' | 'loop-segment' | 'play-segment-once' | 'loop-selected-segments';
export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest';
@ -65,3 +83,11 @@ export type FfprobeStream = any;
export type FfprobeFormat = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type FfprobeChapter = any;
export type FormatTimecode = (a: { seconds: number, shorten?: boolean | undefined, fileNameFriendly?: boolean | undefined }) => string;
export type GetFrameCount = (sec: number) => number | undefined;
export type UpdateSegAtIndex = (index: number, newProps: Partial<StateSegment>) => void;
export type ContextMenuTemplate = (MenuItemConstructorOptions | MenuItem)[];

Wyświetl plik

@ -73,7 +73,7 @@ export async function havePermissionToReadFile(filePath: string) {
return true;
}
export async function checkDirWriteAccess(dirPath) {
export async function checkDirWriteAccess(dirPath: string) {
try {
await fsExtra.access(dirPath, fsExtra.constants.W_OK);
} catch (err) {
@ -86,7 +86,7 @@ export async function checkDirWriteAccess(dirPath) {
return true;
}
export async function pathExists(pathIn) {
export async function pathExists(pathIn: string) {
return fsExtra.pathExists(pathIn);
}
@ -99,7 +99,7 @@ export async function getPathReadAccessError(pathIn: string) {
}
}
export async function dirExists(dirPath) {
export async function dirExists(dirPath: string) {
return (await pathExists(dirPath)) && (await lstat(dirPath)).isDirectory();
}
@ -107,7 +107,7 @@ export async function dirExists(dirPath) {
const testFailFsOperation = false;
// Retry because sometimes write operations fail on windows due to the file being locked for various reasons (often anti-virus) #272 #1797 #1704
export async function fsOperationWithRetry(operation, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }: Options & { retries?: number | undefined, minTimeout?: number | undefined, maxTimeout?: number | undefined } = {}) {
export async function fsOperationWithRetry(operation: () => Promise<unknown>, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }: Options & { retries?: number | undefined, minTimeout?: number | undefined, maxTimeout?: number | undefined } = {}) {
return pRetry(async () => {
if (testFailFsOperation && Math.random() > 0.3) throw Object.assign(new Error('test delete failure'), { code: 'EPERM' });
await operation();
@ -130,11 +130,13 @@ export const utimesWithRetry = async (path: string, atime: number, mtime: number
export const getFrameDuration = (fps?: number) => 1 / (fps ?? 30);
export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo = 0, duration = 0, treatInputFileModifiedTimeAsStart = true, treatOutputFileModifiedTimeAsStart }) {
export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo = 0, duration = 0, treatInputFileModifiedTimeAsStart = true, treatOutputFileModifiedTimeAsStart }: {
inPath: string, outPath: string, cutFrom?: number | undefined, cutTo?: number | undefined, duration?: number | undefined, treatInputFileModifiedTimeAsStart?: boolean, treatOutputFileModifiedTimeAsStart: boolean | null | undefined
}) {
if (treatOutputFileModifiedTimeAsStart == null) return; // null means disabled;
// see https://github.com/mifi/lossless-cut/issues/1017#issuecomment-1049097115
function calculateTime(fileTime) {
function calculateTime(fileTime: number) {
if (treatInputFileModifiedTimeAsStart && treatOutputFileModifiedTimeAsStart) {
return fileTime + cutFrom;
}

Wyświetl plik

@ -1,15 +1,20 @@
import color from 'color';
import invariant from 'tiny-invariant';
import { SegmentColorIndex } from '../types';
// http://phrogz.net/css/distinct-colors.html
const colorStrings = '#ff5100, #ffc569, #ddffd1, #00ccff, #e9d1ff, #ff0084, #ff6975, #ffe6d1, #ffff69, #69ff96, #008cff, #ae00ff, #ff002b, #ff8c00, #8cff00, #69ffff, #0044ff, #ff00d4, #ffd1d9'.split(',').map((str) => str.trim());
const colors = colorStrings.map((str) => color(str));
function getColor(n) {
return colors[n % colors.length];
function getColor(n: number) {
const ret = colors[n % colors.length];
invariant(ret != null);
return ret;
}
// eslint-disable-next-line import/prefer-default-export
export function getSegColor(seg) {
export function getSegColor(seg: SegmentColorIndex) {
if (!seg) {
return color({
h: 0,

Wyświetl plik

@ -1,37 +0,0 @@
// anything more than this will probably cause the UI to become unusably slow
export const maxSegmentsAllowed = 2000;
export const ffmpegExtractWindow = 60;
export const zoomMax = 2 ** 14;
export const rightBarWidth = 200;
export const leftBarWidth = 240;
// https://www.electronjs.org/docs/api/locales
// See i18n.js
export const langNames = {
en: 'English',
cs: 'Čeština',
de: 'Deutsch',
es: 'Español',
fr: 'Français',
it: 'Italiano',
nl: 'Nederlands',
nb: 'Norsk (bokmål)',
nn: 'Norsk (nynorsk)',
pl: 'Polski',
pt: 'Português',
pt_BR: 'Português do Brasil',
sl: 'Slovenščina',
fi: 'Suomi',
ru: 'Русский',
// sr: 'Cрпски',
tr: 'Türkçe',
vi: 'Tiếng Việt',
ja: '日本語',
zh: '中文',
zh_Hant: '繁體中文',
zh_Hans: '简体中文',
ko: '한국어',
};

Wyświetl plik

@ -0,0 +1,9 @@
// anything more than this will probably cause the UI to become unusably slow
export const maxSegmentsAllowed = 2000;
export const ffmpegExtractWindow = 60;
export const zoomMax = 2 ** 14;
export const rightBarWidth = 200;
export const leftBarWidth = 240;

Wyświetl plik

@ -5,7 +5,7 @@ import { PlatformPath } from 'path';
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
import isDev from '../isDev';
import { getSegmentTags, formatSegNum } from '../segments';
import { SegmentToExport } from '../types';
import { FormatTimecode, SegmentToExport } from '../types';
export const segNumVariable = 'SEG_NUM';
@ -118,7 +118,7 @@ function interpolateSegmentFileName({ template, epochMs, inputFileNameWithoutExt
}
export function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }: {
segments: SegmentToExport[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat: string, filePath: string, outputDir: string, safeOutputFileName: boolean, maxLabelLength: number, outputFileNameMinZeroPadding: number,
segments: SegmentToExport[], template: string, formatTimecode: FormatTimecode, isCustomFormatSelected: boolean, fileFormat: string, filePath: string, outputDir: string, safeOutputFileName: boolean, maxLabelLength: number, outputFileNameMinZeroPadding: number,
}) {
function generate({ template, forceSafeOutputFileName }: { template: string, forceSafeOutputFileName: boolean }) {
const epochMs = Date.now();

Wyświetl plik

@ -1,6 +1,7 @@
{
"references": [
{ "path": "./tsconfig.web.json" }
{ "path": "./tsconfig.web.json" },
{ "path": "./tsconfig.main.json" },
],
"files": [],
}

21
tsconfig.main.json 100644
Wyświetl plik

@ -0,0 +1,21 @@
{
"extends": ["@tsconfig/strictest"],
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"outDir": "ts-dist",
"tsBuildInfoFile": "ts-dist/tsconfig.tsbuildinfo",
"lib": ["es2023"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noImplicitAny": false, // todo
"checkJs": true, // todo
"allowJs": true, // todo
},
"include": [
"public/**/*", "types.ts",
],
}

Wyświetl plik

@ -2,13 +2,15 @@
"extends": ["@tsconfig/strictest", "@tsconfig/vite-react/tsconfig.json"],
"compilerOptions": {
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"plugins": [{ "name": "typescript-plugin-css-modules" }],
"noEmit": true,
"noImplicitAny": false, // todo
"checkJs": false, // todo
"allowJs": true, // todo
},
"references": [
{ "path": "./tsconfig.main.json" },
],
"include": [
"src/**/*",
],

102
types.ts 100644
Wyświetl plik

@ -0,0 +1,102 @@
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit';
export interface KeyBinding {
keys: string,
action: KeyboardAction,
}
export type CaptureFormat = 'jpeg' | 'png' | 'webp';
// https://www.electronjs.org/docs/api/locales
// See i18n.js
export const langNames = {
en: 'English',
cs: 'Čeština',
de: 'Deutsch',
es: 'Español',
fr: 'Français',
it: 'Italiano',
nl: 'Nederlands',
nb: 'Norsk (bokmål)',
nn: 'Norsk (nynorsk)',
pl: 'Polski',
pt: 'Português',
pt_BR: 'Português do Brasil',
sl: 'Slovenščina',
fi: 'Suomi',
ru: 'Русский',
// sr: 'Cрпски',
tr: 'Türkçe',
vi: 'Tiếng Việt',
ja: '日本語',
zh: '中文',
zh_Hant: '繁體中文',
zh_Hans: '简体中文',
ko: '한국어',
};
export type LanguageKey = keyof typeof langNames;
export type TimecodeFormat = 'timecodeWithDecimalFraction' | 'frameCount' | 'timecodeWithFramesFraction';
export interface Config {
captureFormat: CaptureFormat,
customOutDir: string | undefined,
keyframeCut: boolean,
autoMerge: boolean,
autoDeleteMergedSegments: boolean,
segmentsToChaptersOnly: boolean,
enableSmartCut: boolean,
timecodeFormat: TimecodeFormat,
invertCutSegments: boolean,
autoExportExtraStreams: boolean,
exportConfirmEnabled: boolean,
askBeforeClose: boolean,
enableAskForImportChapters: boolean,
enableAskForFileOpenAction: boolean,
playbackVolume: number,
autoSaveProjectFile: boolean,
wheelSensitivity: number,
language: LanguageKey | undefined,
ffmpegExperimental: boolean,
preserveMovData: boolean,
movFastStart: boolean,
avoidNegativeTs: 'make_zero' | 'auto' | 'make_non_negative' | 'disabled',
hideNotifications: 'all' | undefined,
autoLoadTimecode: boolean,
segmentsToChapters: boolean,
preserveMetadataOnMerge: boolean,
simpleMode: boolean,
outSegTemplate: string | undefined,
keyboardSeekAccFactor: number,
keyboardNormalSeekSpeed: number,
treatInputFileModifiedTimeAsStart: boolean,
treatOutputFileModifiedTimeAsStart: boolean | undefined | null,
outFormatLocked: string | undefined,
safeOutputFileName: boolean,
windowBounds: { x: number, y: number, width: number, height: number } | undefined,
enableAutoHtml5ify: boolean,
keyBindings: KeyBinding[],
customFfPath: string | undefined,
storeProjectInWorkingDir: boolean,
enableOverwriteOutput: boolean,
mouseWheelZoomModifierKey: string,
captureFrameMethod: 'videotag' | 'ffmpeg',
captureFrameQuality: number,
captureFrameFileNameFormat: 'timestamp' | 'index',
enableNativeHevc: boolean,
enableUpdateCheck: boolean,
cleanupChoices: {
trashTmpFiles: boolean, askForCleanup: boolean, closeFile: boolean, cleanupAfterExport?: boolean | undefined,
},
allowMultipleInstances: boolean,
darkMode: boolean,
preferStrongColors: boolean,
outputFileNameMinZeroPadding: number,
cutFromAdjustmentFrames: number,
invertTimelineScroll: boolean | undefined,
}
export type StoreGetConfig = <T extends keyof Config>(key: T) => Config[T];
export type StoreSetConfig = <T extends keyof Config>(key: T, value: Config[T]) => void;
export type StoreResetConfig = <T extends keyof Config>(key: T) => void;

526
yarn.lock
Wyświetl plik

@ -19,13 +19,6 @@ __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"
@ -1700,6 +1693,38 @@ __metadata:
languageName: node
linkType: hard
"@types/color-convert@npm:*":
version: 2.0.3
resolution: "@types/color-convert@npm:2.0.3"
dependencies:
"@types/color-name": "npm:*"
checksum: 39fe4036c0fb27e796f962eb19e4640aaa7f55cdb05cb8d4b21ef48d4a6f82fc281c06a6664ef1dfb11d9d6d2051a988a0ffa0b7a2f71b3a9418ebced12a9f38
languageName: node
linkType: hard
"@types/color-name@npm:*":
version: 1.1.3
resolution: "@types/color-name@npm:1.1.3"
checksum: 9060d16d0bce2cdf562d6da54e18c5f23e80308ccb58b725b9173a028818f27d8e01c8a5cd96952e76f11145a7388ed7d2f450fb4652f4760383834f2e698263
languageName: node
linkType: hard
"@types/color@npm:^3.0.6":
version: 3.0.6
resolution: "@types/color@npm:3.0.6"
dependencies:
"@types/color-convert": "npm:*"
checksum: 0f16dcf4e202896d5425019c44a1f99713fca998052ab2fd836cdd38048398a0caf5b79a2efe560e41fca4b9401e8d8f6f02ed541cfea30cca3a8cb1b50b80f7
languageName: node
linkType: hard
"@types/css-modules@npm:^1.0.5":
version: 1.0.5
resolution: "@types/css-modules@npm:1.0.5"
checksum: c03c110f5af29e2d37594467ceb79e944d61fd22fa84dd057e0f83803bf11e94f6f3d7153695b2baf9a79a99dc6521bda54920d8c06690ed26cf7e1001f88a5c
languageName: node
linkType: hard
"@types/debug@npm:^4.1.6":
version: 4.1.7
resolution: "@types/debug@npm:4.1.7"
@ -1842,24 +1867,6 @@ __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"
@ -1867,6 +1874,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:^18.2.22":
version: 18.2.22
resolution: "@types/react-dom@npm:18.2.22"
dependencies:
"@types/react": "npm:*"
checksum: 310da22244c1bb65a7f213f8727bda821dd211cfb2dd62d1f9b28dd50ef1c196d59e908494bd5f25c13a3844343f3a6135f39fb830aca6f79646fa56c1b56c08
languageName: node
linkType: hard
"@types/react-transition-group@npm:^4.4.0":
version: 4.4.4
resolution: "@types/react-transition-group@npm:4.4.4"
@ -1898,6 +1914,17 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:^18.2.66":
version: 18.2.66
resolution: "@types/react@npm:18.2.66"
dependencies:
"@types/prop-types": "npm:*"
"@types/scheduler": "npm:*"
csstype: "npm:^3.0.2"
checksum: 8a82bda6c254681536fa8348dc15d52345d8203d5d322406feef865f74ebfe2475ebde0be4e2f9a18ffbb587dac946dfb5d0974b598779ff282259aff7e8209a
languageName: node
linkType: hard
"@types/responselike@npm:^1.0.0":
version: 1.0.0
resolution: "@types/responselike@npm:1.0.0"
@ -2330,16 +2357,6 @@ __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"
@ -2747,13 +2764,6 @@ __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"
@ -2841,7 +2851,7 @@ __metadata:
languageName: node
linkType: hard
"braces@npm:^3.0.1, braces@npm:~3.0.2":
"braces@npm:^3.0.1":
version: 3.0.2
resolution: "braces@npm:3.0.2"
dependencies:
@ -3198,25 +3208,6 @@ __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"
@ -3628,15 +3619,6 @@ __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"
@ -3748,15 +3730,6 @@ __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"
@ -4168,13 +4141,6 @@ __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"
@ -4420,17 +4386,6 @@ __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-ex@npm:^1.3.1":
version: 1.3.2
resolution: "error-ex@npm:1.3.2"
@ -5975,7 +5930,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:
@ -6135,13 +6090,6 @@ __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"
@ -6521,7 +6469,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@ -6530,15 +6478,6 @@ __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"
@ -6560,15 +6499,6 @@ __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"
@ -6583,13 +6513,6 @@ __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"
@ -6774,15 +6697,6 @@ __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"
@ -6902,7 +6816,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.1":
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3":
version: 4.0.3
resolution: "is-glob@npm:4.0.3"
dependencies:
@ -7136,13 +7050,6 @@ __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"
@ -7503,41 +7410,6 @@ __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"
@ -7557,13 +7429,6 @@ __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"
@ -7616,13 +7481,6 @@ __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"
@ -7706,9 +7564,13 @@ __metadata:
"@radix-ui/react-switch": "npm:^1.0.1"
"@tsconfig/strictest": "npm:^2.0.2"
"@tsconfig/vite-react": "npm:^3.0.0"
"@types/color": "npm:^3.0.6"
"@types/css-modules": "npm:^1.0.5"
"@types/eslint": "npm:^8"
"@types/lodash": "npm:^4.14.202"
"@types/node": "npm:18"
"@types/react": "npm:^18.2.66"
"@types/react-dom": "npm:^18.2.22"
"@types/sortablejs": "npm:^1.15.0"
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"
"@typescript-eslint/parser": "npm:^6.12.0"
@ -7779,8 +7641,8 @@ __metadata:
string-to-stream: "npm:^1.1.1"
sweetalert2: "npm:^11.0.0"
sweetalert2-react-content: "npm:^5.0.7"
tiny-invariant: "npm:^1.3.3"
typescript: "npm:~5.2.0"
typescript-plugin-css-modules: "npm:^5.1.0"
use-debounce: "npm:^5.1.0"
use-trace-update: "npm:^1.3.0"
vite: "npm:^4.5.2"
@ -7883,16 +7745,6 @@ __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"
@ -8029,7 +7881,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:1.6.0, mime@npm:^1.4.1":
"mime@npm:1.6.0":
version: 1.6.0
resolution: "mime@npm:1.6.0"
bin:
@ -8363,18 +8215,6 @@ __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"
@ -8494,13 +8334,6 @@ __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"
@ -8906,13 +8739,6 @@ __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"
@ -9051,20 +8877,13 @@ __metadata:
languageName: node
linkType: hard
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3":
"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"
@ -9134,85 +8953,6 @@ __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.27":
version: 8.4.33
resolution: "postcss@npm:8.4.33"
@ -9224,6 +8964,17 @@ __metadata:
languageName: node
linkType: hard
"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
"prebuild-install@npm:^7.0.0":
version: 7.1.0
resolution: "prebuild-install@npm:7.1.0"
@ -9385,13 +9136,6 @@ __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"
@ -9745,15 +9489,6 @@ __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
"reflect.getprototypeof@npm:^1.0.4":
version: 1.0.5
resolution: "reflect.getprototypeof@npm:1.0.5"
@ -9882,13 +9617,6 @@ __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"
@ -10222,19 +9950,6 @@ __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"
@ -10242,13 +9957,6 @@ __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"
@ -10288,7 +9996,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0":
"semver@npm:2 || 3 || 4 || 5":
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
@ -10647,7 +10355,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2":
"source-map-js@npm:^1.0.2":
version: 1.0.2
resolution: "source-map-js@npm:1.0.2"
checksum: 38e2d2dd18d2e331522001fc51b54127ef4a5d473f53b1349c5cca2123562400e0986648b52e9407e348eaaed53bce49248b6e2641e6d793ca57cb2c360d6d51
@ -10671,20 +10379,13 @@ __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.0, source-map@npm:^0.6.1":
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"
@ -11050,21 +10751,6 @@ __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"
@ -11305,6 +10991,13 @@ __metadata:
languageName: node
linkType: hard
"tiny-invariant@npm:^1.3.3":
version: 1.3.3
resolution: "tiny-invariant@npm:1.3.3"
checksum: 5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe
languageName: node
linkType: hard
"tinybench@npm:^2.5.1":
version: 2.6.0
resolution: "tinybench@npm:2.6.0"
@ -11463,17 +11156,6 @@ __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"
@ -11488,13 +11170,6 @@ __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"
@ -11636,32 +11311,6 @@ __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"
@ -12412,13 +12061,6 @@ __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"