kopia lustrzana https://github.com/mifi/lossless-cut
rodzic
be8131a2fa
commit
2506117b6d
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
117
yarn.lock
117
yarn.lock
|
|
@ -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
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue