diff --git a/src/features/dataset/DatasetClassList.tsx b/src/features/dataset/DatasetClassList.tsx new file mode 100644 index 0000000..2a16dbe --- /dev/null +++ b/src/features/dataset/DatasetClassList.tsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { api, endpoints } from '../../api' +import { getClassColor, FALLBACK_CLASS_NAMES } from '../../class-colors' +import type { DetectionClass, ClassDistributionItem } from '../../types' + +const FALLBACK_CLASSES: DetectionClass[] = FALLBACK_CLASS_NAMES.map((name, i) => ({ + id: i + 1, + name, + shortName: name.slice(0, 3), + color: getClassColor(i), + maxSizeM: 10, + photoMode: 0, +})) + +interface DatasetClassListProps { + selectedClassNum: number + onSelect: (classNum: number) => void +} + +export default function DatasetClassList({ selectedClassNum, onSelect }: DatasetClassListProps) { + const { t } = useTranslation() + const [classes, setClasses] = useState([]) + const [counts, setCounts] = useState>({}) + + useEffect(() => { + api.get(endpoints.annotations.classes()) + .then(list => setClasses(list?.length ? list : FALLBACK_CLASSES)) + .catch(() => setClasses(FALLBACK_CLASSES)) + }, []) + + useEffect(() => { + api.get(endpoints.annotations.datasetClassDistribution()) + .then(data => { + const map: Record = {} + for (const d of data) map[d.classNum] = d.count + setCounts(map) + }) + .catch(() => {}) + }, []) + + const regularClasses = useMemo(() => classes.filter(c => c.photoMode === 0), [classes]) + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + const num = parseInt(e.key) + if (num >= 1 && num <= 9) { + const cls = regularClasses[num - 1] + if (cls) onSelect(cls.id) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [regularClasses, onSelect]) + + return ( + <> +
+ {t('annotations.classes')} + + {regularClasses.length.toString().padStart(2, '0')} + +
+
+ {regularClasses.map(c => { + const isActive = c.id === selectedClassNum + const count = counts[c.id] ?? 0 + return ( +
onSelect(c.id)} + onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') onSelect(c.id) }} + className={`flex items-center gap-2.5 h-7 px-2 rounded-[2px] cursor-pointer transition-colors ${ + isActive + ? 'bg-surface-2 text-text-primary' + : 'text-text-secondary hover:bg-surface-2 hover:text-text-primary' + }`} + > + + {c.name} + + {count.toLocaleString()} + +
+ ) + })} +
+ + ) +} diff --git a/src/features/dataset/DatasetFilterBar.tsx b/src/features/dataset/DatasetFilterBar.tsx new file mode 100644 index 0000000..43e61de --- /dev/null +++ b/src/features/dataset/DatasetFilterBar.tsx @@ -0,0 +1,187 @@ +import { useTranslation } from 'react-i18next' +import { AnnotationStatus } from '../../types' + +interface DatasetFilterBarProps { + fromDate: string + toDate: string + onFromDateChange: (v: string) => void + onToDateChange: (v: string) => void + statusFilter: AnnotationStatus | null + onStatusFilterChange: (s: AnnotationStatus | null) => void + flightName: string | null + shownCount: number + totalCount: number +} + +export default function DatasetFilterBar({ + fromDate, + toDate, + onFromDateChange, + onToDateChange, + statusFilter, + onStatusFilterChange, + flightName, + shownCount, + totalCount, +}: DatasetFilterBarProps) { + const { t } = useTranslation() + + const STATUS_OPTIONS = [ + { + value: null, + label: t('dataset.status.none'), + tone: 'muted' as const, + dot: 'var(--text-muted)', + }, + { + value: AnnotationStatus.Created, + label: t('dataset.status.created'), + tone: 'amber' as const, + dot: 'var(--accent-amber)', + }, + { + value: AnnotationStatus.Edited, + label: t('dataset.status.edited'), + tone: 'blue' as const, + dot: 'var(--accent-blue)', + }, + { + value: AnnotationStatus.Validated, + label: t('dataset.status.validated'), + tone: 'green' as const, + dot: 'var(--accent-green)', + }, + ] + + return ( +
+ + + {/* Range group */} +
+ {t('dataset.range')} + onFromDateChange(e.target.value)} + onClick={e => e.currentTarget.showPicker?.()} + /> + + onToDateChange(e.target.value)} + onClick={e => e.currentTarget.showPicker?.()} + /> +
+ + {/* divider */} + + + {/* Flight group — display-only chip */} +
+ {t('dataset.flight')} +
+ + + {flightName ?? '—'} + + +
+
+ + {/* divider */} + + + {/* Status chips */} +
+ {t('dataset.statusLabel')} + {STATUS_OPTIONS.map(opt => { + const isActive = statusFilter === opt.value + const stateCls = !isActive + ? 'text-text-secondary border-border-hair hover:text-text-primary hover:border-border-raised' + : opt.tone === 'muted' + ? 'text-text-primary border-border-raised bg-text-muted/20' + : opt.tone === 'amber' + ? 'text-accent-amber border-accent-amber bg-accent-amber/10' + : opt.tone === 'blue' + ? 'text-accent-blue border-accent-blue bg-accent-blue/10' + : /* green */ 'text-accent-green border-accent-green bg-accent-green/10' + return ( + + ) + })} +
+ + {/* right side */} +
+ + {t('dataset.showing')} + + + {shownCount.toLocaleString()} + / {totalCount.toLocaleString()} + + + + +
+
+ ) +} diff --git a/src/features/dataset/DatasetLeftPanel.tsx b/src/features/dataset/DatasetLeftPanel.tsx new file mode 100644 index 0000000..532ee77 --- /dev/null +++ b/src/features/dataset/DatasetLeftPanel.tsx @@ -0,0 +1,102 @@ +import { useTranslation } from 'react-i18next' +import DatasetClassList from './DatasetClassList' + +interface DatasetLeftPanelProps { + selectedClassNum: number + onSelectClass: (n: number) => void + objectsOnly: boolean + onObjectsOnlyChange: (v: boolean) => void + search: string + onSearchChange: (v: string) => void + totalCount: number + validatedCount: number +} + +export default function DatasetLeftPanel({ + selectedClassNum, + onSelectClass, + objectsOnly, + onObjectsOnlyChange, + search, + onSearchChange, + totalCount, + validatedCount, +}: DatasetLeftPanelProps) { + const { t } = useTranslation() + + return ( + + ) +} diff --git a/src/features/dataset/DatasetPage.tsx b/src/features/dataset/DatasetPage.tsx index 6814b2c..54b7e51 100644 --- a/src/features/dataset/DatasetPage.tsx +++ b/src/features/dataset/DatasetPage.tsx @@ -1,30 +1,25 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { FaPen } from 'react-icons/fa' import { api, endpoints } from '../../api' -import { useDebounce, useResizablePanel } from '../../hooks' -import { useFlight, DetectionClasses } from '../../components' +import { useDebounce } from '../../hooks' +import { useFlight } from '../../components' import { useSavedAnnotations } from '../../components/SavedAnnotationsContext' import CanvasEditor from '../annotations/CanvasEditor' import { recaptureThumbnails } from '../annotations/thumbnail' import type { SavedDetection } from '../../components/SavedAnnotationsContext' -import type { DatasetItem, PaginatedResponse, ClassDistributionItem, AnnotationListItem, Detection, Media } from '../../types' +import type { + DatasetItem, + PaginatedResponse, + ClassDistributionItem, + AnnotationListItem, + Detection, + Media, +} from '../../types' import { AnnotationSource, AnnotationStatus } from '../../types' - -interface DatasetCard { - annotationId: string - imageName: string - status: AnnotationStatus - createdDate: string - thumbnailUrl: string - isSeed: boolean - isLocal: boolean - detections?: Detection[] - mediaId?: string - time?: string | null - fullFrame?: string - annotationLocalId?: string -} +import DatasetLeftPanel from './DatasetLeftPanel' +import DatasetFilterBar from './DatasetFilterBar' +import DatasetTile, { type DatasetCard } from './DatasetTile' +import DatasetStatusBar from './DatasetStatusBar' type Tab = 'annotations' | 'editor' | 'distribution' @@ -32,7 +27,6 @@ export default function DatasetPage() { const { t } = useTranslation() const { selectedFlight } = useFlight() const { saved: savedAnnotations, removeSaved, replaceGroup, updateStatus } = useSavedAnnotations() - const leftPanel = useResizablePanel(250, 200, 400) const [items, setItems] = useState([]) const [totalCount, setTotalCount] = useState(0) @@ -45,12 +39,14 @@ export default function DatasetPage() { const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 400) const [selectedClassNum, setSelectedClassNum] = useState(0) - const [photoMode, setPhotoMode] = useState(0) const [selectedIds, setSelectedIds] = useState>(new Set()) const [tab, setTab] = useState('annotations') const [editorAnnotation, setEditorAnnotation] = useState(null) const [editorDetections, setEditorDetections] = useState([]) const [distribution, setDistribution] = useState([]) + const [editorFullFrame, setEditorFullFrame] = useState('') + const [editorLocalGroupId, setEditorLocalGroupId] = useState(null) + const [editorSaving, setEditorSaving] = useState(false) const fetchItems = useCallback(async () => { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }) @@ -107,11 +103,7 @@ export default function DatasetPage() { })) return [...localCards, ...remoteCards] - }, [savedAnnotations, items, selectedFlight, statusFilter, objectsOnly, selectedClassNum, debouncedSearch, fromDate, toDate]) - - const [editorFullFrame, setEditorFullFrame] = useState('') - const [editorLocalGroupId, setEditorLocalGroupId] = useState(null) - const [editorSaving, setEditorSaving] = useState(false) + }, [savedAnnotations, items, selectedFlight, statusFilter, selectedClassNum, debouncedSearch, fromDate, toDate]) const handleDoubleClick = async (card: DatasetCard) => { if (card.isLocal && card.detections && card.mediaId) { @@ -151,7 +143,7 @@ export default function DatasetPage() { const existing = savedAnnotations.find(s => s.annotationLocalId === editorLocalGroupId) const thumbs = await recaptureThumbnails(editorFullFrame, editorDetections) const now = new Date().toISOString() - const items: SavedDetection[] = editorDetections.map((d, i) => ({ + const replacement: SavedDetection[] = editorDetections.map((d, i) => ({ id: `${editorLocalGroupId}:${d.id ?? i}`, annotationLocalId: editorLocalGroupId, mediaId: editorAnnotation.mediaId, @@ -165,7 +157,7 @@ export default function DatasetPage() { time: editorAnnotation.time, flightId: existing?.flightId ?? null, })) - replaceGroup(editorLocalGroupId, items) + replaceGroup(editorLocalGroupId, replacement) } setTab('annotations') } finally { @@ -210,100 +202,121 @@ export default function DatasetPage() { const maxDistCount = Math.max(...distribution.map(d => d.count), 1) const totalPages = Math.ceil(totalCount / pageSize) + const grandTotal = totalCount + savedAnnotations.length + const validatedCount = useMemo( + () => cards.filter(c => c.status === AnnotationStatus.Validated).length, + [cards], + ) + + const firstSelectedName = useMemo(() => { + const firstId = selectedIds.values().next().value + if (!firstId) return null + return cards.find(c => c.annotationId === firstId)?.imageName ?? null + }, [selectedIds, cards]) const editorMedia: Media | null = editorAnnotation ? { id: editorAnnotation.mediaId, name: '', path: editorFullFrame, mediaType: 1, mediaStatus: 0, duration: null, annotationCount: 0, waypointId: null, userId: '', } : null - const statusButtons = [ - { label: 'All', value: null }, - { label: t('dataset.status.created'), value: AnnotationStatus.Created }, - { label: t('dataset.status.edited'), value: AnnotationStatus.Edited }, - { label: t('dataset.status.validated'), value: AnnotationStatus.Validated }, - ] - return ( -
- {/* Left panel */} -
- + + +
+ { setStatusFilter(s); setPage(1) }} + flightName={selectedFlight?.name ?? null} + shownCount={cards.length} + totalCount={grandTotal} /> -
- -
-
- setSearch(e.target.value)} - placeholder={t('dataset.search')} - className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none" - /> -
-
-
- {/* Main area */} -
- {/* Filter bar */} -
- setFromDate(e.target.value)} className="bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" /> - setToDate(e.target.value)} className="bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" /> - {statusButtons.map(sb => ( +
+ + + {/* Tab strip */} +
- ))} -
- {selectedIds.size > 0 && ( - - )} -
- - {/* Tabs */} -
- {(['annotations', 'editor', 'distribution'] as Tab[]).map(tb => ( + - ))} -
- {/* Content */} - {tab === 'annotations' && ( -
-
- {cards.map(card => { - const statusPill = - card.status === AnnotationStatus.Validated ? { cls: 'bg-az-green text-white', label: t('dataset.status.validated') } : - card.status === AnnotationStatus.Edited ? { cls: 'bg-az-blue text-white', label: t('dataset.status.edited') } : - { cls: 'bg-az-orange text-white', label: t('dataset.status.created') } - const isSelected = selectedIds.has(card.annotationId) - return ( -
+ + {t('dataset.liveSync')} +
+
+ + {/* Content */} + {tab === 'annotations' && ( +
+
+ {cards.map(card => ( + { - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { setSelectedIds(prev => { const n = new Set(prev) - n.has(card.annotationId) ? n.delete(card.annotationId) : n.add(card.annotationId) + if (n.has(card.annotationId)) n.delete(card.annotationId) + else n.add(card.annotationId) return n }) } else { @@ -316,119 +329,121 @@ export default function DatasetPage() { e.preventDefault() removeSaved(card.annotationId) }} - title={card.imageName} - className={`aspect-square bg-az-panel rounded border overflow-hidden cursor-pointer relative transition-colors ${ - isSelected ? 'border-az-orange' : 'border-az-border hover:border-az-blue' - } ${card.isSeed ? 'ring-2 ring-az-red' : ''}`} + onEditClick={() => handleDoubleClick(card)} + /> + ))} +
+ {cards.length === 0 && ( +
{t('common.noData')}
+ )} + {totalPages > 1 && ( +
+ + + {page} / {totalPages} + + +
+ )} +
+ )} + + {tab === 'editor' && editorMedia && editorAnnotation && ( +
+
+
+ + + + {editorDetections.length} detection{editorDetections.length !== 1 ? 's' : ''} + + {!editorLocalGroupId && ( + + remote save not wired yet + + )} +
+
+
+ +
+
+
+
+ )} + + {tab === 'distribution' && ( +
+ {distribution.map(d => { + const pct = (d.count / maxDistCount) * 100 + return ( +
+
+ + {d.label} + + {d.count.toLocaleString()} - {card.isLocal && ( - - local - - )} -
) })} + {distribution.length === 0 && ( +
{t('common.noData')}
+ )}
- {cards.length === 0 && ( -
{t('common.noData')}
- )} - {/* Pagination */} - {totalPages > 1 && ( -
- - {page} / {totalPages} - -
- )} -
- )} + )} +
- {tab === 'editor' && editorMedia && editorAnnotation && ( -
-
-
- - - - {editorDetections.length} detection{editorDetections.length !== 1 ? 's' : ''} - - {!editorLocalGroupId && ( - - remote save not wired yet - - )} -
-
-
- -
-
-
-
- )} - - {tab === 'distribution' && ( -
- {distribution.map(d => { - const pct = (d.count / maxDistCount) * 100 - return ( -
-
-
- {d.label}: {d.count} - {d.count} -
-
- ) - })} -
- )} -
+ 0} + onValidate={handleValidate} + /> +
) } diff --git a/src/features/dataset/DatasetStatusBar.tsx b/src/features/dataset/DatasetStatusBar.tsx new file mode 100644 index 0000000..97cb1f1 --- /dev/null +++ b/src/features/dataset/DatasetStatusBar.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next' + +interface DatasetStatusBarProps { + selectedCount: number + totalShown: number + firstSelectedName: string | null + canValidate: boolean + onValidate: () => void + onRefreshThumbnails?: () => void +} + +export default function DatasetStatusBar({ + selectedCount, + totalShown, + firstSelectedName, + canValidate, + onValidate, + onRefreshThumbnails, +}: DatasetStatusBarProps) { + const { t } = useTranslation() + + return ( +
+ + + + + + + + +
+ {t('dataset.selected')} + + {firstSelectedName ?? '—'} + +
+ +
+ + {t('dataset.ofSelected', { count: selectedCount, total: totalShown })} + +
+
+ ) +} diff --git a/src/features/dataset/DatasetTile.tsx b/src/features/dataset/DatasetTile.tsx new file mode 100644 index 0000000..1827236 --- /dev/null +++ b/src/features/dataset/DatasetTile.tsx @@ -0,0 +1,151 @@ +import { useTranslation } from 'react-i18next' +import { FaPen } from 'react-icons/fa' +import { AnnotationStatus } from '../../types' +import type { Detection } from '../../types' + +export interface DatasetCard { + annotationId: string + imageName: string + status: AnnotationStatus + createdDate: string + thumbnailUrl: string + isSeed: boolean + isLocal: boolean + detections?: Detection[] + mediaId?: string + time?: string | null + fullFrame?: string + annotationLocalId?: string +} + +interface DatasetTileProps { + card: DatasetCard + isSelected: boolean + onClick: (e: React.MouseEvent) => void + onDoubleClick: () => void + onContextMenu: (e: React.MouseEvent) => void + onEditClick: () => void +} + +export function formatTileDate(iso: string): string { + try { + const d = new Date(iso) + if (isNaN(d.getTime())) return '' + return new Intl.DateTimeFormat('en', { day: '2-digit', month: 'short' }) + .format(d) + .toUpperCase() + } catch { + return '' + } +} + +export default function DatasetTile({ + card, + isSelected, + onClick, + onDoubleClick, + onContextMenu, + onEditClick, +}: DatasetTileProps) { + const { t } = useTranslation() + + const statusPill = + card.status === AnnotationStatus.Validated + ? { cls: 'pill-green', label: t('dataset.status.validated') } + : card.status === AnnotationStatus.Edited + ? { cls: 'pill-blue', label: t('dataset.status.edited') } + : card.status === AnnotationStatus.Created + ? { cls: 'pill-amber', label: t('dataset.status.created') } + : { cls: 'pill-muted', label: t('dataset.status.none') } + + const borderCls = isSelected + ? 'border-2 border-accent-amber' + : card.isSeed + ? 'border border-accent-red' + : 'border border-border-hair hover:border-accent-amber' + + return ( +
+ {card.thumbnailUrl ? ( + {card.imageName} + ) : ( +
+ )} + + {/* composite scrim: grid lines + bottom fade (matches design .tile .scrim) */} +
+ + {/* corner-tag top-right */} + {formatTileDate(card.createdDate) && ( +
+ {formatTileDate(card.createdDate)} +
+ )} + + {/* local badge — top-left */} + {card.isLocal && ( +
+ {t('dataset.local').toUpperCase()} +
+ )} + + {/* selected check badge (only when selected & not local — local already has top-left badge) */} + {isSelected && !card.isLocal && ( +
+ + + +
+ )} + + {/* status pill bottom-left */} + + + {statusPill.label} + + + {/* edit ibtn bottom-right (reveal on hover) */} + +
+ ) +} diff --git a/src/i18n/en.json b/src/i18n/en.json index d124ab2..0ad77b4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -133,12 +133,30 @@ "editor": "Editor", "classDistribution": "Class Distribution", "objectsOnly": "Show with objects only", - "search": "Search...", + "hideEmpty": "Hide empty frames", + "search": "Search annotation name…", "validate": "Validate", + "edit": "Edit", + "filters": "Filters", + "total": "Total", + "validatedCount": "Validated", + "range": "Range", + "flight": "Flight", + "showing": "Showing", + "liveSync": "Live sync", + "selected": "Selected", + "refreshThumbnails": "Refresh Thumbnails", + "ofSelected": "{{count}} of {{total}} selected", + "local": "Local", + "sort": "Sort", + "gridDensity": "Grid density", + "statusLabel": "Status", "status": { "created": "Created", "edited": "Edited", - "validated": "Validated" + "validated": "Validated", + "all": "All", + "none": "None" } }, "admin": { diff --git a/src/i18n/ua.json b/src/i18n/ua.json index 92d4cab..670db7b 100644 --- a/src/i18n/ua.json +++ b/src/i18n/ua.json @@ -135,12 +135,30 @@ "editor": "Редактор", "classDistribution": "Розподіл класів", "objectsOnly": "Тільки з об'єктами", - "search": "Пошук...", + "hideEmpty": "Приховати порожні кадри", + "search": "Пошук за назвою анотації…", "validate": "Валідувати", + "edit": "Редагувати", + "filters": "Фільтри", + "total": "Всього", + "validatedCount": "Валідовано", + "range": "Діапазон", + "flight": "Політ", + "showing": "Показано", + "liveSync": "Жива синхронізація", + "selected": "Вибрано", + "refreshThumbnails": "Оновити мініатюри", + "ofSelected": "{{count}} з {{total}} вибрано", + "local": "Локально", + "sort": "Сортування", + "gridDensity": "Щільність сітки", + "statusLabel": "Статус", "status": { "created": "Створено", "edited": "Відредаговано", - "validated": "Валідовано" + "validated": "Валідовано", + "all": "Всі", + "none": "Жоден" } }, "admin": { diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index ce55d93..b9c2b17 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/sse.ts","./src/auth/authcontext.tsx","./src/auth/protectedroute.tsx","./src/components/confirmdialog.tsx","./src/components/detectionclasses.tsx","./src/components/flightcontext.tsx","./src/components/header.tsx","./src/components/helpmodal.tsx","./src/components/savedannotationscontext.tsx","./src/features/admin/adminpage.tsx","./src/features/annotations/annotationspage.tsx","./src/features/annotations/annotationssidebar.tsx","./src/features/annotations/canvaseditor.tsx","./src/features/annotations/medialist.tsx","./src/features/annotations/videoplayer.tsx","./src/features/annotations/classcolors.ts","./src/features/annotations/thumbnail.ts","./src/features/dataset/datasetpage.tsx","./src/features/flights/altitudechart.tsx","./src/features/flights/altitudedialog.tsx","./src/features/flights/drawcontrol.tsx","./src/features/flights/flightlistsidebar.tsx","./src/features/flights/flightmap.tsx","./src/features/flights/flightparamspanel.tsx","./src/features/flights/flightspage.tsx","./src/features/flights/jsoneditordialog.tsx","./src/features/flights/mappoint.tsx","./src/features/flights/minimap.tsx","./src/features/flights/waypointlist.tsx","./src/features/flights/windeffect.tsx","./src/features/flights/flightplanutils.ts","./src/features/flights/mapicons.ts","./src/features/flights/types.ts","./src/features/login/loginpage.tsx","./src/features/settings/settingspage.tsx","./src/hooks/usedebounce.ts","./src/hooks/useresizablepanel.ts","./src/i18n/i18n.ts","./src/types/index.ts"],"version":"5.7.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/endpoints.ts","./src/api/index.ts","./src/api/sse.ts","./src/auth/authcontext.tsx","./src/auth/protectedroute.tsx","./src/auth/index.ts","./src/class-colors/classcolors.ts","./src/class-colors/index.ts","./src/components/confirmdialog.tsx","./src/components/detectionclasses.tsx","./src/components/flightcontext.tsx","./src/components/header.tsx","./src/components/helpmodal.tsx","./src/components/savedannotationscontext.tsx","./src/components/index.ts","./src/features/admin/adminpage.tsx","./src/features/admin/classeditrow.tsx","./src/features/admin/modal.tsx","./src/features/admin/numberstepper.tsx","./src/features/admin/index.ts","./src/features/admin/useaisettings.ts","./src/features/admin/usegpssettings.ts","./src/features/annotations/annotationspage.tsx","./src/features/annotations/annotationssidebar.tsx","./src/features/annotations/canvaseditor.tsx","./src/features/annotations/medialist.tsx","./src/features/annotations/scrubber.tsx","./src/features/annotations/videoplayer.tsx","./src/features/annotations/index.ts","./src/features/annotations/thumbnail.ts","./src/features/annotations/time.ts","./src/features/dataset/datasetleftpanel.tsx","./src/features/dataset/datasetpage.tsx","./src/features/dataset/index.ts","./src/features/flights/altitudechart.tsx","./src/features/flights/altitudedialog.tsx","./src/features/flights/drawcontrol.tsx","./src/features/flights/flightlistsidebar.tsx","./src/features/flights/flightmap.tsx","./src/features/flights/flightparamspanel.tsx","./src/features/flights/flightspage.tsx","./src/features/flights/jsoneditordialog.tsx","./src/features/flights/mappoint.tsx","./src/features/flights/minimap.tsx","./src/features/flights/waypointlist.tsx","./src/features/flights/windeffect.tsx","./src/features/flights/flightplanutils.ts","./src/features/flights/index.ts","./src/features/flights/mapicons.ts","./src/features/flights/types.ts","./src/features/login/loginpage.tsx","./src/features/login/index.ts","./src/features/settings/settingspage.tsx","./src/features/settings/index.ts","./src/hooks/index.ts","./src/hooks/usedebounce.ts","./src/hooks/useresizablepanel.ts","./src/i18n/i18n.ts","./src/i18n/index.ts","./src/types/index.ts"],"version":"5.7.3"} \ No newline at end of file