This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-09-29 14:40:43 +03:00
25 changed files with 417 additions and 158 deletions
@@ -1,61 +1,47 @@
.controls {
margin-top: 4px;
}
.input-group { .input-group {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 0 4px; background: #222531;
padding: 0 20px;
border-radius: 4px;
}
.time {
color: #fff;
} }
.video-slider { .video-slider {
width: 100%; margin: 12px 26px;
margin: 12px 20px; }
.MuiSlider-root {
color: #fff !important;
} }
.buttons-group { .buttons-group {
display: flex; display: flex;
justify-content: center; flex-direction: row;
position: relative; gap: 10px;
z-index: 1; margin-top: 6px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
margin-top: 10px;
} }
.control-btn { .control-btn {
width: 40px;
height: 40px;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
gap: 4px; background: #222531;
padding: 10px 20px; padding: 4px;
font-size: 16px; border: 0;
margin: 0 5px;
border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
.play-btn { .control-btn:hover {
background: #8aff8a; background: #535b77;
}
.pause-btn {
background: #ff8a8a;
}
.arrow-btn {
background: #e0e0e0;
}
.stop-btn {
background: #e8a1a5;
}
.save-btn {
background: #8ad4ff;
}
.delete-btn {
background: #ffb38a;
}
.clean-btn{
background: #ff8a8a;
} }
@@ -1,7 +1,13 @@
import { Slider } from '@mui/material'; import { Slider } from '@mui/material';
import { FaStop } from "react-icons/fa";
import { MdCleaningServices } from "react-icons/md";
import './AnnotationControls.css'; import './AnnotationControls.css';
import PreviousIcon from '../../icons/PreviousIcon';
import PlayIcon from '../../icons/PlayIcon';
import PauseIcon from '../../icons/PauseIcon';
import NextIcon from '../../icons/NextIcon';
import StopIcon from '../../icons/StopIcon';
import SaveIcon from '../../icons/SaveIcon';
import CleanIcon from '../../icons/CleanIcon';
import DeleteIcon from '../../icons/DeleteIcon';
function AnnotationControls({ function AnnotationControls({
videoRef, videoRef,
@@ -32,7 +38,7 @@ function AnnotationControls({
return ( return (
<div className='controls'> <div className='controls'>
<div className='input-group'> <div className='input-group'>
<p>{formatDuration(currentTime)}</p> <p className='time'>{formatDuration(currentTime)}</p>
<Slider <Slider
aria-label='time-indicator' aria-label='time-indicator'
value={currentTime} value={currentTime}
@@ -43,8 +49,8 @@ function AnnotationControls({
className='video-slider' className='video-slider'
/> />
{videoRef.current !== null {videoRef.current !== null
? <p>{formatDuration(videoRef.current.duration - currentTime)}</p> ? <p className='time'>{formatDuration(videoRef.current.duration - currentTime)}</p>
: <p>{formatDuration(0)}</p> : <p className='time'>{formatDuration(0)}</p>
} }
</div> </div>
@@ -55,46 +61,48 @@ function AnnotationControls({
onClick={onFrameBackward} onClick={onFrameBackward}
title="Previous Frame" title="Previous Frame"
> >
<PreviousIcon />
</button> </button>
<button <button
className={isPlaying ? 'control-btn pause-btn' : 'control-btn play-btn'} className={isPlaying ? 'control-btn pause-btn' : 'control-btn play-btn'}
onClick={onPlayPause} onClick={onPlayPause}
> >
{isPlaying ? '⏸️ Pause' : '▶️ Play'} {isPlaying ? <PauseIcon /> : <PlayIcon />}
</button> </button>
<button <button
className='control-btn arrow-btn' className='control-btn arrow-btn'
onClick={onFrameForward} onClick={onFrameForward}
title="Next Frame" title="Next Frame"
> >
<NextIcon />
</button> </button>
<button <button
className='control-btn stop-btn' className='control-btn stop-btn'
onClick={onStop} onClick={onStop}
title='Stop' title='Stop'
> >
<FaStop /> Stop <StopIcon />
</button> </button>
<button <button
className='control-btn save-btn' className='control-btn save-btn'
onClick={onSaveAnnotation} onClick={onSaveAnnotation}
title='Save'
> >
💾 Save <SaveIcon />
</button> </button>
<button <button
className='control-btn delete-btn' className='control-btn delete-btn'
onClick={onDelete} onClick={onDelete}
title='Delete'
> >
🗑 Delete <DeleteIcon />
</button> </button>
<button <button
className='control-btn clean-btn' className='control-btn clean-btn'
onClick={onDeleteAll} onClick={onDeleteAll}
title='DeleteAll' title='DeleteAll'
> >
<MdCleaningServices /> Delete All <CleanIcon />
</button> </button>
</div> </div>
</div> </div>
-20
View File
@@ -1,20 +0,0 @@
// src/components/AnnotationList.js
// No changes
import React from 'react';
function AnnotationList({ annotations, onAnnotationClick }) {
return (
<div>
<h3 className='menu-title'>Annotations</h3>
<ul>
{annotations.map((annotation, index) => (
<li key={index} onClick={() => onAnnotationClick(annotation.time)}>
Frame {index + 1} - {annotation.annotations.length} objects
</li>
))}
</ul>
</div>
);
}
export default AnnotationList;
@@ -0,0 +1,28 @@
.annotation-section {
background: #222531;
border-radius: 4px;
padding: 8px;
height: 80%;
}
.annotation-list {
display: flex;
flex-direction: column;
gap: 4px;
list-style-type: none;
padding: 0;
}
.annotation-list-item {
box-sizing: border-box;
display: flex;
align-items: center;
height: 22px;
background: #858CA2;
padding: 4px;
color: #fff;
font-size: 14px;
line-height: 1;
font-weight: 600;
border-radius: 4px ;
}
@@ -0,0 +1,18 @@
import './AnnotationList.css'
function AnnotationList({ annotations, onAnnotationClick }) {
return (
<div className='annotation-section'>
<h3 className='menu-title'>Annotations</h3>
<ul className='annotation-list'>
{annotations.map((annotation, index) => (
<li className='annotation-list-item' key={index} onClick={() => onAnnotationClick(index)}>
Frame {index + 1} - {annotation.detections.length} objects
</li>
))}
</ul>
</div>
);
}
export default AnnotationList;
@@ -1,33 +1,32 @@
.content-wrapper { .content-wrapper {
display: flex; display: flex;
gap: 4px;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background: #0D1421;
padding: 4px;
} }
.side-menu { .side-menu {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 15%; width: 228px;
height: 100%; height: 100vh;
}
.left-menu{
border-right: 1px solid #ccc;
} }
.right-menu{ .right-menu{
overflow-y: auto; overflow-y: auto;
border-left: 1px solid #ccc;
} }
.player-wrapper { .player-wrapper {
width: 70%; width: calc(100% - 464px);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.error-message { .error-message {
position: absolute;
background: #ffdddd; background: #ffdddd;
color: #d8000c; color: #d8000c;
padding: 6px; padding: 6px;
@@ -51,4 +50,6 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
border-radius: 8px;
border: 1px solid #222531;
} }
+23 -14
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import VideoPlayer from '../VideoPlayer/VideoPlayer'; import VideoPlayer from '../VideoPlayer/VideoPlayer';
import AnnotationList from '../AnnotationList'; import AnnotationList from '../AnnotationList/AnnotationList';
import MediaList from '../MediaList/MediaList'; import MediaList from '../MediaList/MediaList';
import DetectionClassList from '../DetectionClassList/DetectionClassList'; import DetectionClassList from '../DetectionClassList/DetectionClassList';
import CanvasEditor from '../CanvasEditor/CanvasEditor'; import CanvasEditor from '../CanvasEditor/CanvasEditor';
@@ -8,11 +8,12 @@ import * as AnnotationService from '../../services/AnnotationService';
import AnnotationControls from '../AnnotationControls/AnnotationControls'; import AnnotationControls from '../AnnotationControls/AnnotationControls';
import saveAnnotation from '../../services/DataHandler'; import saveAnnotation from '../../services/DataHandler';
import './AnnotationMain.css'; import './AnnotationMain.css';
import { detectionTypes } from '../../constants/detectionTypes';
function AnnotationMain() { function AnnotationMain() {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const [annotations, setAnnotations] = useState({}); const [annotations, setAnnotations] = useState([]);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [selectedClass, setSelectedClass] = useState(null); const [selectedClass, setSelectedClass] = useState(null);
const [detections, setDetections] = useState([]); const [detections, setDetections] = useState([]);
@@ -21,6 +22,7 @@ function AnnotationMain() {
const [videoWidth, setVideoWidth] = useState(640); const [videoWidth, setVideoWidth] = useState(640);
const [videoHeight, setVideoHeight] = useState(480); const [videoHeight, setVideoHeight] = useState(480);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [detectionType, setDetectionType] = useState(detectionTypes.day)
const videoRef = useRef(null); const videoRef = useRef(null);
const containerRef = useRef(null); const containerRef = useRef(null);
@@ -34,7 +36,7 @@ function AnnotationMain() {
if (!file) return; if (!file) return;
setSelectedFile(file); setSelectedFile(file);
setAnnotations({}); setAnnotations([]);
setDetections([]); setDetections([]);
setSelectedDetectionIndices([]); setSelectedDetectionIndices([]);
setCurrentTime(0); setCurrentTime(0);
@@ -71,16 +73,18 @@ function AnnotationMain() {
const imageData = AnnotationService.createAnnotationImage( const imageData = AnnotationService.createAnnotationImage(
videoRef, videoRef,
detections, detections,
safeContainerRef safeContainerRef,
detectionType
); );
if (imageData) { if (imageData) {
const newAnnotations = { const newAnnotations = {
...annotations, time: currentTime,
[currentTime]: { time: currentTime, annotations: detections, imageData } detections: detections,
imageData: imageData
}; };
setAnnotations(newAnnotations); setAnnotations(prevAnnotation => [...prevAnnotation, newAnnotations]);
saveAnnotation(currentTime, detections, imageData); saveAnnotation(currentTime, detections, imageData);
setErrorMessage(""); setErrorMessage("");
@@ -103,15 +107,15 @@ function AnnotationMain() {
setDetections([]); setDetections([]);
} }
const handleAnnotationClick = (time) => { const handleAnnotationClick = (index) => {
setCurrentTime(time); const annotation = annotations[index];
const annotation = annotations[time];
if (annotation) { if (annotation) {
setDetections(annotation.annotations || []); setCurrentTime(annotation.time);
setDetections(annotation.detections || []);
setSelectedDetectionIndices([]); setSelectedDetectionIndices([]);
} }
if (videoRef.current) { if (videoRef.current) {
videoRef.current.currentTime = time; videoRef.current.currentTime = annotation.time;
} }
setIsPlaying(false); setIsPlaying(false);
}; };
@@ -190,7 +194,11 @@ function AnnotationMain() {
onDropNewFiles={handleDropNewFiles} onDropNewFiles={handleDropNewFiles}
/> />
<DetectionClassList onClassSelect={handleClassSelect} /> <DetectionClassList
onClassSelect={handleClassSelect}
detectionType={detectionType}
setDetectionType={setDetectionType}
/>
</div> </div>
<div className='player-wrapper' > <div className='player-wrapper' >
@@ -218,6 +226,7 @@ function AnnotationMain() {
onDetectionsChange={handleDetectionsChange} onDetectionsChange={handleDetectionsChange}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
detectionClass={selectedClass} detectionClass={selectedClass}
detectionType={detectionType}
/> />
</VideoPlayer> </VideoPlayer>
</div> </div>
@@ -240,7 +249,7 @@ function AnnotationMain() {
<div className='side-menu right-menu'> <div className='side-menu right-menu'>
<AnnotationList <AnnotationList
annotations={Object.values(annotations)} annotations={annotations}
onAnnotationClick={handleAnnotationClick} onAnnotationClick={handleAnnotationClick}
/> />
</div> </div>
+3 -1
View File
@@ -12,7 +12,8 @@ function CanvasEditor({
onDetectionsChange, onDetectionsChange,
onSelectionChange, onSelectionChange,
children, children,
detectionClass detectionClass,
detectionType
}) { }) {
const containerRef = useRef(null); const containerRef = useRef(null);
const [currentDetection, setCurrentDetection] = useState(initialCurrentDetection); const [currentDetection, setCurrentDetection] = useState(initialCurrentDetection);
@@ -258,6 +259,7 @@ function CanvasEditor({
onDetectionMouseDown={handleDetectionMouseDown} onDetectionMouseDown={handleDetectionMouseDown}
currentDetection={currentDetection} currentDetection={currentDetection}
onResize={handleResize} onResize={handleResize}
detectionType={detectionType}
/> />
</div> </div>
</div> </div>
+4 -8
View File
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { detectionTypes } from '../constants/detectionTypes';
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { function Detection({ detection, isSelected, onDetectionMouseDown, onResize, detectionType }) {
if (!detection || !detection.class) { if (!detection || !detection.class) {
return null; return null;
} }
@@ -13,10 +14,6 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
} }
// Use startsWith to correctly handle RGBA and hex colors // Use startsWith to correctly handle RGBA and hex colors
const backgroundColor = color.startsWith('rgba')
? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)')
: color.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)');
const borderColor = color.startsWith('rgba') const borderColor = color.startsWith('rgba')
? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)') ? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)')
: color; : color;
@@ -40,7 +37,6 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
top: `${detection.y1}px`, top: `${detection.y1}px`,
width: `${detection.x2 - detection.x1}px`, width: `${detection.x2 - detection.x1}px`,
height: `${detection.y2 - detection.y1}px`, height: `${detection.y2 - detection.y1}px`,
backgroundColor: backgroundColor,
border: `2px solid ${borderColor}`, border: `2px solid ${borderColor}`,
boxSizing: 'border-box', boxSizing: 'border-box',
cursor: isSelected ? 'move' : 'default', cursor: isSelected ? 'move' : 'default',
@@ -50,7 +46,7 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
if (isSelected) { if (isSelected) {
style.border = `3px solid black`; style.border = `3px solid black`;
style.boxShadow = `0 0 4px 2px ${borderColor}`; style.boxShadow = `0 0 4px 4px ${borderColor}`;
} }
const handleMouseDown = (e) => { const handleMouseDown = (e) => {
@@ -92,7 +88,7 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
textShadow: '1px 1px 2px black', textShadow: '1px 1px 2px black',
pointerEvents: 'none' pointerEvents: 'none'
}}> }}>
{detection.class.Name} {detection.class.Name} {detectionType !== detectionTypes.day && '(' + detectionType + ')'}
</span> </span>
</div> </div>
); );
@@ -1,24 +1,80 @@
.detection {
margin-top: 4px;
}
.class-list { .class-list {
flex-grow: 1; display: flex;
flex-direction: column;
background: #858CA2;
border-radius: 4px;
padding: 4px;
height: 48vh;
}
.menu-title {
margin-bottom: 6px;
} }
.class-list-group { .class-list-group {
list-style-type: none; display: flex;
flex-direction: column;
gap: 3px;
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 40vh;
min-height: 300px;
overflow: auto; overflow: auto;
scrollbar-width: none;
list-style-type: none;
}
.class-list-group::-webkit-scrollbar {
display: none;
} }
.class-list-item { .class-list-item {
display: flex; display: flex;
align-items: center; align-items: center;
height: 12px; height: 30px;
cursor: pointer; cursor: pointer;
padding: 8px; padding: 8px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
margin-bottom: 2px;
border-radius: 4px; border-radius: 4px;
} }
.detection-type-group {
background: #222531;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 16px 9px;
margin-top: 4px;
border-radius: 4px;
}
.detection-type-btn {
width: 66px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
background: #3862fb41;
color: #3861FB;
font-size: 30px;
padding: 5px 17px;
border-radius: 4px;
border: 0;
}
.detection-type-btn:hover {
background: #0e2060;
}
.active-type {
color: white;
background: #3861FB;
}
.active-type:hover {
cursor: default;
background: #3861FB;
}
@@ -1,8 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import DetectionClass from '../../models/DetectionClass'; import DetectionClass from '../../models/DetectionClass';
import './DetectionClassList.css'; import './DetectionClassList.css';
import { MdOutlineNightlightRound, MdOutlineWbSunny } from "react-icons/md";
import { FaRegSnowflake } from 'react-icons/fa';
import { detectionTypes } from '../../constants/detectionTypes';
function DetectionClassList({ onClassSelect }) { function DetectionClassList({ onClassSelect, detectionType, setDetectionType }) {
const [detectionClasses, setDetectionClasses] = useState([]); const [detectionClasses, setDetectionClasses] = useState([]);
const [selectedClass, setSelectedClass] = useState(null); const [selectedClass, setSelectedClass] = useState(null);
@@ -83,7 +86,13 @@ function DetectionClassList({ onClassSelect }) {
onClassSelect && onClassSelect(cls); onClassSelect && onClassSelect(cls);
}; };
const handleTypeClick = (type) => {
setDetectionType(type);
}
return ( return (
<div className='detection'>
<div className='class-list'> <div className='class-list'>
<h3 className='menu-title'>Classes</h3> <h3 className='menu-title'>Classes</h3>
<ul className='class-list-group' > <ul className='class-list-group' >
@@ -97,7 +106,7 @@ function DetectionClassList({ onClassSelect }) {
key={cls.Id} key={cls.Id}
className='class-list-item' className='class-list-item'
style={{ style={{
border: `1px solid ${isSelected ? '#000' : '#eee'}`, border: `1px solid ${isSelected ? '#000' : '#eee0'}`,
backgroundColor: isSelected ? darkBg : backgroundColor, backgroundColor: isSelected ? darkBg : backgroundColor,
}} }}
onClick={() => handleClassClick(cls)} onClick={() => handleClassClick(cls)}
@@ -108,6 +117,30 @@ function DetectionClassList({ onClassSelect }) {
})} })}
</ul> </ul>
</div> </div>
<div className='detection-type-group'>
<button className={detectionType == detectionTypes.day
? 'detection-type-btn active-type'
: 'detection-type-btn'} title='День'
onClick={() => handleTypeClick(detectionTypes.day)}>
<MdOutlineWbSunny />
</button>
<button className={detectionType == detectionTypes.night
? 'detection-type-btn active-type'
: 'detection-type-btn'} title='Ніч'
onClick={() => handleTypeClick(detectionTypes.night)}>
<MdOutlineNightlightRound />
</button>
<button className={detectionType == detectionTypes.winter
? 'detection-type-btn active-type'
: 'detection-type-btn'} title='Зима'
onClick={() => handleTypeClick(detectionTypes.winter)}>
<FaRegSnowflake />
</button>
</div>
</div>
); );
} }
+3 -1
View File
@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import Detection from './Detection'; import Detection from './Detection';
function DetectionContainer({ detections, selectedDetectionIndices, onDetectionMouseDown, currentDetection, onResize }) { function DetectionContainer({ detections, selectedDetectionIndices, onDetectionMouseDown, currentDetection, onResize, detectionType }) {
return ( return (
<> <>
@@ -13,6 +13,7 @@ function DetectionContainer({ detections, selectedDetectionIndices, onDetectionM
isSelected={selectedDetectionIndices.includes(index)} isSelected={selectedDetectionIndices.includes(index)}
onDetectionMouseDown={(e) => onDetectionMouseDown(e, index)} onDetectionMouseDown={(e) => onDetectionMouseDown(e, index)}
onResize={(e, position) => onResize(e, index, position)} onResize={(e, position) => onResize(e, index, position)}
detectionType={detectionType}
/> />
))} ))}
{currentDetection && ( {currentDetection && (
@@ -21,6 +22,7 @@ function DetectionContainer({ detections, selectedDetectionIndices, onDetectionM
isSelected={false} isSelected={false}
onDetectionMouseDown={() => {}} // No-op handler for the current detection onDetectionMouseDown={() => {}} // No-op handler for the current detection
onResize={() => {}} // No-op handler for the current detection onResize={() => {}} // No-op handler for the current detection
detectionType={detectionType}
/> />
)} )}
</> </>
+60 -8
View File
@@ -1,26 +1,73 @@
.explorer{
height: 40vh ;
background: #222531;
padding: 8px;
border-radius: 4px;
min-height: 180px;
}
.explorer-head{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 6px;
}
.menu-title { .menu-title {
font-size: 14px; font-size: 18px;
margin: 15px 0; line-height: 20px;
color: white;
margin: 0;
margin-right: 10px;
}
.open-btn{
width: 80px;
height: 20px;
background: #6188FF;
color: white;
border: 0;
border-radius: 4px;
padding: 0;
}
.open-btn:hover{
background: #295cf7;
} }
.file-filter{ .file-filter{
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
padding: 4px 8px; height: 26px;
background: white;
padding: 6px 12px;
border: 0;
border-radius: 2px;
font-size: 14px;
} }
.file-list-group { .file-list-group {
display: flex;
flex-direction: column;
gap: 4px;
padding: 0; padding: 0;
margin: 12px 0;
list-style-type: none; list-style-type: none;
max-height: 20vh;
overflow: auto; overflow: auto;
scrollbar-width: none;
max-height: 36%;
}
.file-list-group::-webkit-scrollbar {
display: none;
} }
.file-list-item { .file-list-item {
cursor: pointer; padding: 7px 6px;
padding: 6px;
border-bottom: 1px solid #eee;
font-size: 12px; font-size: 12px;
color: white;
cursor: pointer;
border-radius: 2px;
} }
.label { .label {
@@ -28,9 +75,14 @@
} }
.file-input-block { .file-input-block {
display: flex;
justify-content: center;
align-items: center;
height: 12%;
color: white;
border: 2px dashed #ccc; border: 2px dashed #ccc;
border-radius: 4px;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
margin-top: 10px;
cursor: pointer; cursor: pointer;
} }
+23 -5
View File
@@ -3,14 +3,21 @@ import { useDropzone } from 'react-dropzone';
import './MediaList.css' import './MediaList.css'
function MediaList({ files, selectedFile, onFileSelect, onDropNewFiles }) { function MediaList({ files, selectedFile, onFileSelect, onDropNewFiles }) {
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive, open: openFileDialog } = useDropzone({
onDrop: onDropNewFiles, onDrop: onDropNewFiles,
multiple: true,
}); });
const { getRootProps: getFolderRootProps, getInputProps: getFolderInputProps, open: openFolderDialog } = useDropzone({
onDrop: onDropNewFiles,
multiple: true
});
const [filteredFiles, setFilteredFiles] = useState(files); const [filteredFiles, setFilteredFiles] = useState(files);
useEffect(()=>{ useEffect(() => {
setFilteredFiles(files); setFilteredFiles(files);
},[files]) }, [files])
const handleInputChange = (e) => { const handleInputChange = (e) => {
@@ -21,15 +28,23 @@ function MediaList({ files, selectedFile, onFileSelect, onDropNewFiles }) {
return ( return (
<div className='explorer'> <div className='explorer'>
<div className='explorer-head'>
<h3 className='menu-title' >Files</h3> <h3 className='menu-title' >Files</h3>
<input className='file-filter' type='text' placeholder='Filename' onChange={handleInputChange}/> <button className='open-btn' type="button" onClick={openFileDialog}>
Open File
</button>
<button className='open-btn' type="button" onClick={openFolderDialog}>
Open Folder
</button>
</div>
<input className='file-filter' type='text' placeholder='Filename' onChange={handleInputChange} />
<ul className='file-list-group' > <ul className='file-list-group' >
{filteredFiles.map((file) => ( {filteredFiles.map((file) => (
<li <li
className='file-list-item' className='file-list-item'
key={file.name} key={file.name}
style={{ style={{
backgroundColor: selectedFile === file ? '#f0f0f0' : 'transparent' backgroundColor: selectedFile === file ? '#474A52' : '#858CA2'
}} }}
onClick={() => onFileSelect(file)} onClick={() => onFileSelect(file)}
> >
@@ -39,6 +54,9 @@ function MediaList({ files, selectedFile, onFileSelect, onDropNewFiles }) {
</ul> </ul>
<div className='file-input-block' {...getRootProps()} > <div className='file-input-block' {...getRootProps()} >
<input {...getInputProps()} /> <input {...getInputProps()} />
<div style={{ display: 'none' }}>
<input {...getFolderInputProps()} webkitdirectory="true" mozdirectory="true" />
</div>
{isDragActive ? ( {isDragActive ? (
<p className='label' >Drop here</p> <p className='label' >Drop here</p>
) : ( ) : (
+5
View File
@@ -0,0 +1,5 @@
export const detectionTypes = {
day: 'day',
night: 'night',
winter: 'winter'
}
+7
View File
@@ -0,0 +1,7 @@
const CleanIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.0339 1.78576C24.5443 1.79004 24.0845 1.98731 23.8248 2.38613L19.6867 10.2253C19.9933 10.3325 20.6829 10.6413 22.9734 11.6834L23.0074 11.7005C23.4033 11.8806 23.7056 12.0093 23.8248 12.0607C23.8652 12.08 23.9036 12.11 23.944 12.1293L26.9582 3.72411C27.1476 3.05941 26.675 2.29393 25.9194 1.9916C25.6384 1.85008 25.3276 1.78361 25.0339 1.78576ZM17.2516 10.8943C16.1979 10.9736 15.2805 11.5011 14.6291 12.421C15.0399 13.1178 15.7594 13.9691 16.7748 13.9476C17.043 13.9369 17.2686 14.132 17.3197 14.3936C17.5262 14.7174 18.7948 15.292 19.1588 15.1484C19.3121 15.0883 19.476 15.1012 19.6186 15.1827C19.7612 15.2642 19.8634 15.3971 19.8911 15.5601C19.9124 15.6501 20.1508 15.9653 20.6404 16.1947C21.0256 16.3748 21.4003 16.4199 21.611 16.3148C21.7473 16.2462 21.8963 16.2441 22.0368 16.2977C22.1794 16.3513 22.3028 16.4649 22.3603 16.6064C22.7286 17.5113 24.4102 17.5777 25.5618 17.4813C25.7299 16.7458 25.6852 15.9803 25.4255 15.2684C25.0658 14.2885 24.3527 13.5145 23.382 13.0728C23.2799 13.0278 22.9499 12.8691 22.5306 12.6783C20.5893 11.7949 19.493 11.3146 19.278 11.2545C18.5671 10.9715 17.8838 10.8471 17.2516 10.8943ZM14.0501 13.5188C13.8798 13.9262 13.6861 14.3078 13.4882 14.6681C13.7223 15.2427 14.4141 16.5743 15.6849 16.2805C15.8318 16.2462 15.9872 16.2655 16.1106 16.3491C16.2362 16.4327 16.3065 16.5614 16.332 16.7093C16.3682 16.9152 16.6087 17.1596 16.9451 17.344C17.4219 17.6035 17.937 17.6614 18.1541 17.567C18.2967 17.5048 18.4564 17.5006 18.5969 17.567C18.7374 17.6335 18.8438 17.76 18.8864 17.9101C18.963 18.1717 19.2312 18.4397 19.6016 18.6305C20.038 18.8535 20.5361 18.935 20.8617 18.8192C21.1129 18.7313 21.3811 18.8385 21.5088 19.0765C21.9899 19.975 23.5694 20.0714 24.7955 19.9857C24.9509 19.5397 25.0956 19.0808 25.2382 18.5962C25.0913 18.6048 24.9381 18.6134 24.7784 18.6134C23.6375 18.6134 22.273 18.3861 21.594 17.4469C21.1342 17.5091 20.6063 17.4083 20.0784 17.1382C19.6953 16.9409 19.2738 16.6386 19.0226 16.2634C18.3287 16.2634 17.5304 15.8753 17.1324 15.6287C16.8237 15.4378 16.5832 15.2491 16.4342 15.0454C15.3528 14.9447 14.5801 14.2349 14.0501 13.5188ZM12.807 15.7487C9.42885 20.4767 3.66025 20.3373 3.59426 20.3287C3.37927 20.3094 3.16853 20.4252 3.06636 20.6204C2.96418 20.8155 2.97908 21.0492 3.11744 21.2207C4.09023 22.4301 5.13539 23.4528 6.19971 24.3255C6.70206 24.4263 9.86309 24.8766 12.9943 21.2207C13.1901 20.9913 13.5329 20.972 13.7606 21.1693C13.9884 21.3665 14.0075 21.7117 13.8117 21.9412C11.6277 24.4906 9.32454 25.2883 7.71529 25.4577C9.06059 26.3711 10.4165 27.0615 11.6831 27.5847C12.4877 27.4153 15.1868 26.6027 17.5751 22.9361C17.7412 22.6831 18.0903 22.5973 18.3414 22.7645C18.5926 22.9318 18.6607 23.2834 18.4947 23.5365C16.6939 26.3003 14.7228 27.5976 13.3349 28.2022C16.1681 29.1264 18.2818 29.2078 18.4606 29.2143H18.4777C18.5798 29.2143 18.6799 29.1843 18.7672 29.1285C18.9204 29.032 22.0751 26.9564 24.3868 21.1178C24.2974 21.1199 24.2058 21.135 24.1143 21.135C22.9223 21.135 21.545 20.8927 20.7936 19.9514C20.2657 20.0157 19.6612 19.8935 19.1077 19.6083C18.6565 19.3746 18.2989 19.0615 18.069 18.6992C17.49 18.7485 16.8642 18.5383 16.4342 18.3046C15.9914 18.0623 15.6445 17.7579 15.4465 17.4126C14.1927 17.4791 13.3221 16.6193 12.807 15.7487Z" fill="#858CA2" />
</svg>
);
export default CleanIcon;
+8
View File
@@ -0,0 +1,8 @@
const DeleteIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.6667 5.33335V6.66669H20V5.33335H12V6.66669H9.33334V5.33335C9.33334 4.62611 9.61429 3.94783 10.1144 3.44774C10.6145 2.94764 11.2928 2.66669 12 2.66669H20C20.7073 2.66669 21.3855 2.94764 21.8856 3.44774C22.3857 3.94783 22.6667 4.62611 22.6667 5.33335Z" fill="#858CA2" />
<path d="M26.6667 8H5.33333C4.97971 8 4.64057 8.14048 4.39052 8.39052C4.14048 8.64057 4 8.97971 4 9.33333C4 9.68696 4.14048 10.0261 4.39052 10.2761C4.64057 10.5262 4.97971 10.6667 5.33333 10.6667H6.76L7.92 26.8533C7.96732 27.5277 8.26903 28.159 8.76409 28.6194C9.25915 29.0798 9.91061 29.335 10.5867 29.3333H21.44C22.1161 29.335 22.7675 29.0798 23.2626 28.6194C23.7576 28.159 24.0593 27.5277 24.1067 26.8533L25.24 10.6667H26.6667C27.0203 10.6667 27.3594 10.5262 27.6095 10.2761C27.8595 10.0261 28 9.68696 28 9.33333C28 8.97971 27.8595 8.64057 27.6095 8.39052C27.3594 8.14048 27.0203 8 26.6667 8ZM17.3333 22.6667C17.3333 23.0203 17.1929 23.3594 16.9428 23.6095C16.6928 23.8595 16.3536 24 16 24C15.6464 24 15.3072 23.8595 15.0572 23.6095C14.8071 23.3594 14.6667 23.0203 14.6667 22.6667V14.6667C14.6667 14.313 14.8071 13.9739 15.0572 13.7239C15.3072 13.4738 15.6464 13.3333 16 13.3333C16.3536 13.3333 16.6928 13.4738 16.9428 13.7239C17.1929 13.9739 17.3333 14.313 17.3333 14.6667V22.6667Z" fill="#858CA2" />
</svg>
);
export default DeleteIcon;
+8
View File
@@ -0,0 +1,8 @@
const NextIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.01213 22.3739V9.61386C5.01213 7.00053 7.85213 5.36053 10.1188 6.6672L15.6521 9.85387L21.1854 13.0539C23.4521 14.3605 23.4521 17.6272 21.1854 18.9339L15.6521 22.1339L10.1188 25.3206C7.85213 26.6272 5.01213 25.0006 5.01213 22.3739Z" fill="#858CA2" />
<path d="M26.9844 6.75958C27.531 6.75958 27.9844 7.21291 27.9844 7.75958V24.2396C27.9844 24.7862 27.531 25.2396 26.9844 25.2396C26.4377 25.2396 25.9844 24.7862 25.9844 24.2396V7.75958C25.9844 7.21291 26.4377 6.75958 26.9844 6.75958Z" fill="#858CA2" />
</svg>
);
export default NextIcon;
+8
View File
@@ -0,0 +1,8 @@
const PauseIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66669 8.00002C2.66669 5.48586 2.66669 4.22878 3.44774 3.44774C4.22878 2.66669 5.48586 2.66669 8.00002 2.66669C10.5142 2.66669 11.7713 2.66669 12.5523 3.44774C13.3334 4.22878 13.3334 5.48586 13.3334 8.00002V24C13.3334 26.5142 13.3334 27.7712 12.5523 28.5523C11.7713 29.3334 10.5142 29.3334 8.00002 29.3334C5.48586 29.3334 4.22878 29.3334 3.44774 28.5523C2.66669 27.7712 2.66669 26.5142 2.66669 24V8.00002Z" fill="#858CA2" />
<path d="M18.6667 8.00002C18.6667 5.48586 18.6667 4.22878 19.4478 3.44774C20.2288 2.66669 21.4859 2.66669 24 2.66669C26.5142 2.66669 27.7712 2.66669 28.5523 3.44774C29.3334 4.22878 29.3334 5.48586 29.3334 8.00002V24C29.3334 26.5142 29.3334 27.7712 28.5523 28.5523C27.7712 29.3334 26.5142 29.3334 24 29.3334C21.4859 29.3334 20.2288 29.3334 19.4478 28.5523C18.6667 27.7712 18.6667 26.5142 18.6667 24V8.00002Z" fill="#858CA2" />
</svg>
);
export default PauseIcon;
+7
View File
@@ -0,0 +1,7 @@
const PlayIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.5448 12.4701C31.374 14.0087 31.374 17.9914 28.5448 19.5299L11.4622 28.8194C8.71248 30.3147 5.33334 28.3684 5.33334 25.2895V6.71055C5.33334 3.63159 8.71248 1.68538 11.4622 3.18066L28.5448 12.4701Z" fill="#858CA2" />
</svg>
);
export default PlayIcon;
+8
View File
@@ -0,0 +1,8 @@
const PreviousIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.9879 9.62609V22.3861C26.9879 24.9995 24.1479 26.6395 21.8812 25.3328L16.3479 22.1461L10.8146 18.9461C8.54793 17.6395 8.54793 14.3728 10.8146 13.0661L16.3479 9.86609L21.8812 6.67943C24.1479 5.37276 26.9879 6.99943 26.9879 9.62609Z" fill="#858CA2" />
<path d="M5.01562 25.2404C4.46896 25.2404 4.01562 24.7871 4.01562 24.2404V7.76044C4.01562 7.21377 4.46896 6.76044 5.01562 6.76044C5.56229 6.76044 6.01563 7.21377 6.01563 7.76044V24.2404C6.01563 24.7871 5.56229 25.2404 5.01562 25.2404Z" fill="#858CA2" />
</svg>
);
export default PreviousIcon;
+7
View File
@@ -0,0 +1,7 @@
const SaveIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30 28.4444V10.4218C29.9999 10.0093 29.836 9.61366 29.5442 9.322L22.678 2.45578C22.3863 2.16403 21.9907 2.00009 21.5782 2H3.55556C3.143 2 2.74733 2.16389 2.45561 2.45561C2.16389 2.74733 2 3.143 2 3.55556V28.4444C2 28.857 2.16389 29.2527 2.45561 29.5444C2.74733 29.8361 3.143 30 3.55556 30H28.4444C28.857 30 29.2527 29.8361 29.5444 29.5444C29.8361 29.2527 30 28.857 30 28.4444ZM11.3333 9.77778H17.5556C17.9681 9.77778 18.3638 9.94167 18.6555 10.2334C18.9472 10.5251 19.1111 10.9208 19.1111 11.3333C19.1111 11.7459 18.9472 12.1416 18.6555 12.4333C18.3638 12.725 17.9681 12.8889 17.5556 12.8889H11.3333C10.9208 12.8889 10.5251 12.725 10.2334 12.4333C9.94167 12.1416 9.77778 11.7459 9.77778 11.3333C9.77778 10.9208 9.94167 10.5251 10.2334 10.2334C10.5251 9.94167 10.9208 9.77778 11.3333 9.77778ZM22.2222 24.8889C22.2222 25.9935 21.3268 26.8889 20.2222 26.8889H11.7778C10.6732 26.8889 9.77778 25.9935 9.77778 24.8889V20.6667C9.77778 20.2541 9.94167 19.8584 10.2334 19.5667C10.5251 19.275 10.9208 19.1111 11.3333 19.1111H20.6667C21.0792 19.1111 21.4749 19.275 21.7666 19.5667C22.0583 19.8584 22.2222 20.2541 22.2222 20.6667V24.8889Z" fill="#858CA2" />
</svg>
);
export default SaveIcon;
+7
View File
@@ -0,0 +1,7 @@
const StopIcon = ({ width = 32, height = 32 }) => (
<svg width={width} height={height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66666 16C2.66666 9.71462 2.66666 6.57193 4.61928 4.61931C6.5719 2.66669 9.71459 2.66669 16 2.66669C22.2853 2.66669 25.4281 2.66669 27.3807 4.61931C29.3333 6.57193 29.3333 9.71462 29.3333 16C29.3333 22.2854 29.3333 25.4282 27.3807 27.3807C25.4281 29.3334 22.2853 29.3334 16 29.3334C9.71459 29.3334 6.5719 29.3334 4.61928 27.3807C2.66666 25.4282 2.66666 22.2854 2.66666 16Z" fill="#858CA2" />
</svg>
);
export default StopIcon;
+4
View File
@@ -7,6 +7,10 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
* {
box-sizing: border-box;
}
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
+8 -7
View File
@@ -1,3 +1,5 @@
import { detectionTypes } from "../constants/detectionTypes";
export const calculateRelativeCoordinates = (e, containerRef) => { export const calculateRelativeCoordinates = (e, containerRef) => {
if (!containerRef.current) return { x: 0, y: 0 }; if (!containerRef.current) return { x: 0, y: 0 };
const containerRect = containerRef.current.getBoundingClientRect(); const containerRect = containerRef.current.getBoundingClientRect();
@@ -13,7 +15,7 @@ export const isMouseOverDetection = (x, y, detection, containerRef) => {
return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2; return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2;
}; };
export const createAnnotationImage = (videoRef, detections, containerRef) => { export const createAnnotationImage = (videoRef, detections, containerRef, detectionType) => {
if (!videoRef?.current || !containerRef?.current) { if (!videoRef?.current || !containerRef?.current) {
console.warn("Missing video or container reference"); console.warn("Missing video or container reference");
return null; return null;
@@ -39,17 +41,12 @@ export const createAnnotationImage = (videoRef, detections, containerRef) => {
detections.forEach(detection => { detections.forEach(detection => {
if (!detection?.class) return; if (!detection?.class) return;
// Ensure proper opacity for background - consistently using 0.4 opacity
const bgColor = detection.class.Color?.startsWith('rgba')
? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)')
: detection.class.Color?.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)') || 'rgba(255, 0, 0, 0.4)';
// Ensure full opacity for border // Ensure full opacity for border
const borderColor = detection.class.Color?.startsWith('rgba') const borderColor = detection.class.Color?.startsWith('rgba')
? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)') ? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)')
: detection.class.Color || 'rgba(255, 0, 0, 1)'; : detection.class.Color || 'rgba(255, 0, 0, 1)';
ctx.fillStyle = bgColor; ctx.fillStyle = 'rgba(255, 255, 255, 0)';
ctx.strokeStyle = borderColor; ctx.strokeStyle = borderColor;
const x = Math.max(0, detection.x1 || 0) * detection.kw; const x = Math.max(0, detection.x1 || 0) * detection.kw;
@@ -63,7 +60,11 @@ export const createAnnotationImage = (videoRef, detections, containerRef) => {
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.font = '12px Arial'; ctx.font = '12px Arial';
if (detectionType === detectionTypes.day){
ctx.fillText(detection.class.Name || 'Unknown', x, y - 5); ctx.fillText(detection.class.Name || 'Unknown', x, y - 5);
} else {
ctx.fillText(`${detection.class.Name} (${detectionType}) `|| 'Unknown', x, y - 5);
}
}); });
} }