1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

improve error handling

and types
This commit is contained in:
Mikael Finstad 2024-09-26 13:11:07 +02:00
parent e84b9a8266
commit 9cdd5edbfb
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
6 changed files with 146 additions and 131 deletions

13
src/renderer/errors.ts Normal file
View File

@ -0,0 +1,13 @@
export class DirectoryAccessDeclinedError extends Error {
constructor() {
super();
this.name = 'DirectoryAccessDeclinedError';
}
}
export class UnsupportedFileError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnsupportedFileError';
}
}

View File

@ -24,7 +24,7 @@ import useKeyboard from './hooks/useKeyboard';
import useFileFormatState from './hooks/useFileFormatState'; import useFileFormatState from './hooks/useFileFormatState';
import useFrameCapture from './hooks/useFrameCapture'; import useFrameCapture from './hooks/useFrameCapture';
import useSegments from './hooks/useSegments'; import useSegments from './hooks/useSegments';
import useDirectoryAccess, { DirectoryAccessDeclinedError } from './hooks/useDirectoryAccess'; import useDirectoryAccess from './hooks/useDirectoryAccess';
import { UserSettingsContext, SegColorsContext, UserSettingsContextType } from './contexts'; import { UserSettingsContext, SegColorsContext, UserSettingsContextType } from './contexts';
@ -72,6 +72,7 @@ import {
isMuxNotSupported, isMuxNotSupported,
getDownloadMediaOutPath, getDownloadMediaOutPath,
isAbortedError, isAbortedError,
withErrorHandling,
} from './util'; } from './util';
import { toast, errorToast, showPlaybackFailedMessage } from './swal'; import { toast, errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator'; import { adjustRate } from './util/rate-calculator';
@ -98,6 +99,7 @@ import useSubtitles from './hooks/useSubtitles';
import useStreamsMeta from './hooks/useStreamsMeta'; import useStreamsMeta from './hooks/useStreamsMeta';
import { bottomStyle, videoStyle } from './styles'; import { bottomStyle, videoStyle } from './styles';
import styles from './App.module.css'; import styles from './App.module.css';
import { DirectoryAccessDeclinedError } from '../errors';
const electron = window.require('electron'); const electron = window.require('electron');
const { exists } = window.require('fs-extra'); const { exists } = window.require('fs-extra');
@ -492,13 +494,14 @@ function App() {
} }
const subtitleStream = index != null && subtitleStreams.find((s) => s.index === index); const subtitleStream = index != null && subtitleStreams.find((s) => s.index === index);
if (!subtitleStream || workingRef.current) return; if (!subtitleStream || workingRef.current) return;
try {
setWorking({ text: i18n.t('Loading subtitle') }); setWorking({ text: i18n.t('Loading subtitle') });
try {
await withErrorHandling(async () => {
invariant(filePath != null); invariant(filePath != null);
await loadSubtitle({ filePath, index, subtitleStream }); await loadSubtitle({ filePath, index, subtitleStream });
setActiveSubtitleStreamIndex(index); setActiveSubtitleStreamIndex(index);
} catch (err) { }, i18n.t('Failed to load subtitles from track {{index}}', { index }));
handleError(`Failed to extract subtitles for stream ${index}`, err instanceof Error && err.message);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
@ -646,10 +649,10 @@ function App() {
if (!speed) return; if (!speed) return;
if (workingRef.current) return; if (workingRef.current) return;
try {
setWorking({ text: i18n.t('Batch converting to supported format') }); setWorking({ text: i18n.t('Batch converting to supported format') });
setCutProgress(0); setCutProgress(0);
try {
await withErrorHandling(async () => {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const path of filePaths) { for (const path of filePaths) {
try { try {
@ -670,9 +673,7 @@ function App() {
} }
if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}`, timer: null as unknown as undefined, showConfirmButton: true }); if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}`, timer: null as unknown as undefined, showConfirmButton: true });
} catch (err) { }, i18n.t('Failed to batch convert to supported format'));
errorToast(i18n.t('Failed to batch convert to supported format'));
console.error('Failed to html5ify', err);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
setCutProgress(undefined); setCutProgress(undefined);
@ -906,7 +907,7 @@ function App() {
resetState(); resetState();
} }
try { await withErrorHandling(async () => {
const abortController = new AbortController(); const abortController = new AbortController();
setWorking({ text: i18n.t('Cleaning up'), abortController }); setWorking({ text: i18n.t('Cleaning up'), abortController });
console.log('Cleaning up files', cleanupChoices2); console.log('Cleaning up files', cleanupChoices2);
@ -917,10 +918,7 @@ function App() {
if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath); if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath);
await deleteFiles({ paths: pathsToDelete, deleteIfTrashFails: cleanupChoices2.deleteIfTrashFails, signal: abortController.signal }); await deleteFiles({ paths: pathsToDelete, deleteIfTrashFails: cleanupChoices2.deleteIfTrashFails, signal: abortController.signal });
} catch (err) { }, (err) => i18n.t('Unable to delete file: {{message}}', { message: err instanceof Error ? err.message : String(err) }));
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err instanceof Error ? err.message : String(err) }));
console.error(err);
}
}, [batchListRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]); }, [batchListRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]);
const askForCleanupChoices = useCallback(async () => { const askForCleanupChoices = useCallback(async () => {
@ -1148,7 +1146,7 @@ function App() {
const captureSnapshot = useCallback(async () => { const captureSnapshot = useCallback(async () => {
if (!filePath) return; if (!filePath) return;
try { await withErrorHandling(async () => {
const currentTime = getRelevantTime(); const currentTime = getRelevantTime();
const video = videoRef.current; const video = videoRef.current;
if (video == null) throw new Error(); if (video == null) throw new Error();
@ -1158,10 +1156,7 @@ function App() {
: await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality: captureFrameQuality }); : await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality: captureFrameQuality });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` }); if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
} catch (err) { }, i18n.t('Failed to capture frame'));
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
}, [filePath, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]); }, [filePath, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);
const extractSegmentFramesAsImages = useCallback(async (segIds: string[]) => { const extractSegmentFramesAsImages = useCallback(async (segIds: string[]) => {
@ -1199,7 +1194,6 @@ function App() {
} }
} catch (err) { } catch (err) {
showOsNotification(i18n.t('Failed to extract frames')); showOsNotification(i18n.t('Failed to extract frames'));
handleError(err); handleError(err);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
@ -1577,10 +1571,9 @@ function App() {
if (workingRef.current) return; if (workingRef.current) return;
try { try {
setWorking({ text: i18n.t('Converting to supported format') }); setWorking({ text: i18n.t('Converting to supported format') });
await withErrorHandling(async () => {
await html5ifyAndLoad(customOutDir, filePath, selectedOption, hasVideo, hasAudio); await html5ifyAndLoad(customOutDir, filePath, selectedOption, hasVideo, hasAudio);
} catch (err) { }, i18n.t('Failed to convert file. Try a different conversion'));
errorToast(i18n.t('Failed to convert file. Try a different conversion'));
console.error('Failed to html5ify file', err);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
@ -1605,6 +1598,7 @@ function App() {
const tryFixInvalidDuration = useCallback(async () => { const tryFixInvalidDuration = useCallback(async () => {
if (!checkFileOpened() || workingRef.current) return; if (!checkFileOpened() || workingRef.current) return;
try { try {
await withErrorHandling(async () => {
setWorking({ text: i18n.t('Fixing file duration') }); setWorking({ text: i18n.t('Fixing file duration') });
setCutProgress(0); setCutProgress(0);
invariant(fileFormat != null); invariant(fileFormat != null);
@ -1612,9 +1606,7 @@ function App() {
showNotification({ 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) { }, i18n.t('Failed to fix file duration'));
errorToast(i18n.t('Failed to fix file duration'));
console.error('Failed to fix file duration', err);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
setCutProgress(undefined); setCutProgress(undefined);
@ -1652,15 +1644,12 @@ function App() {
const captureSnapshotAsCoverArt = useCallback(async () => { const captureSnapshotAsCoverArt = useCallback(async () => {
if (!filePath) return; if (!filePath) return;
try { await withErrorHandling(async () => {
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;
showNotification({ 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) { }, i18n.t('Failed to capture frame'));
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, showNotification]); }, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, showNotification]);
const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => { const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => {
@ -1681,7 +1670,7 @@ function App() {
}, []); }, []);
const userOpenFiles = useCallback(async (filePathsIn?: string[]) => { const userOpenFiles = useCallback(async (filePathsIn?: string[]) => {
try { await withErrorHandling(async () => {
let filePaths = filePathsIn; let filePaths = filePathsIn;
if (!filePaths || filePaths.length === 0) return; if (!filePaths || filePaths.length === 0) return;
@ -1804,10 +1793,7 @@ function App() {
} finally { } finally {
setWorking(undefined); setWorking(undefined);
} }
} catch (err) { }, i18n.t('Failed to open file'));
console.error('userOpenFiles', err);
handleError(i18n.t('Failed to open file'), err);
}
}, [workingRef, alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]); }, [workingRef, alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]);
const openFilesDialog = useCallback(async () => { const openFilesDialog = useCallback(async () => {
@ -1840,14 +1826,12 @@ function App() {
}, [isFileOpened, selectedSegments]); }, [isFileOpened, selectedSegments]);
const showIncludeExternalStreamsDialog = useCallback(async () => { const showIncludeExternalStreamsDialog = useCallback(async () => {
try { await withErrorHandling(async () => {
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], title: t('Include more tracks from other file') }); const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], title: t('Include more tracks from other file') });
const [firstFilePath] = filePaths; const [firstFilePath] = filePaths;
if (canceled || firstFilePath == null) return; if (canceled || firstFilePath == null) return;
await addStreamSourceFile(firstFilePath); await addStreamSourceFile(firstFilePath);
} catch (err) { }, i18n.t('Failed to include track'));
handleError(err);
}
}, [addStreamSourceFile, t]); }, [addStreamSourceFile, t]);
const toggleFullscreenVideo = useCallback(async () => { const toggleFullscreenVideo = useCallback(async () => {
@ -1881,6 +1865,7 @@ function App() {
const promptDownloadMediaUrlWrapper = useCallback(async () => { const promptDownloadMediaUrlWrapper = useCallback(async () => {
try { try {
setWorking({ text: t('Downloading URL') }); setWorking({ text: t('Downloading URL') });
await withErrorHandling(async () => {
const newCustomOutDir = await ensureWritableOutDir({ outDir: customOutDir }); const newCustomOutDir = await ensureWritableOutDir({ outDir: customOutDir });
if (newCustomOutDir == null) { if (newCustomOutDir == null) {
errorToast(i18n.t('Please select a working directory first')); errorToast(i18n.t('Please select a working directory first'));
@ -1889,9 +1874,7 @@ function App() {
const outPath = getDownloadMediaOutPath(newCustomOutDir, `downloaded-media-${Date.now()}.mkv`); const outPath = getDownloadMediaOutPath(newCustomOutDir, `downloaded-media-${Date.now()}.mkv`);
const downloaded = await promptDownloadMediaUrl(outPath); const downloaded = await promptDownloadMediaUrl(outPath);
if (downloaded) await loadMedia({ filePath: outPath }); if (downloaded) await loadMedia({ filePath: outPath });
} catch (err) { }, i18n.t('Failed to download URL'));
if (err instanceof DirectoryAccessDeclinedError) return;
handleError(err);
} finally { } finally {
setWorking(); setWorking();
} }
@ -1902,7 +1885,6 @@ function App() {
const mainActions = useMemo(() => { const mainActions = useMemo(() => {
async function exportYouTube() { async function exportYouTube() {
if (!checkFileOpened()) return; if (!checkFileOpened()) return;
await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments)); await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments));
} }
@ -2186,29 +2168,24 @@ function App() {
const tryExportEdlFile = useCallback(async (type: EdlExportType) => { const tryExportEdlFile = useCallback(async (type: EdlExportType) => {
if (!checkFileOpened()) return; if (!checkFileOpened()) return;
try { await withErrorHandling(async () => {
await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount }); await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount });
} catch (err) { }, i18n.t('Failed to export project'));
errorToast(i18n.t('Failed to export project'));
console.error('Failed to export project', type, err);
}
}, [checkFileOpened, customOutDir, filePath, getFrameCount, selectedSegments]); }, [checkFileOpened, customOutDir, filePath, getFrameCount, selectedSegments]);
const importEdlFile = useCallback(async (type: EdlImportType) => { const importEdlFile = useCallback(async (type: EdlImportType) => {
if (!checkFileOpened()) return; if (!checkFileOpened()) return;
try { await withErrorHandling(async () => {
const edl = await askForEdlImport({ type, fps: detectedFps }); const edl = await askForEdlImport({ type, fps: detectedFps });
if (edl.length > 0) loadCutSegments(edl, true); if (edl.length > 0) loadCutSegments(edl, true);
} catch (err) { }, i18n.t('Failed to import project file'));
handleError(err);
}
}, [checkFileOpened, detectedFps, loadCutSegments]); }, [checkFileOpened, detectedFps, loadCutSegments]);
useEffect(() => { useEffect(() => {
const openFiles = (filePaths: string[]) => { userOpenFiles(filePaths.map((p) => resolvePathIfNeeded(p))); }; const openFiles = (filePaths: string[]) => { userOpenFiles(filePaths.map((p) => resolvePathIfNeeded(p))); };
async function actionWithCatch(fn: () => void) { async function actionWithCatch(fn: () => Promise<void>) {
try { try {
await fn(); await fn();
} catch (err) { } catch (err) {

View File

@ -544,8 +544,8 @@ export async function showConcatFailedDialog({ fileFormat }: { fileFormat: strin
return value; return value;
} }
export function openYouTubeChaptersDialog(text: string) { export async function openYouTubeChaptersDialog(text: string) {
ReactSwal.fire({ await ReactSwal.fire({
showCloseButton: true, showCloseButton: true,
title: i18n.t('YouTube Chapters'), title: i18n.t('YouTube Chapters'),
html: ( html: (

View File

@ -11,6 +11,7 @@ import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats'; import { parseSrt, parseSrtToSegments } from './edlFormats';
import { CopyfileStreams, LiteFFprobeStream } from './types'; import { CopyfileStreams, LiteFFprobeStream } from './types';
import { UnsupportedFileError } from '../errors';
const { pathExists } = window.require('fs-extra'); const { pathExists } = window.require('fs-extra');
@ -368,7 +369,7 @@ export async function readFileMeta(filePath: string) {
return { format, streams, chapters }; return { format, streams, chapters };
} catch (err) { } catch (err) {
if (isExecaError(err)) { if (isExecaError(err)) {
throw Object.assign(new Error(`Unsupported file: ${err.message}`), { code: 'LLC_FFPROBE_UNSUPPORTED_FILE' }); throw new UnsupportedFileError(err.message);
} }
throw err; throw err;
} }

View File

@ -5,14 +5,9 @@ import invariant from 'tiny-invariant';
import { getOutDir, getFileDir, checkDirWriteAccess, dirExists, isMasBuild } from '../util'; import { getOutDir, getFileDir, checkDirWriteAccess, dirExists, isMasBuild } from '../util';
import { askForOutDir, askForInputDir } from '../dialogs'; import { askForOutDir, askForInputDir } from '../dialogs';
import { errorToast } from '../swal'; import { errorToast } from '../swal';
import { DirectoryAccessDeclinedError } from '../../errors';
// import isDev from '../isDev'; // import isDev from '../isDev';
export class DirectoryAccessDeclinedError extends Error {
constructor() {
super();
this.name = 'DirectoryAccessDeclinedError';
}
}
// MacOS App Store sandbox doesn't allow reading/writing anywhere, // MacOS App Store sandbox doesn't allow reading/writing anywhere,
// except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog // except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog

View File

@ -10,6 +10,8 @@ import isDev from './isDev';
import Swal, { errorToast, toast } from './swal'; import Swal, { errorToast, toast } from './swal';
import { ffmpegExtractWindow } from './util/constants'; import { ffmpegExtractWindow } from './util/constants';
import { appName } from '../../main/common'; import { appName } from '../../main/common';
import { DirectoryAccessDeclinedError, UnsupportedFileError } from '../errors';
import { Html5ifyMode } from '../../../types';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path'); const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra'); const fsExtra = window.require('fs-extra');
@ -159,29 +161,6 @@ export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo =
} }
} }
export function handleError(arg1: unknown, arg2?: unknown) {
console.error('handleError', arg1, arg2);
let err: Error | undefined;
let str: string | undefined;
if (typeof arg1 === 'string') str = arg1;
else if (typeof arg2 === 'string') str = arg2;
if (arg1 instanceof Error) err = arg1;
else if (arg2 instanceof Error) err = arg2;
if (err != null && 'code' in err && err.code === 'LLC_FFPROBE_UNSUPPORTED_FILE') {
errorToast(i18n.t('Unsupported file'));
} else {
Swal.fire({
icon: 'error',
title: str || i18n.t('An error has occurred.'),
text: err?.message ? err?.message.slice(0, 300) : undefined,
});
}
}
export function filenamify(name: string) { export function filenamify(name: string) {
return name.replaceAll(/[^\w.-]/g, '_'); return name.replaceAll(/[^\w.-]/g, '_');
} }
@ -193,7 +172,7 @@ export function withBlur(cb) {
}; };
} }
export function dragPreventer(ev) { export function dragPreventer(ev: DragEvent) {
ev.preventDefault(); ev.preventDefault();
} }
@ -237,7 +216,7 @@ export const resolvePathIfNeeded = (inPath: string) => (isAbsolute(inPath) ? inP
export const html5ifiedPrefix = 'html5ified-'; export const html5ifiedPrefix = 'html5ified-';
export const html5dummySuffix = 'dummy'; export const html5dummySuffix = 'dummy';
export async function findExistingHtml5FriendlyFile(fp, cod) { export async function findExistingHtml5FriendlyFile(fp: string, cod: string | undefined) {
// The order is the priority we will search: // The order is the priority we will search:
const suffixes = ['slowest', 'slow-audio', 'slow', 'fast-audio-remux', 'fast-audio', 'fast', html5dummySuffix]; const suffixes = ['slowest', 'slow-audio', 'slow', 'fast-audio-remux', 'fast-audio', 'fast', html5dummySuffix];
const prefix = getSuffixedFileName(fp, html5ifiedPrefix); const prefix = getSuffixedFileName(fp, html5ifiedPrefix);
@ -270,7 +249,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
}; };
} }
export function getHtml5ifiedPath(cod: string | undefined, fp, type) { export function getHtml5ifiedPath(cod: string | undefined, fp: string, type: Html5ifyMode) {
// See also inside ffmpegHtml5ify // See also inside ffmpegHtml5ify
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv'; const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` }); return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
@ -335,6 +314,55 @@ export const isMuxNotSupported = (err: InvariantExecaError) => (
&& /Could not write header .*incorrect codec parameters .*Invalid argument/.test(getStdioString(err.stderr) ?? '') && /Could not write header .*incorrect codec parameters .*Invalid argument/.test(getStdioString(err.stderr) ?? '')
); );
export function handleError(arg1: unknown, arg2?: unknown) {
console.error('handleError', arg1, arg2);
let err: Error | undefined;
let str: string | undefined;
if (typeof arg1 === 'string') str = arg1;
else if (typeof arg2 === 'string') str = arg2;
if (arg1 instanceof Error) err = arg1;
else if (arg2 instanceof Error) err = arg2;
if (err instanceof UnsupportedFileError) {
errorToast(i18n.t('Unsupported file'));
} else {
Swal.fire({
icon: 'error',
title: str || i18n.t('An error has occurred.'),
text: err?.message ? err?.message.slice(0, 300) : undefined,
});
}
}
/**
* Run an operation with error handling
*/
export async function withErrorHandling(operation: () => Promise<void>, errorMsgOrFn?: string | ((err: unknown) => string)) {
try {
await operation();
} catch (err) {
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
if (err instanceof UnsupportedFileError) {
errorToast(i18n.t('Unsupported file'));
return;
}
let errorMsg: string | undefined;
if (typeof errorMsgOrFn === 'string') errorMsg = errorMsgOrFn;
if (typeof errorMsgOrFn === 'function') errorMsg = errorMsgOrFn(err);
if (errorMsg != null) {
console.error(errorMsg, err);
errorToast(errorMsg);
} else {
handleError(err);
}
}
}
export async function checkAppPath() { export async function checkAppPath() {
try { try {
const forceCheck = false; const forceCheck = false;
@ -364,10 +392,10 @@ export async function checkAppPath() {
} }
// https://stackoverflow.com/a/2450976/6519037 // https://stackoverflow.com/a/2450976/6519037
export function shuffleArray(arrayIn) { export function shuffleArray<T>(arrayIn: T[]) {
const array = [...arrayIn]; const array = [...arrayIn];
let currentIndex = array.length; let currentIndex = array.length;
let randomIndex; let randomIndex: number;
// While there remain elements to shuffle... // While there remain elements to shuffle...
while (currentIndex !== 0) { while (currentIndex !== 0) {
@ -377,23 +405,24 @@ export function shuffleArray(arrayIn) {
// And swap it with the current element. // And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [ [array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]]; array[randomIndex]!, array[currentIndex]!,
] as const;
} }
return array; return array;
} }
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
export function escapeRegExp(string) { export function escapeRegExp(str: string) {
// eslint-disable-next-line unicorn/better-regex // eslint-disable-next-line unicorn/better-regex
return string.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string return str.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
} }
export const readFileSize = async (path) => (await stat(path)).size; export const readFileSize = async (path: string) => (await stat(path)).size;
export const readFileSizes = (paths) => pMap(paths, async (path) => readFileSize(path), { concurrency: 5 }); export const readFileSizes = (paths: string[]) => pMap(paths, async (path) => readFileSize(path), { concurrency: 5 });
export function checkFileSizes(inputSize, outputSize) { export function checkFileSizes(inputSize: number, outputSize: number) {
const diff = Math.abs(outputSize - inputSize); const diff = Math.abs(outputSize - inputSize);
const relDiff = diff / inputSize; const relDiff = diff / inputSize;
const maxDiffPercent = 5; const maxDiffPercent = 5;