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 }