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:
Nicolas Mowen 2024-03-02 20:59:50 -07:00 committed by GitHub
parent 3c4b1fb6f2
commit 8645545ef4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 107 additions and 28 deletions

View File

@ -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

View File

@ -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"]
}`} }`}
> >

View File

@ -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" />

View File

@ -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"

View File

@ -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,31 +75,73 @@ 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 {
if (aspect > 16 / 9) {
return "absolute left-12 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%]";
} }
} 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" : ""}`
}
> >
{!fullscreen ? (
<Button <Button
className={`rounded-lg ${isMobile ? "ml-2" : "ml-0"}`} className={`rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
size={isMobile ? "icon" : "default"} size={isMobile ? "icon" : "default"}
@ -99,12 +150,30 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
<IoMdArrowBack className="size-5 lg:mr-[10px]" /> <IoMdArrowBack className="size-5 lg:mr-[10px]" />
{isDesktop && "Back"} {isDesktop && "Back"}
</Button> </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}

View File

@ -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))",

View File

@ -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%;