diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 15461e14..c8f49e43 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -79,7 +79,7 @@ import {
import { toast, errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator';
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 { fallbackLng } from './i18n';
import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers } from './segments';
@@ -178,7 +178,7 @@ function App() {
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
const keyBindingByAction = useMemo(() => Object.fromEntries(keyBindings.map((binding) => [binding.action, binding])), [keyBindings]);
@@ -894,7 +894,7 @@ function App() {
if (!hideAllNotifications) {
showOsNotification(i18n.t('Merge finished'));
- openConcatFinishedToast({ filePath: outPath, notices, warnings });
+ openConcatFinishedDialog({ filePath: outPath, notices, warnings });
}
} catch (err) {
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
@@ -926,7 +926,7 @@ function App() {
setWorking(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) => {
// Store paths before we reset state
@@ -1133,7 +1133,7 @@ function App() {
const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0]!.path;
if (!hideAllNotifications) {
showOsNotification(i18n.t('Export finished'));
- openExportFinishedToast({ filePath: revealPath, warnings, notices });
+ openCutFinishedDialog({ filePath: revealPath, warnings, notices });
}
shootConfetti({ ticks: 50 });
@@ -1171,7 +1171,7 @@ function App() {
setWorking(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 () => {
if (!filePath) return;
@@ -1200,12 +1200,12 @@ function App() {
: await captureFrameFromTag({ customOutDir, filePath, time: currentTime, captureFormat, quality: captureFrameQuality, video });
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'));
} finally {
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[]) => {
if (!filePath || detectedFps == null || workingRef.current || segments.length === 0) return;
@@ -1242,7 +1242,7 @@ function App() {
}
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 }) });
+ openExportFinishedDialog({ filePath: lastOutPath, children: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
}
} catch (err) {
showOsNotification(i18n.t('Failed to extract frames'));
@@ -1251,7 +1251,7 @@ function App() {
setWorking(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(() => {
if (currentCutSeg != null) extractSegmentsFramesAsImages([currentCutSeg]);
@@ -1579,7 +1579,7 @@ function App() {
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainCopiedStreams });
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') });
+ openExportFinishedDialog({ filePath: firstExtractedPath, children: i18n.t('All streams have been extracted as separate files') });
}
} catch (err) {
showOsNotification(i18n.t('Failed to extract tracks'));
@@ -1593,7 +1593,7 @@ function App() {
} finally {
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 () => {
@@ -2125,7 +2125,7 @@ function App() {
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainStreams.filter((s) => s.index === index) });
if (!hideAllNotifications && firstExtractedPath != null) {
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) {
showOsNotification(i18n.t('Failed to extract track'));
@@ -2139,7 +2139,7 @@ function App() {
} finally {
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]);
diff --git a/src/renderer/src/components/GenericDialog.tsx b/src/renderer/src/components/GenericDialog.tsx
index 3094611a..f9f63201 100644
--- a/src/renderer/src/components/GenericDialog.tsx
+++ b/src/renderer/src/components/GenericDialog.tsx
@@ -1,10 +1,13 @@
import React, { MouseEventHandler, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
+import { FaCheckCircle, FaInfoCircle } from 'react-icons/fa';
import * as Dialog from './Dialog';
import * as AlertDialog from './AlertDialog';
import { DialogButton } from './Button';
+import { showItemInFolder } from '../util';
+import { ListItem, Notices, OutputIncorrectSeeHelpMenu, UnorderedList, Warnings } from '../dialogs';
export interface GenericDialogParams {
@@ -56,7 +59,9 @@ export default function GenericDialog({ dialog, onOpenChange }: {
- {dialog?.content}
+
+ {dialog?.content}
+
);
@@ -120,10 +125,89 @@ export function useDialog() {
});
}), [showGenericDialog, t]);
+ const openExportFinishedDialog = useCallback(async ({ filePath, children, width = '30em' }: { filePath: string, children: ReactNode, width?: string }) => {
+ const response = await new Promise((resolve) => {
+ function ExportFinishedDialog() {
+ const { onOpenChange } = useGenericDialogContext();
+
+ const handleConfirmClick = useCallback>((e) => {
+ e.preventDefault();
+ resolve(true);
+ onOpenChange(false);
+ }, [onOpenChange]);
+
+ return (
+
+ {t('Success!')}
+
+ {children}
+
+
+
+ {t('Close')}
+
+
+ {t('Show')}
+
+
+ );
+ }
+
+ showGenericDialog({
+ content: ,
+ 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: (
+
+ } 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!')}
+ }>{t('Please test the output file in your desired player/editor before you delete the source file.')}
+
+
+
+
+ ),
+ });
+ }, [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: (
+
+ } iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? t('Files merged with warning(s)', { count: warnings.length }) : t('Files merged!')}
+ }>{t('Please test the output files in your desired player/editor before you delete the source files.')}
+
+
+
+
+ ),
+ });
+ }, [openExportFinishedDialog, t]);
+
return {
genericDialog,
closeGenericDialog,
showGenericDialog,
confirmDialog,
+ openExportFinishedDialog,
+ openCutFinishedDialog,
+ openConcatFinishedDialog,
};
}
diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx
index c7e529fa..9fca453d 100644
--- a/src/renderer/src/dialogs/index.tsx
+++ b/src/renderer/src/dialogs/index.tsx
@@ -1,16 +1,15 @@
import { CSSProperties, ReactNode, useState } from 'react';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
-import type { SweetAlertOptions } from 'sweetalert2';
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 Swal, { ReactSwal, swalToastOptions, toast } from '../swal';
+import Swal, { ReactSwal } from '../swal';
import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton';
import Checkbox from '../components/Checkbox';
-import { isWindows, showItemInFolder } from '../util';
+import { isWindows } from '../util';
import { ParseTimecode } from '../types';
import { FindKeyframeMode } from '../ffmpeg';
import { dangerColor } from '../colors';
@@ -549,74 +548,26 @@ export async function selectSegmentsByLabelDialog(currentName?: string | undefin
return value;
}
-export async function openDirToast({ filePath, text, html, ...props }: SweetAlertOptions & { filePath: string }) {
- const swal = text ? toast : ReactSwal;
-
- // @ts-expect-error todo
- const { value } = await swal.fire({
- ...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 }) => (
+export const UnorderedList = ({ children }: { children: ReactNode }) => (
);
-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 }) => (
{icon}
{children}
);
-const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
+export const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
} iconColor="var(--blue-9)">{msg}
));
-const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
+export const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
} iconColor="var(--orange-8)">{msg}
));
-const OutputIncorrectSeeHelpMenu = () => (
+export const OutputIncorrectSeeHelpMenu = () => (
}>{i18n.t('If output does not look right, see the Help menu.')}
);
-export async function openExportFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
- const hasWarnings = warnings.length > 0;
- const html = (
-
- } 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!')}
- }>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}
-
-
-
-
- );
-
- // 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 = (
-
- } iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Files merged with warning(s)', { count: warnings.length }) : i18n.t('Files merged!')}
- }>{i18n.t('Please test the output files in your desired player/editor before you delete the source files.')}
-
-
-
-
- );
-
- await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
-}
-
export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) {
const fps = detectedFps || 1;
const currentFps = fps * outputPlaybackRate;
diff --git a/src/renderer/src/swal.ts b/src/renderer/src/swal.ts
index 7750d1e1..df5a71ef 100644
--- a/src/renderer/src/swal.ts
+++ b/src/renderer/src/swal.ts
@@ -33,7 +33,7 @@ const Swal = SwalRaw.mixin({
export default Swal;
-export const swalToastOptions: SweetAlertOptions = {
+const swalToastOptions: SweetAlertOptions = {
...commonSwalOptions,
toast: true,
width: '50vw',