Skip to content

Commit 251101c

Browse files
committed
improve export complete dialogs
closes #2589
1 parent 251101d commit 251101c

File tree

4 files changed

+108
-73
lines changed

4 files changed

+108
-73
lines changed

src/renderer/src/App.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ import {
7979
import { toast, errorToast, showPlaybackFailedMessage } from './swal';
8080
import { adjustRate } from './util/rate-calculator';
8181
import { askExtractFramesAsImages } from './dialogs/extractFrames';
82-
import { askForOutDir, askForImportChapters, askForFileOpenAction, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
82+
import { askForOutDir, askForImportChapters, askForFileOpenAction, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
8383
import { openSendReportDialog } from './reporting';
8484
import { fallbackLng } from './i18n';
8585
import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers } from './segments';
@@ -178,7 +178,7 @@ function App() {
178178

179179
const { withErrorHandling, handleError, genericError, setGenericError } = useErrorHandling();
180180

181-
const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog } = useDialog();
181+
const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog, openExportFinishedDialog, openCutFinishedDialog, openConcatFinishedDialog } = useDialog();
182182

183183
// Note that each action may be multiple key bindings and this will only be the first binding for each action
184184
const keyBindingByAction = useMemo(() => Object.fromEntries(keyBindings.map((binding) => [binding.action, binding])), [keyBindings]);
@@ -894,7 +894,7 @@ function App() {
894894

895895
if (!hideAllNotifications) {
896896
showOsNotification(i18n.t('Merge finished'));
897-
openConcatFinishedToast({ filePath: outPath, notices, warnings });
897+
openConcatFinishedDialog({ filePath: outPath, notices, warnings });
898898
}
899899
} catch (err) {
900900
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
@@ -926,7 +926,7 @@ function App() {
926926
setWorking(undefined);
927927
setProgress(undefined);
928928
}
929-
}, [workingRef, setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, showOsNotification, handleConcatFailed]);
929+
}, [workingRef, setWorking, ensureWritableOutDir, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, showOsNotification, openConcatFinishedDialog, handleConcatFailed]);
930930

931931
const cleanupFiles = useCallback(async (cleanupChoices2: CleanupChoicesType) => {
932932
// Store paths before we reset state
@@ -1133,7 +1133,7 @@ function App() {
11331133
const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0]!.path;
11341134
if (!hideAllNotifications) {
11351135
showOsNotification(i18n.t('Export finished'));
1136-
openExportFinishedToast({ filePath: revealPath, warnings, notices });
1136+
openCutFinishedDialog({ filePath: revealPath, warnings, notices });
11371137
}
11381138

11391139
shootConfetti({ ticks: 50 });
@@ -1171,7 +1171,7 @@ function App() {
11711171
setWorking(undefined);
11721172
setProgress(undefined);
11731173
}
1174-
}, [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]);
1174+
}, [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]);
11751175

11761176
const onExportPress = useCallback(async () => {
11771177
if (!filePath) return;
@@ -1200,12 +1200,12 @@ function App() {
12001200
: await captureFrameFromTag({ customOutDir, filePath, time: currentTime, captureFormat, quality: captureFrameQuality, video });
12011201

12021202
shootConfetti();
1203-
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
1203+
if (!hideAllNotifications) openExportFinishedDialog({ filePath: outPath, children: `${i18n.t('Screenshot captured to:')} ${outPath}` });
12041204
}, i18n.t('Failed to capture frame'));
12051205
} finally {
12061206
setWorking(undefined);
12071207
}
1208-
}, [filePath, workingRef, setWorking, withErrorHandling, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);
1208+
}, [filePath, workingRef, setWorking, withErrorHandling, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications, openExportFinishedDialog]);
12091209

12101210
const extractSegmentsFramesAsImages = useCallback(async (segments: SegmentBase[]) => {
12111211
if (!filePath || detectedFps == null || workingRef.current || segments.length === 0) return;
@@ -1242,7 +1242,7 @@ function App() {
12421242
}
12431243
if (!hideAllNotifications && lastOutPath != null) {
12441244
showOsNotification(i18n.t('Frames have been extracted'));
1245-
openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
1245+
openExportFinishedDialog({ filePath: lastOutPath, children: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
12461246
}
12471247
} catch (err) {
12481248
showOsNotification(i18n.t('Failed to extract frames'));
@@ -1251,7 +1251,7 @@ function App() {
12511251
setWorking(undefined);
12521252
setProgress(undefined);
12531253
}
1254-
}, [filePath, detectedFps, workingRef, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, outputDir, handleError]);
1254+
}, [filePath, detectedFps, workingRef, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, openExportFinishedDialog, outputDir, handleError]);
12551255

12561256
const extractCurrentSegmentFramesAsImages = useCallback(() => {
12571257
if (currentCutSeg != null) extractSegmentsFramesAsImages([currentCutSeg]);
@@ -1579,7 +1579,7 @@ function App() {
15791579
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainCopiedStreams });
15801580
if (!hideAllNotifications && firstExtractedPath != null) {
15811581
showOsNotification(i18n.t('All tracks have been extracted'));
1582-
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') });
1582+
openExportFinishedDialog({ filePath: firstExtractedPath, children: i18n.t('All streams have been extracted as separate files') });
15831583
}
15841584
} catch (err) {
15851585
showOsNotification(i18n.t('Failed to extract tracks'));
@@ -1593,7 +1593,7 @@ function App() {
15931593
} finally {
15941594
setWorking(undefined);
15951595
}
1596-
}, [confirmDialog, customOutDir, extractStreams, filePath, hideAllNotifications, mainCopiedStreams, setWorking, showOsNotification, t, workingRef]);
1596+
}, [confirmDialog, customOutDir, extractStreams, filePath, hideAllNotifications, mainCopiedStreams, openExportFinishedDialog, setWorking, showOsNotification, t, workingRef]);
15971597

15981598

15991599
const askStartTimeOffset = useCallback(async () => {
@@ -2125,7 +2125,7 @@ function App() {
21252125
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainStreams.filter((s) => s.index === index) });
21262126
if (!hideAllNotifications && firstExtractedPath != null) {
21272127
showOsNotification(i18n.t('Track has been extracted'));
2128-
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') });
2128+
openExportFinishedDialog({ filePath: firstExtractedPath, children: i18n.t('Track has been extracted') });
21292129
}
21302130
} catch (err) {
21312131
showOsNotification(i18n.t('Failed to extract track'));
@@ -2139,7 +2139,7 @@ function App() {
21392139
} finally {
21402140
setWorking(undefined);
21412141
}
2142-
}, [customOutDir, extractStreams, filePath, hideAllNotifications, mainStreams, setWorking, showOsNotification, workingRef]);
2142+
}, [customOutDir, extractStreams, filePath, hideAllNotifications, mainStreams, openExportFinishedDialog, setWorking, showOsNotification, workingRef]);
21432143

21442144
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);
21452145

src/renderer/src/components/GenericDialog.tsx

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { MouseEventHandler, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import invariant from 'tiny-invariant';
4+
import { FaCheckCircle, FaInfoCircle } from 'react-icons/fa';
45

56
import * as Dialog from './Dialog';
67
import * as AlertDialog from './AlertDialog';
78
import { DialogButton } from './Button';
9+
import { showItemInFolder } from '../util';
10+
import { ListItem, Notices, OutputIncorrectSeeHelpMenu, UnorderedList, Warnings } from '../dialogs';
811

912

1013
export interface GenericDialogParams {
@@ -56,7 +59,9 @@ export default function GenericDialog({ dialog, onOpenChange }: {
5659
<Dialog.Portal>
5760
<Dialog.Overlay />
5861

59-
{dialog?.content}
62+
<GenericDialogContext.Provider value={context}>
63+
{dialog?.content}
64+
</GenericDialogContext.Provider>
6065
</Dialog.Portal>
6166
</Dialog.Root>
6267
);
@@ -120,10 +125,89 @@ export function useDialog() {
120125
});
121126
}), [showGenericDialog, t]);
122127

128+
const openExportFinishedDialog = useCallback(async ({ filePath, children, width = '30em' }: { filePath: string, children: ReactNode, width?: string }) => {
129+
const response = await new Promise<boolean>((resolve) => {
130+
function ExportFinishedDialog() {
131+
const { onOpenChange } = useGenericDialogContext();
132+
133+
const handleConfirmClick = useCallback<MouseEventHandler<HTMLButtonElement>>((e) => {
134+
e.preventDefault();
135+
resolve(true);
136+
onOpenChange(false);
137+
}, [onOpenChange]);
138+
139+
return (
140+
<Dialog.Content aria-describedby={undefined} style={{ width }}>
141+
<Dialog.Title>{t('Success!')}</Dialog.Title>
142+
143+
{children}
144+
145+
<Dialog.ButtonRow>
146+
<Dialog.Close asChild>
147+
<DialogButton>{t('Close')}</DialogButton>
148+
</Dialog.Close>
149+
150+
<DialogButton primary onClick={handleConfirmClick}>{t('Show')}</DialogButton>
151+
</Dialog.ButtonRow>
152+
</Dialog.Content>
153+
);
154+
}
155+
156+
showGenericDialog({
157+
content: <ExportFinishedDialog />,
158+
onClose: () => resolve(false),
159+
});
160+
});
161+
162+
if (response) {
163+
showItemInFolder(filePath);
164+
}
165+
}, [showGenericDialog, t]);
166+
167+
const openCutFinishedDialog = useCallback(async ({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) => {
168+
const hasWarnings = warnings.length > 0;
169+
170+
// https://github.com/mifi/lossless-cut/issues/2048
171+
await openExportFinishedDialog({
172+
filePath,
173+
width: '60em',
174+
children: (
175+
<UnorderedList>
176+
<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>
177+
<ListItem icon={<FaInfoCircle />}>{t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
178+
<OutputIncorrectSeeHelpMenu />
179+
<Notices notices={notices} />
180+
<Warnings warnings={warnings} />
181+
</UnorderedList>
182+
),
183+
});
184+
}, [openExportFinishedDialog, t]);
185+
186+
const openConcatFinishedDialog = useCallback(async ({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) => {
187+
const hasWarnings = warnings.length > 0;
188+
189+
await openExportFinishedDialog({
190+
filePath,
191+
width: '60em',
192+
children: (
193+
<UnorderedList>
194+
<ListItem icon={<FaCheckCircle />} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? t('Files merged with warning(s)', { count: warnings.length }) : t('Files merged!')}</ListItem>
195+
<ListItem icon={<FaInfoCircle />}>{t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
196+
<OutputIncorrectSeeHelpMenu />
197+
<Notices notices={notices} />
198+
<Warnings warnings={warnings} />
199+
</UnorderedList>
200+
),
201+
});
202+
}, [openExportFinishedDialog, t]);
203+
123204
return {
124205
genericDialog,
125206
closeGenericDialog,
126207
showGenericDialog,
127208
confirmDialog,
209+
openExportFinishedDialog,
210+
openCutFinishedDialog,
211+
openConcatFinishedDialog,
128212
};
129213
}

src/renderer/src/dialogs/index.tsx

Lines changed: 8 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { CSSProperties, ReactNode, useState } from 'react';
22
import i18n from 'i18next';
33
import { Trans } from 'react-i18next';
4-
import type { SweetAlertOptions } from 'sweetalert2';
54
import invariant from 'tiny-invariant';
6-
import { FaArrowRight, FaCheckCircle, FaExclamationTriangle, FaInfoCircle, FaQuestionCircle } from 'react-icons/fa';
5+
import { FaArrowRight, FaExclamationTriangle, FaInfoCircle, FaQuestionCircle } from 'react-icons/fa';
76

87
import { formatDuration } from '../util/duration';
9-
import Swal, { ReactSwal, swalToastOptions, toast } from '../swal';
8+
import Swal, { ReactSwal } from '../swal';
109
import { parseYouTube } from '../edlFormats';
1110
import CopyClipboardButton from '../components/CopyClipboardButton';
1211
import Checkbox from '../components/Checkbox';
13-
import { isWindows, showItemInFolder } from '../util';
12+
import { isWindows } from '../util';
1413
import { ParseTimecode } from '../types';
1514
import { FindKeyframeMode } from '../ffmpeg';
1615
import { dangerColor } from '../colors';
@@ -549,74 +548,26 @@ export async function selectSegmentsByLabelDialog(currentName?: string | undefin
549548
return value;
550549
}
551550

552-
export async function openDirToast({ filePath, text, html, ...props }: SweetAlertOptions & { filePath: string }) {
553-
const swal = text ? toast : ReactSwal;
554-
555-
// @ts-expect-error todo
556-
const { value } = await swal.fire<string>({
557-
...swalToastOptions,
558-
showConfirmButton: true,
559-
confirmButtonText: i18n.t('Show'),
560-
showCancelButton: true,
561-
cancelButtonText: i18n.t('Close'),
562-
text,
563-
html,
564-
...props,
565-
});
566-
if (value) showItemInFolder(filePath);
567-
}
568-
569-
const UnorderedList = ({ children }: { children: ReactNode }) => (
551+
export const UnorderedList = ({ children }: { children: ReactNode }) => (
570552
<ul style={{ paddingLeft: '1em' }}>{children}</ul>
571553
);
572-
const ListItem = ({ icon, iconColor, children, style }: { icon: ReactNode, iconColor?: string, children: ReactNode, style?: CSSProperties }) => (
554+
export const ListItem = ({ icon, iconColor, children, style }: { icon: ReactNode, iconColor?: string, children: ReactNode, style?: CSSProperties }) => (
573555
<li style={{ listStyle: 'none', color: iconColor, ...style }}>
574556
<span style={{ fontSize: '.8em', marginRight: '.3em' }}>{icon}</span>
575557
{children}
576558
</li>
577559
);
578560

579-
const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
561+
export const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
580562
<ListItem key={msg} icon={<FaInfoCircle />} iconColor="var(--blue-9)">{msg}</ListItem>
581563
));
582-
const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
564+
export const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
583565
<ListItem key={msg} icon={<FaExclamationTriangle />} iconColor="var(--orange-8)">{msg}</ListItem>
584566
));
585-
const OutputIncorrectSeeHelpMenu = () => (
567+
export const OutputIncorrectSeeHelpMenu = () => (
586568
<ListItem icon={<FaQuestionCircle />}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>
587569
);
588570

589-
export async function openExportFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
590-
const hasWarnings = warnings.length > 0;
591-
const html = (
592-
<UnorderedList>
593-
<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>
594-
<ListItem icon={<FaInfoCircle />}>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
595-
<OutputIncorrectSeeHelpMenu />
596-
<Notices notices={notices} />
597-
<Warnings warnings={warnings} />
598-
</UnorderedList>
599-
);
600-
601-
// https://github.com/mifi/lossless-cut/issues/2048
602-
await openDirToast({ filePath, html, width: 800, position: 'center', timer: undefined });
603-
}
604-
605-
export async function openConcatFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
606-
const hasWarnings = warnings.length > 0;
607-
const html = (
608-
<UnorderedList>
609-
<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>
610-
<ListItem icon={<FaInfoCircle />}>{i18n.t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
611-
<OutputIncorrectSeeHelpMenu />
612-
<Notices notices={notices} />
613-
<Warnings warnings={warnings} />
614-
</UnorderedList>
615-
);
616-
617-
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
618-
}
619-
620571
export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) {
621572
const fps = detectedFps || 1;
622573
const currentFps = fps * outputPlaybackRate;

src/renderer/src/swal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const Swal = SwalRaw.mixin({
3333

3434
export default Swal;
3535

36-
export const swalToastOptions: SweetAlertOptions = {
36+
const swalToastOptions: SweetAlertOptions = {
3737
...commonSwalOptions,
3838
toast: true,
3939
width: '50vw',

0 commit comments

Comments
 (0)