virtualize segment list

to improve performance
closes #1727
pull/2499/head
Mikael Finstad 5 months ago
parent be8131a2fa
commit 2506117b6d
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -4,6 +4,7 @@ module.exports = {
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/interactive-supports-focus': 0,
'jsx-a11y/control-has-associated-label': 0,
'react/no-unused-prop-types': 0,
},
overrides: [

@ -40,10 +40,14 @@
"license": "GPL-2.0-only",
"devDependencies": {
"@adamscybot/react-leaflet-component-marker": "^2.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@fontsource/open-sans": "^4.5.14",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-switch": "^1.2.2",
"@tanstack/react-virtual": "^3.13.10",
"@tsconfig/node18": "^18.2.2",
"@tsconfig/node20": "^20.1.4",
"@tsconfig/strictest": "^2.0.2",
@ -110,7 +114,6 @@
"react-icons": "^4.1.0",
"react-leaflet": "^4.2.1",
"react-lottie-player": "^1.5.0",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.4.3",
"react-use": "^17.4.0",
"rimraf": "^5.0.5",

@ -1,12 +1,13 @@
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode, MouseEventHandler } from 'react';
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode, MouseEventHandler, CSSProperties, useEffect } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai';
import { MotionStyle, motion } from 'framer-motion';
import { motion } from 'framer-motion';
import { useTranslation, Trans } from 'react-i18next';
import { ReactSortable } from 'react-sortablejs';
import isEqual from 'lodash/isEqual';
import useDebounce from 'react-use/lib/useDebounce';
import scrollIntoView from 'scroll-into-view-if-needed';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, UniqueIdentifier } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { useVirtualizer } from '@tanstack/react-virtual';
import { CSS } from '@dnd-kit/utilities';
import Dialog, { ConfirmButton } from './components/Dialog';
import Swal from './swal';
@ -32,7 +33,8 @@ const neutralButtonColor = 'var(--gray-9)';
const Segment = memo(({
seg,
index,
currentSegIndex,
isActive,
dragging,
formatTimecode,
getFrameCount,
updateSegOrder,
@ -62,7 +64,8 @@ const Segment = memo(({
}: {
seg: StateSegment | InverseCutSegment,
index: number,
currentSegIndex: number,
isActive?: boolean | undefined,
dragging?: boolean | undefined,
formatTimecode: FormatTimecode,
getFrameCount: GetFrameCount,
updateSegOrder: UseSegments['updateSegOrder'],
@ -72,7 +75,7 @@ const Segment = memo(({
onLabelSelectedSegments: UseSegments['labelSelectedSegments'],
onReorderPress: (i: number) => Promise<void>,
onLabelPress: UseSegments['labelSegment'],
selected: boolean,
selected: boolean | undefined,
onSelectSingleSegment: UseSegments['selectOnlySegment'],
onToggleSegmentSelected: UseSegments['toggleSegmentSelected'],
onDeselectAllSegments: UseSegments['deselectAllSegments'],
@ -94,7 +97,7 @@ const Segment = memo(({
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement | null>(null);
const contextMenuTemplate = useMemo<ContextMenuTemplate>(() => {
if (invertCutSegments) return [];
@ -152,12 +155,6 @@ const Segment = memo(({
: `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}`
), [formatTimecode, seg]);
const isActive = !invertCutSegments && currentSegIndex === index;
useDebounce(() => {
if (isActive && ref.current) scrollIntoView(ref.current, { behavior: 'smooth', scrollMode: 'if-needed' });
}, 300, [isActive]);
function renderNumber() {
if (invertCutSegments || !('segColorIndex' in seg)) {
return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
@ -169,7 +166,7 @@ const Segment = memo(({
const borderColor = darkMode ? color.lighten(0.5) : color.darken(0.3);
return (
<b style={{ cursor: 'grab', color: 'white', padding: '0 4px', marginRight: 3, marginLeft: -3, background: color.string(), border: `1px solid ${isActive ? borderColor.string() : 'transparent'}`, borderRadius: 10, fontSize: 12 }}>
<b style={{ color: 'white', padding: '0 4px', marginRight: 3, marginLeft: -3, background: color.string(), border: `1px solid ${isActive ? borderColor.string() : 'transparent'}`, borderRadius: 10, fontSize: 12 }}>
{index + 1}
</b>
);
@ -187,28 +184,64 @@ const Segment = memo(({
onToggleSegmentSelected(seg);
}, [onToggleSegmentSelected, seg]);
const cursor = invertCutSegments ? undefined : 'grab';
const cursor = invertCutSegments ? undefined : (dragging ? 'grabbing' : 'grab');
const tags = useMemo(() => getSegmentTags('tags' in seg ? seg : {}), [seg]);
const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]);
const motionStyle = useMemo<MotionStyle>(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray-2)', border: isActive ? '1px solid var(--gray-10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]);
const sortable = useSortable({
id: seg.segId,
transition: {
duration: 150,
easing: 'ease-in-out',
},
disabled: invertCutSegments,
});
const style = useMemo<CSSProperties>(() => {
const transitions = [
...(sortable.transition ? [sortable.transition] : []),
'opacity 100ms ease-out',
];
return {
visibility: sortable.isDragging ? 'hidden' : undefined,
padding: '3px 5px',
margin: '1px 0',
boxSizing: 'border-box',
originY: 0,
position: 'relative',
transform: CSS.Transform.toString(sortable.transform),
transition: transitions.length > 0 ? transitions.join(', ') : undefined,
background: 'var(--gray-2)',
border: `1px solid ${isActive ? 'var(--gray-10)' : 'transparent'}`,
borderRadius: 5,
opacity: !selected && !invertCutSegments ? 0.5 : undefined,
};
}, [invertCutSegments, isActive, selected, sortable.isDragging, sortable.transform, sortable.transition]);
const setRef = useCallback((node: HTMLDivElement | null) => {
sortable.setNodeRef(node);
ref.current = node;
}, [sortable]);
return (
<motion.div
ref={ref}
<div
ref={setRef}
role="button"
onClick={maybeOnClick}
onDoubleClick={onDoubleClick}
layout
style={motionStyle}
initial={{ scaleY: 0 }}
animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }}
exit={{ scaleY: 0 }}
style={style}
className="segment-list-entry"
>
<div className="segment-handle" style={{ cursor, color: 'var(--gray-12)', marginBottom: duration != null ? 3 : undefined, display: 'flex', alignItems: 'center', height: 16 }}>
<div
// eslint-disable-next-line react/jsx-props-no-spreading
{...sortable.attributes}
// eslint-disable-next-line react/jsx-props-no-spreading
{...sortable.listeners}
role="button"
style={{ cursor, color: 'var(--gray-12)', marginBottom: duration != null ? 3 : undefined, display: 'flex', alignItems: 'center', height: 16 }}
>
{renderNumber()}
<span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 12), whiteSpace: 'nowrap' }}>{timeStr}</span>
</div>
@ -229,12 +262,12 @@ const Segment = memo(({
</>
)}
{!invertCutSegments && (
{!invertCutSegments && selected != null && (
<div style={{ position: 'absolute', right: 3, bottom: 3 }}>
<CheckIcon className="enabled" size={20} color="var(--gray-12)" onClick={onToggleSegmentSelectedClick} />
<CheckIcon className="selected" size={20} color="var(--gray-12)" onClick={onToggleSegmentSelectedClick} />
</div>
)}
</motion.div>
</div>
);
});
@ -321,6 +354,7 @@ function SegmentList({
}) {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>();
const { invertCutSegments, simpleMode, darkMode } = useUserSettings();
@ -334,11 +368,6 @@ function SegmentList({
const sortableList = useMemo(() => segmentsOrInverse.map((seg) => ({ id: seg.segId, seg })), [segmentsOrInverse]);
const setSortableList = useCallback((newList: typeof sortableList) => {
if (isEqual(segmentsOrInverse.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
updateSegOrders(newList.map((list) => list.id));
}, [segmentsOrInverse, updateSegOrders]);
let header: ReactNode = t('Segments to export:');
if (segmentsOrInverse.length === 0) {
header = invertCutSegments ? (
@ -458,6 +487,88 @@ function SegmentList({
onSegmentTagsCloseComplete();
}, [editingSegmentTags, editingSegmentTagsSegmentIndex, onSegmentTagsCloseComplete, updateSegAtIndex]);
const scrollerRef = useRef<HTMLDivElement>(null);
const sensors = useSensors(useSensor(PointerSensor, {
activationConstraint: {
distance: 10,
},
}));
const rowVirtualizer = useVirtualizer({
count: sortableList.length,
getScrollElement: () => scrollerRef.current,
estimateSize: () => 66, // todo this probably needs to be changed if the segment height changes
overscan: 5,
getItemKey: (index) => sortableList[index]!.id,
});
useEffect(() => {
if (invertCutSegments) return;
rowVirtualizer.scrollToIndex(currentSegIndex, { behavior: 'smooth', align: 'auto' });
}, [currentSegIndex, invertCutSegments, rowVirtualizer]);
const handleDragStart = (event: DragStartEvent) => {
setDraggingId(event.active.id);
};
const handleDragEnd = (event: DragEndEvent) => {
setDraggingId(undefined);
const { active, over } = event;
if (over != null && active.id !== over?.id) {
const ids = sortableList.map((s) => s.id);
const oldIndex = ids.indexOf(active.id as string);
const newIndex = ids.indexOf(over.id as string);
const newList = arrayMove(sortableList, oldIndex, newIndex);
updateSegOrders(newList.map((item) => item.id));
}
};
const draggingSeg = useMemo(() => sortableList.find((s) => s.id === draggingId), [sortableList, draggingId]);
function renderSegment({ seg, index, selected, isActive, dragging }: {
seg: StateSegment | InverseCutSegment,
index: number,
selected?: boolean,
isActive?: boolean,
dragging?: boolean,
}) {
return (
<Segment
seg={seg}
index={index}
isActive={isActive}
dragging={dragging}
selected={selected}
onClick={onSegClick}
addSegment={addSegment}
onRemoveSelected={onRemoveSelected}
onRemovePress={removeSegment}
onReorderPress={onReorderSegs}
onLabelPress={onLabelSegment}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
updateSegOrder={updateSegOrder}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
onSelectSingleSegment={onSelectSingleSegment}
onToggleSegmentSelected={onToggleSegmentSelected}
onDeselectAllSegments={onDeselectAllSegments}
onSelectAllSegments={onSelectAllSegments}
onEditSegmentTags={onEditSegmentTags}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onMutateSegmentsByExpr={onMutateSegmentsByExpr}
onExtractSegmentsFramesAsImages={onExtractSegmentsFramesAsImages}
onExtractSelectedSegmentsFramesAsImages={onExtractSelectedSegmentsFramesAsImages}
onLabelSelectedSegments={onLabelSelectedSegments}
onSelectAllMarkers={onSelectAllMarkers}
onInvertSelectedSegments={onInvertSelectedSegments}
onDuplicateSegmentClick={onDuplicateSegmentClick}
/>
);
}
return (
<>
{editingSegmentTagsSegmentIndex != null && (
@ -471,7 +582,7 @@ function SegmentList({
)}
<motion.div
style={{ width, background: controlsBackground, borderLeft: '1px solid var(--gray-7)', color: 'var(--gray-11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
style={{ width, background: controlsBackground, borderLeft: '1px solid var(--gray-7)', color: 'var(--gray-11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
initial={{ x: width }}
animate={{ x: 0 }}
exit={{ x: width }}
@ -489,47 +600,40 @@ function SegmentList({
{header}
</div>
<div style={{ padding: '0 .1em 0 .3em', overflowX: 'hidden', overflowY: 'scroll', flexGrow: 1 }} className="consistent-scrollbar">
<ReactSortable list={sortableList} setList={setSortableList} disabled={!!invertCutSegments} handle=".segment-handle">
{sortableList.map(({ id, seg }, index) => {
const selected = 'selected' in seg ? seg.selected : true;
return (
<Segment
key={id}
seg={seg}
index={index}
selected={selected}
onClick={onSegClick}
addSegment={addSegment}
onRemoveSelected={onRemoveSelected}
onRemovePress={removeSegment}
onReorderPress={onReorderSegs}
onLabelPress={onLabelSegment}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
updateSegOrder={updateSegOrder}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
currentSegIndex={currentSegIndex}
onSelectSingleSegment={onSelectSingleSegment}
onToggleSegmentSelected={onToggleSegmentSelected}
onDeselectAllSegments={onDeselectAllSegments}
onSelectAllSegments={onSelectAllSegments}
onEditSegmentTags={onEditSegmentTags}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onMutateSegmentsByExpr={onMutateSegmentsByExpr}
onExtractSegmentsFramesAsImages={onExtractSegmentsFramesAsImages}
onExtractSelectedSegmentsFramesAsImages={onExtractSelectedSegmentsFramesAsImages}
onLabelSelectedSegments={onLabelSelectedSegments}
onSelectAllMarkers={onSelectAllMarkers}
onInvertSelectedSegments={onInvertSelectedSegments}
onDuplicateSegmentClick={onDuplicateSegmentClick}
/>
);
})}
</ReactSortable>
</div>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragStart={handleDragStart} modifiers={[restrictToVerticalAxis]}>
<SortableContext items={sortableList} strategy={verticalListSortingStrategy}>
<div ref={scrollerRef} style={{ padding: '0 .1em 0 .3em', overflowX: 'hidden', overflowY: 'scroll', flexGrow: 1 }} className="consistent-scrollbar">
<div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative', overflowX: 'hidden' }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const { id, seg } = sortableList[virtualRow.index]!;
const selected = 'selected' in seg ? seg.selected : true;
const isActive = !invertCutSegments && currentSegIndex === virtualRow.index;
return (
<div
key={id}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
{renderSegment({ seg, index: virtualRow.index, selected, isActive })}
</div>
);
})}
</div>
</div>
</SortableContext>
<DragOverlay>
{draggingSeg ? renderSegment({ seg: draggingSeg.seg, index: sortableList.indexOf(draggingSeg), dragging: true }) : null}
</DragOverlay>
</DndContext>
{renderFooter()}
</motion.div>

@ -1,33 +1,73 @@
import { memo, useRef, useMemo } from 'react';
import { memo, useRef, useMemo, useCallback, CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { FaAngleRight, FaFile } from 'react-icons/fa';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import useContextMenu from '../hooks/useContextMenu';
import { primaryTextColor } from '../colors';
function BatchFile({ path, index, isOpen, isSelected, name, onSelect, onDelete }: {
function BatchFile({ path, index, isOpen, isSelected, name, onSelect, onDelete, dragging }: {
path: string,
index: number,
isOpen: boolean,
isSelected: boolean,
isOpen?: boolean,
isSelected?: boolean,
name: string,
onSelect: (a: string) => void,
onDelete: (a: string) => void,
onSelect?: (a: string) => void,
onDelete?: (a: string) => void,
dragging?: boolean,
}) {
const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation();
const contextMenuTemplate = useMemo(() => [
{ label: t('Remove'), click: () => onDelete(path) },
{ label: t('Remove'), click: () => onDelete?.(path) },
], [t, onDelete, path]);
useContextMenu(ref, contextMenuTemplate);
const sortable = useSortable({
id: path,
transition: {
duration: 150,
easing: 'ease-in-out',
},
});
const setRef = useCallback((node: HTMLDivElement | null) => {
sortable.setNodeRef(node);
ref.current = node;
}, [sortable]);
const style = useMemo<CSSProperties>(() => ({
visibility: sortable.isDragging ? 'hidden' : undefined,
opacity: dragging ? 0.6 : 1,
transform: CSS.Transform.toString(sortable.transform),
transition: sortable.transition,
background: isSelected ? 'var(--gray-7)' : undefined,
cursor: dragging ? 'grabbing' : 'pointer',
fontSize: 13,
padding: '3px 6px',
display: 'flex',
alignItems: 'center',
alignContent: 'flex-start',
}), [sortable.isDragging, sortable.transform, sortable.transition, isSelected, dragging]);
return (
<div ref={ref} role="button" style={{ background: isSelected ? 'var(--gray-7)' : undefined, fontSize: 13, padding: '3px 6px', display: 'flex', alignItems: 'center', alignContent: 'flex-start' }} title={path} onClick={() => onSelect(path)}>
<div
// eslint-disable-next-line react/jsx-props-no-spreading
{...sortable.attributes}
// eslint-disable-next-line react/jsx-props-no-spreading
{...sortable.listeners}
ref={setRef}
role="button"
style={style}
title={path}
onClick={() => onSelect?.(path)}
>
<FaFile size={14} style={{ color: isSelected ? primaryTextColor : undefined, flexShrink: 0 }} />
<div style={{ flexBasis: 4, flexShrink: 0 }} />
<div style={{ whiteSpace: 'nowrap', cursor: 'pointer', overflow: 'hidden' }}>{index + 1}. {name}</div>
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>{index + 1}. {name}</div>
<div style={{ flexGrow: 1 }} />
{isOpen && <FaAngleRight size={14} style={{ color: 'var(--gray-9)', marginRight: -5, flexShrink: 0 }} />}
</div>

@ -1,10 +1,12 @@
import { DragEventHandler, memo, useCallback, useState } from 'react';
import { DragEventHandler, memo, useCallback, useMemo, 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 { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, UniqueIdentifier } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import BatchFile from './BatchFile';
import { controlsBackground, darkModeTransition, primaryColor } from '../colors';
@ -37,13 +39,10 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
const { t } = useTranslation();
const [sortDesc, setSortDesc] = useState<boolean>();
const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>();
const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile }));
const setSortableList = useCallback((newList: { batchFile: BatchFileType }[]) => {
setBatchFiles(newList.map(({ batchFile }) => batchFile));
}, [setBatchFiles]);
const onSortClick = useCallback(() => {
const newSortDesc = sortDesc == null ? false : !sortDesc;
const sortedFiles = [...batchFiles];
@ -56,6 +55,30 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
const SortIcon = sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon;
const sensors = useSensors(useSensor(PointerSensor, {
activationConstraint: {
distance: 10,
},
}));
const handleDragStart = (event: DragStartEvent) => {
setDraggingId(event.active.id);
};
const handleDragEnd = (event: DragEndEvent) => {
setDraggingId(undefined);
const { active, over } = event;
if (over != null && active.id !== over?.id) {
const ids = sortableList.map((s) => s.id);
const oldIndex = ids.indexOf(active.id as string);
const newIndex = ids.indexOf(over.id as string);
const newList = arrayMove(sortableList, oldIndex, newIndex);
setBatchFiles(newList.map((item) => item.batchFile));
}
};
const draggingFile = useMemo(() => sortableList.find((s) => s.id === draggingId), [sortableList, draggingId]);
return (
<motion.div
className="no-user-select"
@ -75,13 +98,19 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
<FaTimes size={20} role="button" title={t('Close batch')} style={{ ...iconStyle, color: 'var(--gray-11)' }} onClick={closeBatch} />
</div>
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
<ReactSortable list={sortableList} setList={setSortableList}>
{sortableList.map(({ batchFile: { path, name } }, index) => (
<BatchFile key={path} index={index} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchListRemoveFile} />
))}
</ReactSortable>
</div>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragStart={handleDragStart} modifiers={[restrictToVerticalAxis]}>
<SortableContext items={sortableList} strategy={verticalListSortingStrategy}>
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
{sortableList.map(({ batchFile: { path, name } }, index) => (
<BatchFile key={path} index={index} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchListRemoveFile} />
))}
</div>
</SortableContext>
<DragOverlay>
{draggingFile ? <BatchFile dragging index={sortableList.indexOf(draggingFile)} path={draggingFile.batchFile.path} name={draggingFile.batchFile.name} /> : null}
</DragOverlay>
</DndContext>
</motion.div>
);
}

@ -208,8 +208,8 @@ async function askForNumSegments() {
const { value } = await Swal.fire({
input: 'number',
inputAttributes: {
min: 0 as unknown as string,
max: maxSegments as unknown as string,
min: String(0),
max: String(maxSegments),
},
showCancelButton: true,
inputValue: '2',
@ -226,13 +226,13 @@ async function askForNumSegments() {
return parseInt(value, 10);
}
export async function createNumSegments(fileDuration: number) {
export async function createNumSegments(totalDuration: number) {
const numSegments = await askForNumSegments();
if (numSegments == null) return undefined;
const edl: { start: number, end: number }[] = [];
const segDuration = fileDuration / numSegments;
const segDuration = totalDuration / numSegments;
for (let i = 0; i < numSegments; i += 1) {
edl.push({ start: i * segDuration, end: i === numSegments - 1 ? fileDuration : (i + 1) * segDuration });
edl.push({ start: i * segDuration, end: i === numSegments - 1 ? totalDuration : (i + 1) * segDuration });
}
return edl;
}

@ -22,6 +22,7 @@ https://www.radix-ui.com/docs/colors/palette-composition/understanding-the-scale
html {
font-family: 'Open Sans', 'Noto Sans SemiCondensed', 'Noto Sans', sans-serif;
font-size: 16px;
overflow: hidden;
}
body {
@ -82,11 +83,11 @@ code.highlighted {
text-align: left;
}
.segment-list-entry .enabled {
.segment-list-entry .selected {
display: none;
}
.segment-list-entry:hover .enabled {
.segment-list-entry:hover .selected {
display: inherit;
}

@ -290,6 +290,68 @@ __metadata:
languageName: node
linkType: hard
"@dnd-kit/accessibility@npm:^3.1.1":
version: 3.1.1
resolution: "@dnd-kit/accessibility@npm:3.1.1"
dependencies:
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
checksum: 10/961000456a36700a9cd13be51147a818bc100f7dfabb332b80438d02e06f3b556aa0ff46ddf13bdff3b70bc8f9b63dd5a392cc285597ab1f7026e672660c54b6
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.3.1":
version: 6.3.1
resolution: "@dnd-kit/core@npm:6.3.1"
dependencies:
"@dnd-kit/accessibility": "npm:^3.1.1"
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 10/a5ae6fa8404765712aa80e308f58cb79bac9a306c274ec8272c405c2a59dd277d24b966348fe8ca6340bb3f0d75f90b8a021fa781edcf65255114d3cf2bef891
languageName: node
linkType: hard
"@dnd-kit/modifiers@npm:^9.0.0":
version: 9.0.0
resolution: "@dnd-kit/modifiers@npm:9.0.0"
dependencies:
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
"@dnd-kit/core": ^6.3.0
react: ">=16.8.0"
checksum: 10/2ae238a1b787029e95d92319d7e4a0e2ffba8fceed56c4b58dfee7ed6890df207bf89ce522d4126411051121954222bd8e1444fae321485b594ae518c7c4397d
languageName: node
linkType: hard
"@dnd-kit/sortable@npm:^10.0.0":
version: 10.0.0
resolution: "@dnd-kit/sortable@npm:10.0.0"
dependencies:
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
"@dnd-kit/core": ^6.3.0
react: ">=16.8.0"
checksum: 10/bc61c25e76905204a53f91294b8116bf106fa27247eebca2c66478450b2051d7177115a384054e7e5639e6c4430083ade63056f79ee45f549da537cf05bc5288
languageName: node
linkType: hard
"@dnd-kit/utilities@npm:^3.2.2":
version: 3.2.2
resolution: "@dnd-kit/utilities@npm:3.2.2"
dependencies:
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
checksum: 10/6cfe46a5fcdaced943982e7ae66b08b89235493e106eb5bc833737c25905e13375c6ecc3aa0c357d136cb21dae3966213dba063f19b7a60b1235a29a7b05ff84
languageName: node
linkType: hard
"@electron/asar@npm:^3.2.1":
version: 3.2.4
resolution: "@electron/asar@npm:3.2.4"
@ -1849,6 +1911,25 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-virtual@npm:^3.13.10":
version: 3.13.10
resolution: "@tanstack/react-virtual@npm:3.13.10"
dependencies:
"@tanstack/virtual-core": "npm:3.13.10"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/3585a8ae112669b88268f47e8c78d17ac37c5a1eebccec98691d8254c53d32ee0ed3fc7baabeca7daf6a777e45898c9ca327295cb9c7f8408547d54de9e9e5ce
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.13.10":
version: 3.13.10
resolution: "@tanstack/virtual-core@npm:3.13.10"
checksum: 10/75be98270bb4f689f5938ac875ed566de5324bc6c1e945cf750a7afeec226e338224d97448448a3f8b06ace8fafdfe09a535cfd3bd0f2c63e6ea43e1213e6d5d
languageName: node
linkType: hard
"@tokenizer/token@npm:^0.3.0":
version: 0.3.0
resolution: "@tokenizer/token@npm:0.3.0"
@ -3626,13 +3707,6 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:2.3.1":
version: 2.3.1
resolution: "classnames@npm:2.3.1"
checksum: 10/28fec94a815d5f570fa6cb4baaa4a7ae1466db3c8f704802f1330180db45d3b85ef8ae612f521fb37ce2cab1c3040d1d78061697b62987bc2909f26d1ad4321f
languageName: node
linkType: hard
"classnames@npm:^2.3.0":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
@ -7696,12 +7770,16 @@ __metadata:
resolution: "lossless-cut@workspace:."
dependencies:
"@adamscybot/react-leaflet-component-marker": "npm:^2.0.0"
"@dnd-kit/core": "npm:^6.3.1"
"@dnd-kit/modifiers": "npm:^9.0.0"
"@dnd-kit/sortable": "npm:^10.0.0"
"@electron/remote": "npm:^2.1.2"
"@fontsource/open-sans": "npm:^4.5.14"
"@octokit/core": "npm:5"
"@radix-ui/colors": "npm:^3.0.0"
"@radix-ui/react-checkbox": "npm:^1.2.3"
"@radix-ui/react-switch": "npm:^1.2.2"
"@tanstack/react-virtual": "npm:^3.13.10"
"@tsconfig/node18": "npm:^18.2.2"
"@tsconfig/node20": "npm:^20.1.4"
"@tsconfig/strictest": "npm:^2.0.2"
@ -7782,7 +7860,6 @@ __metadata:
react-icons: "npm:^4.1.0"
react-leaflet: "npm:^4.2.1"
react-lottie-player: "npm:^1.5.0"
react-sortablejs: "npm:^6.1.4"
react-syntax-highlighter: "npm:^15.4.3"
react-use: "npm:^17.4.0"
rimraf: "npm:^5.0.5"
@ -9255,21 +9332,6 @@ __metadata:
languageName: node
linkType: hard
"react-sortablejs@npm:^6.1.4":
version: 6.1.4
resolution: "react-sortablejs@npm:6.1.4"
dependencies:
classnames: "npm:2.3.1"
tiny-invariant: "npm:1.2.0"
peerDependencies:
"@types/sortablejs": 1
react: ">=16.9.0"
react-dom: ">=16.9.0"
sortablejs: 1
checksum: 10/44e7ed04b437ab1f3636070ed65bcca237c0a4f6425a9c6cb5a0aa2d2a9a82b8e5e66d3d9995834adb49a65c828d86fca9a9909436f15be039aa0a09c2ae31b3
languageName: node
linkType: hard
"react-syntax-highlighter@npm:^15.4.3":
version: 15.4.5
resolution: "react-syntax-highlighter@npm:15.4.5"
@ -10777,13 +10839,6 @@ __metadata:
languageName: node
linkType: hard
"tiny-invariant@npm:1.2.0":
version: 1.2.0
resolution: "tiny-invariant@npm:1.2.0"
checksum: 10/e09a718a7c4a499ba592cdac61f015d87427a0867ca07f50c11fd9b623f90cdba18937b515d4a5e4f43dac92370498d7bdaee0d0e7a377a61095e02c4a92eade
languageName: node
linkType: hard
"tiny-invariant@npm:^1.3.3":
version: 1.3.3
resolution: "tiny-invariant@npm:1.3.3"
@ -10963,7 +11018,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0":
"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7

Loading…
Cancel
Save