mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	FEAT: Ability to reorder & ability to hide Cameras in UI (#2981)
* Add options for reordering and hiding cameras selectively * Add newline at end of camera file * Make each camera for birdseye togglable as well * Update names to be less ambiguous * Update defaults * Include sidebar change * Remove birdseye toggle (will be added in separate PR) * Remove birdseye toggle (will be added in separate PR) * Remove birdseye toggle (will be added in separate PR) * Update sidebar to only sort cameras once * Simplify sorting logic
This commit is contained in:
		
							parent
							
								
									41f58c7692
								
							
						
					
					
						commit
						a5016afdd4
					
				@ -436,4 +436,12 @@ cameras:
 | 
			
		||||
      quality: 70
 | 
			
		||||
      # Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones)
 | 
			
		||||
      required_zones: []
 | 
			
		||||
 | 
			
		||||
    # Optional: Configuration for how camera is handled in the GUI.
 | 
			
		||||
    ui:
 | 
			
		||||
      # Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
 | 
			
		||||
      # By default the cameras are sorted alphabetically.
 | 
			
		||||
      order: 0
 | 
			
		||||
      # Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
 | 
			
		||||
      dashboard: True
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -514,6 +514,11 @@ class CameraLiveConfig(FrigateBaseModel):
 | 
			
		||||
    quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CameraUiConfig(FrigateBaseModel):
 | 
			
		||||
    order: int = Field(default=0, title="Order of camera in UI.")
 | 
			
		||||
    dashboard: bool = Field(default=True, title="Show this camera in Frigate dashboard UI.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CameraConfig(FrigateBaseModel):
 | 
			
		||||
    name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
 | 
			
		||||
    ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
 | 
			
		||||
@ -546,6 +551,9 @@ class CameraConfig(FrigateBaseModel):
 | 
			
		||||
    detect: DetectConfig = Field(
 | 
			
		||||
        default_factory=DetectConfig, title="Object detection configuration."
 | 
			
		||||
    )
 | 
			
		||||
    ui: CameraUiConfig = Field(
 | 
			
		||||
        default_factory=CameraUiConfig, title="Camera UI Modifications."
 | 
			
		||||
    )
 | 
			
		||||
    birdseye: BirdseyeCameraConfig = Field(
 | 
			
		||||
        default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ export const handlers = [
 | 
			
		||||
            detect: { width: 1280, height: 720 },
 | 
			
		||||
            snapshots: {},
 | 
			
		||||
            live: { height: 720 },
 | 
			
		||||
            ui: { dashboard: true, order: 0 },
 | 
			
		||||
          },
 | 
			
		||||
          side: {
 | 
			
		||||
            name: 'side',
 | 
			
		||||
@ -28,6 +29,7 @@ export const handlers = [
 | 
			
		||||
            detect: { width: 1280, height: 720 },
 | 
			
		||||
            snapshots: {},
 | 
			
		||||
            live: { height: 720 },
 | 
			
		||||
            ui: { dashboard: true, order: 1 },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,27 @@ import LinkedLogo from './components/LinkedLogo';
 | 
			
		||||
import { Match } from 'preact-router/match';
 | 
			
		||||
import { memo } from 'preact/compat';
 | 
			
		||||
import { ENV } from './env';
 | 
			
		||||
import { useMemo } from 'preact/hooks'
 | 
			
		||||
import useSWR from 'swr';
 | 
			
		||||
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
 | 
			
		||||
 | 
			
		||||
export default function Sidebar() {
 | 
			
		||||
  const { data: config } = useSWR('config');
 | 
			
		||||
 | 
			
		||||
  const sortedCameras = useMemo(() => {
 | 
			
		||||
    if (!config) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Object.entries(config.cameras)
 | 
			
		||||
      .filter(([_, conf]) => conf.ui.dashboard)
 | 
			
		||||
      .sort(([_, aConf], [__, bConf]) => aConf.ui.order - bConf.ui.order);
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  if (!config) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  const { cameras, birdseye } = config;
 | 
			
		||||
  const { birdseye } = config;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <NavigationDrawer header={<Header />}>
 | 
			
		||||
@ -19,31 +31,14 @@ export default function Sidebar() {
 | 
			
		||||
      <Match path="/cameras/:camera/:other?">
 | 
			
		||||
        {({ matches }) =>
 | 
			
		||||
          matches ? (
 | 
			
		||||
            <Fragment>
 | 
			
		||||
              <Separator />
 | 
			
		||||
              {Object.keys(cameras).map((camera) => (
 | 
			
		||||
                <Destination key={camera} href={`/cameras/${camera}`} text={camera} />
 | 
			
		||||
              ))}
 | 
			
		||||
              <Separator />
 | 
			
		||||
            </Fragment>
 | 
			
		||||
            <CameraSection sortedCameras={sortedCameras} />
 | 
			
		||||
          ) : null
 | 
			
		||||
        }
 | 
			
		||||
      </Match>
 | 
			
		||||
      <Match path="/recording/:camera/:date?/:hour?/:seconds?">
 | 
			
		||||
        {({ matches }) =>
 | 
			
		||||
          matches ? (
 | 
			
		||||
            <Fragment>
 | 
			
		||||
              <Separator />
 | 
			
		||||
              {Object.keys(cameras).map((camera) => (
 | 
			
		||||
                <Destination
 | 
			
		||||
                  key={camera}
 | 
			
		||||
                  path={`/recording/${camera}/:date?/:hour?/:seconds?`}
 | 
			
		||||
                  href={`/recording/${camera}`}
 | 
			
		||||
                  text={camera}
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
              <Separator />
 | 
			
		||||
            </Fragment>
 | 
			
		||||
            <RecordingSection sortedCameras={sortedCameras} />
 | 
			
		||||
          ) : null
 | 
			
		||||
        }
 | 
			
		||||
      </Match>
 | 
			
		||||
@ -64,6 +59,39 @@ export default function Sidebar() {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CameraSection({ sortedCameras }) {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      <Separator />
 | 
			
		||||
      {sortedCameras.map(([camera]) => (
 | 
			
		||||
        <Destination key={camera} href={`/cameras/${camera}`} text={camera} />
 | 
			
		||||
      ))}
 | 
			
		||||
      <Separator />
 | 
			
		||||
    </Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RecordingSection({ sortedCameras }) {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      <Separator />
 | 
			
		||||
      {sortedCameras.map(([camera, _]) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Destination
 | 
			
		||||
            key={camera}
 | 
			
		||||
            path={`/recording/${camera}/:date?/:hour?/:seconds?`}
 | 
			
		||||
            href={`/recording/${camera}`}
 | 
			
		||||
            text={camera}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
      <Separator />
 | 
			
		||||
    </Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Header = memo(() => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="text-gray-500">
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { h } from 'preact';
 | 
			
		||||
import { h, Fragment } from 'preact';
 | 
			
		||||
import ActivityIndicator from '../components/ActivityIndicator';
 | 
			
		||||
import Card from '../components/Card';
 | 
			
		||||
import CameraImage from '../components/CameraImage';
 | 
			
		||||
@ -16,10 +16,25 @@ export default function Cameras() {
 | 
			
		||||
    <ActivityIndicator />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <div className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4 p-2 px-4">
 | 
			
		||||
      {Object.entries(config.cameras).map(([camera, conf]) => (
 | 
			
		||||
      <SortedCameras unsortedCameras={config.cameras} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SortedCameras({ unsortedCameras }) {
 | 
			
		||||
 | 
			
		||||
  const sortedCameras = useMemo(() =>
 | 
			
		||||
    Object.entries(unsortedCameras)
 | 
			
		||||
      .filter(([_, conf]) => conf.ui.dashboard)
 | 
			
		||||
      .sort(([_, aConf], [__, bConf]) => aConf.ui.order - bConf.ui.order),
 | 
			
		||||
  [unsortedCameras]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      {sortedCameras.map(([camera, conf]) => (
 | 
			
		||||
        <Camera key={camera} name={camera} conf={conf} />
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
    </Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user