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/click-events-have-key-events': 0, | ||||||
|     'jsx-a11y/interactive-supports-focus': 0, |     'jsx-a11y/interactive-supports-focus': 0, | ||||||
|     'jsx-a11y/control-has-associated-label': 0, |     'jsx-a11y/control-has-associated-label': 0, | ||||||
|  |     'react/no-unused-prop-types': 0, | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   overrides: [ |   overrides: [ | ||||||
|  |  | ||||||
|  | @ -40,10 +40,14 @@ | ||||||
|   "license": "GPL-2.0-only", |   "license": "GPL-2.0-only", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@adamscybot/react-leaflet-component-marker": "^2.0.0", |     "@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", |     "@fontsource/open-sans": "^4.5.14", | ||||||
|     "@radix-ui/colors": "^3.0.0", |     "@radix-ui/colors": "^3.0.0", | ||||||
|     "@radix-ui/react-checkbox": "^1.2.3", |     "@radix-ui/react-checkbox": "^1.2.3", | ||||||
|     "@radix-ui/react-switch": "^1.2.2", |     "@radix-ui/react-switch": "^1.2.2", | ||||||
|  |     "@tanstack/react-virtual": "^3.13.10", | ||||||
|     "@tsconfig/node18": "^18.2.2", |     "@tsconfig/node18": "^18.2.2", | ||||||
|     "@tsconfig/node20": "^20.1.4", |     "@tsconfig/node20": "^20.1.4", | ||||||
|     "@tsconfig/strictest": "^2.0.2", |     "@tsconfig/strictest": "^2.0.2", | ||||||
|  | @ -110,7 +114,6 @@ | ||||||
|     "react-icons": "^4.1.0", |     "react-icons": "^4.1.0", | ||||||
|     "react-leaflet": "^4.2.1", |     "react-leaflet": "^4.2.1", | ||||||
|     "react-lottie-player": "^1.5.0", |     "react-lottie-player": "^1.5.0", | ||||||
|     "react-sortablejs": "^6.1.4", |  | ||||||
|     "react-syntax-highlighter": "^15.4.3", |     "react-syntax-highlighter": "^15.4.3", | ||||||
|     "react-use": "^17.4.0", |     "react-use": "^17.4.0", | ||||||
|     "rimraf": "^5.0.5", |     "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 { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa'; | ||||||
| import { AiOutlineSplitCells } from 'react-icons/ai'; | import { AiOutlineSplitCells } from 'react-icons/ai'; | ||||||
| import { MotionStyle, motion } from 'framer-motion'; | import { motion } from 'framer-motion'; | ||||||
| import { useTranslation, Trans } from 'react-i18next'; | import { useTranslation, Trans } from 'react-i18next'; | ||||||
| import { ReactSortable } from 'react-sortablejs'; | import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, UniqueIdentifier } from '@dnd-kit/core'; | ||||||
| import isEqual from 'lodash/isEqual'; | import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable'; | ||||||
| import useDebounce from 'react-use/lib/useDebounce'; | import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; | ||||||
| import scrollIntoView from 'scroll-into-view-if-needed'; | import { useVirtualizer } from '@tanstack/react-virtual'; | ||||||
|  | import { CSS } from '@dnd-kit/utilities'; | ||||||
| 
 | 
 | ||||||
| import Dialog, { ConfirmButton } from './components/Dialog'; | import Dialog, { ConfirmButton } from './components/Dialog'; | ||||||
| import Swal from './swal'; | import Swal from './swal'; | ||||||
|  | @ -32,7 +33,8 @@ const neutralButtonColor = 'var(--gray-9)'; | ||||||
| const Segment = memo(({ | const Segment = memo(({ | ||||||
|   seg, |   seg, | ||||||
|   index, |   index, | ||||||
|   currentSegIndex, |   isActive, | ||||||
|  |   dragging, | ||||||
|   formatTimecode, |   formatTimecode, | ||||||
|   getFrameCount, |   getFrameCount, | ||||||
|   updateSegOrder, |   updateSegOrder, | ||||||
|  | @ -62,7 +64,8 @@ const Segment = memo(({ | ||||||
| }: { | }: { | ||||||
|   seg: StateSegment | InverseCutSegment, |   seg: StateSegment | InverseCutSegment, | ||||||
|   index: number, |   index: number, | ||||||
|   currentSegIndex: number, |   isActive?: boolean | undefined, | ||||||
|  |   dragging?: boolean | undefined, | ||||||
|   formatTimecode: FormatTimecode, |   formatTimecode: FormatTimecode, | ||||||
|   getFrameCount: GetFrameCount, |   getFrameCount: GetFrameCount, | ||||||
|   updateSegOrder: UseSegments['updateSegOrder'], |   updateSegOrder: UseSegments['updateSegOrder'], | ||||||
|  | @ -72,7 +75,7 @@ const Segment = memo(({ | ||||||
|   onLabelSelectedSegments: UseSegments['labelSelectedSegments'], |   onLabelSelectedSegments: UseSegments['labelSelectedSegments'], | ||||||
|   onReorderPress: (i: number) => Promise<void>, |   onReorderPress: (i: number) => Promise<void>, | ||||||
|   onLabelPress: UseSegments['labelSegment'], |   onLabelPress: UseSegments['labelSegment'], | ||||||
|   selected: boolean, |   selected: boolean | undefined, | ||||||
|   onSelectSingleSegment: UseSegments['selectOnlySegment'], |   onSelectSingleSegment: UseSegments['selectOnlySegment'], | ||||||
|   onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], |   onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], | ||||||
|   onDeselectAllSegments: UseSegments['deselectAllSegments'], |   onDeselectAllSegments: UseSegments['deselectAllSegments'], | ||||||
|  | @ -94,7 +97,7 @@ const Segment = memo(({ | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { getSegColor } = useSegColors(); |   const { getSegColor } = useSegColors(); | ||||||
| 
 | 
 | ||||||
|   const ref = useRef<HTMLDivElement>(null); |   const ref = useRef<HTMLDivElement | null>(null); | ||||||
| 
 | 
 | ||||||
|   const contextMenuTemplate = useMemo<ContextMenuTemplate>(() => { |   const contextMenuTemplate = useMemo<ContextMenuTemplate>(() => { | ||||||
|     if (invertCutSegments) return []; |     if (invertCutSegments) return []; | ||||||
|  | @ -152,12 +155,6 @@ const Segment = memo(({ | ||||||
|       : `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}` |       : `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}` | ||||||
|   ), [formatTimecode, seg]); |   ), [formatTimecode, seg]); | ||||||
| 
 | 
 | ||||||
|   const isActive = !invertCutSegments && currentSegIndex === index; |  | ||||||
| 
 |  | ||||||
|   useDebounce(() => { |  | ||||||
|     if (isActive && ref.current) scrollIntoView(ref.current, { behavior: 'smooth', scrollMode: 'if-needed' }); |  | ||||||
|   }, 300, [isActive]); |  | ||||||
| 
 |  | ||||||
|   function renderNumber() { |   function renderNumber() { | ||||||
|     if (invertCutSegments || !('segColorIndex' in seg)) { |     if (invertCutSegments || !('segColorIndex' in seg)) { | ||||||
|       return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />; |       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); |     const borderColor = darkMode ? color.lighten(0.5) : color.darken(0.3); | ||||||
| 
 | 
 | ||||||
|     return ( |     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} |         {index + 1} | ||||||
|       </b> |       </b> | ||||||
|     ); |     ); | ||||||
|  | @ -187,28 +184,64 @@ const Segment = memo(({ | ||||||
|     onToggleSegmentSelected(seg); |     onToggleSegmentSelected(seg); | ||||||
|   }, [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 tags = useMemo(() => getSegmentTags('tags' in seg ? seg : {}), [seg]); | ||||||
| 
 | 
 | ||||||
|   const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]); |   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 ( |   return ( | ||||||
|     <motion.div |     <div | ||||||
|       ref={ref} |       ref={setRef} | ||||||
|       role="button" |       role="button" | ||||||
|       onClick={maybeOnClick} |       onClick={maybeOnClick} | ||||||
|       onDoubleClick={onDoubleClick} |       onDoubleClick={onDoubleClick} | ||||||
|       layout |       style={style} | ||||||
|       style={motionStyle} |  | ||||||
|       initial={{ scaleY: 0 }} |  | ||||||
|       animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }} |  | ||||||
|       exit={{ scaleY: 0 }} |  | ||||||
|       className="segment-list-entry" |       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()} |         {renderNumber()} | ||||||
|         <span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 12), whiteSpace: 'nowrap' }}>{timeStr}</span> |         <span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 12), whiteSpace: 'nowrap' }}>{timeStr}</span> | ||||||
|       </div> |       </div> | ||||||
|  | @ -229,12 +262,12 @@ const Segment = memo(({ | ||||||
|         </> |         </> | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {!invertCutSegments && ( |       {!invertCutSegments && selected != null && ( | ||||||
|         <div style={{ position: 'absolute', right: 3, bottom: 3 }}> |         <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> |         </div> | ||||||
|       )} |       )} | ||||||
|     </motion.div> |     </div> | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -321,6 +354,7 @@ function SegmentList({ | ||||||
| }) { | }) { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { getSegColor } = useSegColors(); |   const { getSegColor } = useSegColors(); | ||||||
|  |   const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>(); | ||||||
| 
 | 
 | ||||||
|   const { invertCutSegments, simpleMode, darkMode } = useUserSettings(); |   const { invertCutSegments, simpleMode, darkMode } = useUserSettings(); | ||||||
| 
 | 
 | ||||||
|  | @ -334,11 +368,6 @@ function SegmentList({ | ||||||
| 
 | 
 | ||||||
|   const sortableList = useMemo(() => segmentsOrInverse.map((seg) => ({ id: seg.segId, seg })), [segmentsOrInverse]); |   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:'); |   let header: ReactNode = t('Segments to export:'); | ||||||
|   if (segmentsOrInverse.length === 0) { |   if (segmentsOrInverse.length === 0) { | ||||||
|     header = invertCutSegments ? ( |     header = invertCutSegments ? ( | ||||||
|  | @ -458,6 +487,88 @@ function SegmentList({ | ||||||
|     onSegmentTagsCloseComplete(); |     onSegmentTagsCloseComplete(); | ||||||
|   }, [editingSegmentTags, editingSegmentTagsSegmentIndex, onSegmentTagsCloseComplete, updateSegAtIndex]); |   }, [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 ( |   return ( | ||||||
|     <> |     <> | ||||||
|       {editingSegmentTagsSegmentIndex != null && ( |       {editingSegmentTagsSegmentIndex != null && ( | ||||||
|  | @ -471,7 +582,7 @@ function SegmentList({ | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       <motion.div |       <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 }} |         initial={{ x: width }} | ||||||
|         animate={{ x: 0 }} |         animate={{ x: 0 }} | ||||||
|         exit={{ x: width }} |         exit={{ x: width }} | ||||||
|  | @ -489,47 +600,40 @@ function SegmentList({ | ||||||
|           {header} |           {header} | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div style={{ padding: '0 .1em 0 .3em', overflowX: 'hidden', overflowY: 'scroll', flexGrow: 1 }} className="consistent-scrollbar"> |         <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragStart={handleDragStart} modifiers={[restrictToVerticalAxis]}> | ||||||
|           <ReactSortable list={sortableList} setList={setSortableList} disabled={!!invertCutSegments} handle=".segment-handle"> |           <SortableContext items={sortableList} strategy={verticalListSortingStrategy}> | ||||||
|             {sortableList.map(({ id, seg }, index) => { |             <div ref={scrollerRef} style={{ padding: '0 .1em 0 .3em', overflowX: 'hidden', overflowY: 'scroll', flexGrow: 1 }} className="consistent-scrollbar"> | ||||||
|               const selected = 'selected' in seg ? seg.selected : true; |               <div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative', overflowX: 'hidden' }}> | ||||||
|               return ( |                 {rowVirtualizer.getVirtualItems().map((virtualRow) => { | ||||||
|                 <Segment |                   const { id, seg } = sortableList[virtualRow.index]!; | ||||||
|                   key={id} |                   const selected = 'selected' in seg ? seg.selected : true; | ||||||
|                   seg={seg} |                   const isActive = !invertCutSegments && currentSegIndex === virtualRow.index; | ||||||
|                   index={index} | 
 | ||||||
|                   selected={selected} |                   return ( | ||||||
|                   onClick={onSegClick} |                     <div | ||||||
|                   addSegment={addSegment} |                       key={id} | ||||||
|                   onRemoveSelected={onRemoveSelected} |                       data-index={virtualRow.index} | ||||||
|                   onRemovePress={removeSegment} |                       ref={rowVirtualizer.measureElement} | ||||||
|                   onReorderPress={onReorderSegs} |                       style={{ | ||||||
|                   onLabelPress={onLabelSegment} |                         position: 'absolute', | ||||||
|                   jumpSegStart={jumpSegStart} |                         top: 0, | ||||||
|                   jumpSegEnd={jumpSegEnd} |                         left: 0, | ||||||
|                   updateSegOrder={updateSegOrder} |                         width: '100%', | ||||||
|                   getFrameCount={getFrameCount} |                         transform: `translateY(${virtualRow.start}px)`, | ||||||
|                   formatTimecode={formatTimecode} |                       }} | ||||||
|                   currentSegIndex={currentSegIndex} |                     > | ||||||
|                   onSelectSingleSegment={onSelectSingleSegment} |                       {renderSegment({ seg, index: virtualRow.index, selected, isActive })} | ||||||
|                   onToggleSegmentSelected={onToggleSegmentSelected} |                     </div> | ||||||
|                   onDeselectAllSegments={onDeselectAllSegments} |                   ); | ||||||
|                   onSelectAllSegments={onSelectAllSegments} |                 })} | ||||||
|                   onEditSegmentTags={onEditSegmentTags} |               </div> | ||||||
|                   onSelectSegmentsByLabel={onSelectSegmentsByLabel} |             </div> | ||||||
|                   onSelectSegmentsByExpr={onSelectSegmentsByExpr} |           </SortableContext> | ||||||
|                   onMutateSegmentsByExpr={onMutateSegmentsByExpr} | 
 | ||||||
|                   onExtractSegmentsFramesAsImages={onExtractSegmentsFramesAsImages} |           <DragOverlay> | ||||||
|                   onExtractSelectedSegmentsFramesAsImages={onExtractSelectedSegmentsFramesAsImages} |             {draggingSeg ? renderSegment({ seg: draggingSeg.seg, index: sortableList.indexOf(draggingSeg), dragging: true }) : null} | ||||||
|                   onLabelSelectedSegments={onLabelSelectedSegments} |           </DragOverlay> | ||||||
|                   onSelectAllMarkers={onSelectAllMarkers} |         </DndContext> | ||||||
|                   onInvertSelectedSegments={onInvertSelectedSegments} |  | ||||||
|                   onDuplicateSegmentClick={onDuplicateSegmentClick} |  | ||||||
|                 /> |  | ||||||
|               ); |  | ||||||
|             })} |  | ||||||
|           </ReactSortable> |  | ||||||
|         </div> |  | ||||||
| 
 | 
 | ||||||
|         {renderFooter()} |         {renderFooter()} | ||||||
|       </motion.div> |       </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 { useTranslation } from 'react-i18next'; | ||||||
| import { FaAngleRight, FaFile } from 'react-icons/fa'; | 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 useContextMenu from '../hooks/useContextMenu'; | ||||||
| import { primaryTextColor } from '../colors'; | 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, |   path: string, | ||||||
|   index: number, |   index: number, | ||||||
|   isOpen: boolean, |   isOpen?: boolean, | ||||||
|   isSelected: boolean, |   isSelected?: boolean, | ||||||
|   name: string, |   name: string, | ||||||
|   onSelect: (a: string) => void, |   onSelect?: (a: string) => void, | ||||||
|   onDelete: (a: string) => void, |   onDelete?: (a: string) => void, | ||||||
|  |   dragging?: boolean, | ||||||
| }) { | }) { | ||||||
|   const ref = useRef<HTMLDivElement>(null); |   const ref = useRef<HTMLDivElement | null>(null); | ||||||
| 
 | 
 | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const contextMenuTemplate = useMemo(() => [ |   const contextMenuTemplate = useMemo(() => [ | ||||||
|     { label: t('Remove'), click: () => onDelete(path) }, |     { label: t('Remove'), click: () => onDelete?.(path) }, | ||||||
|   ], [t, onDelete, path]); |   ], [t, onDelete, path]); | ||||||
| 
 | 
 | ||||||
|   useContextMenu(ref, contextMenuTemplate); |   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 ( |   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 }} /> |       <FaFile size={14} style={{ color: isSelected ? primaryTextColor : undefined, flexShrink: 0 }} /> | ||||||
|       <div style={{ flexBasis: 4, 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 }} /> |       <div style={{ flexGrow: 1 }} /> | ||||||
|       {isOpen && <FaAngleRight size={14} style={{ color: 'var(--gray-9)', marginRight: -5, flexShrink: 0 }} />} |       {isOpen && <FaAngleRight size={14} style={{ color: 'var(--gray-9)', marginRight: -5, flexShrink: 0 }} />} | ||||||
|     </div> |     </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 { useTranslation } from 'react-i18next'; | ||||||
| import { motion } from 'framer-motion'; | import { motion } from 'framer-motion'; | ||||||
| import { FaTimes, FaHatWizard } from 'react-icons/fa'; | import { FaTimes, FaHatWizard } from 'react-icons/fa'; | ||||||
| import { AiOutlineMergeCells } from 'react-icons/ai'; | import { AiOutlineMergeCells } from 'react-icons/ai'; | ||||||
| import { ReactSortable } from 'react-sortablejs'; |  | ||||||
| import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui'; | 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 BatchFile from './BatchFile'; | ||||||
| import { controlsBackground, darkModeTransition, primaryColor } from '../colors'; | import { controlsBackground, darkModeTransition, primaryColor } from '../colors'; | ||||||
|  | @ -37,13 +39,10 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
| 
 | 
 | ||||||
|   const [sortDesc, setSortDesc] = useState<boolean>(); |   const [sortDesc, setSortDesc] = useState<boolean>(); | ||||||
|  |   const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>(); | ||||||
| 
 | 
 | ||||||
|   const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile })); |   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 onSortClick = useCallback(() => { | ||||||
|     const newSortDesc = sortDesc == null ? false : !sortDesc; |     const newSortDesc = sortDesc == null ? false : !sortDesc; | ||||||
|     const sortedFiles = [...batchFiles]; |     const sortedFiles = [...batchFiles]; | ||||||
|  | @ -56,6 +55,30 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa | ||||||
| 
 | 
 | ||||||
|   const SortIcon = sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon; |   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 ( |   return ( | ||||||
|     <motion.div |     <motion.div | ||||||
|       className="no-user-select" |       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} /> |         <FaTimes size={20} role="button" title={t('Close batch')} style={{ ...iconStyle, color: 'var(--gray-11)' }} onClick={closeBatch} /> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div style={{ overflowX: 'hidden', overflowY: 'auto' }}> |       <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragStart={handleDragStart} modifiers={[restrictToVerticalAxis]}> | ||||||
|         <ReactSortable list={sortableList} setList={setSortableList}> |         <SortableContext items={sortableList} strategy={verticalListSortingStrategy}> | ||||||
|           {sortableList.map(({ batchFile: { path, name } }, index) => ( |           <div style={{ overflowX: 'hidden', overflowY: 'auto' }}> | ||||||
|             <BatchFile key={path} index={index} path={path} name={name} isSelected={selectedBatchFiles.includes(path)} isOpen={filePath === path} onSelect={onBatchFileSelect} onDelete={batchListRemoveFile} /> |             {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> |           </div> | ||||||
|  |         </SortableContext> | ||||||
|  | 
 | ||||||
|  |         <DragOverlay> | ||||||
|  |           {draggingFile ? <BatchFile dragging index={sortableList.indexOf(draggingFile)} path={draggingFile.batchFile.path} name={draggingFile.batchFile.name} /> : null} | ||||||
|  |         </DragOverlay> | ||||||
|  |       </DndContext> | ||||||
|     </motion.div> |     </motion.div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -208,8 +208,8 @@ async function askForNumSegments() { | ||||||
|   const { value } = await Swal.fire({ |   const { value } = await Swal.fire({ | ||||||
|     input: 'number', |     input: 'number', | ||||||
|     inputAttributes: { |     inputAttributes: { | ||||||
|       min: 0 as unknown as string, |       min: String(0), | ||||||
|       max: maxSegments as unknown as string, |       max: String(maxSegments), | ||||||
|     }, |     }, | ||||||
|     showCancelButton: true, |     showCancelButton: true, | ||||||
|     inputValue: '2', |     inputValue: '2', | ||||||
|  | @ -226,13 +226,13 @@ async function askForNumSegments() { | ||||||
|   return parseInt(value, 10); |   return parseInt(value, 10); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createNumSegments(fileDuration: number) { | export async function createNumSegments(totalDuration: number) { | ||||||
|   const numSegments = await askForNumSegments(); |   const numSegments = await askForNumSegments(); | ||||||
|   if (numSegments == null) return undefined; |   if (numSegments == null) return undefined; | ||||||
|   const edl: { start: number, end: number }[] = []; |   const edl: { start: number, end: number }[] = []; | ||||||
|   const segDuration = fileDuration / numSegments; |   const segDuration = totalDuration / numSegments; | ||||||
|   for (let i = 0; i < numSegments; i += 1) { |   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; |   return edl; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ https://www.radix-ui.com/docs/colors/palette-composition/understanding-the-scale | ||||||
| html { | html { | ||||||
|   font-family: 'Open Sans', 'Noto Sans SemiCondensed', 'Noto Sans', sans-serif; |   font-family: 'Open Sans', 'Noto Sans SemiCondensed', 'Noto Sans', sans-serif; | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
|  |   overflow: hidden; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| body { | body { | ||||||
|  | @ -82,11 +83,11 @@ code.highlighted { | ||||||
|   text-align: left; |   text-align: left; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .segment-list-entry .enabled { | .segment-list-entry .selected { | ||||||
|   display: none; |   display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .segment-list-entry:hover .enabled { | .segment-list-entry:hover .selected { | ||||||
|   display: inherit; |   display: inherit; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										117
									
								
								yarn.lock
								
								
								
								
							
							
						
						
									
										117
									
								
								yarn.lock
								
								
								
								
							|  | @ -290,6 +290,68 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "@electron/asar@npm:^3.2.1": | ||||||
|   version: 3.2.4 |   version: 3.2.4 | ||||||
|   resolution: "@electron/asar@npm:3.2.4" |   resolution: "@electron/asar@npm:3.2.4" | ||||||
|  | @ -1849,6 +1911,25 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "@tokenizer/token@npm:^0.3.0": | ||||||
|   version: 0.3.0 |   version: 0.3.0 | ||||||
|   resolution: "@tokenizer/token@npm:0.3.0" |   resolution: "@tokenizer/token@npm:0.3.0" | ||||||
|  | @ -3626,13 +3707,6 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "classnames@npm:^2.3.0": | ||||||
|   version: 2.3.2 |   version: 2.3.2 | ||||||
|   resolution: "classnames@npm:2.3.2" |   resolution: "classnames@npm:2.3.2" | ||||||
|  | @ -7696,12 +7770,16 @@ __metadata: | ||||||
|   resolution: "lossless-cut@workspace:." |   resolution: "lossless-cut@workspace:." | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@adamscybot/react-leaflet-component-marker": "npm:^2.0.0" |     "@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" |     "@electron/remote": "npm:^2.1.2" | ||||||
|     "@fontsource/open-sans": "npm:^4.5.14" |     "@fontsource/open-sans": "npm:^4.5.14" | ||||||
|     "@octokit/core": "npm:5" |     "@octokit/core": "npm:5" | ||||||
|     "@radix-ui/colors": "npm:^3.0.0" |     "@radix-ui/colors": "npm:^3.0.0" | ||||||
|     "@radix-ui/react-checkbox": "npm:^1.2.3" |     "@radix-ui/react-checkbox": "npm:^1.2.3" | ||||||
|     "@radix-ui/react-switch": "npm:^1.2.2" |     "@radix-ui/react-switch": "npm:^1.2.2" | ||||||
|  |     "@tanstack/react-virtual": "npm:^3.13.10" | ||||||
|     "@tsconfig/node18": "npm:^18.2.2" |     "@tsconfig/node18": "npm:^18.2.2" | ||||||
|     "@tsconfig/node20": "npm:^20.1.4" |     "@tsconfig/node20": "npm:^20.1.4" | ||||||
|     "@tsconfig/strictest": "npm:^2.0.2" |     "@tsconfig/strictest": "npm:^2.0.2" | ||||||
|  | @ -7782,7 +7860,6 @@ __metadata: | ||||||
|     react-icons: "npm:^4.1.0" |     react-icons: "npm:^4.1.0" | ||||||
|     react-leaflet: "npm:^4.2.1" |     react-leaflet: "npm:^4.2.1" | ||||||
|     react-lottie-player: "npm:^1.5.0" |     react-lottie-player: "npm:^1.5.0" | ||||||
|     react-sortablejs: "npm:^6.1.4" |  | ||||||
|     react-syntax-highlighter: "npm:^15.4.3" |     react-syntax-highlighter: "npm:^15.4.3" | ||||||
|     react-use: "npm:^17.4.0" |     react-use: "npm:^17.4.0" | ||||||
|     rimraf: "npm:^5.0.5" |     rimraf: "npm:^5.0.5" | ||||||
|  | @ -9255,21 +9332,6 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "react-syntax-highlighter@npm:^15.4.3": | ||||||
|   version: 15.4.5 |   version: 15.4.5 | ||||||
|   resolution: "react-syntax-highlighter@npm:15.4.5" |   resolution: "react-syntax-highlighter@npm:15.4.5" | ||||||
|  | @ -10777,13 +10839,6 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "tiny-invariant@npm:^1.3.3": | ||||||
|   version: 1.3.3 |   version: 1.3.3 | ||||||
|   resolution: "tiny-invariant@npm:1.3.3" |   resolution: "tiny-invariant@npm:1.3.3" | ||||||
|  | @ -10963,7 +11018,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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 |   version: 2.8.1 | ||||||
|   resolution: "tslib@npm:2.8.1" |   resolution: "tslib@npm:2.8.1" | ||||||
|   checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 |   checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 | ||||||
|  |  | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Mikael Finstad
						Mikael Finstad