mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 22:28:23 +03:00
Merge pull request #32 from ibs0d/codex/add-independent-zoom-for-camera-cards
Add independent Shift+Wheel zoom for draggable live grid cards
This commit is contained in:
commit
91976498c5
@ -49,6 +49,13 @@ import { Toaster } from "@/components/ui/sonner";
|
|||||||
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||||
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
CAMERA_ZOOM_MIN_SCALE,
|
||||||
|
CameraZoomRuntimeTransform,
|
||||||
|
clampScale,
|
||||||
|
getCursorRelativeZoomTransform,
|
||||||
|
getNextScaleFromWheelDelta,
|
||||||
|
} from "@/utils/cameraZoom";
|
||||||
|
|
||||||
type DraggableGridLayoutProps = {
|
type DraggableGridLayoutProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -424,6 +431,51 @@ export default function DraggableGridLayout({
|
|||||||
const [audioStates, setAudioStates] = useState<AudioState>({});
|
const [audioStates, setAudioStates] = useState<AudioState>({});
|
||||||
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
||||||
const [statsStates, setStatsStates] = useState<StatsState>({});
|
const [statsStates, setStatsStates] = useState<StatsState>({});
|
||||||
|
const [cameraZoomStates, setCameraZoomStates] = useState<
|
||||||
|
Record<string, CameraZoomRuntimeTransform>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const getDefaultZoomTransform = useCallback(
|
||||||
|
(): CameraZoomRuntimeTransform => ({
|
||||||
|
scale: CAMERA_ZOOM_MIN_SCALE,
|
||||||
|
positionX: 0,
|
||||||
|
positionY: 0,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCardWheelZoom = useCallback(
|
||||||
|
(cameraName: string, event: React.WheelEvent<HTMLDivElement>) => {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const bounds = event.currentTarget.getBoundingClientRect();
|
||||||
|
const cursorX = event.clientX - bounds.left;
|
||||||
|
const cursorY = event.clientY - bounds.top;
|
||||||
|
|
||||||
|
setCameraZoomStates((prev) => {
|
||||||
|
const current = prev[cameraName] ?? getDefaultZoomTransform();
|
||||||
|
const nextScale = clampScale(
|
||||||
|
getNextScaleFromWheelDelta(current.scale, event.deltaY),
|
||||||
|
);
|
||||||
|
const next = getCursorRelativeZoomTransform(
|
||||||
|
current,
|
||||||
|
nextScale,
|
||||||
|
cursorX,
|
||||||
|
cursorY,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[cameraName]: next,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[getDefaultZoomTransform],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialStreamStatsState = getStreamStatsFromStorage();
|
const initialStreamStatsState = getStreamStatsFromStorage();
|
||||||
@ -629,6 +681,8 @@ export default function DraggableGridLayout({
|
|||||||
const useWebGL =
|
const useWebGL =
|
||||||
currentGroupStreamingSettings?.[camera.name]
|
currentGroupStreamingSettings?.[camera.name]
|
||||||
?.compatibilityMode || false;
|
?.compatibilityMode || false;
|
||||||
|
const zoomTransform =
|
||||||
|
cameraZoomStates[camera.name] ?? getDefaultZoomTransform();
|
||||||
return (
|
return (
|
||||||
<GridLiveContextMenu
|
<GridLiveContextMenu
|
||||||
className="size-full"
|
className="size-full"
|
||||||
@ -660,45 +714,64 @@ export default function DraggableGridLayout({
|
|||||||
config={config}
|
config={config}
|
||||||
streamMetadata={streamMetadata}
|
streamMetadata={streamMetadata}
|
||||||
>
|
>
|
||||||
<LivePlayer
|
<div
|
||||||
key={camera.name}
|
className="size-full overflow-hidden rounded-lg md:rounded-2xl"
|
||||||
streamName={streamName}
|
onWheel={(event) => handleCardWheelZoom(camera.name, event)}
|
||||||
autoLive={autoLive ?? globalAutoLive}
|
>
|
||||||
showStillWithoutActivity={showStillWithoutActivity ?? true}
|
<div
|
||||||
alwaysShowCameraName={displayCameraNames}
|
className="size-full"
|
||||||
useWebGL={useWebGL}
|
style={{
|
||||||
cameraRef={cameraRef}
|
transform: `translate(${zoomTransform.positionX}px, ${zoomTransform.positionY}px) scale(${zoomTransform.scale})`,
|
||||||
className={cn(
|
transformOrigin: "0 0",
|
||||||
"draggable-live-grid-mse-cover size-full rounded-lg bg-black md:rounded-2xl",
|
}}
|
||||||
camera.ui?.rotate &&
|
>
|
||||||
"draggable-live-grid-rotated [--frigate-mse-grid-rotated:1] [--frigate-mse-grid-rotation:rotate(90deg)]",
|
<LivePlayer
|
||||||
isEditMode &&
|
key={camera.name}
|
||||||
showCircles &&
|
streamName={streamName}
|
||||||
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
|
autoLive={autoLive ?? globalAutoLive}
|
||||||
)}
|
showStillWithoutActivity={
|
||||||
windowVisible={
|
showStillWithoutActivity ?? true
|
||||||
windowVisible && visibleCameras.includes(camera.name)
|
|
||||||
}
|
|
||||||
cameraConfig={camera}
|
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
|
||||||
playInBackground={false}
|
|
||||||
showStats={statsStates[camera.name] ?? true}
|
|
||||||
onClick={() => {
|
|
||||||
!isEditMode && onSelectCamera(camera.name);
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
setPreferredLiveModes((prevModes) => {
|
|
||||||
const newModes = { ...prevModes };
|
|
||||||
if (e === "mse-decode") {
|
|
||||||
delete newModes[camera.name];
|
|
||||||
}
|
}
|
||||||
return newModes;
|
alwaysShowCameraName={displayCameraNames}
|
||||||
});
|
useWebGL={useWebGL}
|
||||||
}}
|
cameraRef={cameraRef}
|
||||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
className={cn(
|
||||||
playAudio={audioStates[camera.name]}
|
"draggable-live-grid-mse-cover size-full rounded-lg bg-black md:rounded-2xl",
|
||||||
volume={volumeStates[camera.name]}
|
camera.ui?.rotate &&
|
||||||
/>
|
"draggable-live-grid-rotated [--frigate-mse-grid-rotated:1] [--frigate-mse-grid-rotation:rotate(90deg)]",
|
||||||
|
isEditMode &&
|
||||||
|
showCircles &&
|
||||||
|
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
|
||||||
|
)}
|
||||||
|
windowVisible={
|
||||||
|
windowVisible && visibleCameras.includes(camera.name)
|
||||||
|
}
|
||||||
|
cameraConfig={camera}
|
||||||
|
preferredLiveMode={
|
||||||
|
preferredLiveModes[camera.name] ?? "mse"
|
||||||
|
}
|
||||||
|
playInBackground={false}
|
||||||
|
showStats={statsStates[camera.name] ?? true}
|
||||||
|
onClick={() => {
|
||||||
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
setPreferredLiveModes((prevModes) => {
|
||||||
|
const newModes = { ...prevModes };
|
||||||
|
if (e === "mse-decode") {
|
||||||
|
delete newModes[camera.name];
|
||||||
|
}
|
||||||
|
return newModes;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onResetLiveMode={() =>
|
||||||
|
resetPreferredLiveMode(camera.name)
|
||||||
|
}
|
||||||
|
playAudio={audioStates[camera.name]}
|
||||||
|
volume={volumeStates[camera.name]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isEditMode && showCircles && <CornerCircles />}
|
{isEditMode && showCircles && <CornerCircles />}
|
||||||
</GridLiveContextMenu>
|
</GridLiveContextMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user