mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Add support for live fullscreen mode (#10191)
* Fix timeline colors * Add support for full screen mode * Add support for live view full screen * Cleanup * Add border to sidebar and statusbar
This commit is contained in:
parent
3c4b1fb6f2
commit
8645545ef4
@ -28,7 +28,7 @@ export default function Statusbar() {
|
|||||||
}, [stats]);
|
}, [stats]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute left-0 bottom-0 right-0 w-full h-8 flex items-center px-4 bg-primary z-10 text-secondary-foreground">
|
<div className="absolute left-0 bottom-0 right-0 w-full h-8 flex items-center px-4 bg-primary z-10 text-secondary-foreground border-t border-secondary-highlight">
|
||||||
{cpuPercent && (
|
{cpuPercent && (
|
||||||
<div className="flex items-center text-sm mr-4">
|
<div className="flex items-center text-sm mr-4">
|
||||||
<MdCircle
|
<MdCircle
|
||||||
|
@ -8,18 +8,18 @@ import { isDesktop } from "react-device-detect";
|
|||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: {
|
primary: {
|
||||||
active: "font-bold text-primary-foreground bg-selected",
|
active: "font-bold text-white bg-selected rounded-lg",
|
||||||
inactive: "text-secondary-foreground bg-secondary",
|
inactive: "text-secondary-foreground bg-secondary rounded-lg",
|
||||||
},
|
},
|
||||||
secondary: {
|
overlay: {
|
||||||
active: "font-bold text-primary",
|
active: "font-bold text-white bg-selected rounded-full",
|
||||||
inactive: "text-secondary-foreground",
|
inactive: "text-primary-white rounded-full bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type CameraFeatureToggleProps = {
|
type CameraFeatureToggleProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: "primary" | "secondary";
|
variant?: "primary" | "overlay";
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
Icon: IconType;
|
Icon: IconType;
|
||||||
title: string;
|
title: string;
|
||||||
@ -37,7 +37,7 @@ export default function CameraFeatureToggle({
|
|||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`${className} flex flex-col justify-center items-center rounded-lg ${
|
className={`${className} flex flex-col justify-center items-center ${
|
||||||
variants[variant][isActive ? "active" : "inactive"]
|
variants[variant][isActive ? "active" : "inactive"]
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -5,7 +5,7 @@ import NavItem from "./NavItem";
|
|||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-primary">
|
<aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-primary border-r border-secondary-highlight">
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
<div className="w-full flex flex-col gap-0 items-center">
|
<div className="w-full flex flex-col gap-0 items-center">
|
||||||
<Logo className="w-8 h-8 mb-6" />
|
<Logo className="w-8 h-8 mb-6" />
|
||||||
|
@ -61,7 +61,7 @@ function MinimapBounds({
|
|||||||
<>
|
<>
|
||||||
{isFirstSegmentInMinimap && (
|
{isFirstSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary font-medium z-20 text-center text-[10px] scroll-mt-8"
|
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] scroll-mt-8"
|
||||||
ref={firstMinimapSegmentRef}
|
ref={firstMinimapSegmentRef}
|
||||||
>
|
>
|
||||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||||
@ -73,7 +73,7 @@ function MinimapBounds({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLastSegmentInMinimap && (
|
{isLastSegmentInMinimap && (
|
||||||
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary font-medium z-20 text-center text-[10px]">
|
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px]">
|
||||||
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -247,7 +247,7 @@ export function EventSegment({
|
|||||||
const segmentClasses = `h-2 relative w-full ${
|
const segmentClasses = `h-2 relative w-full ${
|
||||||
showMinimap
|
showMinimap
|
||||||
? isInMinimapRange
|
? isInMinimapRange
|
||||||
? "bg-card"
|
? "bg-secondary-highlight"
|
||||||
: isLastSegmentInMinimap
|
: isLastSegmentInMinimap
|
||||||
? ""
|
? ""
|
||||||
: "opacity-70"
|
: "opacity-70"
|
||||||
|
@ -18,7 +18,13 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
|||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { CameraConfig } from "@/types/frigateConfig";
|
import { CameraConfig } from "@/types/frigateConfig";
|
||||||
import { CameraPtzInfo } from "@/types/ptz";
|
import { CameraPtzInfo } from "@/types/ptz";
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
isDesktop,
|
isDesktop,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -31,6 +37,8 @@ import {
|
|||||||
FaAngleLeft,
|
FaAngleLeft,
|
||||||
FaAngleRight,
|
FaAngleRight,
|
||||||
FaAngleUp,
|
FaAngleUp,
|
||||||
|
FaCompress,
|
||||||
|
FaExpand,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
|
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
|
||||||
import { IoMdArrowBack } from "react-icons/io";
|
import { IoMdArrowBack } from "react-icons/io";
|
||||||
@ -52,6 +60,7 @@ type LiveCameraViewProps = {
|
|||||||
export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isPortrait } = useMobileOrientation();
|
const { isPortrait } = useMobileOrientation();
|
||||||
|
const mainRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// camera features
|
// camera features
|
||||||
|
|
||||||
@ -66,45 +75,105 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
);
|
);
|
||||||
const { payload: audioState, send: sendAudio } = useAudioState(camera.name);
|
const { payload: audioState, send: sendAudio } = useAudioState(camera.name);
|
||||||
|
|
||||||
|
// fullscreen state
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mainRef.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
setFullscreen(document.fullscreenElement != null);
|
||||||
|
};
|
||||||
|
document.addEventListener("fullscreenchange", listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("fullscreenchange", listener);
|
||||||
|
};
|
||||||
|
}, [mainRef]);
|
||||||
|
|
||||||
// playback state
|
// playback state
|
||||||
|
|
||||||
const [audio, setAudio] = useState(false);
|
const [audio, setAudio] = useState(false);
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
const growClassName = useMemo(() => {
|
const growClassName = useMemo(() => {
|
||||||
|
const aspect = camera.detect.width / camera.detect.height;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (isPortrait) {
|
if (isPortrait) {
|
||||||
return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
|
return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
|
||||||
} else {
|
} else {
|
||||||
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
|
if (aspect > 16 / 9) {
|
||||||
|
return "absolute left-12 top-[50%] -translate-y-[50%]";
|
||||||
|
} else {
|
||||||
|
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (camera.detect.width / camera.detect.height > 2) {
|
}
|
||||||
|
|
||||||
|
if (fullscreen) {
|
||||||
|
if (aspect > 16 / 9) {
|
||||||
|
return "absolute inset-x-0 top-[50%] -translate-y-[50%]";
|
||||||
|
} else {
|
||||||
|
return "absolute inset-y-0 left-[50%] -translate-x-[50%]";
|
||||||
|
}
|
||||||
|
} else if (aspect > 2) {
|
||||||
return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
|
return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
|
||||||
} else {
|
} else {
|
||||||
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
|
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
|
||||||
}
|
}
|
||||||
}, [camera, isPortrait]);
|
}, [camera, fullscreen, isPortrait]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`size-full flex flex-col ${isMobile ? "landscape:flex-row" : ""}`}
|
ref={mainRef}
|
||||||
|
className={
|
||||||
|
fullscreen
|
||||||
|
? `fixed inset-0 bg-black z-30`
|
||||||
|
: `size-full flex flex-col ${isMobile ? "landscape:flex-row" : ""}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-full h-12 flex flex-row items-center justify-between ${isMobile ? "landscape:w-min landscape:h-full landscape:flex-col" : ""}`}
|
className={
|
||||||
|
fullscreen
|
||||||
|
? `absolute right-32 top-1 z-40 ${isMobile ? "landscape:left-2 landscape:right-auto landscape:bottom-1 landscape:top-auto" : ""}`
|
||||||
|
: `w-full h-12 flex flex-row items-center justify-between ${isMobile ? "landscape:w-min landscape:h-full landscape:flex-col" : ""}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
{!fullscreen ? (
|
||||||
className={`rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
|
<Button
|
||||||
size={isMobile ? "icon" : "default"}
|
className={`rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
|
||||||
onClick={() => navigate(-1)}
|
size={isMobile ? "icon" : "default"}
|
||||||
>
|
onClick={() => navigate(-1)}
|
||||||
<IoMdArrowBack className="size-5 lg:mr-[10px]" />
|
>
|
||||||
{isDesktop && "Back"}
|
<IoMdArrowBack className="size-5 lg:mr-[10px]" />
|
||||||
</Button>
|
{isDesktop && "Back"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-row items-center gap-2 mr-1 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`}
|
className={`flex flex-row items-center gap-2 mr-1 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`}
|
||||||
>
|
>
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
|
Icon={fullscreen ? FaCompress : FaExpand}
|
||||||
|
isActive={fullscreen}
|
||||||
|
title={fullscreen ? "Close" : "Fullscreen"}
|
||||||
|
onClick={() => {
|
||||||
|
if (fullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
mainRef.current?.requestFullscreen();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CameraFeatureToggle
|
||||||
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={audio ? GiSpeaker : GiSpeakerOff}
|
Icon={audio ? GiSpeaker : GiSpeakerOff}
|
||||||
isActive={audio}
|
isActive={audio}
|
||||||
title={`${audio ? "Disable" : "Enable"} Camera Audio`}
|
title={`${audio ? "Disable" : "Enable"} Camera Audio`}
|
||||||
@ -112,6 +181,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
/>
|
/>
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={detectState == "ON" ? MdPersonSearch : MdPersonOff}
|
Icon={detectState == "ON" ? MdPersonSearch : MdPersonOff}
|
||||||
isActive={detectState == "ON"}
|
isActive={detectState == "ON"}
|
||||||
title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
|
title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
|
||||||
@ -119,6 +189,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
/>
|
/>
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={recordState == "ON" ? LuVideo : LuVideoOff}
|
Icon={recordState == "ON" ? LuVideo : LuVideoOff}
|
||||||
isActive={recordState == "ON"}
|
isActive={recordState == "ON"}
|
||||||
title={`${recordState == "ON" ? "Disable" : "Enable"} Recording`}
|
title={`${recordState == "ON" ? "Disable" : "Enable"} Recording`}
|
||||||
@ -126,6 +197,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
/>
|
/>
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={snapshotState == "ON" ? MdPhotoCamera : MdNoPhotography}
|
Icon={snapshotState == "ON" ? MdPhotoCamera : MdNoPhotography}
|
||||||
isActive={snapshotState == "ON"}
|
isActive={snapshotState == "ON"}
|
||||||
title={`${snapshotState == "ON" ? "Disable" : "Enable"} Snapshots`}
|
title={`${snapshotState == "ON" ? "Disable" : "Enable"} Snapshots`}
|
||||||
@ -134,6 +206,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
{camera.audio.enabled_in_config && (
|
{camera.audio.enabled_in_config && (
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={audioState == "ON" ? LuEar : LuEarOff}
|
Icon={audioState == "ON" ? LuEar : LuEarOff}
|
||||||
isActive={audioState == "ON"}
|
isActive={audioState == "ON"}
|
||||||
title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
|
title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
|
||||||
@ -143,7 +216,6 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative size-full">
|
<div className="relative size-full">
|
||||||
<div
|
<div
|
||||||
className={growClassName}
|
className={growClassName}
|
||||||
@ -151,7 +223,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
|||||||
>
|
>
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
className="size-full"
|
className={`size-full ${fullscreen ? "*:rounded-none" : ""}`}
|
||||||
windowVisible
|
windowVisible
|
||||||
showStillWithoutActivity={false}
|
showStillWithoutActivity={false}
|
||||||
cameraConfig={camera}
|
cameraConfig={camera}
|
||||||
|
@ -52,6 +52,7 @@ module.exports = {
|
|||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
highlight: "hsl(var(--secondary-highlight))",
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
@ -30,6 +30,9 @@
|
|||||||
--secondary-foreground: hsl(0, 0%, 45%);
|
--secondary-foreground: hsl(0, 0%, 45%);
|
||||||
--secondary-foreground: 0, 0%, 45%;
|
--secondary-foreground: 0, 0%, 45%;
|
||||||
|
|
||||||
|
--secondary-highlight: hsl(0, 0%, 94%);
|
||||||
|
--secondary-highlight: 0, 0%, 94%;
|
||||||
|
|
||||||
--muted: hsl(210 40% 96.1%);
|
--muted: hsl(210 40% 96.1%);
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
|
|
||||||
@ -103,6 +106,9 @@
|
|||||||
--secondary-foreground: hsl(0, 0%, 83%);
|
--secondary-foreground: hsl(0, 0%, 83%);
|
||||||
--secondary-foreground: 0, 0%, 83%;
|
--secondary-foreground: 0, 0%, 83%;
|
||||||
|
|
||||||
|
--secondary-highlight: hsl(0, 0%, 25%);
|
||||||
|
--secondary-highlight: 0, 0%, 25%;
|
||||||
|
|
||||||
--muted: hsl(0, 0%, 8%);
|
--muted: hsl(0, 0%, 8%);
|
||||||
--muted: 0, 0%, 8%;
|
--muted: 0, 0%, 8%;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user