diff --git a/cli.md b/cli.md
index d631bd5b..59e5e528 100644
--- a/cli.md
+++ b/cli.md
@@ -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).
diff --git a/src/renderer/src/NoFileLoaded.tsx b/src/renderer/src/NoFileLoaded.tsx
index 40ae1ac4..5d19f58f 100644
--- a/src/renderer/src/NoFileLoaded.tsx
+++ b/src/renderer/src/NoFileLoaded.tsx
@@ -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 (
-
setDragging(true)}
+ onDragLeave={() => setDragging(false)}
role="button"
onClick={onClick}
>
@@ -62,7 +85,7 @@ function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode, keyBindingBy
{ e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
) : undefined}
-
+
);
}
diff --git a/src/renderer/src/TimelineSeg.tsx b/src/renderer/src/TimelineSeg.tsx
index c5acc989..351485a9 100644
--- a/src/renderer/src/TimelineSeg.tsx
+++ b/src/renderer/src/TimelineSeg.tsx
@@ -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(() => ({
+ 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(() => ({
+ borderRadius: '50%', backgroundColor: pinColor, width: 14, height: 14, marginLeft: -7, flexShrink: 0, textAlign: 'center', border: `1px solid ${borderColor}`,
+ }), [pinColor, borderColor]);
+
return (
-
+
onClick()}
>
@@ -69,78 +79,70 @@ 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
& { 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 (
-
- );
- }
-
- const cutSectionWidth = `${((seg.end - seg.start) / fileDurationNonZero) * 100}%`;
-
- const wrapperStyle: MotionStyle = {
- position: 'absolute',
- top: 0,
- bottom: 0,
- left: getTimePercent(seg.start),
- width: cutSectionWidth,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- originX: 0,
- boxSizing: 'border-box',
- color: 'white',
- overflow: 'hidden',
-
- borderLeft: vertBorder,
- borderTopLeftRadius: vertBorderRadius,
- borderBottomLeftRadius: vertBorderRadius,
-
- borderRight: vertBorder,
- 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);
+ 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(() => {
+ const cutSectionWidth = getTimePercent(seg.end - seg.start);
+ return {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: getTimePercent(seg.start),
+ width: cutSectionWidth,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ originX: 0,
+ boxSizing: 'border-box',
+ color: 'white',
+ overflow: 'hidden',
+
+ borderLeft: vertBorder,
+ borderTopLeftRadius: vertBorderRadius,
+ borderBottomLeftRadius: vertBorderRadius,
+
+ borderRight: vertBorder,
+ borderTopRightRadius: vertBorderRadius,
+ borderBottomRightRadius: vertBorderRadius,
+ };
+ }, [getTimePercent, seg.end, seg.start, vertBorder]);
return (
{segNum + 1}
@@ -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 & { end: number }} segNum={segNum} color={segColor} selected={selected} isActive={isActive} onClick={onThisSegClick} getTimePercent={getTimePercent} formatTimecode={formatTimecode} invertCutSegments={invertCutSegments} />;
+ }
+
+ return (
+
+ );
+}
+
+export default memo(SegmentOrMarker);
diff --git a/src/renderer/src/edlStore.ts b/src/renderer/src/edlStore.ts
index 47617a7a..4a07c484 100644
--- a/src/renderer/src/edlStore.ts
+++ b/src/renderer/src/edlStore.ts
@@ -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[]) {
diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts
index 78d14785..840f17a3 100644
--- a/src/renderer/src/hooks/useFfmpegOperations.ts
+++ b/src/renderer/src/hooks/useFfmpegOperations.ts
@@ -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
}