diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index a1b5ef7..6cbb23b 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -2485,10 +2485,10 @@ function DetectionAnalyticsPage({ detectionRunId }: { detectionRunId: string }) {item && ( -
- - -
+
)} @@ -2521,24 +2521,149 @@ function Detail({ label, value }: { label: string; value: React.ReactNode }) { ); } -function DetectionImage({ title, url }: { title: string; url?: string | null }) { - const [fullscreen, setFullscreen] = useState(false); +type DetectionImageVariant = 'raw' | 'annotated'; + +type DetectionImageOption = { + key: DetectionImageVariant; + title: string; + selectorLabel: string; + url?: string | null; +}; + +function DetectionImageComparison({ rawUrl, annotatedUrl }: { rawUrl?: string | null; annotatedUrl?: string | null }) { + const [fullscreenImage, setFullscreenImage] = useState(null); + const images: DetectionImageOption[] = [ + { key: 'raw', title: 'Исходное изображение', selectorLabel: 'Исходное', url: rawUrl }, + { key: 'annotated', title: 'Изображение с разметкой', selectorLabel: 'С разметкой', url: annotatedUrl } + ]; + const availableImages = images.filter(image => Boolean(image.url)); + const selectedImage = images.find(image => image.key === fullscreenImage); + + useEffect(() => { + if (!fullscreenImage) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + + function onFullscreenChange() { + if (!document.fullscreenElement) { + setFullscreenImage(null); + } + } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape' && !document.fullscreenElement) { + setFullscreenImage(null); + return; + } + + if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && availableImages.length > 1) { + event.preventDefault(); + const currentIndex = availableImages.findIndex(image => image.key === fullscreenImage); + const direction = event.key === 'ArrowRight' ? 1 : -1; + const nextIndex = (currentIndex + direction + availableImages.length) % availableImages.length; + setFullscreenImage(availableImages[nextIndex].key); + } + } + + document.addEventListener('fullscreenchange', onFullscreenChange); + window.addEventListener('keydown', onKeyDown); + + return () => { + document.body.style.overflow = previousOverflow; + document.removeEventListener('fullscreenchange', onFullscreenChange); + window.removeEventListener('keydown', onKeyDown); + }; + }, [annotatedUrl, fullscreenImage, rawUrl]); + + async function openFullscreen(image: DetectionImageOption) { + if (!image.url) return; + setFullscreenImage(image.key); + + if (!document.fullscreenElement) { + try { + await document.documentElement.requestFullscreen(); + } catch { + // The fixed overlay remains available when the browser denies native fullscreen. + } + } + } + + async function closeFullscreen() { + if (document.fullscreenElement) { + try { + await document.exitFullscreen(); + } catch { + setFullscreenImage(null); + } + return; + } + + setFullscreenImage(null); + } + + return ( + <> +
+ {images.map(image => ( + openFullscreen(image)} + /> + ))} +
+ + {fullscreenImage && selectedImage?.url && ( +
+
+
+ {images.map(image => ( + + ))} +
+ {selectedImage.title} +
+ +
+ {selectedImage.title} +
+
+ )} + + ); +} + +function DetectionImage({ + title, + url, + onFullscreen +}: { + title: string; + url?: string | null; + onFullscreen: () => void; +}) { return (

{title}

- +
{url ? {title} :
Изображение недоступно
} - {fullscreen && url && ( -
- - {title} -
- )}
); } diff --git a/src/styles.css b/src/styles.css index 868ade4..b543180 100644 --- a/src/styles.css +++ b/src/styles.css @@ -688,6 +688,84 @@ body { background: var(--bg); color: var(--text); font-family: Inter, system-ui, background: #07120c; } +.analytics-detection-fullscreen { + padding: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + place-items: stretch; + background: #020a06; +} + +.analytics-detection-fullscreen-toolbar { + min-width: 0; + min-height: 64px; + padding: 10px 72px 10px 16px; + display: flex; + align-items: center; + gap: 16px; + background: rgba(7, 18, 12, 0.96); + border-bottom: 1px solid rgba(255, 255, 255, 0.16); + color: #fff; +} + +.analytics-detection-fullscreen-selector { + flex: 0 1 420px; + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.3); +} + +.analytics-detection-fullscreen-selector button { + color: rgba(255, 255, 255, 0.82); +} + +.analytics-detection-fullscreen-selector button.active { + background: #fff; + color: #102015; +} + +.analytics-detection-fullscreen-selector button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.analytics-detection-fullscreen-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 700; +} + +.analytics-detection-fullscreen-stage { + min-height: 0; + display: grid; + place-items: center; + overflow: hidden; +} + +.analytics-detection-fullscreen-stage img { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + object-fit: contain; +} + +@media (max-width: 760px) { + .analytics-detection-fullscreen-toolbar { + min-height: 0; + padding: 10px 64px 10px 10px; + align-items: stretch; + flex-direction: column; + gap: 8px; + } + + .analytics-detection-fullscreen-selector { + flex-basis: auto; + width: 100%; + } +} + .analytics-feedback-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));