From 4cda6579a758a581d4dcc2492c133a1bebd2fac9 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 16 Feb 2023 18:47:49 +0800 Subject: [PATCH] improve cleanup after export #1425 --- public/configStore.js | 3 ++ src/App.jsx | 49 +++++++++++++++++++------------ src/Settings.jsx | 10 ++++++- src/components/BatchFilesList.jsx | 4 +-- src/dialogs/index.jsx | 19 +++++++----- src/hooks/useUserSettingsRoot.js | 4 +++ src/util.js | 47 ++++++++++------------------- 7 files changed, 76 insertions(+), 60 deletions(-) diff --git a/public/configStore.js b/public/configStore.js index 2b223d28..c5f9fe92 100644 --- a/public/configStore.js +++ b/public/configStore.js @@ -118,6 +118,9 @@ const defaults = { captureFrameFileNameFormat: 'timestamp', enableNativeHevc: true, enableUpdateCheck: true, + cleanupChoices: { + trashTmpFiles: true, askForCleanup: true, + }, }; // For portable app: https://github.com/mifi/lossless-cut/issues/645 diff --git a/src/App.jsx b/src/App.jsx index 06005906..a2a862ec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -147,7 +147,6 @@ const App = memo(() => { const [timelineMode, setTimelineMode] = useState(); const [keyframesEnabled, setKeyframesEnabled] = useState(true); const [showRightBar, setShowRightBar] = useState(true); - const [cleanupChoices, setCleanupChoices] = useState({ tmpFiles: true }); const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState(); const [lastCommandsVisible, setLastCommandsVisible] = useState(false); const [settingsVisible, setSettingsVisible] = useState(false); @@ -180,7 +179,7 @@ const App = memo(() => { const allUserSettings = useUserSettingsRoot(); const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, } = allUserSettings; useEffect(() => { @@ -823,7 +822,7 @@ const App = memo(() => { setSelectedBatchFiles([]); }, [askBeforeClose]); - const batchRemoveFile = useCallback((path) => { + const batchListRemoveFile = useCallback((path) => { setBatchFiles((existingBatch) => { const index = existingBatch.findIndex((existingFile) => existingFile.path === path); if (index < 0) return existingBatch; @@ -947,41 +946,52 @@ const App = memo(() => { // Store paths before we reset state const savedPaths = { previewFilePath, sourceFilePath: filePath, projectFilePath: projectFileSavePath }; + batchListRemoveFile(savedPaths.sourceFilePath); + + // close the file resetState(); - batchRemoveFile(savedPaths.sourceFilePath); - - if (!cleanupChoices2.tmpFiles && !cleanupChoices2.projectFile && !cleanupChoices2.sourceFile) return; - try { setWorking(i18n.t('Cleaning up')); - console.log('trashing', cleanupChoices2); - await deleteFiles({ toDelete: cleanupChoices2, paths: savedPaths }); + console.log('Cleaning up files', cleanupChoices2); + + const pathsToDelete = []; + if (cleanupChoices2.trashTmpFiles && savedPaths.previewFilePath) pathsToDelete.push(savedPaths.previewFilePath); + if (cleanupChoices2.trashProjectFile && savedPaths.projectFilePath) pathsToDelete.push(savedPaths.projectFilePath); + if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath); + + await deleteFiles(pathsToDelete, cleanupChoices2.deleteIfTrashFails); } catch (err) { errorToast(i18n.t('Unable to delete file: {{message}}', { message: err.message })); console.error(err); } - }, [batchRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]); + }, [batchListRemoveFile, filePath, previewFilePath, projectFileSavePath, resetState, setWorking]); + + const askForCleanupChoices = useCallback(async () => { + const trashResponse = await showCleanupFilesDialog(cleanupChoices); + if (!trashResponse) return undefined; // Canceled + setCleanupChoices(trashResponse); // Store for next time + return trashResponse; + }, [cleanupChoices, setCleanupChoices]); const cleanupFilesDialog = useCallback(async () => { if (!isFileOpened) return; - let trashResponse = cleanupChoices; - if (!cleanupChoices.dontShowAgain) { - trashResponse = await showCleanupFilesDialog(cleanupChoices); - console.log('trashResponse', trashResponse); - if (!trashResponse) return; // Cancelled - setCleanupChoices(trashResponse); // Store for next time + let response = cleanupChoices; + if (cleanupChoices.askForCleanup) { + response = await askForCleanupChoices(); + console.log('trashResponse', response); + if (!response) return; // Canceled } if (workingRef.current) return; try { - await cleanupFiles(trashResponse); + await cleanupFiles(response); } finally { setWorking(); } - }, [isFileOpened, cleanupChoices, cleanupFiles, setWorking]); + }, [isFileOpened, cleanupChoices, askForCleanupChoices, cleanupFiles, setWorking]); // For invertCutSegments we do not support filtering const selectedSegmentsOrInverseRaw = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegmentsRaw), [inverseCutSegments, invertCutSegments, selectedSegmentsRaw]); @@ -2118,7 +2128,7 @@ const App = memo(() => { batchFiles={batchFiles} setBatchFiles={setBatchFiles} onBatchFileSelect={onBatchFileSelect} - batchRemoveFile={batchRemoveFile} + batchListRemoveFile={batchListRemoveFile} closeBatch={closeBatch} onMergeFilesClick={concatCurrentBatch} onBatchConvertToSupportedFormatClick={convertFormatBatch} @@ -2349,6 +2359,7 @@ const App = memo(() => { diff --git a/src/Settings.jsx b/src/Settings.jsx index e7e3fab3..52d70f79 100644 --- a/src/Settings.jsx +++ b/src/Settings.jsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { FaYinYang, FaKeyboard } from 'react-icons/fa'; -import { CogIcon, Button, Table, NumericalIcon, KeyIcon, FolderCloseIcon, DocumentIcon, TimeIcon, Checkbox, Select } from 'evergreen-ui'; +import { CleanIcon, CogIcon, Button, Table, NumericalIcon, KeyIcon, FolderCloseIcon, DocumentIcon, TimeIcon, Checkbox, Select } from 'evergreen-ui'; import { useTranslation } from 'react-i18next'; import CaptureFormatButton from './components/CaptureFormatButton'; @@ -28,6 +28,7 @@ const Header = ({ title }) => ( const Settings = memo(({ onTunerRequested, onKeyboardShortcutsDialogRequested, + askForCleanupChoices, }) => { const { t } = useTranslation(); @@ -205,6 +206,13 @@ const Settings = memo(({ + + {t('Cleanup files after export?')} + + + + +
diff --git a/src/components/BatchFilesList.jsx b/src/components/BatchFilesList.jsx index 0fb7b7ca..2f03ef2e 100644 --- a/src/components/BatchFilesList.jsx +++ b/src/components/BatchFilesList.jsx @@ -20,7 +20,7 @@ const iconStyle = { padding: '3px 5px', }; -const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) => { +const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) => { const { t } = useTranslation(); const [sortDesc, setSortDesc] = useState(); @@ -64,7 +64,7 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
{sortableList.map(({ batchFile: { path, name } }) => ( - + ))}
diff --git a/src/dialogs/index.jsx b/src/dialogs/index.jsx index 895b3453..afca1a4b 100644 --- a/src/dialogs/index.jsx +++ b/src/dialogs/index.jsx @@ -337,21 +337,26 @@ const CleanupChoices = ({ cleanupChoicesInitial, onChange: onChangeProp }) => { return (
-

{i18n.t('Do you want to move the original file and/or any generated files to trash?')}

+

{i18n.t('What do you want to do after exporting a file or when pressing the "delete source file" button?')}

- onChange('tmpFiles', e.target.checked)} /> - onChange('projectFile', e.target.checked)} /> - onChange('sourceFile', e.target.checked)} /> +
- onChange('dontShowAgain', e.target.checked)} /> - onChange('cleanupAfterExport', e.target.checked)} /> + onChange('trashTmpFiles', e.target.checked)} /> + onChange('trashProjectFile', e.target.checked)} /> + onChange('trashSourceFile', e.target.checked)} /> + onChange('deleteIfTrashFails', e.target.checked)} /> +
+ +
+ onChange('askForCleanup', e.target.checked)} /> + onChange('cleanupAfterExport', e.target.checked)} />
); }; -export async function showCleanupFilesDialog(cleanupChoicesIn = {}) { +export async function showCleanupFilesDialog(cleanupChoicesIn) { let cleanupChoices = cleanupChoicesIn; const { value } = await ReactSwal.fire({ diff --git a/src/hooks/useUserSettingsRoot.js b/src/hooks/useUserSettingsRoot.js index f1599513..0cb24b82 100644 --- a/src/hooks/useUserSettingsRoot.js +++ b/src/hooks/useUserSettingsRoot.js @@ -129,6 +129,8 @@ export default () => { useEffect(() => safeSetConfig({ enableNativeHevc }), [enableNativeHevc]); const [enableUpdateCheck, setEnableUpdateCheck] = useState(safeGetConfigInitial('enableUpdateCheck')); useEffect(() => safeSetConfig({ enableUpdateCheck }), [enableUpdateCheck]); + const [cleanupChoices, setCleanupChoices] = useState(safeGetConfigInitial('cleanupChoices')); + useEffect(() => safeSetConfig({ cleanupChoices }), [cleanupChoices]); const resetKeyBindings = useCallback(() => { @@ -236,5 +238,7 @@ export default () => { setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, + cleanupChoices, + setCleanupChoices, }; }; diff --git a/src/util.js b/src/util.js index bcc05428..13c1fd7e 100644 --- a/src/util.js +++ b/src/util.js @@ -236,48 +236,33 @@ export function getHtml5ifiedPath(cod, fp, type) { return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` }); } -export async function deleteFiles({ toDelete, paths: { previewFilePath, sourceFilePath, projectFilePath } }) { +export async function deleteFiles(paths, deleteIfTrashFails) { const failedToTrashFiles = []; - if (toDelete.tmpFiles && previewFilePath) { + // eslint-disable-next-line no-restricted-syntax + for (const path of paths) { try { - await trashFile(previewFilePath); + // eslint-disable-next-line no-await-in-loop + await trashFile(path); } catch (err) { console.error(err); - failedToTrashFiles.push(previewFilePath); - } - } - if (toDelete.projectFile && projectFilePath) { - try { - // throw new Error('test'); - await trashFile(projectFilePath); - } catch (err) { - console.error(err); - failedToTrashFiles.push(projectFilePath); - } - } - if (toDelete.sourceFile) { - try { - await trashFile(sourceFilePath); - } catch (err) { - console.error(err); - failedToTrashFiles.push(sourceFilePath); + failedToTrashFiles.push(path); } } if (failedToTrashFiles.length === 0) return; // All good! - // todo allow bypassing trash altogether? https://github.com/mifi/lossless-cut/discussions/1425 - const { value } = await Swal.fire({ - icon: 'warning', - text: i18n.t('Unable to move file to trash. Do you want to permanently delete it?'), - confirmButtonText: i18n.t('Permanently delete'), - showCancelButton: true, - }); - - if (value) { - await pMap(failedToTrashFiles, async (path) => unlink(path), { concurrency: 1 }); + if (!deleteIfTrashFails) { + const { value } = await Swal.fire({ + icon: 'warning', + text: i18n.t('Unable to move file to trash. Do you want to permanently delete it?'), + confirmButtonText: i18n.t('Permanently delete'), + showCancelButton: true, + }); + if (!value) return; } + + await pMap(failedToTrashFiles, async (path) => unlink(path), { concurrency: 1 }); } export const deleteDispositionValue = 'llc_disposition_remove';