diff --git a/ui/package.json b/ui/package.json index 624daf146..454c0147c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -38,6 +38,7 @@ "react-markdown": "^9.0.1", "react-plotly.js": "^2.6.0", "react-router-dom": "^6.8.2", + "react-zoom-pan-pinch": "^4.0.3", "tailwind-merge": "^3.6.0" }, "scripts": { diff --git a/ui/src/data-services/models/capture.ts b/ui/src/data-services/models/capture.ts index e4e1fc137..4d6d7b303 100644 --- a/ui/src/data-services/models/capture.ts +++ b/ui/src/data-services/models/capture.ts @@ -167,17 +167,19 @@ export class Capture { return this._capture.event?.name ?? '' } - get thumbnail_small(): string { - if (this._capture.thumbnails && this._capture.thumbnails.small) { + get thumbnailSmall(): string { + if (this._capture.thumbnails?.small) { return this._capture.thumbnails.small } + return this._capture.url } - get thumbnail_medium(): string { - if (this._capture.thumbnails && this._capture.thumbnails.medium) { + get thumbnailMedium(): string { + if (this._capture.thumbnails?.medium) { return this._capture.thumbnails.medium } + return this._capture.url } diff --git a/ui/src/pages/captures/capture-columns.tsx b/ui/src/pages/captures/capture-columns.tsx index 2e743ebaf..a17a8eedf 100644 --- a/ui/src/pages/captures/capture-columns.tsx +++ b/ui/src/pages/captures/capture-columns.tsx @@ -39,7 +39,7 @@ export const columns = ({ return ( diff --git a/ui/src/pages/captures/capture-gallery.tsx b/ui/src/pages/captures/capture-gallery.tsx index ddf478ef7..0e69782d9 100644 --- a/ui/src/pages/captures/capture-gallery.tsx +++ b/ui/src/pages/captures/capture-gallery.tsx @@ -20,7 +20,7 @@ export const CaptureGallery = ({ () => captures.map((c) => ({ id: c.id, - image: { src: c.thumbnail_small }, + image: { src: c.thumbnailSmall }, title: c.dateTimeLabel, to: c.sessionId ? getAppRoute({ diff --git a/ui/src/pages/session-details/capture/capture.module.scss b/ui/src/pages/session-details/capture/capture.module.scss index 04ddb8334..a07d80c29 100644 --- a/ui/src/pages/session-details/capture/capture.module.scss +++ b/ui/src/pages/session-details/capture/capture.module.scss @@ -1,14 +1,6 @@ -.wrapper { - position: relative; - width: 100%; - height: 0; -} - -.image, .overlay, .details, -.detections, -.loadingWrapper { +.detections { position: absolute; width: 100%; height: 100%; @@ -56,15 +48,3 @@ } } } - -.loadingWrapper { - display: flex; - align-items: center; - justify-content: center; -} - -@media only screen and (max-width: $breakpoint-md) { - .wrapper { - grid-column: span 2; - } -} diff --git a/ui/src/pages/session-details/capture/capture.tsx b/ui/src/pages/session-details/capture/capture.tsx index a2bb43e86..6e2848ee7 100644 --- a/ui/src/pages/session-details/capture/capture.tsx +++ b/ui/src/pages/session-details/capture/capture.tsx @@ -8,6 +8,11 @@ import { TABS, } from 'pages/occurrence-details/occurrence-details' import { useLayoutEffect, useMemo, useRef, useState } from 'react' +import { + ReactZoomPanPinchRef, + TransformComponent, + TransformWrapper, +} from 'react-zoom-pan-pinch' import { SCORE_THRESHOLDS } from 'utils/constants' import { STRING, translate } from 'utils/language' import { useActiveOccurrences } from '../hooks/useActiveOccurrences' @@ -28,6 +33,7 @@ interface CaptureProps { height: number | null showDetections?: boolean src?: string + transformRef: React.RefObject width: number | null } @@ -37,6 +43,7 @@ export const Capture = ({ height, showDetections, src, + transformRef, width, }: CaptureProps) => { const [naturalSize, setNaturalSize] = useState<{ @@ -118,31 +125,33 @@ export const Capture = ({ }, [width, height, naturalSize]) return ( -
- -
- {renderOverlay && } - -
- {isLoading && ( -
+
+ + + +
+ {renderOverlay ? : null} + +
+
+
+ {isLoading ? ( +
- )} + ) : null}
) } diff --git a/ui/src/pages/session-details/session-details.tsx b/ui/src/pages/session-details/session-details.tsx index 66abf074d..a55dd1ca2 100644 --- a/ui/src/pages/session-details/session-details.tsx +++ b/ui/src/pages/session-details/session-details.tsx @@ -13,9 +13,10 @@ import { Tabs, } from 'nova-ui-kit' import { cn } from 'nova-ui-kit/utils' -import { useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' +import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch' import { BreadcrumbContext } from 'utils/breadcrumbContext' import { STRING, translate } from 'utils/language' import { useUser } from 'utils/user/userContext' @@ -31,6 +32,7 @@ import { SessionPlots } from './session-plots' import { StarButton } from './star-button' import { TimelineSlider } from './timeline-slider/timeline-slider' import { ViewSettings } from './view-settings' +import { ZoomSettings } from './zoom-settings' const TABS = { SESSION: 'session', @@ -83,6 +85,7 @@ export const SessionDetailsPage = () => { const Content = ({ session }: { session: SessionDetails }) => { // Settings const [poll, setPoll] = useState(false) + const transformRef = useRef(null) const [settings, setSettings] = useState({ defaultFilters: true, showDetections: true, @@ -125,8 +128,8 @@ const Content = ({ session }: { session: SessionDetails }) => { > {user.loggedIn ? : null} -
- +
+ { detections={activeCapture?.detections ?? []} height={activeCapture?.height ?? session.firstCapture.height} showDetections={settings.showDetections} - src={activeCapture?.thumbnail_medium} + src={activeCapture?.url} + transformRef={transformRef} width={activeCapture?.width ?? session.firstCapture.width} />
-
+
{activeCapture ? ( <> @@ -203,14 +207,15 @@ const Content = ({ session }: { session: SessionDetails }) => { )}
-
+
-
+
+ {
-
+
diff --git a/ui/src/pages/session-details/zoom-settings.tsx b/ui/src/pages/session-details/zoom-settings.tsx new file mode 100644 index 000000000..4bf435f11 --- /dev/null +++ b/ui/src/pages/session-details/zoom-settings.tsx @@ -0,0 +1,39 @@ +import { MinusIcon, PlusIcon } from 'lucide-react' +import { BasicTooltip, Button } from 'nova-ui-kit' +import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch' +import { STRING, translate } from 'utils/language' + +export const ZoomSettings = ({ + transformRef, +}: { + transformRef: React.RefObject +}) => ( + <> + + + + + + + + + +) diff --git a/ui/src/utils/language.ts b/ui/src/utils/language.ts index c07f6038c..6c6b3c2be 100644 --- a/ui/src/utils/language.ts +++ b/ui/src/utils/language.ts @@ -49,6 +49,8 @@ export enum STRING { VIEW_ALL, VIEW_DOCS, VIEW_PUBLIC_PROJECTS, + ZOOM_IN, + ZOOM_OUT, /* ENTITY */ ENTITY_ADD, @@ -407,6 +409,8 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { [STRING.VIEW_ALL]: 'View all', [STRING.VIEW_DOCS]: 'View docs', [STRING.VIEW_PUBLIC_PROJECTS]: 'View public projects', + [STRING.ZOOM_IN]: 'Zoom in', + [STRING.ZOOM_OUT]: 'Zoom out', /* FIELD_LABEL */ [STRING.FIELD_LABEL_ADDED_AT]: 'Added at', diff --git a/ui/yarn.lock b/ui/yarn.lock index 0a7fecfac..188e5ec68 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4752,6 +4752,7 @@ __metadata: react-markdown: "npm:^9.0.1" react-plotly.js: "npm:^2.6.0" react-router-dom: "npm:^6.8.2" + react-zoom-pan-pinch: "npm:^4.0.3" sass: "npm:^1.58.3" tailwind-merge: "npm:^3.6.0" tailwindcss: "npm:^3.4.14" @@ -11396,6 +11397,16 @@ __metadata: languageName: node linkType: hard +"react-zoom-pan-pinch@npm:^4.0.3": + version: 4.0.3 + resolution: "react-zoom-pan-pinch@npm:4.0.3" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 611bc498891550c5e59da5ee94996ff9c31eae533affa10f2fa0b0cb7b5333b51c1e7aa1bb918dcfff2a103c42de0b1963e1fdfe4fa87fcae36b046c37a822b1 + languageName: node + linkType: hard + "react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0"