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) => { | ||||
|         <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 ( | ||||
|                 <Segment | ||||
|                     <div | ||||
|                       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} | ||||
|                 /> | ||||
|                       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> | ||||
|                   ); | ||||
|                 })} | ||||
|           </ReactSortable> | ||||
|               </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> | ||||
| 
 | ||||
|       <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragStart={handleDragStart} modifiers={[restrictToVerticalAxis]}> | ||||
|         <SortableContext items={sortableList} strategy={verticalListSortingStrategy}> | ||||
|           <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> | ||||
|         </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
	
	 Mikael Finstad
						Mikael Finstad