kopia lustrzana https://github.com/mifi/lossless-cut
improve types
rodzic
96baa9a931
commit
27df6c20e6
|
@ -1,2 +1,3 @@
|
|||
/dist
|
||||
/vite-dist
|
||||
/ts-dist
|
|
@ -18,10 +18,7 @@ module.exports = {
|
|||
browser: true,
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': ['error', {
|
||||
devDependencies: true,
|
||||
optionalDependencies: false,
|
||||
}],
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,3 +17,4 @@ node_modules
|
|||
/doc
|
||||
/ffmpeg
|
||||
/app.log
|
||||
/ts-dist
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)?.();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -442,5 +442,6 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
|
|||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
59
src/App.tsx
59
src/App.tsx
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 "invert segments" 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}
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }} />
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
|
||||
export const UserSettingsContext = React.createContext();
|
||||
export const SegColorsContext = React.createContext();
|
||||
|
||||
export const useSegColors = () => useContext(SegColorsContext);
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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 +0,0 @@
|
|||
declare module '*.module.css';
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { UserSettingsContext } from '../contexts';
|
||||
|
||||
export default () => useContext(UserSettingsContext);
|
|
@ -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;
|
||||
};
|
|
@ -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]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
36
src/types.ts
36
src/types.ts
|
@ -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)[];
|
||||
|
|
14
src/util.ts
14
src/util.ts
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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: '한국어',
|
||||
};
|
|
@ -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;
|
|
@ -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();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"references": [
|
||||
{ "path": "./tsconfig.web.json" }
|
||||
{ "path": "./tsconfig.web.json" },
|
||||
{ "path": "./tsconfig.main.json" },
|
||||
],
|
||||
"files": [],
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
}
|
|
@ -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/**/*",
|
||||
],
|
||||
|
|
|
@ -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
526
yarn.lock
|
@ -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"
|
||||
|
|
Ładowanie…
Reference in New Issue