mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +01:00
implement OS notifications
closes #1510 also respect hideAllNotifications for more notifications
This commit is contained in:
parent
1abf243735
commit
f2f98d40a7
@ -110,6 +110,7 @@ const defaults: Config = {
|
|||||||
movFastStart: true,
|
movFastStart: true,
|
||||||
avoidNegativeTs: 'make_zero',
|
avoidNegativeTs: 'make_zero',
|
||||||
hideNotifications: undefined,
|
hideNotifications: undefined,
|
||||||
|
hideOsNotifications: undefined,
|
||||||
autoLoadTimecode: false,
|
autoLoadTimecode: false,
|
||||||
segmentsToChapters: false,
|
segmentsToChapters: false,
|
||||||
preserveMetadataOnMerge: false,
|
preserveMetadataOnMerge: false,
|
||||||
|
@ -3,7 +3,7 @@ process.traceProcessWarnings = true;
|
|||||||
|
|
||||||
/* eslint-disable import/first */
|
/* eslint-disable import/first */
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain } from 'electron';
|
import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain, Notification, NotificationConstructorOptions } from 'electron';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import yargsParser from 'yargs-parser';
|
import yargsParser from 'yargs-parser';
|
||||||
@ -408,3 +408,10 @@ export function quitApp() {
|
|||||||
export const hasDisabledNetworking = () => !!disableNetworking;
|
export const hasDisabledNetworking = () => !!disableNetworking;
|
||||||
|
|
||||||
export const setProgressBar = (v: number) => mainWindow?.setProgressBar(v);
|
export const setProgressBar = (v: number) => mainWindow?.setProgressBar(v);
|
||||||
|
|
||||||
|
export function sendOsNotification(options: NotificationConstructorOptions) {
|
||||||
|
if (!Notification.isSupported()) return;
|
||||||
|
const notification = new Notification(options);
|
||||||
|
notification.on('failed', (_e, error) => logger.warn('Notification failed', error));
|
||||||
|
notification.show();
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ import flatMap from 'lodash/flatMap';
|
|||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import sum from 'lodash/sum';
|
import sum from 'lodash/sum';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
|
import { SweetAlertOptions } from 'sweetalert2';
|
||||||
|
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
import useTimelineScroll from './hooks/useTimelineScroll';
|
import useTimelineScroll from './hooks/useTimelineScroll';
|
||||||
@ -97,7 +98,7 @@ const { exists } = window.require('fs-extra');
|
|||||||
const { lstat } = window.require('fs/promises');
|
const { lstat } = window.require('fs/promises');
|
||||||
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
|
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
|
||||||
|
|
||||||
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar } = window.require('@electron/remote').require('./index.js');
|
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification } = window.require('@electron/remote').require('./index.js');
|
||||||
|
|
||||||
|
|
||||||
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
|
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
|
||||||
@ -201,7 +202,7 @@ function App() {
|
|||||||
const allUserSettings = useUserSettingsRoot();
|
const allUserSettings = useUserSettingsRoot();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames,
|
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames,
|
||||||
} = allUserSettings;
|
} = allUserSettings;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -240,11 +241,25 @@ function App() {
|
|||||||
|
|
||||||
const toggleShowThumbnails = useCallback(() => setThumbnailsEnabled((v) => !v), []);
|
const toggleShowThumbnails = useCallback(() => setThumbnailsEnabled((v) => !v), []);
|
||||||
|
|
||||||
|
const hideAllNotifications = hideNotifications === 'all';
|
||||||
|
|
||||||
|
const showNotification = useCallback((opts: SweetAlertOptions) => {
|
||||||
|
if (!hideAllNotifications) {
|
||||||
|
toast.fire(opts);
|
||||||
|
}
|
||||||
|
}, [hideAllNotifications]);
|
||||||
|
|
||||||
|
const showOsNotification = useCallback((text: string) => {
|
||||||
|
if (hideOsNotifications == null) {
|
||||||
|
sendOsNotification({ title: text });
|
||||||
|
}
|
||||||
|
}, [hideOsNotifications]);
|
||||||
|
|
||||||
const toggleExportConfirmEnabled = useCallback(() => setExportConfirmEnabled((v) => {
|
const toggleExportConfirmEnabled = useCallback(() => setExportConfirmEnabled((v) => {
|
||||||
const newVal = !v;
|
const newVal = !v;
|
||||||
toast.fire({ text: newVal ? i18n.t('Export options will be shown before exporting.') : i18n.t('Export options will not be shown before exporting.') });
|
showNotification({ text: newVal ? i18n.t('Export options will be shown before exporting.') : i18n.t('Export options will not be shown before exporting.') });
|
||||||
return newVal;
|
return newVal;
|
||||||
}), [setExportConfirmEnabled]);
|
}), [setExportConfirmEnabled, showNotification]);
|
||||||
|
|
||||||
const toggleSegmentsToChapters = useCallback(() => setSegmentsToChapters((v) => !v), [setSegmentsToChapters]);
|
const toggleSegmentsToChapters = useCallback(() => setSegmentsToChapters((v) => !v), [setSegmentsToChapters]);
|
||||||
|
|
||||||
@ -254,11 +269,11 @@ function App() {
|
|||||||
setKeyframesEnabled((old) => {
|
setKeyframesEnabled((old) => {
|
||||||
const enabled = !old;
|
const enabled = !old;
|
||||||
if (enabled && !calcShouldShowKeyframes(zoomedDuration)) {
|
if (enabled && !calcShouldShowKeyframes(zoomedDuration)) {
|
||||||
toast.fire({ text: i18n.t('Key frames will show on the timeline. You need to zoom in to view them') });
|
showNotification({ text: i18n.t('Key frames will show on the timeline. You need to zoom in to view them') });
|
||||||
}
|
}
|
||||||
return enabled;
|
return enabled;
|
||||||
});
|
});
|
||||||
}, [zoomedDuration]);
|
}, [showNotification, zoomedDuration]);
|
||||||
|
|
||||||
const appendLastCommandsLog = useCallback((command: string) => {
|
const appendLastCommandsLog = useCallback((command: string) => {
|
||||||
setFfmpegCommandLog((old) => [...old, { command, time: new Date() }]);
|
setFfmpegCommandLog((old) => [...old, { command, time: new Date() }]);
|
||||||
@ -277,23 +292,21 @@ function App() {
|
|||||||
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
|
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
|
||||||
}, [setCopyStreamIdsForPath]);
|
}, [setCopyStreamIdsForPath]);
|
||||||
|
|
||||||
const hideAllNotifications = hideNotifications === 'all';
|
|
||||||
|
|
||||||
const toggleWaveformMode = useCallback(() => {
|
const toggleWaveformMode = useCallback(() => {
|
||||||
if (waveformMode === 'waveform') {
|
if (waveformMode === 'waveform') {
|
||||||
setWaveformMode('big-waveform');
|
setWaveformMode('big-waveform');
|
||||||
} else if (waveformMode === 'big-waveform') {
|
} else if (waveformMode === 'big-waveform') {
|
||||||
setWaveformMode(undefined);
|
setWaveformMode(undefined);
|
||||||
} else {
|
} else {
|
||||||
if (!hideAllNotifications) toast.fire({ text: i18n.t('Mini-waveform has been enabled. Click again to enable full-screen waveform') });
|
showNotification({ text: i18n.t('Mini-waveform has been enabled. Click again to enable full-screen waveform') });
|
||||||
setWaveformMode('waveform');
|
setWaveformMode('waveform');
|
||||||
}
|
}
|
||||||
}, [hideAllNotifications, waveformMode]);
|
}, [showNotification, waveformMode]);
|
||||||
|
|
||||||
const toggleSafeOutputFileName = useCallback(() => setSafeOutputFileName((v) => {
|
const toggleSafeOutputFileName = useCallback(() => setSafeOutputFileName((v) => {
|
||||||
if (v && !hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('Output file name will not be sanitized, and any special characters will be preserved. This may cause the export to fail and can cause other funny issues. Use at your own risk!') });
|
if (v) showNotification({ icon: 'info', text: i18n.t('Output file name will not be sanitized, and any special characters will be preserved. This may cause the export to fail and can cause other funny issues. Use at your own risk!') });
|
||||||
return !v;
|
return !v;
|
||||||
}), [setSafeOutputFileName, hideAllNotifications]);
|
}), [setSafeOutputFileName, showNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current) videoRef.current.volume = playbackVolume;
|
if (videoRef.current) videoRef.current.volume = playbackVolume;
|
||||||
@ -557,8 +570,8 @@ function App() {
|
|||||||
setHideMediaSourcePlayer(false);
|
setHideMediaSourcePlayer(false);
|
||||||
// Matroska is known not to work, so we warn user. See https://github.com/mifi/lossless-cut/discussions/661
|
// Matroska is known not to work, so we warn user. See https://github.com/mifi/lossless-cut/discussions/661
|
||||||
const supportsRotation = !(fileFormat != null && ['matroska', 'webm'].includes(fileFormat));
|
const supportsRotation = !(fileFormat != null && ['matroska', 'webm'].includes(fileFormat));
|
||||||
if (!supportsRotation && !hideAllNotifications) toast.fire({ text: i18n.t('Lossless rotation might not work with this file format. You may try changing to MP4') });
|
if (!supportsRotation) showNotification({ text: i18n.t('Lossless rotation might not work with this file format. You may try changing to MP4') });
|
||||||
}, [hideAllNotifications, fileFormat]);
|
}, [fileFormat, showNotification]);
|
||||||
|
|
||||||
const { ensureWritableOutDir, ensureAccessToSourceDir } = useDirectoryAccess({ setCustomOutDir });
|
const { ensureWritableOutDir, ensureAccessToSourceDir } = useDirectoryAccess({ setCustomOutDir });
|
||||||
|
|
||||||
@ -575,23 +588,23 @@ function App() {
|
|||||||
|
|
||||||
const toggleKeyframeCut = useCallback((showMessage?: boolean) => setKeyframeCut((val) => {
|
const toggleKeyframeCut = useCallback((showMessage?: boolean) => setKeyframeCut((val) => {
|
||||||
const newVal = !val;
|
const newVal = !val;
|
||||||
if (showMessage && !hideAllNotifications) {
|
if (showMessage) {
|
||||||
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.') });
|
if (newVal) showNotification({ 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.') });
|
||||||
else toast.fire({ title: i18n.t('Keyframe cut disabled'), text: i18n.t('Will now cut at the exact position, but may leave an empty portion at the beginning of the file. You may have to set the cutpoint a few frames before the next keyframe to achieve a precise cut'), timer: 7000 });
|
else showNotification({ title: i18n.t('Keyframe cut disabled'), text: i18n.t('Will now cut at the exact position, but may leave an empty portion at the beginning of the file. You may have to set the cutpoint a few frames before the next keyframe to achieve a precise cut'), timer: 7000 });
|
||||||
}
|
}
|
||||||
return newVal;
|
return newVal;
|
||||||
}), [hideAllNotifications, setKeyframeCut]);
|
}), [showNotification, setKeyframeCut]);
|
||||||
|
|
||||||
const togglePreserveMovData = useCallback(() => setPreserveMovData((val) => !val), [setPreserveMovData]);
|
const togglePreserveMovData = useCallback(() => setPreserveMovData((val) => !val), [setPreserveMovData]);
|
||||||
|
|
||||||
const toggleMovFastStart = useCallback(() => setMovFastStart((val) => !val), [setMovFastStart]);
|
const toggleMovFastStart = useCallback(() => setMovFastStart((val) => !val), [setMovFastStart]);
|
||||||
|
|
||||||
const toggleSimpleMode = useCallback(() => setSimpleMode((v) => {
|
const toggleSimpleMode = useCallback(() => setSimpleMode((v) => {
|
||||||
if (!hideAllNotifications) toast.fire({ text: v ? i18n.t('Advanced view has been enabled. You will now also see non-essential buttons and functions') : i18n.t('Advanced view disabled. You will now see only the most essential buttons and functions') });
|
showNotification({ text: v ? i18n.t('Advanced view has been enabled. You will now also see non-essential buttons and functions') : i18n.t('Advanced view disabled. You will now see only the most essential buttons and functions') });
|
||||||
const newValue = !v;
|
const newValue = !v;
|
||||||
if (newValue) setInvertCutSegments(false);
|
if (newValue) setInvertCutSegments(false);
|
||||||
return newValue;
|
return newValue;
|
||||||
}), [hideAllNotifications, setInvertCutSegments, setSimpleMode]);
|
}), [setInvertCutSegments, setSimpleMode, showNotification]);
|
||||||
|
|
||||||
const effectiveExportMode = useMemo(() => {
|
const effectiveExportMode = useMemo(() => {
|
||||||
if (segmentsToChaptersOnly) return 'segments_to_chapters';
|
if (segmentsToChaptersOnly) return 'segments_to_chapters';
|
||||||
@ -836,12 +849,12 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
const showUnsupportedFileMessage = useCallback(() => {
|
const showUnsupportedFileMessage = useCallback(() => {
|
||||||
if (!hideAllNotifications) toast.fire({ timer: 13000, text: i18n.t('File is not natively supported. Preview playback may be slow and of low quality, but the final export will be lossless. You may convert the file from the menu for a better preview.') });
|
showNotification({ timer: 13000, text: i18n.t('File is not natively supported. Preview playback may be slow and of low quality, but the final export will be lossless. You may convert the file from the menu for a better preview.') });
|
||||||
}, [hideAllNotifications]);
|
}, [showNotification]);
|
||||||
|
|
||||||
const showPreviewFileLoadedMessage = useCallback((fileName) => {
|
const showPreviewFileLoadedMessage = useCallback((fileName) => {
|
||||||
if (!hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) });
|
showNotification({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) });
|
||||||
}, [hideAllNotifications]);
|
}, [showNotification]);
|
||||||
|
|
||||||
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, duration)), [duration, segmentsToExport]);
|
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, duration)), [duration, segmentsToExport]);
|
||||||
const needSmartCut = !!(areWeCutting && enableSmartCut);
|
const needSmartCut = !!(areWeCutting && enableSmartCut);
|
||||||
@ -1130,8 +1143,14 @@ function App() {
|
|||||||
|
|
||||||
if (clearBatchFilesAfterConcat) closeBatch();
|
if (clearBatchFilesAfterConcat) closeBatch();
|
||||||
if (!includeAllStreams && haveExcludedStreams) notices.push(i18n.t('Some extra tracks have been discarded. You can change this option before merging.'));
|
if (!includeAllStreams && haveExcludedStreams) notices.push(i18n.t('Some extra tracks have been discarded. You can change this option before merging.'));
|
||||||
if (!hideAllNotifications) openConcatFinishedToast({ filePath: outPath, notices, warnings });
|
|
||||||
|
if (!hideAllNotifications) {
|
||||||
|
showOsNotification(i18n.t('Merge finished'));
|
||||||
|
openConcatFinishedToast({ filePath: outPath, notices, warnings });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
showOsNotification(i18n.t('Failed to merge'));
|
||||||
|
|
||||||
if (err instanceof DirectoryAccessDeclinedError) return;
|
if (err instanceof DirectoryAccessDeclinedError) return;
|
||||||
|
|
||||||
if (isExecaError(err)) {
|
if (isExecaError(err)) {
|
||||||
@ -1161,7 +1180,7 @@ function App() {
|
|||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
setCutProgress(undefined);
|
setCutProgress(undefined);
|
||||||
}
|
}
|
||||||
}, [setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, handleConcatFailed]);
|
}, [setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, showOsNotification, handleConcatFailed]);
|
||||||
|
|
||||||
const cleanupFiles = useCallback(async (cleanupChoices2) => {
|
const cleanupFiles = useCallback(async (cleanupChoices2) => {
|
||||||
// Store paths before we reset state
|
// Store paths before we reset state
|
||||||
@ -1351,7 +1370,10 @@ function App() {
|
|||||||
|
|
||||||
const revealPath = willMerge ? mergedOutFilePath : outFiles[0];
|
const revealPath = willMerge ? mergedOutFilePath : outFiles[0];
|
||||||
invariant(revealPath != null);
|
invariant(revealPath != null);
|
||||||
if (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices });
|
if (!hideAllNotifications) {
|
||||||
|
showOsNotification(i18n.t('Export finished'));
|
||||||
|
openExportFinishedToast({ filePath: revealPath, warnings, notices });
|
||||||
|
}
|
||||||
|
|
||||||
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
|
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
|
||||||
|
|
||||||
@ -1366,6 +1388,8 @@ function App() {
|
|||||||
console.log('stdout:', getStdioString(err.stdout));
|
console.log('stdout:', getStdioString(err.stdout));
|
||||||
console.error('stderr:', getStdioString(err.stderr));
|
console.error('stderr:', getStdioString(err.stderr));
|
||||||
|
|
||||||
|
showOsNotification(i18n.t('Failed to export'));
|
||||||
|
|
||||||
if (isOutOfSpaceError(err)) {
|
if (isOutOfSpaceError(err)) {
|
||||||
showDiskFull();
|
showDiskFull();
|
||||||
return;
|
return;
|
||||||
@ -1378,12 +1402,13 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showOsNotification(i18n.t('Failed to export'));
|
||||||
handleError(err);
|
handleError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
setCutProgress(undefined);
|
setCutProgress(undefined);
|
||||||
}
|
}
|
||||||
}, [numStreamsToCopy, segmentsToExport, haveInvalidSegs, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, mergedOutFilePath, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, resetMergedOutFileName, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, filePath, handleExportFailed]);
|
}, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, mergedOutFilePath, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, resetMergedOutFileName, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, showOsNotification, handleExportFailed]);
|
||||||
|
|
||||||
const onExportPress = useCallback(async () => {
|
const onExportPress = useCallback(async () => {
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
@ -1443,14 +1468,19 @@ function App() {
|
|||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
lastOutPath = await captureFramesRange({ customOutDir, filePath, fps: detectedFps, fromTime: start, toTime: end, estimatedMaxNumFiles: captureFramesResponse.estimatedMaxNumFiles, captureFormat, quality: captureFrameQuality, filter: captureFramesResponse.filter, outputTimestamps: captureFrameFileNameFormat === 'timestamp', onProgress });
|
lastOutPath = await captureFramesRange({ customOutDir, filePath, fps: detectedFps, fromTime: start, toTime: end, estimatedMaxNumFiles: captureFramesResponse.estimatedMaxNumFiles, captureFormat, quality: captureFrameQuality, filter: captureFramesResponse.filter, outputTimestamps: captureFrameFileNameFormat === 'timestamp', onProgress });
|
||||||
}
|
}
|
||||||
if (!hideAllNotifications && lastOutPath != null) openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
|
if (!hideAllNotifications && lastOutPath != null) {
|
||||||
|
showOsNotification(i18n.t('Frames have been extracted'));
|
||||||
|
openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
showOsNotification(i18n.t('Failed to extract frames'));
|
||||||
|
|
||||||
handleError(err);
|
handleError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
setCutProgress(undefined);
|
setCutProgress(undefined);
|
||||||
}
|
}
|
||||||
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, captureFramesRange, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
|
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, captureFramesRange, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking, showOsNotification]);
|
||||||
|
|
||||||
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages([currentCutSeg?.segId]), [currentCutSeg?.segId, extractSegmentFramesAsImages]);
|
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages([currentCutSeg?.segId]), [currentCutSeg?.segId, extractSegmentFramesAsImages]);
|
||||||
const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentFramesAsImages(selectedSegments.map((seg) => seg.segId)), [extractSegmentFramesAsImages, selectedSegments]);
|
const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentFramesAsImages(selectedSegments.map((seg) => seg.segId)), [extractSegmentFramesAsImages, selectedSegments]);
|
||||||
@ -1466,10 +1496,10 @@ function App() {
|
|||||||
video!.play();
|
video!.play();
|
||||||
} else {
|
} else {
|
||||||
const newRate = adjustRate(video!.playbackRate, dir, rateMultiplier);
|
const newRate = adjustRate(video!.playbackRate, dir, rateMultiplier);
|
||||||
toast.fire({ title: `${i18n.t('Playback rate:')} ${Math.round(newRate * 100)}%`, timer: 1000 });
|
showNotification({ title: `${i18n.t('Playback rate:')} ${Math.round(newRate * 100)}%`, timer: 1000 });
|
||||||
video!.playbackRate = newRate;
|
video!.playbackRate = newRate;
|
||||||
}
|
}
|
||||||
}, [compatPlayerEnabled]);
|
}, [compatPlayerEnabled, showNotification]);
|
||||||
|
|
||||||
const loadEdlFile = useCallback(async ({ path, type, append }: { path: string, type: EdlFileType, append?: boolean }) => {
|
const loadEdlFile = useCallback(async ({ path, type, append }: { path: string, type: EdlFileType, append?: boolean }) => {
|
||||||
console.log('Loading EDL file', type, path, append);
|
console.log('Loading EDL file', type, path, append);
|
||||||
@ -1628,7 +1658,7 @@ function App() {
|
|||||||
} else if (needsAutoHtml5ify) {
|
} else if (needsAutoHtml5ify) {
|
||||||
showUnsupportedFileMessage();
|
showUnsupportedFileMessage();
|
||||||
} else if (isAudioDefinitelyNotSupported(fileMeta.streams)) {
|
} else if (isAudioDefinitelyNotSupported(fileMeta.streams)) {
|
||||||
if (!hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('The audio track is not supported. You can convert to a supported format from the menu') });
|
showNotification({ icon: 'info', text: i18n.t('The audio track is not supported. You can convert to a supported format from the menu') });
|
||||||
} else if (!validDuration) {
|
} else if (!validDuration) {
|
||||||
toast.fire({ icon: 'warning', timer: 10000, text: i18n.t('This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu') });
|
toast.fire({ icon: 'warning', timer: 10000, text: i18n.t('This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu') });
|
||||||
}
|
}
|
||||||
@ -1642,7 +1672,7 @@ function App() {
|
|||||||
resetState();
|
resetState();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, hideAllNotifications]);
|
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, showNotification]);
|
||||||
|
|
||||||
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
|
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
|
||||||
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
|
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
|
||||||
@ -1777,18 +1807,23 @@ function App() {
|
|||||||
setWorking({ text: i18n.t('Extracting all streams') });
|
setWorking({ text: i18n.t('Extracting all streams') });
|
||||||
setStreamsSelectorShown(false);
|
setStreamsSelectorShown(false);
|
||||||
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
|
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
|
||||||
if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') });
|
if (!hideAllNotifications && firstExtractedPath != null) {
|
||||||
|
showOsNotification(i18n.t('All tracks have been extracted'));
|
||||||
|
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
showOsNotification(i18n.t('Failed to extract tracks'));
|
||||||
|
|
||||||
if (err instanceof RefuseOverwriteError) {
|
if (err instanceof RefuseOverwriteError) {
|
||||||
showRefuseToOverwrite();
|
showRefuseToOverwrite();
|
||||||
return;
|
} else {
|
||||||
|
errorToast(i18n.t('Failed to extract all streams'));
|
||||||
|
console.error('Failed to extract all streams', err);
|
||||||
}
|
}
|
||||||
errorToast(i18n.t('Failed to extract all streams'));
|
|
||||||
console.error('Failed to extract all streams', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
}
|
}
|
||||||
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking]);
|
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking, showOsNotification]);
|
||||||
|
|
||||||
|
|
||||||
const userHtml5ifyCurrentFile = useCallback(async ({ ignoreRememberedValue }: { ignoreRememberedValue?: boolean } = {}) => {
|
const userHtml5ifyCurrentFile = useCallback(async ({ ignoreRememberedValue }: { ignoreRememberedValue?: boolean } = {}) => {
|
||||||
@ -1846,7 +1881,7 @@ function App() {
|
|||||||
setCutProgress(0);
|
setCutProgress(0);
|
||||||
invariant(fileFormat != null);
|
invariant(fileFormat != null);
|
||||||
const path = await fixInvalidDuration({ fileFormat, customOutDir, duration, onProgress: setCutProgress });
|
const path = await fixInvalidDuration({ fileFormat, customOutDir, duration, onProgress: setCutProgress });
|
||||||
if (!hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('Duration has been fixed') });
|
showNotification({ icon: 'info', text: i18n.t('Duration has been fixed') });
|
||||||
|
|
||||||
await loadMedia({ filePath: path });
|
await loadMedia({ filePath: path });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1856,7 +1891,7 @@ function App() {
|
|||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
setCutProgress(undefined);
|
setCutProgress(undefined);
|
||||||
}
|
}
|
||||||
}, [checkFileOpened, customOutDir, duration, fileFormat, fixInvalidDuration, hideAllNotifications, loadMedia, setWorking]);
|
}, [checkFileOpened, customOutDir, duration, fileFormat, fixInvalidDuration, loadMedia, setWorking, showNotification]);
|
||||||
|
|
||||||
const addStreamSourceFile = useCallback(async (path: string) => {
|
const addStreamSourceFile = useCallback(async (path: string) => {
|
||||||
if (allFilesMeta[path]) return undefined; // Already added?
|
if (allFilesMeta[path]) return undefined; // Already added?
|
||||||
@ -1893,12 +1928,12 @@ function App() {
|
|||||||
const currentTime = getRelevantTime();
|
const currentTime = getRelevantTime();
|
||||||
const path = await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality });
|
const path = await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality });
|
||||||
if (!(await addFileAsCoverArt(path))) return;
|
if (!(await addFileAsCoverArt(path))) return;
|
||||||
if (!hideAllNotifications) toast.fire({ text: i18n.t('Current frame has been set as cover art') });
|
showNotification({ text: i18n.t('Current frame has been set as cover art') });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
errorToast(i18n.t('Failed to capture frame'));
|
errorToast(i18n.t('Failed to capture frame'));
|
||||||
}
|
}
|
||||||
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, hideAllNotifications]);
|
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, showNotification]);
|
||||||
|
|
||||||
const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => {
|
const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => {
|
||||||
setBatchFiles((existingFiles) => {
|
setBatchFiles((existingFiles) => {
|
||||||
@ -2335,18 +2370,23 @@ function App() {
|
|||||||
setWorking({ text: i18n.t('Extracting track') });
|
setWorking({ text: i18n.t('Extracting track') });
|
||||||
// setStreamsSelectorShown(false);
|
// setStreamsSelectorShown(false);
|
||||||
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
|
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
|
||||||
if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') });
|
if (!hideAllNotifications && firstExtractedPath != null) {
|
||||||
|
showOsNotification(i18n.t('Track has been extracted'));
|
||||||
|
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
showOsNotification(i18n.t('Failed to extract track'));
|
||||||
|
|
||||||
if (err instanceof RefuseOverwriteError) {
|
if (err instanceof RefuseOverwriteError) {
|
||||||
showRefuseToOverwrite();
|
showRefuseToOverwrite();
|
||||||
return;
|
} else {
|
||||||
|
errorToast(i18n.t('Failed to extract track'));
|
||||||
|
console.error('Failed to extract track', err);
|
||||||
}
|
}
|
||||||
errorToast(i18n.t('Failed to extract track'));
|
|
||||||
console.error('Failed to extract track', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setWorking(undefined);
|
setWorking(undefined);
|
||||||
}
|
}
|
||||||
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking]);
|
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking, showOsNotification]);
|
||||||
|
|
||||||
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);
|
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ function Settings({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);
|
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);
|
||||||
|
|
||||||
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 { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, 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<ChangeEventHandler<HTMLSelectElement>>((e) => {
|
const onLangChange = useCallback<ChangeEventHandler<HTMLSelectElement>>((e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
@ -447,7 +447,14 @@ function Settings({
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<KeyCell>{t('Show informational notifications')}</KeyCell>
|
<KeyCell>{t('Show notifications')}</KeyCell>
|
||||||
|
<td>
|
||||||
|
<Switch checked={!hideOsNotifications} onCheckedChange={(v) => setHideOsNotifications(v ? undefined : 'all')} />
|
||||||
|
</td>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<KeyCell>{t('Show informational in-app notifications')}</KeyCell>
|
||||||
<td>
|
<td>
|
||||||
<Switch checked={!hideNotifications} onCheckedChange={(v) => setHideNotifications(v ? undefined : 'all')} />
|
<Switch checked={!hideNotifications} onCheckedChange={(v) => setHideNotifications(v ? undefined : 'all')} />
|
||||||
</td>
|
</td>
|
||||||
|
@ -81,6 +81,8 @@ export default () => {
|
|||||||
useEffect(() => safeSetConfig({ ffmpegExperimental }), [ffmpegExperimental]);
|
useEffect(() => safeSetConfig({ ffmpegExperimental }), [ffmpegExperimental]);
|
||||||
const [hideNotifications, setHideNotifications] = useState(safeGetConfigInitial('hideNotifications'));
|
const [hideNotifications, setHideNotifications] = useState(safeGetConfigInitial('hideNotifications'));
|
||||||
useEffect(() => safeSetConfig({ hideNotifications }), [hideNotifications]);
|
useEffect(() => safeSetConfig({ hideNotifications }), [hideNotifications]);
|
||||||
|
const [hideOsNotifications, setHideOsNotifications] = useState(safeGetConfigInitial('hideOsNotifications'));
|
||||||
|
useEffect(() => safeSetConfig({ hideOsNotifications }), [hideOsNotifications]);
|
||||||
const [autoLoadTimecode, setAutoLoadTimecode] = useState(safeGetConfigInitial('autoLoadTimecode'));
|
const [autoLoadTimecode, setAutoLoadTimecode] = useState(safeGetConfigInitial('autoLoadTimecode'));
|
||||||
useEffect(() => safeSetConfig({ autoLoadTimecode }), [autoLoadTimecode]);
|
useEffect(() => safeSetConfig({ autoLoadTimecode }), [autoLoadTimecode]);
|
||||||
const [autoDeleteMergedSegments, setAutoDeleteMergedSegments] = useState(safeGetConfigInitial('autoDeleteMergedSegments'));
|
const [autoDeleteMergedSegments, setAutoDeleteMergedSegments] = useState(safeGetConfigInitial('autoDeleteMergedSegments'));
|
||||||
@ -206,6 +208,8 @@ export default () => {
|
|||||||
setFfmpegExperimental,
|
setFfmpegExperimental,
|
||||||
hideNotifications,
|
hideNotifications,
|
||||||
setHideNotifications,
|
setHideNotifications,
|
||||||
|
hideOsNotifications,
|
||||||
|
setHideOsNotifications,
|
||||||
autoLoadTimecode,
|
autoLoadTimecode,
|
||||||
setAutoLoadTimecode,
|
setAutoLoadTimecode,
|
||||||
autoDeleteMergedSegments,
|
autoDeleteMergedSegments,
|
||||||
|
1
types.ts
1
types.ts
@ -66,6 +66,7 @@ export interface Config {
|
|||||||
movFastStart: boolean,
|
movFastStart: boolean,
|
||||||
avoidNegativeTs: AvoidNegativeTs,
|
avoidNegativeTs: AvoidNegativeTs,
|
||||||
hideNotifications: 'all' | undefined,
|
hideNotifications: 'all' | undefined,
|
||||||
|
hideOsNotifications: 'all' | undefined,
|
||||||
autoLoadTimecode: boolean,
|
autoLoadTimecode: boolean,
|
||||||
segmentsToChapters: boolean,
|
segmentsToChapters: boolean,
|
||||||
preserveMetadataOnMerge: boolean,
|
preserveMetadataOnMerge: boolean,
|
||||||
|
Loading…
Reference in New Issue
Block a user