1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

make sure that all actions are key bindable

adds the following actions:
- Convert to supported format
- Create segments from keyframes
- Detect black scenes
- Detect silent scenes
- Detect scene changes
- Edit tracks / metadata tags
- Set custom start offset/timecode
- Settings
- Open
- Start times as YouTube Chapters
- Report an error
This commit is contained in:
Mikael Finstad 2023-12-22 13:43:22 +08:00
parent 3206ba3868
commit ccb261bf42
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
4 changed files with 138 additions and 91 deletions

View File

@ -33,7 +33,7 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
{ {
label: esc(t('Close batch')), label: esc(t('Close batch')),
async click() { async click() {
mainWindow.webContents.send('closeBatchFiles'); mainWindow.webContents.send('closeBatch');
}, },
}, },
{ type: 'separator' }, { type: 'separator' },
@ -331,7 +331,7 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
{ {
label: esc(t('Merge/concatenate files')), label: esc(t('Merge/concatenate files')),
click() { click() {
mainWindow.webContents.send('concatCurrentBatch'); mainWindow.webContents.send('concatBatch');
}, },
}, },
{ {

View File

@ -67,7 +67,8 @@ import {
getOutPath, getSuffixedOutPath, handleError, getOutDir, getOutPath, getSuffixedOutPath, handleError, getOutDir,
isStoreBuild, dragPreventer, isStoreBuild, dragPreventer,
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile, havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, getImportProjectType,
calcShouldShowWaveform, calcShouldShowKeyframes,
} from './util'; } from './util';
import { toast, errorToast } from './swal'; import { toast, errorToast } from './swal';
import { formatDuration } from './util/duration'; import { formatDuration } from './util/duration';
@ -93,24 +94,12 @@ const remote = window.require('@electron/remote');
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron'); const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
const calcShouldShowWaveform = (zoomedDuration) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);
const calcShouldShowKeyframes = (zoomedDuration) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);
const videoStyle = { width: '100%', height: '100%', objectFit: 'contain' }; const videoStyle = { width: '100%', height: '100%', objectFit: 'contain' };
const bottomStyle = { background: controlsBackground, transition: darkModeTransition }; const bottomStyle = { background: controlsBackground, transition: darkModeTransition };
let lastOpenedPath;
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback(); const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
hevcPlaybackSupportedPromise.catch((err) => console.error(err)); hevcPlaybackSupportedPromise.catch((err) => console.error(err));
function getImportProjectType(filePath) {
if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt';
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'mplayer', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };
const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`));
if (!matchingExt) return undefined;
return edlFormatForExtension[matchingExt];
}
const App = memo(() => { const App = memo(() => {
// Per project state // Per project state
@ -154,6 +143,7 @@ const App = memo(() => {
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState(); const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
// State per application launch // State per application launch
const lastOpenedPathRef = useRef();
const [waveformMode, setWaveformMode] = useState(); const [waveformMode, setWaveformMode] = useState();
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false); const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
const [keyframesEnabled, setKeyframesEnabled] = useState(true); const [keyframesEnabled, setKeyframesEnabled] = useState(true);
@ -1793,7 +1783,7 @@ const App = memo(() => {
console.log('userOpenFiles'); console.log('userOpenFiles');
console.log(filePaths.join('\n')); console.log(filePaths.join('\n'));
[lastOpenedPath] = filePaths; [lastOpenedPathRef.current] = filePaths;
// https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure // https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure
if (filePaths.length === 1 && /^VIDEO_TS$/i.test(basename(filePaths[0]))) { if (filePaths.length === 1 && /^VIDEO_TS$/i.test(basename(filePaths[0]))) {
@ -1888,12 +1878,12 @@ const App = memo(() => {
}, [alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]); }, [alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]);
const openFilesDialog = useCallback(async () => { const openFilesDialog = useCallback(async () => {
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile', 'multiSelections'], defaultPath: lastOpenedPath }); const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile', 'multiSelections'], defaultPath: lastOpenedPathRef.current });
if (canceled) return; if (canceled) return;
userOpenFiles(filePaths); userOpenFiles(filePaths);
}, [userOpenFiles]); }, [userOpenFiles]);
const concatCurrentBatch = useCallback(() => { const concatBatch = useCallback(() => {
if (batchFiles.length < 2) { if (batchFiles.length < 2) {
openFilesDialog(); openFilesDialog();
return; return;
@ -1909,14 +1899,20 @@ const App = memo(() => {
electron.clipboard.writeText(await formatTsv(selectedSegments)); electron.clipboard.writeText(await formatTsv(selectedSegments));
}, [isFileOpened, selectedSegments]); }, [isFileOpened, selectedSegments]);
const getKeyboardAction = useCallback(({ action, keyup }) => { const mainActions = useMemo(() => {
async function exportEdlYouTube() {
if (!checkFileOpened()) return;
await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments));
}
function seekReset() { function seekReset() {
seekAccelerationRef.current = 1; seekAccelerationRef.current = 1;
} }
// NOTE: Do not change these keys because users have bound keys by these names return {
// For actions, see also KeyboardShortcuts.jsx // NOTE: Do not change these keys because users have bound keys by these names in their config files
const mainActions = { // For actions, see also KeyboardShortcuts.jsx
togglePlayNoResetSpeed: () => togglePlay(), togglePlayNoResetSpeed: () => togglePlay(),
togglePlayResetSpeed: () => togglePlay({ resetPlaybackRate: true }), togglePlayResetSpeed: () => togglePlay({ resetPlaybackRate: true }),
togglePlayOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, playbackMode: 'play-segment-once' }), togglePlayOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, playbackMode: 'play-segment-once' }),
@ -1938,7 +1934,7 @@ const App = memo(() => {
splitCurrentSegment, splitCurrentSegment,
increaseRotation, increaseRotation,
goToTimecode, goToTimecode,
seekBackwards() { seekBackwards({ keyup }) {
if (keyup) { if (keyup) {
seekReset(); seekReset();
return; return;
@ -1946,7 +1942,7 @@ const App = memo(() => {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current * -1); seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current * -1);
seekAccelerationRef.current *= keyboardSeekAccFactor; seekAccelerationRef.current *= keyboardSeekAccFactor;
}, },
seekForwards() { seekForwards({ keyup }) {
if (keyup) { if (keyup) {
seekReset(); seekReset();
return; return;
@ -1998,7 +1994,7 @@ const App = memo(() => {
extractAllStreams, extractAllStreams,
convertFormatCurrentFile: () => userHtml5ifyCurrentFile(), convertFormatCurrentFile: () => userHtml5ifyCurrentFile(),
convertFormatBatch, convertFormatBatch,
concatBatch: concatCurrentBatch, concatBatch,
toggleKeyframeCutMode: () => toggleKeyframeCut(true), toggleKeyframeCutMode: () => toggleKeyframeCut(true),
toggleCaptureFormat, toggleCaptureFormat,
toggleStripAudio, toggleStripAudio,
@ -2017,16 +2013,28 @@ const App = memo(() => {
reloadFile: () => setCacheBuster((v) => v + 1), reloadFile: () => setCacheBuster((v) => v + 1),
quit: () => quitApp(), quit: () => quitApp(),
closeCurrentFile: () => { closeFileWithConfirm(); }, closeCurrentFile: () => { closeFileWithConfirm(); },
exportEdlYouTube,
showStreamsSelector: handleShowStreamsSelectorClick,
askSetStartTimeOffset,
html5ify: () => userHtml5ifyCurrentFile({ ignoreRememberedValue: true }),
openFilesDialog,
toggleKeyboardShortcuts,
toggleSettings,
openSendReportDialog: () => { openSendReportDialogWithState(); },
detectBlackScenes,
detectSilentScenes,
detectSceneChanges,
createSegmentsFromKeyframes,
}; };
}, [addSegment, alignSegmentTimesToKeyframes, apparentCutSegments, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, checkFileOpened, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, duplicateCurrentSegment, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, handleShowStreamsSelectorClick, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, openFilesDialog, openSendReportDialogWithState, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleSettings, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
return mainActions[action]; const getKeyboardAction = useCallback((action) => mainActions[action], [mainActions]);
}, [addSegment, alignSegmentTimesToKeyframes, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, duplicateCurrentSegment, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
const onKeyPress = useCallback(({ action, keyup }) => { const onKeyPress = useCallback(({ action, keyup }) => {
function tryMainActions() { function tryMainActions() {
const fn = getKeyboardAction({ action, keyup }); const fn = getKeyboardAction(action);
if (!fn) return { match: false }; if (!fn) return { match: false };
const bubble = fn(); const bubble = fn({ keyup });
return { match: true, bubble }; return { match: true, bubble };
} }
@ -2146,7 +2154,7 @@ const App = memo(() => {
}, [fileUri, usingPreviewFile, filePath, setWorking, hasVideo, hasAudio, html5ifyAndLoadWithPreferences, customOutDir, showUnsupportedFileMessage]); }, [fileUri, usingPreviewFile, filePath, setWorking, hasVideo, hasAudio, html5ifyAndLoadWithPreferences, customOutDir, showUnsupportedFileMessage]);
useEffect(() => { useEffect(() => {
async function exportEdlFile2(e, type) { async function tryExportEdlFile(type) {
if (!checkFileOpened()) return; if (!checkFileOpened()) return;
try { try {
await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount }); await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount });
@ -2156,13 +2164,7 @@ const App = memo(() => {
} }
} }
async function exportEdlYouTube() { async function importEdlFile(type) {
if (!checkFileOpened()) return;
await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments));
}
async function importEdlFile(e, type) {
if (!checkFileOpened()) return; if (!checkFileOpened()) return;
try { try {
@ -2176,67 +2178,53 @@ const App = memo(() => {
async function tryApiKeyboardAction(event, { id, action }) { async function tryApiKeyboardAction(event, { id, action }) {
console.log('API keyboard action:', action); console.log('API keyboard action:', action);
try { try {
const fn = getKeyboardAction({ action }); const fn = getKeyboardAction(action);
if (!fn) throw new Error(`Action not found: ${action}`); if (!fn) throw new Error(`Action not found: ${action}`);
await fn(); await fn({ keyup: false });
} catch (err) {
handleError(err);
} finally { } finally {
// todo correlation ids // todo correlation ids
event.sender.send('apiKeyboardActionResponse', { id }); event.sender.send('apiKeyboardActionResponse', { id });
} }
} }
const actions = { const actionsWithArgs = {
openFiles: (event, filePaths) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); }, openFiles: (filePaths) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); },
apiKeyboardAction: tryApiKeyboardAction, // todo separate actions per type and move them into mainActions?
openFilesDialog,
closeCurrentFile: () => { closeFileWithConfirm(); },
closeBatchFiles: () => { closeBatch(); },
html5ify: () => userHtml5ifyCurrentFile({ ignoreRememberedValue: true }),
askSetStartTimeOffset,
extractAllStreams,
showStreamsSelector: handleShowStreamsSelectorClick,
importEdlFile, importEdlFile,
exportEdlFile: exportEdlFile2, exportEdlFile: tryExportEdlFile,
exportEdlYouTube,
toggleLastCommands,
toggleKeyboardShortcuts,
toggleSettings,
openSendReportDialog: () => { openSendReportDialogWithState(); },
clearSegments,
shuffleSegments,
createNumSegments,
createFixedDurationSegments,
createRandomSegments,
invertAllSegments,
fillSegmentsGaps,
combineOverlappingSegments,
combineSelectedSegments,
splitCurrentSegment,
fixInvalidDuration: tryFixInvalidDuration,
reorderSegsByStartTime,
concatCurrentBatch,
detectBlackScenes,
detectSilentScenes,
detectSceneChanges,
createSegmentsFromKeyframes,
shiftAllSegmentTimes,
alignSegmentTimesToKeyframes,
}; };
const actionsWithCatch = Object.entries(actions).map(([key, action]) => [ async function actionWithCatch(fn) {
key, try {
async (...args) => { await fn();
try { } catch (err) {
await action(...args); handleError(err);
} catch (err) { }
handleError(err); }
}
}, const actionsWithCatch = [
]); // actions with arguments:
...Object.entries(actionsWithArgs).map(([key, fn]) => [
key,
async (event, ...args) => actionWithCatch(() => fn(...args)),
]),
// all main actions (no arguments, except keyup which we don't support):
...Object.entries(mainActions).map(([key, fn]) => [
key,
async () => actionWithCatch(() => fn({ keyup: false })),
]),
];
actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.on(key, action)); actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.on(key, action));
return () => actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.removeListener(key, action)); electron.ipcRenderer.on('apiKeyboardAction', tryApiKeyboardAction);
}, [alignSegmentTimesToKeyframes, apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, customOutDir, cutSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, getKeyboardAction, handleShowStreamsSelectorClick, invertAllSegments, loadCutSegments, loadMedia, openFilesDialog, openSendReportDialogWithState, reorderSegsByStartTime, selectedSegments, setWorking, shiftAllSegmentTimes, shuffleSegments, splitCurrentSegment, toggleKeyboardShortcuts, toggleLastCommands, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]);
return () => {
actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.off(key, action));
electron.ipcRenderer.off('apiKeyboardAction', tryApiKeyboardAction);
};
}, [checkFileOpened, customOutDir, detectedFps, filePath, getFrameCount, getKeyboardAction, loadCutSegments, mainActions, selectedSegments, userOpenFiles]);
const showAddStreamSourceDialog = useCallback(async () => { const showAddStreamSourceDialog = useCallback(async () => {
try { try {
@ -2337,7 +2325,7 @@ const App = memo(() => {
onBatchFileSelect={onBatchFileSelect} onBatchFileSelect={onBatchFileSelect}
batchListRemoveFile={batchListRemoveFile} batchListRemoveFile={batchListRemoveFile}
closeBatch={closeBatch} closeBatch={closeBatch}
onMergeFilesClick={concatCurrentBatch} onMergeFilesClick={concatBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch} onBatchConvertToSupportedFormatClick={convertFormatBatch}
/> />
)} )}
@ -2571,7 +2559,7 @@ const App = memo(() => {
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} /> <ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> <KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} mainActions={mainActions} />
</div> </div>
</ThemeProvider> </ThemeProvider>
</UserSettingsContext.Provider> </UserSettingsContext.Provider>

View File

@ -107,7 +107,7 @@ const CreateBinding = memo(({
const rowStyle = { display: 'flex', alignItems: 'center', margin: '6px 0' }; const rowStyle = { display: 'flex', alignItems: 'center', margin: '6px 0' };
const KeyboardShortcuts = memo(({ const KeyboardShortcuts = memo(({
keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, mainActions,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -193,6 +193,10 @@ const KeyboardShortcuts = memo(({
name: t('Reload current media'), name: t('Reload current media'),
category: playbackCategory, category: playbackCategory,
}, },
html5ify: {
name: t('Convert to supported format'),
category: playbackCategory,
},
// selectivePlaybackCategory // selectivePlaybackCategory
togglePlayOnlyCurrentSegment: { togglePlayOnlyCurrentSegment: {
@ -327,6 +331,10 @@ const KeyboardShortcuts = memo(({
name: t('Align segment times to keyframes'), name: t('Align segment times to keyframes'),
category: segmentsAndCutpointsCategory, category: segmentsAndCutpointsCategory,
}, },
createSegmentsFromKeyframes: {
name: t('Create segments from keyframes'),
category: segmentsAndCutpointsCategory,
},
createFixedDurationSegments: { createFixedDurationSegments: {
name: t('Create fixed duration segments'), name: t('Create fixed duration segments'),
category: segmentsAndCutpointsCategory, category: segmentsAndCutpointsCategory,
@ -339,6 +347,18 @@ const KeyboardShortcuts = memo(({
name: t('Create random segments'), name: t('Create random segments'),
category: segmentsAndCutpointsCategory, category: segmentsAndCutpointsCategory,
}, },
detectBlackScenes: {
name: t('Detect black scenes'),
category: segmentsAndCutpointsCategory,
},
detectSilentScenes: {
name: t('Detect silent scenes'),
category: segmentsAndCutpointsCategory,
},
detectSceneChanges: {
name: t('Detect scene changes'),
category: segmentsAndCutpointsCategory,
},
shuffleSegments: { shuffleSegments: {
name: t('Shuffle segments order'), name: t('Shuffle segments order'),
category: segmentsAndCutpointsCategory, category: segmentsAndCutpointsCategory,
@ -392,6 +412,10 @@ const KeyboardShortcuts = memo(({
name: t('Extract all tracks'), name: t('Extract all tracks'),
category: streamsCategory, category: streamsCategory,
}, },
showStreamsSelector: {
name: t('Edit tracks / metadata tags'),
category: streamsCategory,
},
// zoomOperationsCategory // zoomOperationsCategory
timelineZoomIn: { timelineZoomIn: {
@ -500,6 +524,26 @@ const KeyboardShortcuts = memo(({
name: t('Copy selected segments times to clipboard'), name: t('Copy selected segments times to clipboard'),
category: otherCategory, category: otherCategory,
}, },
askSetStartTimeOffset: {
name: t('Set custom start offset/timecode'),
category: otherCategory,
},
toggleSettings: {
name: t('Settings'),
category: otherCategory,
},
openSendReportDialog: {
name: t('Report an error'),
category: otherCategory,
},
openFilesDialog: {
name: t('Open'),
category: otherCategory,
},
exportEdlYouTube: {
name: t('Start times as YouTube Chapters'),
category: otherCategory,
},
closeActiveScreen: { closeActiveScreen: {
name: t('Close current screen'), name: t('Close current screen'),
category: otherCategory, category: otherCategory,
@ -574,6 +618,9 @@ const KeyboardShortcuts = memo(({
}); });
}, [setKeyBindings]); }, [setKeyBindings]);
const missingAction = Object.keys(mainActions).find((key) => actionsMap[key] == null);
if (missingAction) throw new Error(`Action missing: ${missingAction}`);
return ( return (
<> <>
<div style={{ color: 'black' }}> <div style={{ color: 'black' }}>
@ -631,7 +678,7 @@ const KeyboardShortcuts = memo(({
}); });
const KeyboardShortcutsDialog = memo(({ const KeyboardShortcutsDialog = memo(({
isShown, onHide, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, isShown, onHide, keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, mainActions,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -645,7 +692,7 @@ const KeyboardShortcutsDialog = memo(({
onConfirm={onHide} onConfirm={onHide}
topOffset="3vh" topOffset="3vh"
> >
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> : <div />} {isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} mainActions={mainActions} /> : <div />}
</Dialog> </Dialog>
); );
}); });

View File

@ -7,6 +7,7 @@ import pRetry from 'p-retry';
import isDev from './isDev'; import isDev from './isDev';
import Swal, { toast } from './swal'; import Swal, { toast } from './swal';
import { ffmpegExtractWindow } from './util/constants';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path'); const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra'); const fsExtra = window.require('fs-extra');
@ -393,3 +394,14 @@ export async function readVideoTs(videoTsPath) {
if (ret.length === 0) throw new Error('No VTS vob files found in folder'); if (ret.length === 0) throw new Error('No VTS vob files found in folder');
return ret; return ret;
} }
export function getImportProjectType(filePath) {
if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt';
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'mplayer', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };
const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`));
if (!matchingExt) return undefined;
return edlFormatForExtension[matchingExt];
}
export const calcShouldShowWaveform = (zoomedDuration) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);
export const calcShouldShowKeyframes = (zoomedDuration) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);