kopia lustrzana https://github.com/mifi/lossless-cut
rodzic
bcbf2d5dff
commit
100011750c
|
@ -8,7 +8,7 @@ const { formatDuration } = require('./util');
|
||||||
|
|
||||||
|
|
||||||
const TimelineSeg = ({
|
const TimelineSeg = ({
|
||||||
duration, cutStart, cutEnd, isActive, segNum,
|
duration, cutStart, cutEnd, isActive, segNum, name,
|
||||||
onSegClick, invertCutSegments, segBgColor, segActiveBgColor, segBorderColor,
|
onSegClick, invertCutSegments, segBgColor, segActiveBgColor, segBorderColor,
|
||||||
}) => {
|
}) => {
|
||||||
const cutSectionWidth = `${((cutEnd - cutStart) / duration) * 100}%`;
|
const cutSectionWidth = `${((cutEnd - cutStart) / duration) * 100}%`;
|
||||||
|
@ -42,6 +42,9 @@ const TimelineSeg = ({
|
||||||
|
|
||||||
const onThisSegClick = () => onSegClick(segNum);
|
const onThisSegClick = () => onSegClick(segNum);
|
||||||
|
|
||||||
|
const durationStr = cutEnd > cutStart ? `${formatDuration({ seconds: cutEnd - cutStart })} ` : '';
|
||||||
|
const title = `${durationStr}${name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
style={wrapperStyle}
|
style={wrapperStyle}
|
||||||
|
@ -51,7 +54,7 @@ const TimelineSeg = ({
|
||||||
exit={{ opacity: 0, scaleX: 0 }}
|
exit={{ opacity: 0, scaleX: 0 }}
|
||||||
role="button"
|
role="button"
|
||||||
onClick={onThisSegClick}
|
onClick={onThisSegClick}
|
||||||
title={cutEnd > cutStart ? formatDuration({ seconds: cutEnd - cutStart }) : undefined}
|
title={title}
|
||||||
>
|
>
|
||||||
<div style={{ alignSelf: 'flex-start', flexShrink: 1, fontSize: 10, minWidth: 0, overflow: 'hidden' }}>{segNum + 1}</div>
|
<div style={{ alignSelf: 'flex-start', flexShrink: 1, fontSize: 10, minWidth: 0, overflow: 'hidden' }}>{segNum + 1}</div>
|
||||||
|
|
||||||
|
@ -71,6 +74,10 @@ const TimelineSeg = ({
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{name && <div style={{ flexBasis: 4, flexShrink: 1 }} />}
|
||||||
|
|
||||||
|
{name && <div style={{ flexShrink: 1, fontSize: 11, minWidth: 0, overflow: 'hidden', whiteSpace: 'nowrap' }}>{name}</div>}
|
||||||
|
|
||||||
<div style={{ flexGrow: 1 }} />
|
<div style={{ flexGrow: 1 }} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { memo, useEffect, useState, useCallback, useRef, Fragment } from 'react';
|
import React, { memo, useEffect, useState, useCallback, useRef, Fragment } from 'react';
|
||||||
import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io';
|
import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io';
|
||||||
import { FaPlus, FaMinus, FaHandPointRight, FaHandPointLeft, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang, FaFileExport } from 'react-icons/fa';
|
import { FaPlus, FaMinus, FaHandPointRight, FaHandPointLeft, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang, FaFileExport, FaTag } from 'react-icons/fa';
|
||||||
import { MdRotate90DegreesCcw, MdCallSplit, MdCallMerge } from 'react-icons/md';
|
import { MdRotate90DegreesCcw, MdCallSplit, MdCallMerge } from 'react-icons/md';
|
||||||
import { FiScissors } from 'react-icons/fi';
|
import { FiScissors } from 'react-icons/fi';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
@ -64,6 +64,7 @@ function createSegment({ start, end } = {}) {
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
name: '',
|
||||||
color: generateColor(),
|
color: generateColor(),
|
||||||
uuid: uuid.v4(),
|
uuid: uuid.v4(),
|
||||||
};
|
};
|
||||||
|
@ -302,7 +303,6 @@ const App = memo(() => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const setCutTime = useCallback((type, time) => {
|
const setCutTime = useCallback((type, time) => {
|
||||||
const cloned = cloneDeep(cutSegments);
|
|
||||||
const currentSeg = currentCutSeg;
|
const currentSeg = currentCutSeg;
|
||||||
if (type === 'start' && time >= getSegApparentEnd(currentSeg)) {
|
if (type === 'start' && time >= getSegApparentEnd(currentSeg)) {
|
||||||
throw new Error('Start time must precede end time');
|
throw new Error('Start time must precede end time');
|
||||||
|
@ -310,12 +310,19 @@ const App = memo(() => {
|
||||||
if (type === 'end' && time <= getSegApparentStart(currentSeg)) {
|
if (type === 'end' && time <= getSegApparentStart(currentSeg)) {
|
||||||
throw new Error('Start time must precede end time');
|
throw new Error('Start time must precede end time');
|
||||||
}
|
}
|
||||||
|
const cloned = cloneDeep(cutSegments);
|
||||||
cloned[currentSegIndexSafe][type] = Math.min(Math.max(time, 0), duration);
|
cloned[currentSegIndexSafe][type] = Math.min(Math.max(time, 0), duration);
|
||||||
setCutSegments(cloned);
|
setCutSegments(cloned);
|
||||||
}, [
|
}, [
|
||||||
currentSegIndexSafe, getSegApparentEnd, cutSegments, currentCutSeg, setCutSegments, duration,
|
currentSegIndexSafe, getSegApparentEnd, cutSegments, currentCutSeg, setCutSegments, duration,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const setCurrentSegmentName = (name) => {
|
||||||
|
const cloned = cloneDeep(cutSegments);
|
||||||
|
cloned[currentSegIndexSafe].name = name;
|
||||||
|
setCutSegments(cloned);
|
||||||
|
};
|
||||||
|
|
||||||
function formatTimecode(sec) {
|
function formatTimecode(sec) {
|
||||||
return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined });
|
return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined });
|
||||||
}
|
}
|
||||||
|
@ -1063,10 +1070,21 @@ const App = memo(() => {
|
||||||
const otherFormatsMap = fromPairs(Object.entries(allOutFormats)
|
const otherFormatsMap = fromPairs(Object.entries(allOutFormats)
|
||||||
.filter(([f]) => ![...commonFormats, detectedFileFormat].includes(f)));
|
.filter(([f]) => ![...commonFormats, detectedFileFormat].includes(f)));
|
||||||
|
|
||||||
const segColor = (currentCutSeg || {}).color;
|
function getSegColors(seg) {
|
||||||
const segBgColor = segColor.alpha(0.5).string();
|
if (!seg) return {};
|
||||||
const segActiveBgColor = segColor.lighten(0.5).alpha(0.5).string();
|
const { color } = seg;
|
||||||
const segBorderColor = segColor.lighten(0.5).string();
|
return {
|
||||||
|
segBgColor: color.alpha(0.5).string(),
|
||||||
|
segActiveBgColor: color.lighten(0.5).alpha(0.5).string(),
|
||||||
|
segBorderColor: color.lighten(0.5).string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
segBgColor: currentSegBgColor,
|
||||||
|
segActiveBgColor: currentSegActiveBgColor,
|
||||||
|
segBorderColor: currentSegBorderColor,
|
||||||
|
} = getSegColors(currentCutSeg);
|
||||||
|
|
||||||
const jumpCutButtonStyle = {
|
const jumpCutButtonStyle = {
|
||||||
position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px',
|
position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px',
|
||||||
|
@ -1277,16 +1295,27 @@ const App = memo(() => {
|
||||||
return () => window.removeEventListener('keydown', keyScrollPreventer);
|
return () => window.removeEventListener('keydown', keyScrollPreventer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function onLabelSegmentPress() {
|
||||||
|
const { value } = await Swal.fire({
|
||||||
|
showCancelButton: true,
|
||||||
|
title: 'Label current segment',
|
||||||
|
inputValue: currentCutSeg.name,
|
||||||
|
input: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value != null) setCurrentSegmentName(value);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSetCutpointButton(side) {
|
function renderSetCutpointButton(side) {
|
||||||
const start = side === 'start';
|
const start = side === 'start';
|
||||||
const Icon = start ? FaHandPointLeft : FaHandPointRight;
|
const Icon = start ? FaHandPointLeft : FaHandPointRight;
|
||||||
const border = `4px solid ${segBorderColor}`;
|
const border = `4px solid ${currentSegBorderColor}`;
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
size={13}
|
size={13}
|
||||||
title="Set cut end to current position"
|
title="Set cut end to current position"
|
||||||
role="button"
|
role="button"
|
||||||
style={{ padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start && border, borderRight: !start && border, background: segActiveBgColor, borderRadius: 6 }}
|
style={{ padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start && border, borderRight: !start && border, background: currentSegActiveBgColor, borderRadius: 6 }}
|
||||||
onClick={start ? setCutStart : setCutEnd}
|
onClick={start ? setCutStart : setCutEnd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1485,22 +1514,29 @@ const App = memo(() => {
|
||||||
{currentTimePos !== undefined && <motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePos }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'black', width: currentTimeWidth, pointerEvents: 'none' }} />}
|
{currentTimePos !== undefined && <motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePos }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'black', width: currentTimeWidth, pointerEvents: 'none' }} />}
|
||||||
{commandedTimePos !== undefined && <div style={{ left: commandedTimePos, position: 'absolute', bottom: 0, top: 0, zIndex: 4, backgroundColor: 'white', width: currentTimeWidth, pointerEvents: 'none' }} />}
|
{commandedTimePos !== undefined && <div style={{ left: commandedTimePos, position: 'absolute', bottom: 0, top: 0, zIndex: 4, backgroundColor: 'white', width: currentTimeWidth, pointerEvents: 'none' }} />}
|
||||||
|
|
||||||
{apparentCutSegments.map((seg, i) => (
|
{apparentCutSegments.map((seg, i) => {
|
||||||
<TimelineSeg
|
const {
|
||||||
key={seg.uuid}
|
segBgColor, segActiveBgColor, segBorderColor,
|
||||||
segNum={i}
|
} = getSegColors(seg);
|
||||||
segBgColor={segBgColor}
|
|
||||||
segActiveBgColor={segActiveBgColor}
|
return (
|
||||||
segBorderColor={segBorderColor}
|
<TimelineSeg
|
||||||
onSegClick={currentSegIndexNew => setCurrentSegIndex(currentSegIndexNew)}
|
key={seg.uuid}
|
||||||
isActive={i === currentSegIndexSafe}
|
segNum={i}
|
||||||
duration={durationSafe}
|
segBgColor={segBgColor}
|
||||||
cutStart={seg.start}
|
segActiveBgColor={segActiveBgColor}
|
||||||
cutEnd={seg.end}
|
segBorderColor={segBorderColor}
|
||||||
invertCutSegments={invertCutSegments}
|
onSegClick={currentSegIndexNew => setCurrentSegIndex(currentSegIndexNew)}
|
||||||
zoomed={zoomed}
|
isActive={i === currentSegIndexSafe}
|
||||||
/>
|
duration={durationSafe}
|
||||||
))}
|
name={seg.name}
|
||||||
|
cutStart={seg.start}
|
||||||
|
cutEnd={seg.end}
|
||||||
|
invertCutSegments={invertCutSegments}
|
||||||
|
zoomed={zoomed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{inverseCutSegments && inverseCutSegments.map((seg, i) => (
|
{inverseCutSegments && inverseCutSegments.map((seg, i) => (
|
||||||
<InverseCutSegment
|
<InverseCutSegment
|
||||||
|
@ -1599,7 +1635,7 @@ const App = memo(() => {
|
||||||
|
|
||||||
<FaMinus
|
<FaMinus
|
||||||
size={30}
|
size={30}
|
||||||
style={{ margin: '0 5px', background: cutSegments.length < 2 ? undefined : segBgColor, borderRadius: 3, color: 'white' }}
|
style={{ margin: '0 5px', background: cutSegments.length < 2 ? undefined : currentSegBgColor, borderRadius: 3, color: 'white' }}
|
||||||
role="button"
|
role="button"
|
||||||
title={`Delete current segment ${currentSegIndexSafe + 1}`}
|
title={`Delete current segment ${currentSegIndexSafe + 1}`}
|
||||||
onClick={removeCutSegment}
|
onClick={removeCutSegment}
|
||||||
|
@ -1615,6 +1651,15 @@ const App = memo(() => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<FaTag
|
||||||
|
size={14}
|
||||||
|
title="Label segment"
|
||||||
|
role="button"
|
||||||
|
style={{ padding: 4, border: `2px solid ${currentSegBorderColor}`, background: currentSegActiveBgColor, borderRadius: 6 }}
|
||||||
|
onClick={onLabelSegmentPress}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="right-menu" style={{ position: 'absolute', right: 0, bottom: 0, padding: '.3em', display: 'flex', alignItems: 'center' }}>
|
<div className="right-menu" style={{ position: 'absolute', right: 0, bottom: 0, padding: '.3em', display: 'flex', alignItems: 'center' }}>
|
||||||
|
|
Ładowanie…
Reference in New Issue