improve cleanup after export #1425

pull/1452/head
Mikael Finstad 3 years ago
parent ff56c44a32
commit 4cda6579a7
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -118,6 +118,9 @@ const defaults = {
captureFrameFileNameFormat: 'timestamp', captureFrameFileNameFormat: 'timestamp',
enableNativeHevc: true, enableNativeHevc: true,
enableUpdateCheck: true, enableUpdateCheck: true,
cleanupChoices: {
trashTmpFiles: true, askForCleanup: true,
},
}; };
// For portable app: https://github.com/mifi/lossless-cut/issues/645 // For portable app: https://github.com/mifi/lossless-cut/issues/645

@ -147,7 +147,6 @@ const App = memo(() => {
const [timelineMode, setTimelineMode] = useState(); const [timelineMode, setTimelineMode] = useState();
const [keyframesEnabled, setKeyframesEnabled] = useState(true); const [keyframesEnabled, setKeyframesEnabled] = useState(true);
const [showRightBar, setShowRightBar] = useState(true); const [showRightBar, setShowRightBar] = useState(true);
const [cleanupChoices, setCleanupChoices] = useState({ tmpFiles: true });
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState(); const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState();
const [lastCommandsVisible, setLastCommandsVisible] = useState(false); const [lastCommandsVisible, setLastCommandsVisible] = useState(false);
const [settingsVisible, setSettingsVisible] = useState(false); const [settingsVisible, setSettingsVisible] = useState(false);
@ -180,7 +179,7 @@ const App = memo(() => {
const allUserSettings = useUserSettingsRoot(); const allUserSettings = useUserSettingsRoot();
const { 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; } = allUserSettings;
useEffect(() => { useEffect(() => {
@ -823,7 +822,7 @@ const App = memo(() => {
setSelectedBatchFiles([]); setSelectedBatchFiles([]);
}, [askBeforeClose]); }, [askBeforeClose]);
const batchRemoveFile = useCallback((path) => { const batchListRemoveFile = useCallback((path) => {
setBatchFiles((existingBatch) => { setBatchFiles((existingBatch) => {
const index = existingBatch.findIndex((existingFile) => existingFile.path === path); const index = existingBatch.findIndex((existingFile) => existingFile.path === path);
if (index < 0) return existingBatch; if (index < 0) return existingBatch;
@ -947,41 +946,52 @@ const App = memo(() => {
// Store paths before we reset state // Store paths before we reset state
const savedPaths = { previewFilePath, sourceFilePath: filePath, projectFilePath: projectFileSavePath }; const savedPaths = { previewFilePath, sourceFilePath: filePath, projectFilePath: projectFileSavePath };
resetState(); batchListRemoveFile(savedPaths.sourceFilePath);
batchRemoveFile(savedPaths.sourceFilePath);
if (!cleanupChoices2.tmpFiles && !cleanupChoices2.projectFile && !cleanupChoices2.sourceFile) return; // close the file
resetState();
try { try {
setWorking(i18n.t('Cleaning up')); setWorking(i18n.t('Cleaning up'));
console.log('trashing', cleanupChoices2); console.log('Cleaning up files', cleanupChoices2);
await deleteFiles({ toDelete: cleanupChoices2, paths: savedPaths });
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) { } catch (err) {
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err.message })); errorToast(i18n.t('Unable to delete file: {{message}}', { message: err.message }));
console.error(err); 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 () => { const cleanupFilesDialog = useCallback(async () => {
if (!isFileOpened) return; if (!isFileOpened) return;
let trashResponse = cleanupChoices; let response = cleanupChoices;
if (!cleanupChoices.dontShowAgain) { if (cleanupChoices.askForCleanup) {
trashResponse = await showCleanupFilesDialog(cleanupChoices); response = await askForCleanupChoices();
console.log('trashResponse', trashResponse); console.log('trashResponse', response);
if (!trashResponse) return; // Cancelled if (!response) return; // Canceled
setCleanupChoices(trashResponse); // Store for next time
} }
if (workingRef.current) return; if (workingRef.current) return;
try { try {
await cleanupFiles(trashResponse); await cleanupFiles(response);
} finally { } finally {
setWorking(); setWorking();
} }
}, [isFileOpened, cleanupChoices, cleanupFiles, setWorking]); }, [isFileOpened, cleanupChoices, askForCleanupChoices, cleanupFiles, setWorking]);
// For invertCutSegments we do not support filtering // For invertCutSegments we do not support filtering
const selectedSegmentsOrInverseRaw = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegmentsRaw), [inverseCutSegments, invertCutSegments, selectedSegmentsRaw]); const selectedSegmentsOrInverseRaw = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegmentsRaw), [inverseCutSegments, invertCutSegments, selectedSegmentsRaw]);
@ -2118,7 +2128,7 @@ const App = memo(() => {
batchFiles={batchFiles} batchFiles={batchFiles}
setBatchFiles={setBatchFiles} setBatchFiles={setBatchFiles}
onBatchFileSelect={onBatchFileSelect} onBatchFileSelect={onBatchFileSelect}
batchRemoveFile={batchRemoveFile} batchListRemoveFile={batchListRemoveFile}
closeBatch={closeBatch} closeBatch={closeBatch}
onMergeFilesClick={concatCurrentBatch} onMergeFilesClick={concatCurrentBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch} onBatchConvertToSupportedFormatClick={convertFormatBatch}
@ -2349,6 +2359,7 @@ const App = memo(() => {
<Settings <Settings
onTunerRequested={onTunerRequested} onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts} onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
/> />
</Table.Body> </Table.Body>
</Table> </Table>

@ -1,6 +1,6 @@
import React, { memo, useCallback, useMemo } from 'react'; import React, { memo, useCallback, useMemo } from 'react';
import { FaYinYang, FaKeyboard } from 'react-icons/fa'; 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 { useTranslation } from 'react-i18next';
import CaptureFormatButton from './components/CaptureFormatButton'; import CaptureFormatButton from './components/CaptureFormatButton';
@ -28,6 +28,7 @@ const Header = ({ title }) => (
const Settings = memo(({ const Settings = memo(({
onTunerRequested, onTunerRequested,
onKeyboardShortcutsDialogRequested, onKeyboardShortcutsDialogRequested,
askForCleanupChoices,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -205,6 +206,13 @@ const Settings = memo(({
</Table.TextCell> </Table.TextCell>
</Row> </Row>
<Row>
<KeyCell>{t('Cleanup files after export?')}</KeyCell>
<Table.TextCell>
<Button iconBefore={<CleanIcon />} onClick={askForCleanupChoices}>{t('Change preferences')}</Button>
</Table.TextCell>
</Row>
<Header title={t('Snapshots and frame extraction')} /> <Header title={t('Snapshots and frame extraction')} />
<Row> <Row>

@ -20,7 +20,7 @@ const iconStyle = {
padding: '3px 5px', 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 { t } = useTranslation();
const [sortDesc, setSortDesc] = useState(); const [sortDesc, setSortDesc] = useState();
@ -64,7 +64,7 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}> <div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
<ReactSortable list={sortableList} setList={setSortableList}> <ReactSortable list={sortableList} setList={setSortableList}>
{sortableList.map(({ batchFile: { path, name } }) => ( {sortableList.map(({ batchFile: { path, name } }) => (
<BatchFile key={path} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchRemoveFile} /> <BatchFile key={path} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchListRemoveFile} />
))} ))}
</ReactSortable> </ReactSortable>
</div> </div>

@ -337,21 +337,26 @@ const CleanupChoices = ({ cleanupChoicesInitial, onChange: onChangeProp }) => {
return ( return (
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
<p>{i18n.t('Do you want to move the original file and/or any generated files to trash?')}</p> <p>{i18n.t('What do you want to do after exporting a file or when pressing the "delete source file" button?')}</p>
<Checkbox label={i18n.t('Trash auto-generated files')} checked={getVal('tmpFiles')} onChange={(e) => onChange('tmpFiles', e.target.checked)} /> <Checkbox label={i18n.t('Close currently opened file')} checked disabled />
<Checkbox label={i18n.t('Trash project LLC file')} checked={getVal('projectFile')} onChange={(e) => onChange('projectFile', e.target.checked)} />
<Checkbox label={i18n.t('Trash original source file')} checked={getVal('sourceFile')} onChange={(e) => onChange('sourceFile', e.target.checked)} />
<div style={{ marginTop: 25 }}> <div style={{ marginTop: 25 }}>
<Checkbox label={i18n.t('Don\'t show dialog again until restarting app')} checked={getVal('dontShowAgain')} onChange={(e) => onChange('dontShowAgain', e.target.checked)} /> <Checkbox label={i18n.t('Trash auto-generated files')} checked={getVal('trashTmpFiles')} onChange={(e) => onChange('trashTmpFiles', e.target.checked)} />
<Checkbox label={i18n.t('Do this automatically after export')} disabled={!getVal('dontShowAgain')} checked={getVal('cleanupAfterExport')} onChange={(e) => onChange('cleanupAfterExport', e.target.checked)} /> <Checkbox label={i18n.t('Trash project LLC file')} checked={getVal('trashProjectFile')} onChange={(e) => onChange('trashProjectFile', e.target.checked)} />
<Checkbox label={i18n.t('Trash original source file')} checked={getVal('trashSourceFile')} onChange={(e) => onChange('trashSourceFile', e.target.checked)} />
<Checkbox label={i18n.t('Permanently delete the files if trash fails?')} disabled={!(getVal('trashTmpFiles') || getVal('trashProjectFile') || getVal('trashSourceFile'))} checked={getVal('deleteIfTrashFails')} onChange={(e) => onChange('deleteIfTrashFails', e.target.checked)} />
</div>
<div style={{ marginTop: 25 }}>
<Checkbox label={i18n.t('Show this dialog every time?')} checked={getVal('askForCleanup')} onChange={(e) => onChange('askForCleanup', e.target.checked)} />
<Checkbox label={i18n.t('Do all of this automatically after exporting a file?')} checked={getVal('cleanupAfterExport')} onChange={(e) => onChange('cleanupAfterExport', e.target.checked)} />
</div> </div>
</div> </div>
); );
}; };
export async function showCleanupFilesDialog(cleanupChoicesIn = {}) { export async function showCleanupFilesDialog(cleanupChoicesIn) {
let cleanupChoices = cleanupChoicesIn; let cleanupChoices = cleanupChoicesIn;
const { value } = await ReactSwal.fire({ const { value } = await ReactSwal.fire({

@ -129,6 +129,8 @@ export default () => {
useEffect(() => safeSetConfig({ enableNativeHevc }), [enableNativeHevc]); useEffect(() => safeSetConfig({ enableNativeHevc }), [enableNativeHevc]);
const [enableUpdateCheck, setEnableUpdateCheck] = useState(safeGetConfigInitial('enableUpdateCheck')); const [enableUpdateCheck, setEnableUpdateCheck] = useState(safeGetConfigInitial('enableUpdateCheck'));
useEffect(() => safeSetConfig({ enableUpdateCheck }), [enableUpdateCheck]); useEffect(() => safeSetConfig({ enableUpdateCheck }), [enableUpdateCheck]);
const [cleanupChoices, setCleanupChoices] = useState(safeGetConfigInitial('cleanupChoices'));
useEffect(() => safeSetConfig({ cleanupChoices }), [cleanupChoices]);
const resetKeyBindings = useCallback(() => { const resetKeyBindings = useCallback(() => {
@ -236,5 +238,7 @@ export default () => {
setEnableNativeHevc, setEnableNativeHevc,
enableUpdateCheck, enableUpdateCheck,
setEnableUpdateCheck, setEnableUpdateCheck,
cleanupChoices,
setCleanupChoices,
}; };
}; };

@ -236,48 +236,33 @@ export function getHtml5ifiedPath(cod, fp, type) {
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` }); 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 = []; const failedToTrashFiles = [];
if (toDelete.tmpFiles && previewFilePath) { // eslint-disable-next-line no-restricted-syntax
for (const path of paths) {
try { try {
await trashFile(previewFilePath); // eslint-disable-next-line no-await-in-loop
await trashFile(path);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
failedToTrashFiles.push(previewFilePath); failedToTrashFiles.push(path);
}
}
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);
} }
} }
if (failedToTrashFiles.length === 0) return; // All good! if (failedToTrashFiles.length === 0) return; // All good!
// todo allow bypassing trash altogether? https://github.com/mifi/lossless-cut/discussions/1425 if (!deleteIfTrashFails) {
const { value } = await Swal.fire({ const { value } = await Swal.fire({
icon: 'warning', icon: 'warning',
text: i18n.t('Unable to move file to trash. Do you want to permanently delete it?'), text: i18n.t('Unable to move file to trash. Do you want to permanently delete it?'),
confirmButtonText: i18n.t('Permanently delete'), confirmButtonText: i18n.t('Permanently delete'),
showCancelButton: true, showCancelButton: true,
}); });
if (!value) return;
if (value) {
await pMap(failedToTrashFiles, async (path) => unlink(path), { concurrency: 1 });
} }
await pMap(failedToTrashFiles, async (path) => unlink(path), { concurrency: 1 });
} }
export const deleteDispositionValue = 'llc_disposition_remove'; export const deleteDispositionValue = 'llc_disposition_remove';

Loading…
Cancel
Save