improve export complete dialogs

closes #2589
pull/2599/head
Mikael Finstad 8 months ago
parent 251101ded2
commit 251101c14f
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -79,7 +79,7 @@ import {
import { toast, errorToast, showPlaybackFailedMessage } from './swal'; import { toast, errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator'; import { adjustRate } from './util/rate-calculator';
import { askExtractFramesAsImages } from './dialogs/extractFrames'; import { askExtractFramesAsImages } from './dialogs/extractFrames';
import { askForOutDir, askForImportChapters, askForFileOpenAction, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs'; import { askForOutDir, askForImportChapters, askForFileOpenAction, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
import { openSendReportDialog } from './reporting'; import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n'; import { fallbackLng } from './i18n';
import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers } from './segments'; import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers } from './segments';
@ -178,7 +178,7 @@ function App() {
const { withErrorHandling, handleError, genericError, setGenericError } = useErrorHandling(); const { withErrorHandling, handleError, genericError, setGenericError } = useErrorHandling();
const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog } = useDialog(); const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog, openExportFinishedDialog, openCutFinishedDialog, openConcatFinishedDialog } = useDialog();
// Note that each action may be multiple key bindings and this will only be the first binding for each action // Note that each action may be multiple key bindings and this will only be the first binding for each action
const keyBindingByAction = useMemo(() => Object.fromEntries(keyBindings.map((binding) => [binding.action, binding])), [keyBindings]); const keyBindingByAction = useMemo(() => Object.fromEntries(keyBindings.map((binding) => [binding.action, binding])), [keyBindings]);
@ -894,7 +894,7 @@ function App() {
if (!hideAllNotifications) { if (!hideAllNotifications) {
showOsNotification(i18n.t('Merge finished')); showOsNotification(i18n.t('Merge finished'));
openConcatFinishedToast({ filePath: outPath, notices, warnings }); openConcatFinishedDialog({ filePath: outPath, notices, warnings });
} }
} catch (err) { } catch (err) {
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return; if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
@ -926,7 +926,7 @@ function App() {
setWorking(undefined); setWorking(undefined);
setProgress(undefined); setProgress(undefined);
} }
}, [workingRef, setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, showOsNotification, handleConcatFailed]); }, [workingRef, setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, showOsNotification, openConcatFinishedDialog, handleConcatFailed]);
const cleanupFiles = useCallback(async (cleanupChoices2: CleanupChoicesType) => { const cleanupFiles = useCallback(async (cleanupChoices2: CleanupChoicesType) => {
// Store paths before we reset state // Store paths before we reset state
@ -1133,7 +1133,7 @@ function App() {
const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0]!.path; const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0]!.path;
if (!hideAllNotifications) { if (!hideAllNotifications) {
showOsNotification(i18n.t('Export finished')); showOsNotification(i18n.t('Export finished'));
openExportFinishedToast({ filePath: revealPath, warnings, notices }); openCutFinishedDialog({ filePath: revealPath, warnings, notices });
} }
shootConfetti({ ticks: 50 }); shootConfetti({ ticks: 50 });
@ -1171,7 +1171,7 @@ function App() {
setWorking(undefined); setWorking(undefined);
setProgress(undefined); setProgress(undefined);
} }
}, [filePath, numStreamsToCopy, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, segmentsToExport, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, segmentsOrInverse.selected, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, concatCutSegments, autoDeleteMergedSegments, tryDeleteFiles, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]); }, [filePath, numStreamsToCopy, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, segmentsToExport, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, segmentsOrInverse.selected, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, concatCutSegments, autoDeleteMergedSegments, tryDeleteFiles, nonCopiedExtraStreams, extractStreams, showOsNotification, openCutFinishedDialog, handleExportFailed]);
const onExportPress = useCallback(async () => { const onExportPress = useCallback(async () => {
if (!filePath) return; if (!filePath) return;
@ -1200,12 +1200,12 @@ function App() {
: await captureFrameFromTag({ customOutDir, filePath, time: currentTime, captureFormat, quality: captureFrameQuality, video }); : await captureFrameFromTag({ customOutDir, filePath, time: currentTime, captureFormat, quality: captureFrameQuality, video });
shootConfetti(); shootConfetti();
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` }); if (!hideAllNotifications) openExportFinishedDialog({ filePath: outPath, children: `${i18n.t('Screenshot captured to:')} ${outPath}` });
}, i18n.t('Failed to capture frame')); }, i18n.t('Failed to capture frame'));
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
}, [filePath, workingRef, setWorking, withErrorHandling, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]); }, [filePath, workingRef, setWorking, withErrorHandling, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications, openExportFinishedDialog]);
const extractSegmentsFramesAsImages = useCallback(async (segments: SegmentBase[]) => { const extractSegmentsFramesAsImages = useCallback(async (segments: SegmentBase[]) => {
if (!filePath || detectedFps == null || workingRef.current || segments.length === 0) return; if (!filePath || detectedFps == null || workingRef.current || segments.length === 0) return;
@ -1242,7 +1242,7 @@ function App() {
} }
if (!hideAllNotifications && lastOutPath != null) { if (!hideAllNotifications && lastOutPath != null) {
showOsNotification(i18n.t('Frames have been extracted')); showOsNotification(i18n.t('Frames have been extracted'));
openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) }); openExportFinishedDialog({ filePath: lastOutPath, children: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
} }
} catch (err) { } catch (err) {
showOsNotification(i18n.t('Failed to extract frames')); showOsNotification(i18n.t('Failed to extract frames'));
@ -1251,7 +1251,7 @@ function App() {
setWorking(undefined); setWorking(undefined);
setProgress(undefined); setProgress(undefined);
} }
}, [filePath, detectedFps, workingRef, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, outputDir, handleError]); }, [filePath, detectedFps, workingRef, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, openExportFinishedDialog, outputDir, handleError]);
const extractCurrentSegmentFramesAsImages = useCallback(() => { const extractCurrentSegmentFramesAsImages = useCallback(() => {
if (currentCutSeg != null) extractSegmentsFramesAsImages([currentCutSeg]); if (currentCutSeg != null) extractSegmentsFramesAsImages([currentCutSeg]);
@ -1579,7 +1579,7 @@ function App() {
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainCopiedStreams }); const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainCopiedStreams });
if (!hideAllNotifications && firstExtractedPath != null) { if (!hideAllNotifications && firstExtractedPath != null) {
showOsNotification(i18n.t('All tracks have been extracted')); showOsNotification(i18n.t('All tracks have been extracted'));
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') }); openExportFinishedDialog({ filePath: firstExtractedPath, children: i18n.t('All streams have been extracted as separate files') });
} }
} catch (err) { } catch (err) {
showOsNotification(i18n.t('Failed to extract tracks')); showOsNotification(i18n.t('Failed to extract tracks'));
@ -1593,7 +1593,7 @@ function App() {
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
}, [confirmDialog, customOutDir, extractStreams, filePath, hideAllNotifications, mainCopiedStreams, setWorking, showOsNotification, t, workingRef]); }, [confirmDialog, customOutDir, extractStreams, filePath, hideAllNotifications, mainCopiedStreams, openExportFinishedDialog, setWorking, showOsNotification, t, workingRef]);
const askStartTimeOffset = useCallback(async () => { const askStartTimeOffset = useCallback(async () => {
@ -2125,7 +2125,7 @@ function App() {
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainStreams.filter((s) => s.index === index) }); const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainStreams.filter((s) => s.index === index) });
if (!hideAllNotifications && firstExtractedPath != null) { if (!hideAllNotifications && firstExtractedPath != null) {
showOsNotification(i18n.t('Track has been extracted')); showOsNotification(i18n.t('Track has been extracted'));
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') }); openExportFinishedDialog({ filePath: firstExtractedPath, children: i18n.t('Track has been extracted') });
} }
} catch (err) { } catch (err) {
showOsNotification(i18n.t('Failed to extract track')); showOsNotification(i18n.t('Failed to extract track'));
@ -2139,7 +2139,7 @@ function App() {
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
}, [customOutDir, extractStreams, filePath, hideAllNotifications, mainStreams, setWorking, showOsNotification, workingRef]); }, [customOutDir, extractStreams, filePath, hideAllNotifications, mainStreams, openExportFinishedDialog, setWorking, showOsNotification, workingRef]);
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]); const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);

@ -1,10 +1,13 @@
import React, { MouseEventHandler, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react'; import React, { MouseEventHandler, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { FaCheckCircle, FaInfoCircle } from 'react-icons/fa';
import * as Dialog from './Dialog'; import * as Dialog from './Dialog';
import * as AlertDialog from './AlertDialog'; import * as AlertDialog from './AlertDialog';
import { DialogButton } from './Button'; import { DialogButton } from './Button';
import { showItemInFolder } from '../util';
import { ListItem, Notices, OutputIncorrectSeeHelpMenu, UnorderedList, Warnings } from '../dialogs';
export interface GenericDialogParams { export interface GenericDialogParams {
@ -56,7 +59,9 @@ export default function GenericDialog({ dialog, onOpenChange }: {
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay /> <Dialog.Overlay />
{dialog?.content} <GenericDialogContext.Provider value={context}>
{dialog?.content}
</GenericDialogContext.Provider>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
); );
@ -120,10 +125,89 @@ export function useDialog() {
}); });
}), [showGenericDialog, t]); }), [showGenericDialog, t]);
const openExportFinishedDialog = useCallback(async ({ filePath, children, width = '30em' }: { filePath: string, children: ReactNode, width?: string }) => {
const response = await new Promise<boolean>((resolve) => {
function ExportFinishedDialog() {
const { onOpenChange } = useGenericDialogContext();
const handleConfirmClick = useCallback<MouseEventHandler<HTMLButtonElement>>((e) => {
e.preventDefault();
resolve(true);
onOpenChange(false);
}, [onOpenChange]);
return (
<Dialog.Content aria-describedby={undefined} style={{ width }}>
<Dialog.Title>{t('Success!')}</Dialog.Title>
{children}
<Dialog.ButtonRow>
<Dialog.Close asChild>
<DialogButton>{t('Close')}</DialogButton>
</Dialog.Close>
<DialogButton primary onClick={handleConfirmClick}>{t('Show')}</DialogButton>
</Dialog.ButtonRow>
</Dialog.Content>
);
}
showGenericDialog({
content: <ExportFinishedDialog />,
onClose: () => resolve(false),
});
});
if (response) {
showItemInFolder(filePath);
}
}, [showGenericDialog, t]);
const openCutFinishedDialog = useCallback(async ({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) => {
const hasWarnings = warnings.length > 0;
// https://github.com/mifi/lossless-cut/issues/2048
await openExportFinishedDialog({
filePath,
width: '60em',
children: (
<UnorderedList>
<ListItem icon={<FaCheckCircle />} iconColor={hasWarnings ? 'var(--orange-8)' : 'var(--green-11)'} style={{ fontWeight: 'bold' }}>{hasWarnings ? t('Export finished with warning(s)', { count: warnings.length }) : t('Export is done!')}</ListItem>
<ListItem icon={<FaInfoCircle />}>{t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
<Warnings warnings={warnings} />
</UnorderedList>
),
});
}, [openExportFinishedDialog, t]);
const openConcatFinishedDialog = useCallback(async ({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) => {
const hasWarnings = warnings.length > 0;
await openExportFinishedDialog({
filePath,
width: '60em',
children: (
<UnorderedList>
<ListItem icon={<FaCheckCircle />} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? t('Files merged with warning(s)', { count: warnings.length }) : t('Files merged!')}</ListItem>
<ListItem icon={<FaInfoCircle />}>{t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
<Warnings warnings={warnings} />
</UnorderedList>
),
});
}, [openExportFinishedDialog, t]);
return { return {
genericDialog, genericDialog,
closeGenericDialog, closeGenericDialog,
showGenericDialog, showGenericDialog,
confirmDialog, confirmDialog,
openExportFinishedDialog,
openCutFinishedDialog,
openConcatFinishedDialog,
}; };
} }

@ -1,16 +1,15 @@
import { CSSProperties, ReactNode, useState } from 'react'; import { CSSProperties, ReactNode, useState } from 'react';
import i18n from 'i18next'; import i18n from 'i18next';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import type { SweetAlertOptions } from 'sweetalert2';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { FaArrowRight, FaCheckCircle, FaExclamationTriangle, FaInfoCircle, FaQuestionCircle } from 'react-icons/fa'; import { FaArrowRight, FaExclamationTriangle, FaInfoCircle, FaQuestionCircle } from 'react-icons/fa';
import { formatDuration } from '../util/duration'; import { formatDuration } from '../util/duration';
import Swal, { ReactSwal, swalToastOptions, toast } from '../swal'; import Swal, { ReactSwal } from '../swal';
import { parseYouTube } from '../edlFormats'; import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton'; import CopyClipboardButton from '../components/CopyClipboardButton';
import Checkbox from '../components/Checkbox'; import Checkbox from '../components/Checkbox';
import { isWindows, showItemInFolder } from '../util'; import { isWindows } from '../util';
import { ParseTimecode } from '../types'; import { ParseTimecode } from '../types';
import { FindKeyframeMode } from '../ffmpeg'; import { FindKeyframeMode } from '../ffmpeg';
import { dangerColor } from '../colors'; import { dangerColor } from '../colors';
@ -549,74 +548,26 @@ export async function selectSegmentsByLabelDialog(currentName?: string | undefin
return value; return value;
} }
export async function openDirToast({ filePath, text, html, ...props }: SweetAlertOptions & { filePath: string }) { export const UnorderedList = ({ children }: { children: ReactNode }) => (
const swal = text ? toast : ReactSwal;
// @ts-expect-error todo
const { value } = await swal.fire<string>({
...swalToastOptions,
showConfirmButton: true,
confirmButtonText: i18n.t('Show'),
showCancelButton: true,
cancelButtonText: i18n.t('Close'),
text,
html,
...props,
});
if (value) showItemInFolder(filePath);
}
const UnorderedList = ({ children }: { children: ReactNode }) => (
<ul style={{ paddingLeft: '1em' }}>{children}</ul> <ul style={{ paddingLeft: '1em' }}>{children}</ul>
); );
const ListItem = ({ icon, iconColor, children, style }: { icon: ReactNode, iconColor?: string, children: ReactNode, style?: CSSProperties }) => ( export const ListItem = ({ icon, iconColor, children, style }: { icon: ReactNode, iconColor?: string, children: ReactNode, style?: CSSProperties }) => (
<li style={{ listStyle: 'none', color: iconColor, ...style }}> <li style={{ listStyle: 'none', color: iconColor, ...style }}>
<span style={{ fontSize: '.8em', marginRight: '.3em' }}>{icon}</span> <span style={{ fontSize: '.8em', marginRight: '.3em' }}>{icon}</span>
{children} {children}
</li> </li>
); );
const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => ( export const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
<ListItem key={msg} icon={<FaInfoCircle />} iconColor="var(--blue-9)">{msg}</ListItem> <ListItem key={msg} icon={<FaInfoCircle />} iconColor="var(--blue-9)">{msg}</ListItem>
)); ));
const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => ( export const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
<ListItem key={msg} icon={<FaExclamationTriangle />} iconColor="var(--orange-8)">{msg}</ListItem> <ListItem key={msg} icon={<FaExclamationTriangle />} iconColor="var(--orange-8)">{msg}</ListItem>
)); ));
const OutputIncorrectSeeHelpMenu = () => ( export const OutputIncorrectSeeHelpMenu = () => (
<ListItem icon={<FaQuestionCircle />}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem> <ListItem icon={<FaQuestionCircle />}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>
); );
export async function openExportFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
const hasWarnings = warnings.length > 0;
const html = (
<UnorderedList>
<ListItem icon={<FaCheckCircle />} iconColor={hasWarnings ? 'var(--orange-8)' : 'var(--green-11)'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Export finished with warning(s)', { count: warnings.length }) : i18n.t('Export is done!')}</ListItem>
<ListItem icon={<FaInfoCircle />}>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
<Warnings warnings={warnings} />
</UnorderedList>
);
// https://github.com/mifi/lossless-cut/issues/2048
await openDirToast({ filePath, html, width: 800, position: 'center', timer: undefined });
}
export async function openConcatFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
const hasWarnings = warnings.length > 0;
const html = (
<UnorderedList>
<ListItem icon={<FaCheckCircle />} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Files merged with warning(s)', { count: warnings.length }) : i18n.t('Files merged!')}</ListItem>
<ListItem icon={<FaInfoCircle />}>{i18n.t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
<OutputIncorrectSeeHelpMenu />
<Notices notices={notices} />
<Warnings warnings={warnings} />
</UnorderedList>
);
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
}
export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) { export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) {
const fps = detectedFps || 1; const fps = detectedFps || 1;
const currentFps = fps * outputPlaybackRate; const currentFps = fps * outputPlaybackRate;

@ -33,7 +33,7 @@ const Swal = SwalRaw.mixin({
export default Swal; export default Swal;
export const swalToastOptions: SweetAlertOptions = { const swalToastOptions: SweetAlertOptions = {
...commonSwalOptions, ...commonSwalOptions,
toast: true, toast: true,
width: '50vw', width: '50vw',

Loading…
Cancel
Save