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:
parent
e84b9a8266
commit
9cdd5edbfb
13
src/renderer/errors.ts
Normal file
13
src/renderer/errors.ts
Normal 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';
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ import useKeyboard from './hooks/useKeyboard';
|
||||
import useFileFormatState from './hooks/useFileFormatState';
|
||||
import useFrameCapture from './hooks/useFrameCapture';
|
||||
import useSegments from './hooks/useSegments';
|
||||
import useDirectoryAccess, { DirectoryAccessDeclinedError } from './hooks/useDirectoryAccess';
|
||||
import useDirectoryAccess from './hooks/useDirectoryAccess';
|
||||
|
||||
import { UserSettingsContext, SegColorsContext, UserSettingsContextType } from './contexts';
|
||||
|
||||
@ -72,6 +72,7 @@ import {
|
||||
isMuxNotSupported,
|
||||
getDownloadMediaOutPath,
|
||||
isAbortedError,
|
||||
withErrorHandling,
|
||||
} from './util';
|
||||
import { toast, errorToast, showPlaybackFailedMessage } from './swal';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
@ -98,6 +99,7 @@ import useSubtitles from './hooks/useSubtitles';
|
||||
import useStreamsMeta from './hooks/useStreamsMeta';
|
||||
import { bottomStyle, videoStyle } from './styles';
|
||||
import styles from './App.module.css';
|
||||
import { DirectoryAccessDeclinedError } from '../errors';
|
||||
|
||||
const electron = window.require('electron');
|
||||
const { exists } = window.require('fs-extra');
|
||||
@ -492,13 +494,14 @@ function App() {
|
||||
}
|
||||
const subtitleStream = index != null && subtitleStreams.find((s) => s.index === index);
|
||||
if (!subtitleStream || workingRef.current) return;
|
||||
|
||||
setWorking({ text: i18n.t('Loading subtitle') });
|
||||
try {
|
||||
setWorking({ text: i18n.t('Loading subtitle') });
|
||||
invariant(filePath != null);
|
||||
await loadSubtitle({ filePath, index, subtitleStream });
|
||||
setActiveSubtitleStreamIndex(index);
|
||||
} catch (err) {
|
||||
handleError(`Failed to extract subtitles for stream ${index}`, err instanceof Error && err.message);
|
||||
await withErrorHandling(async () => {
|
||||
invariant(filePath != null);
|
||||
await loadSubtitle({ filePath, index, subtitleStream });
|
||||
setActiveSubtitleStreamIndex(index);
|
||||
}, i18n.t('Failed to load subtitles from track {{index}}', { index }));
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
}
|
||||
@ -646,33 +649,31 @@ function App() {
|
||||
if (!speed) return;
|
||||
|
||||
if (workingRef.current) return;
|
||||
setWorking({ text: i18n.t('Batch converting to supported format') });
|
||||
setCutProgress(0);
|
||||
try {
|
||||
setWorking({ text: i18n.t('Batch converting to supported format') });
|
||||
setCutProgress(0);
|
||||
await withErrorHandling(async () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of filePaths) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const newCustomOutDir = await ensureWritableOutDir({ inputPath: path, outDir: customOutDir });
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of filePaths) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const newCustomOutDir = await ensureWritableOutDir({ inputPath: path, outDir: customOutDir });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await html5ify({ customOutDir: newCustomOutDir, filePath: path, speed, hasAudio: true, hasVideo: true, onProgress: setTotalProgress });
|
||||
} catch (err2) {
|
||||
if (err2 instanceof DirectoryAccessDeclinedError) return;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await html5ify({ customOutDir: newCustomOutDir, filePath: path, speed, hasAudio: true, hasVideo: true, onProgress: setTotalProgress });
|
||||
} catch (err2) {
|
||||
if (err2 instanceof DirectoryAccessDeclinedError) return;
|
||||
console.error('Failed to html5ify', path, err2);
|
||||
failedFiles.push(path);
|
||||
}
|
||||
|
||||
console.error('Failed to html5ify', path, err2);
|
||||
failedFiles.push(path);
|
||||
i += 1;
|
||||
setTotalProgress();
|
||||
}
|
||||
|
||||
i += 1;
|
||||
setTotalProgress();
|
||||
}
|
||||
|
||||
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) {
|
||||
errorToast(i18n.t('Failed to batch convert to supported format'));
|
||||
console.error('Failed to html5ify', err);
|
||||
if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}`, timer: null as unknown as undefined, showConfirmButton: true });
|
||||
}, i18n.t('Failed to batch convert to supported format'));
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
setCutProgress(undefined);
|
||||
@ -906,7 +907,7 @@ function App() {
|
||||
resetState();
|
||||
}
|
||||
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
const abortController = new AbortController();
|
||||
setWorking({ text: i18n.t('Cleaning up'), abortController });
|
||||
console.log('Cleaning up files', cleanupChoices2);
|
||||
@ -917,10 +918,7 @@ function App() {
|
||||
if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath);
|
||||
|
||||
await deleteFiles({ paths: pathsToDelete, deleteIfTrashFails: cleanupChoices2.deleteIfTrashFails, signal: abortController.signal });
|
||||
} catch (err) {
|
||||
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err instanceof Error ? err.message : String(err) }));
|
||||
console.error(err);
|
||||
}
|
||||
}, (err) => i18n.t('Unable to delete file: {{message}}', { message: err instanceof Error ? err.message : String(err) }));
|
||||
}, [batchListRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]);
|
||||
|
||||
const askForCleanupChoices = useCallback(async () => {
|
||||
@ -1148,7 +1146,7 @@ function App() {
|
||||
const captureSnapshot = useCallback(async () => {
|
||||
if (!filePath) return;
|
||||
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
const currentTime = getRelevantTime();
|
||||
const video = videoRef.current;
|
||||
if (video == null) throw new Error();
|
||||
@ -1158,10 +1156,7 @@ function App() {
|
||||
: await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality: captureFrameQuality });
|
||||
|
||||
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorToast(i18n.t('Failed to capture frame'));
|
||||
}
|
||||
}, i18n.t('Failed to capture frame'));
|
||||
}, [filePath, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);
|
||||
|
||||
const extractSegmentFramesAsImages = useCallback(async (segIds: string[]) => {
|
||||
@ -1199,7 +1194,6 @@ function App() {
|
||||
}
|
||||
} catch (err) {
|
||||
showOsNotification(i18n.t('Failed to extract frames'));
|
||||
|
||||
handleError(err);
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
@ -1577,10 +1571,9 @@ function App() {
|
||||
if (workingRef.current) return;
|
||||
try {
|
||||
setWorking({ text: i18n.t('Converting to supported format') });
|
||||
await html5ifyAndLoad(customOutDir, filePath, selectedOption, hasVideo, hasAudio);
|
||||
} catch (err) {
|
||||
errorToast(i18n.t('Failed to convert file. Try a different conversion'));
|
||||
console.error('Failed to html5ify file', err);
|
||||
await withErrorHandling(async () => {
|
||||
await html5ifyAndLoad(customOutDir, filePath, selectedOption, hasVideo, hasAudio);
|
||||
}, i18n.t('Failed to convert file. Try a different conversion'));
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
}
|
||||
@ -1605,16 +1598,15 @@ function App() {
|
||||
const tryFixInvalidDuration = useCallback(async () => {
|
||||
if (!checkFileOpened() || workingRef.current) return;
|
||||
try {
|
||||
setWorking({ text: i18n.t('Fixing file duration') });
|
||||
setCutProgress(0);
|
||||
invariant(fileFormat != null);
|
||||
const path = await fixInvalidDuration({ fileFormat, customOutDir, onProgress: setCutProgress });
|
||||
showNotification({ icon: 'info', text: i18n.t('Duration has been fixed') });
|
||||
await withErrorHandling(async () => {
|
||||
setWorking({ text: i18n.t('Fixing file duration') });
|
||||
setCutProgress(0);
|
||||
invariant(fileFormat != null);
|
||||
const path = await fixInvalidDuration({ fileFormat, customOutDir, onProgress: setCutProgress });
|
||||
showNotification({ icon: 'info', text: i18n.t('Duration has been fixed') });
|
||||
|
||||
await loadMedia({ filePath: path });
|
||||
} catch (err) {
|
||||
errorToast(i18n.t('Failed to fix file duration'));
|
||||
console.error('Failed to fix file duration', err);
|
||||
await loadMedia({ filePath: path });
|
||||
}, i18n.t('Failed to fix file duration'));
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
setCutProgress(undefined);
|
||||
@ -1652,15 +1644,12 @@ function App() {
|
||||
|
||||
const captureSnapshotAsCoverArt = useCallback(async () => {
|
||||
if (!filePath) return;
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
const currentTime = getRelevantTime();
|
||||
const path = await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality });
|
||||
if (!(await addFileAsCoverArt(path))) return;
|
||||
showNotification({ text: i18n.t('Current frame has been set as cover art') });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorToast(i18n.t('Failed to capture frame'));
|
||||
}
|
||||
}, i18n.t('Failed to capture frame'));
|
||||
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, showNotification]);
|
||||
|
||||
const batchLoadPaths = useCallback((newPaths: string[], append?: boolean) => {
|
||||
@ -1681,7 +1670,7 @@ function App() {
|
||||
}, []);
|
||||
|
||||
const userOpenFiles = useCallback(async (filePathsIn?: string[]) => {
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
let filePaths = filePathsIn;
|
||||
if (!filePaths || filePaths.length === 0) return;
|
||||
|
||||
@ -1804,10 +1793,7 @@ function App() {
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('userOpenFiles', err);
|
||||
handleError(i18n.t('Failed to open file'), err);
|
||||
}
|
||||
}, i18n.t('Failed to open file'));
|
||||
}, [workingRef, alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]);
|
||||
|
||||
const openFilesDialog = useCallback(async () => {
|
||||
@ -1840,14 +1826,12 @@ function App() {
|
||||
}, [isFileOpened, selectedSegments]);
|
||||
|
||||
const showIncludeExternalStreamsDialog = useCallback(async () => {
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], title: t('Include more tracks from other file') });
|
||||
const [firstFilePath] = filePaths;
|
||||
if (canceled || firstFilePath == null) return;
|
||||
await addStreamSourceFile(firstFilePath);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}, i18n.t('Failed to include track'));
|
||||
}, [addStreamSourceFile, t]);
|
||||
|
||||
const toggleFullscreenVideo = useCallback(async () => {
|
||||
@ -1881,17 +1865,16 @@ function App() {
|
||||
const promptDownloadMediaUrlWrapper = useCallback(async () => {
|
||||
try {
|
||||
setWorking({ text: t('Downloading URL') });
|
||||
const newCustomOutDir = await ensureWritableOutDir({ outDir: customOutDir });
|
||||
if (newCustomOutDir == null) {
|
||||
errorToast(i18n.t('Please select a working directory first'));
|
||||
return;
|
||||
}
|
||||
const outPath = getDownloadMediaOutPath(newCustomOutDir, `downloaded-media-${Date.now()}.mkv`);
|
||||
const downloaded = await promptDownloadMediaUrl(outPath);
|
||||
if (downloaded) await loadMedia({ filePath: outPath });
|
||||
} catch (err) {
|
||||
if (err instanceof DirectoryAccessDeclinedError) return;
|
||||
handleError(err);
|
||||
await withErrorHandling(async () => {
|
||||
const newCustomOutDir = await ensureWritableOutDir({ outDir: customOutDir });
|
||||
if (newCustomOutDir == null) {
|
||||
errorToast(i18n.t('Please select a working directory first'));
|
||||
return;
|
||||
}
|
||||
const outPath = getDownloadMediaOutPath(newCustomOutDir, `downloaded-media-${Date.now()}.mkv`);
|
||||
const downloaded = await promptDownloadMediaUrl(outPath);
|
||||
if (downloaded) await loadMedia({ filePath: outPath });
|
||||
}, i18n.t('Failed to download URL'));
|
||||
} finally {
|
||||
setWorking();
|
||||
}
|
||||
@ -1902,7 +1885,6 @@ function App() {
|
||||
const mainActions = useMemo(() => {
|
||||
async function exportYouTube() {
|
||||
if (!checkFileOpened()) return;
|
||||
|
||||
await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments));
|
||||
}
|
||||
|
||||
@ -2186,29 +2168,24 @@ function App() {
|
||||
|
||||
const tryExportEdlFile = useCallback(async (type: EdlExportType) => {
|
||||
if (!checkFileOpened()) return;
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount });
|
||||
} catch (err) {
|
||||
errorToast(i18n.t('Failed to export project'));
|
||||
console.error('Failed to export project', type, err);
|
||||
}
|
||||
}, i18n.t('Failed to export project'));
|
||||
}, [checkFileOpened, customOutDir, filePath, getFrameCount, selectedSegments]);
|
||||
|
||||
const importEdlFile = useCallback(async (type: EdlImportType) => {
|
||||
if (!checkFileOpened()) return;
|
||||
|
||||
try {
|
||||
await withErrorHandling(async () => {
|
||||
const edl = await askForEdlImport({ type, fps: detectedFps });
|
||||
if (edl.length > 0) loadCutSegments(edl, true);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}, i18n.t('Failed to import project file'));
|
||||
}, [checkFileOpened, detectedFps, loadCutSegments]);
|
||||
|
||||
useEffect(() => {
|
||||
const openFiles = (filePaths: string[]) => { userOpenFiles(filePaths.map((p) => resolvePathIfNeeded(p))); };
|
||||
|
||||
async function actionWithCatch(fn: () => void) {
|
||||
async function actionWithCatch(fn: () => Promise<void>) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (err) {
|
||||
|
@ -544,8 +544,8 @@ export async function showConcatFailedDialog({ fileFormat }: { fileFormat: strin
|
||||
return value;
|
||||
}
|
||||
|
||||
export function openYouTubeChaptersDialog(text: string) {
|
||||
ReactSwal.fire({
|
||||
export async function openYouTubeChaptersDialog(text: string) {
|
||||
await ReactSwal.fire({
|
||||
showCloseButton: true,
|
||||
title: i18n.t('YouTube Chapters'),
|
||||
html: (
|
||||
|
@ -11,6 +11,7 @@ import { isDurationValid } from './segments';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
|
||||
import { parseSrt, parseSrtToSegments } from './edlFormats';
|
||||
import { CopyfileStreams, LiteFFprobeStream } from './types';
|
||||
import { UnsupportedFileError } from '../errors';
|
||||
|
||||
const { pathExists } = window.require('fs-extra');
|
||||
|
||||
@ -368,7 +369,7 @@ export async function readFileMeta(filePath: string) {
|
||||
return { format, streams, chapters };
|
||||
} catch (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;
|
||||
}
|
||||
|
@ -5,14 +5,9 @@ import invariant from 'tiny-invariant';
|
||||
import { getOutDir, getFileDir, checkDirWriteAccess, dirExists, isMasBuild } from '../util';
|
||||
import { askForOutDir, askForInputDir } from '../dialogs';
|
||||
import { errorToast } from '../swal';
|
||||
import { DirectoryAccessDeclinedError } from '../../errors';
|
||||
// import isDev from '../isDev';
|
||||
|
||||
export class DirectoryAccessDeclinedError extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'DirectoryAccessDeclinedError';
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -10,6 +10,8 @@ import isDev from './isDev';
|
||||
import Swal, { errorToast, toast } from './swal';
|
||||
import { ffmpegExtractWindow } from './util/constants';
|
||||
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 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) {
|
||||
return name.replaceAll(/[^\w.-]/g, '_');
|
||||
}
|
||||
@ -193,7 +172,7 @@ export function withBlur(cb) {
|
||||
};
|
||||
}
|
||||
|
||||
export function dragPreventer(ev) {
|
||||
export function dragPreventer(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
@ -237,7 +216,7 @@ export const resolvePathIfNeeded = (inPath: string) => (isAbsolute(inPath) ? inP
|
||||
export const html5ifiedPrefix = 'html5ified-';
|
||||
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:
|
||||
const suffixes = ['slowest', 'slow-audio', 'slow', 'fast-audio-remux', 'fast-audio', 'fast', html5dummySuffix];
|
||||
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
|
||||
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
|
||||
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) ?? '')
|
||||
);
|
||||
|
||||
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() {
|
||||
try {
|
||||
const forceCheck = false;
|
||||
@ -364,10 +392,10 @@ export async function checkAppPath() {
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/2450976/6519037
|
||||
export function shuffleArray(arrayIn) {
|
||||
export function shuffleArray<T>(arrayIn: T[]) {
|
||||
const array = [...arrayIn];
|
||||
let currentIndex = array.length;
|
||||
let randomIndex;
|
||||
let randomIndex: number;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (currentIndex !== 0) {
|
||||
@ -377,23 +405,24 @@ export function shuffleArray(arrayIn) {
|
||||
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [
|
||||
array[randomIndex], array[currentIndex]];
|
||||
array[randomIndex]!, array[currentIndex]!,
|
||||
] as const;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
// 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
|
||||
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 relDiff = diff / inputSize;
|
||||
const maxDiffPercent = 5;
|
||||
|
Loading…
Reference in New Issue
Block a user