mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 11:43:17 +01:00
parent
bdb050492f
commit
eab8a2c9b5
@ -43,7 +43,6 @@
|
||||
"license": "GPL-2.0-only",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.20",
|
||||
"array-move": "^3.0.1",
|
||||
"axios": "^0.21.2",
|
||||
"color": "^3.1.0",
|
||||
"concurrently": "^6.0.0",
|
||||
@ -79,7 +78,6 @@
|
||||
"react-icons": "^4.1.0",
|
||||
"react-lottie-player": "^1.3.3",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-sortablejs": "^6.0.0",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"react-use": "^17.3.2",
|
||||
|
@ -93,7 +93,7 @@ const calcShouldShowKeyframes = (zoomedDuration) => (zoomedDuration != null && z
|
||||
const zoomMax = 2 ** 14;
|
||||
|
||||
const rightBarWidth = 200;
|
||||
const leftBarWidth = 200;
|
||||
const leftBarWidth = 240;
|
||||
|
||||
|
||||
const videoStyle = { width: '100%', height: '100%', objectFit: 'contain' };
|
||||
@ -2224,6 +2224,7 @@ const App = memo(() => {
|
||||
filePath={filePath}
|
||||
width={leftBarWidth}
|
||||
batchFiles={batchFiles}
|
||||
setBatchFiles={setBatchFiles}
|
||||
onBatchFileSelect={onBatchFileSelect}
|
||||
batchRemoveFile={batchRemoveFile}
|
||||
closeBatch={closeBatch}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FaTimes, FaHatWizard } from 'react-icons/fa';
|
||||
import { AiOutlineMergeCells } from 'react-icons/ai';
|
||||
import { ReactSortable } from 'react-sortablejs';
|
||||
import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui';
|
||||
|
||||
import BatchFile from './BatchFile';
|
||||
import { timelineBackground, controlsBackground } from '../colors';
|
||||
@ -16,9 +18,29 @@ const iconStyle = {
|
||||
padding: '3px 5px',
|
||||
};
|
||||
|
||||
const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles, onBatchFileSelect, batchRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) => {
|
||||
const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [sortDesc, setSortDesc] = useState();
|
||||
|
||||
const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile }));
|
||||
|
||||
const setSortableList = useCallback((newList) => {
|
||||
setBatchFiles(newList.map(({ batchFile }) => batchFile));
|
||||
}, [setBatchFiles]);
|
||||
|
||||
const onSortClick = useCallback(() => {
|
||||
const newSortDesc = sortDesc == null ? false : !sortDesc;
|
||||
const sortedFiles = [...batchFiles];
|
||||
const order = newSortDesc ? -1 : 1;
|
||||
// natural language sort (numeric) https://github.com/mifi/lossless-cut/issues/844
|
||||
sortedFiles.sort((a, b) => order * a.name.localeCompare(b.name, 'en-US', { numeric: true }));
|
||||
setBatchFiles(sortedFiles);
|
||||
setSortDesc(newSortDesc);
|
||||
}, [batchFiles, setBatchFiles, sortDesc]);
|
||||
|
||||
const SortIcon = sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="no-user-select"
|
||||
@ -32,13 +54,16 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<FaHatWizard size={17} role="button" title={`${t('Convert to supported format')}...`} style={iconStyle} onClick={onBatchConvertToSupportedFormatClick} />
|
||||
<AiOutlineMergeCells size={20} role="button" title={`${t('Merge/concatenate files')}...`} style={iconStyle} onClick={onMergeFilesClick} />
|
||||
<SortIcon size={25} role="button" title={t('Sort items')} style={iconStyle} onClick={onSortClick} />
|
||||
<FaTimes size={20} role="button" title={t('Close batch')} style={iconStyle} onClick={closeBatch} />
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
|
||||
{batchFiles.map(({ path, name }) => (
|
||||
<BatchFile key={path} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchRemoveFile} />
|
||||
))}
|
||||
<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} />
|
||||
))}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React, { memo, useState, useCallback, useEffect } from 'react';
|
||||
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dialog, Pane, Checkbox, SortAlphabeticalIcon, SortAlphabeticalDescIcon, Button, Paragraph } from 'evergreen-ui';
|
||||
import { sortableContainer, sortableElement } from 'react-sortable-hoc';
|
||||
import arrayMove from 'array-move';
|
||||
import { Dialog, Checkbox, Button, Paragraph } from 'evergreen-ui';
|
||||
import { AiOutlineMergeCells } from 'react-icons/ai';
|
||||
|
||||
import { readFileMeta, getSmarterOutFormat } from '../ffmpeg';
|
||||
@ -14,25 +12,9 @@ const { basename } = window.require('path');
|
||||
const containerStyle = { color: 'black' };
|
||||
|
||||
const rowStyle = {
|
||||
color: 'black', padding: '3px 10px', fontSize: 14, margin: '7px 0', overflowY: 'auto', whiteSpace: 'nowrap', cursor: 'grab',
|
||||
color: 'black', fontSize: 14, margin: '4px 0px', overflowY: 'auto', whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const SortableItem = sortableElement(({ value, sortIndex }) => (
|
||||
<Pane elevation={1} style={rowStyle} title={value}>
|
||||
{sortIndex + 1}
|
||||
{'. '}
|
||||
{basename(value)}
|
||||
</Pane>
|
||||
));
|
||||
|
||||
const SortableContainer = sortableContainer(({ items }) => (
|
||||
<div style={{ padding: '0 3px' }}>
|
||||
{items.map((value, index) => (
|
||||
<SortableItem key={value} index={index} sortIndex={index} value={value} />
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
|
||||
const ConcatDialog = memo(({
|
||||
isShown, onHide, initialPaths, onConcat,
|
||||
segmentsToChapters, setSegmentsToChapters,
|
||||
@ -44,19 +26,20 @@ const ConcatDialog = memo(({
|
||||
|
||||
const [paths, setPaths] = useState(initialPaths);
|
||||
const [includeAllStreams, setIncludeAllStreams] = useState(false);
|
||||
const [sortDesc, setSortDesc] = useState();
|
||||
const [fileMeta, setFileMeta] = useState();
|
||||
|
||||
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
|
||||
|
||||
useEffect(() => {
|
||||
setPaths(initialPaths);
|
||||
|
||||
const firstPath = useMemo(() => {
|
||||
if (initialPaths.length === 0) return undefined;
|
||||
return initialPaths[0];
|
||||
}, [initialPaths]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShown) return undefined;
|
||||
|
||||
let aborted = false;
|
||||
(async () => {
|
||||
const firstPath = initialPaths[0];
|
||||
setFileMeta();
|
||||
setFileFormat();
|
||||
setDetectedFileFormat();
|
||||
@ -70,22 +53,11 @@ const ConcatDialog = memo(({
|
||||
return () => {
|
||||
aborted = true;
|
||||
};
|
||||
}, [initialPaths, setDetectedFileFormat, setFileFormat]);
|
||||
}, [firstPath, isShown, setDetectedFileFormat, setFileFormat]);
|
||||
|
||||
const onSortEnd = useCallback(({ oldIndex, newIndex }) => {
|
||||
const newPaths = arrayMove(paths, oldIndex, newIndex);
|
||||
setPaths(newPaths);
|
||||
}, [paths]);
|
||||
|
||||
const onSortClick = useCallback(() => {
|
||||
const newSortDesc = sortDesc == null ? false : !sortDesc;
|
||||
const sortedPaths = [...paths];
|
||||
const order = newSortDesc ? -1 : 1;
|
||||
// natural language sort (numeric) https://github.com/mifi/lossless-cut/issues/844
|
||||
sortedPaths.sort((a, b) => order * a.localeCompare(b, 'en-US', { numeric: true }));
|
||||
setPaths(sortedPaths);
|
||||
setSortDesc(newSortDesc);
|
||||
}, [paths, sortDesc]);
|
||||
useEffect(() => {
|
||||
setPaths(initialPaths);
|
||||
}, [initialPaths]);
|
||||
|
||||
const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]);
|
||||
|
||||
@ -101,7 +73,6 @@ const ConcatDialog = memo(({
|
||||
footer={(
|
||||
<>
|
||||
{fileFormat && detectedFileFormat && <OutputFormatSelect style={{ maxWidth: 150 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />}
|
||||
<Button iconBefore={sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon} onClick={onSortClick}>{t('Sort items')}</Button>
|
||||
<Button onClick={onHide} style={{ marginLeft: 10 }}>Cancel</Button>
|
||||
<Button iconBefore={<AiOutlineMergeCells />} isLoading={detectedFileFormat == null} appearance="primary" onClick={onConcatClick}>{t('Merge!')}</Button>
|
||||
</>
|
||||
@ -109,16 +80,10 @@ const ConcatDialog = memo(({
|
||||
>
|
||||
<div style={containerStyle}>
|
||||
<div style={{ whiteSpace: 'pre-wrap', fontSize: 14, marginBottom: 10 }}>
|
||||
{t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).\n\nDrag and drop to change the order of your files here:')}
|
||||
{t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).')}
|
||||
</div>
|
||||
|
||||
<SortableContainer
|
||||
items={paths}
|
||||
onSortEnd={onSortEnd}
|
||||
helperClass="dragging-helper-class"
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ marginTop: 10, marginBottom: 10 }}>
|
||||
<Checkbox checked={includeAllStreams} onChange={(e) => setIncludeAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} />
|
||||
|
||||
<Checkbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} />
|
||||
@ -131,6 +96,16 @@ const ConcatDialog = memo(({
|
||||
|
||||
<Paragraph>{t('Note that also other settings from the normal export dialog apply to this merge function. For more information about all options, see the export dialog.')}</Paragraph>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{paths.map((path, index) => (
|
||||
<div style={rowStyle} title={path}>
|
||||
{index + 1}
|
||||
{'. '}
|
||||
<span style={{ color: 'rgba(0,0,0,0.7)' }}>{basename(path)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -46,12 +46,6 @@ kbd {
|
||||
box-shadow: inset 0 -1px 0 #bbb;
|
||||
}
|
||||
|
||||
.dragging-helper-class {
|
||||
color: rgba(0,0,0,0.5);
|
||||
/* https://github.com/clauderic/react-sortable-hoc/issues/803 */
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
23
yarn.lock
23
yarn.lock
@ -1020,7 +1020,7 @@
|
||||
core-js-pure "^3.20.2"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
||||
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
|
||||
@ -2627,11 +2627,6 @@ array-includes@^3.1.3, array-includes@^3.1.4:
|
||||
get-intrinsic "^1.1.1"
|
||||
is-string "^1.0.7"
|
||||
|
||||
array-move@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.1.tgz#179645cc0987b65953a4fc06b6df9045e4ba9618"
|
||||
integrity sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==
|
||||
|
||||
array-union@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
|
||||
@ -6331,13 +6326,6 @@ internal-slot@^1.0.3:
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ip@^1.1.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
@ -9536,15 +9524,6 @@ react-scripts@5.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "^2.3.2"
|
||||
|
||||
react-sortable-hoc@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7"
|
||||
integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
invariant "^2.2.4"
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-sortablejs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sortablejs/-/react-sortablejs-6.0.0.tgz#ba75ded6dce3fa1b5b3b52c70d1928fcdee2003d"
|
||||
|
Loading…
Reference in New Issue
Block a user