Enhance annotations: save/download, fallback classes, photo mode icons

Add local annotation save fallback, PNG+txt download with drawn boxes,
shared classColors helper, photo mode icon toggles, and react-dropzone
/ react-icons dependencies.
This commit is contained in:
Armen Rohalov
2026-04-17 23:33:00 +03:00
parent 567092188d
commit 63cc18e788
15 changed files with 782 additions and 218 deletions
+44 -17
View File
@@ -1,6 +1,9 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MdOutlineWbSunny, MdOutlineNightlightRound } from 'react-icons/md'
import { FaRegSnowflake } from 'react-icons/fa'
import { api } from '../api/client'
import { getClassColor, FALLBACK_CLASS_NAMES } from '../features/annotations/classColors'
import type { DetectionClass } from '../types'
interface Props {
@@ -10,12 +13,25 @@ interface Props {
onPhotoModeChange: (mode: number) => void
}
const FALLBACK_CLASSES: DetectionClass[] = [0, 20, 40].flatMap(modeOffset =>
FALLBACK_CLASS_NAMES.map((name, i) => ({
id: i + modeOffset,
name,
shortName: name.slice(0, 3),
color: getClassColor(i),
maxSizeM: 10,
photoMode: modeOffset,
})),
)
export default function DetectionClasses({ selectedClassNum, onSelect, photoMode, onPhotoModeChange }: Props) {
const { t } = useTranslation()
const [classes, setClasses] = useState<DetectionClass[]>([])
useEffect(() => {
api.get<DetectionClass[]>('/api/annotations/classes').then(setClasses).catch(() => {})
api.get<DetectionClass[]>('/api/annotations/classes')
.then(list => setClasses(list?.length ? list : FALLBACK_CLASSES))
.catch(() => setClasses(FALLBACK_CLASSES))
}, [])
useEffect(() => {
@@ -31,27 +47,25 @@ export default function DetectionClasses({ selectedClassNum, onSelect, photoMode
return () => window.removeEventListener('keydown', handler)
}, [classes, photoMode, onSelect])
// Auto-select first class of current photoMode when mode changes or classes load
useEffect(() => {
const modeClasses = classes.filter(c => c.photoMode === photoMode)
const currentIsInMode = modeClasses.some(c => c.id === selectedClassNum)
if (!currentIsInMode && modeClasses.length > 0) {
onSelect(modeClasses[0].id)
}
}, [classes, photoMode, selectedClassNum, onSelect])
const modes = [
{ value: 0, label: t('annotations.regular') },
{ value: 20, label: t('annotations.winter') },
{ value: 40, label: t('annotations.night') },
{ value: 0, label: t('annotations.regular'), icon: <MdOutlineWbSunny />, activeClass: 'bg-az-orange text-white', iconColor: 'text-az-orange' },
{ value: 20, label: t('annotations.winter'), icon: <FaRegSnowflake />, activeClass: 'bg-az-blue text-white', iconColor: 'text-az-blue' },
{ value: 40, label: t('annotations.night'), icon: <MdOutlineNightlightRound />, activeClass: 'bg-purple-600 text-white', iconColor: 'text-purple-400' },
]
return (
<div className="border-t border-az-border p-2">
<div className="text-xs text-az-muted mb-1 font-semibold">{t('annotations.classes')}</div>
<div className="flex gap-1 mb-2">
{modes.map(m => (
<button
key={m.value}
onClick={() => onPhotoModeChange(m.value)}
className={`text-xs px-2 py-0.5 rounded ${photoMode === m.value ? 'bg-az-orange text-white' : 'bg-az-bg text-az-muted'}`}
>
{m.label}
</button>
))}
</div>
<div className="space-y-0.5 max-h-48 overflow-y-auto">
<div className="space-y-0.5 max-h-48 overflow-y-auto mb-2">
{classes.filter(c => c.photoMode === photoMode).map((c, i) => (
<button
key={c.id}
@@ -60,13 +74,26 @@ export default function DetectionClasses({ selectedClassNum, onSelect, photoMode
selectedClassNum === c.id ? 'bg-az-border text-white' : 'text-az-text hover:bg-az-bg'
}`}
>
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: c.color }} />
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: getClassColor(c.id) }} />
<span className="text-az-muted">{i + 1}.</span>
<span className="truncate">{c.name}</span>
<span className="text-az-muted ml-auto">{c.shortName}</span>
</button>
))}
</div>
<div className="text-xs text-az-muted mb-1 font-semibold">{t('annotations.photoMode')}</div>
<div className="flex gap-1">
{modes.map(m => (
<button
key={m.value}
onClick={() => onPhotoModeChange(m.value)}
title={m.label}
className={`flex-1 flex items-center justify-center px-2 py-1 rounded text-base ${photoMode === m.value ? m.activeClass : `bg-az-bg ${m.iconColor} hover:brightness-125`}`}
>
{m.icon}
</button>
))}
</div>
</div>
)
}