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

improve cleanup after export #1425

This commit is contained in:
Mikael Finstad 2023-02-16 18:47:49 +08:00
parent ff56c44a32
commit 4cda6579a7
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
7 changed files with 76 additions and 60 deletions

View File

@ -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

View File

@ -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(() => {
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
/>
</Table.Body>
</Table>

View File

@ -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(({
</Table.TextCell>
</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')} />
<Row>

View File

@ -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,
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
<ReactSortable list={sortableList} setList={setSortableList}>
{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>
</div>

View File

@ -337,21 +337,26 @@ const CleanupChoices = ({ cleanupChoicesInitial, onChange: onChangeProp }) => {
return (
<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('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)} />
<Checkbox label={i18n.t('Close currently opened file')} checked disabled />
<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('Do this automatically after export')} disabled={!getVal('dontShowAgain')} checked={getVal('cleanupAfterExport')} onChange={(e) => onChange('cleanupAfterExport', 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('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>
);
};
export async function showCleanupFilesDialog(cleanupChoicesIn = {}) {
export async function showCleanupFilesDialog(cleanupChoicesIn) {
let cleanupChoices = cleanupChoicesIn;
const { value } = await ReactSwal.fire({

View File

@ -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,
};
};

View File

@ -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';