Merge pull request #67 from mifi/master

[pull] master from mifi:master
pull[bot] 12 months ago committed by GitHub
commit adf15b4153
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -36,7 +36,7 @@ LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
## Other options
- `--locales-path` Customise path to locales (useful for [translators](./translation.md)).
- `--disable-networking` Turn off all network requests.
- `--disable-networking` Turn off all network requests (see [#1418](https://github.com/mifi/lossless-cut/issues/1418)).
- `--http-api` Start the [HTTP server with an API](./api.md) to control LosslessCut, optionally specifying a port (default `8080`).
- `--keyboard-action` Run a keyboard action (see below.)
- `--config-dir` Path to a directory where the `config.json` file will be stored and loaded from. Note: don't include `config.json` in the path (only the directory containing it).

@ -1,4 +1,5 @@
import { Fragment, memo, useMemo } from 'react';
import { Fragment, memo, useMemo, useState } from 'react';
import { motion, MotionStyle } from 'framer-motion';
import { useTranslation, Trans } from 'react-i18next';
@ -18,6 +19,24 @@ function Keys({ keys }: { keys: string }) {
));
}
const dropzoneStyle: MotionStyle = {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
color: 'var(--gray-12)',
margin: '2em',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'nowrap',
borderWidth: '.7em',
borderStyle: 'dashed',
borderColor: 'var(--gray-3)',
};
function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode, keyBindingByAction }: {
mifiLink: unknown,
currentCutSeg: StateSegment | undefined,
@ -27,13 +46,17 @@ function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode, keyBindingBy
}) {
const { t } = useTranslation();
const { simpleMode } = useUserSettings();
const [dragging, setDragging] = useState(false);
const currentCutSegOrDefault = useMemo(() => currentCutSeg ?? { segColorIndex: 0 }, [currentCutSeg]);
return (
<div
<motion.div
className="no-user-select"
style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, border: '.7em dashed var(--gray-3)', color: 'var(--gray-12)', margin: '2em', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}
style={dropzoneStyle}
animate={{ borderColor: dragging ? 'var(--gray-9)' : 'var(--gray-3)' }}
onDragOver={() => setDragging(true)}
onDragLeave={() => setDragging(false)}
role="button"
onClick={onClick}
>
@ -62,7 +85,7 @@ function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode, keyBindingBy
<div style={{ width: '100%', height: '100%', position: 'absolute', cursor: 'pointer' }} role="button" onClick={(e) => { e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
</div>
) : undefined}
</div>
</motion.div>
);
}

@ -9,6 +9,8 @@ import { useSegColors } from './contexts';
import { FormatTimecode, StateSegment } from './types';
const markerButtonStyle: React.CSSProperties = { fontSize: 10, minWidth: 0, letterSpacing: '-.1em', color: 'white' };
function Marker({
seg, segNum, color, isActive, selected, onClick, getTimePercent, formatTimecode,
}: {
@ -25,9 +27,22 @@ function Marker({
const pinColor = darkMode ? color.saturate(0.2).lightness(40).string() : color.desaturate(0.2).lightness(50).string();
const title = [];
title.push(formatTimecode({ seconds: seg.start, shorten: true }));
if (seg.name) title.push(seg.name);
const title = useMemo(() => {
const parts = [formatTimecode({ seconds: seg.start, shorten: true })];
if (seg.name) parts.push(seg.name);
return parts.join(' ');
}, [formatTimecode, seg.start, seg.name]);
const style = useMemo<MotionStyle>(() => ({
position: 'absolute',
top: 0,
bottom: 0,
left: getTimePercent(seg.start),
width: 2,
marginLeft: -1,
overflow: 'visible',
backgroundColor: 'var(--gray-12)',
}), [getTimePercent, seg.start]);
const borderColor = useMemo(() => {
if (isActive) {
@ -37,28 +52,23 @@ function Marker({
return 'rgba(0,0,0,0)';
}, [darkMode, isActive]);
const segNumStyle = useMemo<React.CSSProperties>(() => ({
borderRadius: '50%', backgroundColor: pinColor, width: 14, height: 14, marginLeft: -7, flexShrink: 0, textAlign: 'center', border: `1px solid ${borderColor}`,
}), [pinColor, borderColor]);
return (
<motion.div
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: getTimePercent(seg.start),
width: 2,
marginLeft: -1,
overflow: 'visible',
backgroundColor: 'var(--gray-12)',
}}
style={style}
layout
transition={mySpring}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: selected ? 1 : 0.5, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
title={title.join(' ')}
title={title}
>
<div style={{ borderRadius: '50%', backgroundColor: pinColor, width: 14, height: 14, marginLeft: -7, flexShrink: 0, textAlign: 'center', border: `1px solid ${borderColor}` }}>
<div style={segNumStyle}>
<div
style={{ fontSize: 10, minWidth: 0, letterSpacing: '-.1em', color: 'white' }}
style={markerButtonStyle}
role="button"
onClick={() => onClick()}
>
@ -69,52 +79,48 @@ function Marker({
);
}
function TimelineSeg({
seg, fileDurationNonZero, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
} : {
seg: StateSegment,
fileDurationNonZero: number,
isActive: boolean,
function Segment({
seg, segNum, color, isActive, selected, onClick, getTimePercent, formatTimecode, invertCutSegments,
}: {
seg: Omit<StateSegment, 'end'> & { end: number },
segNum: number,
onSegClick: (a: number) => void,
invertCutSegments: boolean,
formatTimecode: FormatTimecode,
color: Color,
isActive: boolean,
selected: boolean,
onClick: () => void,
getTimePercent: (a: number) => string,
formatTimecode: FormatTimecode,
invertCutSegments: boolean,
}) {
const { darkMode } = useUserSettings();
const { getSegColor } = useSegColors();
const segColor = useMemo(() => getSegColor(seg), [getSegColor, seg]);
const { name } = seg;
const getTimePercent = (t: number) => `${(t / fileDurationNonZero) * 100}%`;
const vertBorder = useMemo(() => {
if (!isActive) return '2px solid transparent';
return `1.5px solid ${darkMode ? segColor.desaturate(0.1).lightness(70).string() : segColor.desaturate(0.2).lightness(40).string()}`;
}, [darkMode, isActive, segColor]);
return `1.5px solid ${darkMode ? color.desaturate(0.1).lightness(70).string() : color.desaturate(0.2).lightness(40).string()}`;
}, [darkMode, isActive, color]);
const backgroundColor = useMemo(() => {
// we use both transparency and lightness, so that segments can be visible when overlapping
if (invertCutSegments || !selected) return darkMode ? segColor.desaturate(0.3).lightness(30).alpha(0.5).string() : segColor.desaturate(0.3).lightness(70).alpha(0.5).string();
if (isActive) return darkMode ? segColor.saturate(0.2).lightness(60).alpha(0.7).string() : segColor.saturate(0.2).lightness(40).alpha(0.8).string();
return darkMode ? segColor.desaturate(0.2).lightness(50).alpha(0.7).string() : segColor.lightness(35).alpha(0.6).string();
}, [darkMode, invertCutSegments, isActive, segColor, selected]);
if (invertCutSegments || !selected) return darkMode ? color.desaturate(0.3).lightness(30).alpha(0.5).string() : color.desaturate(0.3).lightness(70).alpha(0.5).string();
if (isActive) return darkMode ? color.saturate(0.2).lightness(60).alpha(0.7).string() : color.saturate(0.2).lightness(40).alpha(0.8).string();
return darkMode ? color.desaturate(0.2).lightness(50).alpha(0.7).string() : color.lightness(35).alpha(0.6).string();
}, [darkMode, invertCutSegments, isActive, color, selected]);
const vertBorderRadius = 5;
const onThisSegClick = useCallback(() => onSegClick(segNum), [onSegClick, segNum]);
if (seg.end == null) {
return (
<Marker seg={seg} segNum={segNum} color={segColor} selected={selected} isActive={isActive} onClick={onThisSegClick} getTimePercent={getTimePercent} formatTimecode={formatTimecode} />
);
}
const cutSectionWidth = `${((seg.end - seg.start) / fileDurationNonZero) * 100}%`;
const wrapperStyle: MotionStyle = {
const title = useMemo(() => {
const parts = [
formatTimecode({ seconds: seg.start, shorten: true }),
`- ${formatTimecode({ seconds: seg.end, shorten: true })}`,
];
if (name) parts.push(name);
return parts.join(' ');
}, [formatTimecode, name, seg.end, seg.start]);
const wrapperStyle = useMemo<MotionStyle>(() => {
const cutSectionWidth = getTimePercent(seg.end - seg.start);
return {
position: 'absolute',
top: 0,
bottom: 0,
@ -136,11 +142,7 @@ function TimelineSeg({
borderTopRightRadius: vertBorderRadius,
borderBottomRightRadius: vertBorderRadius,
};
const title: string[] = [];
title.push(formatTimecode({ seconds: seg.start, shorten: true }));
if (seg.end != null) title.push(`- ${formatTimecode({ seconds: seg.end, shorten: true })}`);
if (name) title.push(name);
}, [getTimePercent, seg.end, seg.start, vertBorder]);
return (
<motion.div
@ -151,8 +153,8 @@ function TimelineSeg({
animate={{ opacity: 1, scaleX: 1, backgroundColor }}
exit={{ opacity: 0, scaleX: 0 }}
role="button"
onClick={onThisSegClick}
title={title.join(' ')}
onClick={onClick}
title={title}
>
<div style={{ alignSelf: 'flex-start', flexShrink: 0, fontSize: 10, minWidth: 0, letterSpacing: '-.1em' }}>{segNum + 1}</div>
@ -181,4 +183,33 @@ function TimelineSeg({
);
}
export default memo(TimelineSeg);
function SegmentOrMarker({
seg, fileDurationNonZero, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
} : {
seg: StateSegment,
fileDurationNonZero: number,
isActive: boolean,
segNum: number,
onSegClick: (a: number) => void,
invertCutSegments: boolean,
formatTimecode: FormatTimecode,
selected: boolean,
}) {
const { getSegColor } = useSegColors();
const segColor = useMemo(() => getSegColor(seg), [getSegColor, seg]);
const getTimePercent = (t: number) => `${(t / fileDurationNonZero) * 100}%`;
const onThisSegClick = useCallback(() => onSegClick(segNum), [onSegClick, segNum]);
if (seg.end != null) {
return <Segment seg={seg as Omit<StateSegment, 'end'> & { end: number }} segNum={segNum} color={segColor} selected={selected} isActive={isActive} onClick={onThisSegClick} getTimePercent={getTimePercent} formatTimecode={formatTimecode} invertCutSegments={invertCutSegments} />;
}
return (
<Marker seg={seg} segNum={segNum} color={segColor} selected={selected} isActive={isActive} onClick={onThisSegClick} getTimePercent={getTimePercent} formatTimecode={formatTimecode} />
);
}
export default memo(SegmentOrMarker);

@ -70,7 +70,7 @@ export async function saveCsvFrames({ path, cutSegments, getFrameCount }: {
cutSegments: SegmentBase[],
getFrameCount: GetFrameCount,
}) {
await writeFile(path, await formatCsvFrames({ cutSegments, getFrameCount }));
await writeFile(path, formatCsvFrames({ cutSegments, getFrameCount }));
}
export async function saveTsv(path: string, cutSegments: SegmentBase[]) {

@ -364,6 +364,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
function getPreserveChapters() {
if (chaptersPath) return ['-map_chapters', String(chaptersInputIndex)];
// todo should preserve chapters be hardcoded (and disabled in UI) when segmentsToChaptersOnly mode is enabled?
if (!preserveChapters) return ['-map_chapters', '-1']; // https://github.com/mifi/lossless-cut/issues/2176
return []; // default: includes chapters from input
}

Loading…
Cancel
Save