mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 10:45:21 +03:00
support multiple viewmodes for UI options and menus
This commit is contained in:
parent
160e9ba336
commit
fff14f7dc6
@ -67,6 +67,12 @@ class DateTimeStyleEnum(str, Enum):
|
||||
short = "short"
|
||||
|
||||
|
||||
class ViewModeEnum(str, Enum):
|
||||
user = "user"
|
||||
advanced = "advanced"
|
||||
admin = "admin"
|
||||
|
||||
|
||||
class UIConfig(FrigateBaseModel):
|
||||
live_mode: LiveModeEnum = Field(
|
||||
default=LiveModeEnum.mse, title="Default Live Mode."
|
||||
@ -85,9 +91,9 @@ class UIConfig(FrigateBaseModel):
|
||||
strftime_fmt: Optional[str] = Field(
|
||||
default=None, title="Override date and time format using strftime syntax."
|
||||
)
|
||||
show_advanced_options: bool = Field(
|
||||
default=True,
|
||||
title="Default setting to show Advanced Options, such as Config, Camera Rec/Motion/Snap buttons, go2rtc dashboard and more.",
|
||||
viewmode: ViewModeEnum = Field(
|
||||
default=ViewModeEnum.admin,
|
||||
title="Default setting to display or hide certain options and menus in Frigate.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -8,15 +8,16 @@ import DarkModeIcon from './icons/DarkMode';
|
||||
import SettingsIcon from './icons/Settings';
|
||||
import FrigateRestartIcon from './icons/FrigateRestart';
|
||||
import Prompt from './components/Prompt';
|
||||
import { useDarkMode, useAdvOptions } from './context';
|
||||
import { useDarkMode, useViewMode } from './context';
|
||||
import { useCallback, useRef, useState } from 'preact/hooks';
|
||||
import { useRestart } from './api/ws';
|
||||
import { ViewModeTypes } from './components/ViewOptionEnum'
|
||||
|
||||
export default function AppBar() {
|
||||
const [showMoreMenu, setShowMoreMenu] = useState(false);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [showDialogWait, setShowDialogWait] = useState(false);
|
||||
const { showAdvOptions, setShowAdvOptions } = useAdvOptions();
|
||||
const { viewMode, setViewMode } = useViewMode();
|
||||
const { setDarkMode } = useDarkMode();
|
||||
const { send: sendRestart } = useRestart();
|
||||
|
||||
@ -28,10 +29,13 @@ export default function AppBar() {
|
||||
[setDarkMode, setShowMoreMenu]
|
||||
);
|
||||
|
||||
const handleToggleAdvOptions = useCallback(() => {
|
||||
setShowAdvOptions(showAdvOptions === 1 ? 0 : 1);
|
||||
setShowMoreMenu(false);
|
||||
},[showAdvOptions, setShowAdvOptions, setShowMoreMenu]);
|
||||
const handleSetViewMode = useCallback(
|
||||
(value) => {
|
||||
setViewMode(value);
|
||||
setShowMoreMenu(false);
|
||||
},
|
||||
[setViewMode, setShowMoreMenu]
|
||||
);
|
||||
|
||||
const moreRef = useRef(null);
|
||||
|
||||
@ -68,7 +72,15 @@ export default function AppBar() {
|
||||
<MenuItem icon={LightModeIcon} label="Light" value="light" onSelect={handleSelectDarkMode} />
|
||||
<MenuItem icon={DarkModeIcon} label="Dark" value="dark" onSelect={handleSelectDarkMode} />
|
||||
<MenuSeparator />
|
||||
<MenuItem icon={SettingsIcon} label={showAdvOptions ? 'Hide Adv. Options' : 'Show Adv. Options'} onSelect={handleToggleAdvOptions} />
|
||||
<MenuItem icon={SettingsIcon} label="View:">
|
||||
<select
|
||||
className="flex flex-grow p-0 py-7 px-8"
|
||||
value={viewMode}
|
||||
onChange={(e) => handleSetViewMode(e.target.value)}
|
||||
>
|
||||
{ Object.keys(ViewModeTypes).filter((v) => !isNaN(Number(v))).map(key => <option key={key} value={key}>{ViewModeTypes[key]}</option>) }
|
||||
</select>
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
<MenuItem icon={FrigateRestartIcon} label="Restart Frigate" onSelect={handleRestart} />
|
||||
</Menu>
|
||||
|
||||
@ -4,13 +4,12 @@ import { Match } from 'preact-router/match';
|
||||
import { memo } from 'preact/compat';
|
||||
import { ENV } from './env';
|
||||
import { useMemo } from 'preact/hooks'
|
||||
import { useAdvOptions } from './context';
|
||||
import ViewOption from './components/ViewOption'
|
||||
import useSWR from 'swr';
|
||||
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
|
||||
|
||||
export default function Sidebar() {
|
||||
const { data: config } = useSWR('config');
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
|
||||
const sortedCameras = useMemo(() => {
|
||||
if (!config) {
|
||||
@ -48,11 +47,17 @@ export default function Sidebar() {
|
||||
<Destination href="/events" text="Events" />
|
||||
<Destination href="/exports" text="Exports" />
|
||||
<Separator />
|
||||
<Destination href="/storage" text="Storage" />
|
||||
<Destination href="/system" text="System" />
|
||||
{ showAdvOptions ? <Destination href="/config" text="Config" /> : null}
|
||||
<Destination href="/logs" text="Logs" />
|
||||
<Separator />
|
||||
<ViewOption requiredmode="advanced">
|
||||
<Destination href="/storage" text="Storage" />
|
||||
<Destination href="/system" text="System" />
|
||||
</ViewOption>
|
||||
<ViewOption requiredmode="admin">
|
||||
<Destination href="/config" text="Config" />
|
||||
</ViewOption>
|
||||
<ViewOption requiredmode="advanced">
|
||||
<Destination href="/logs" text="Logs" />
|
||||
<Separator />
|
||||
</ViewOption>
|
||||
<div className="flex flex-grow" />
|
||||
{ENV !== 'production' ? (
|
||||
<Fragment>
|
||||
|
||||
@ -6,7 +6,7 @@ import AppBar from './AppBar';
|
||||
import Cameras from './routes/Cameras';
|
||||
import { Router } from 'preact-router';
|
||||
import Sidebar from './Sidebar';
|
||||
import { DarkModeProvider, DrawerProvider, AdvOptionsProvider } from './context';
|
||||
import { DarkModeProvider, DrawerProvider, ViewModeProvider } from './context';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function App() {
|
||||
@ -16,7 +16,7 @@ export default function App() {
|
||||
return (
|
||||
<DarkModeProvider>
|
||||
<DrawerProvider>
|
||||
<AdvOptionsProvider>
|
||||
<ViewModeProvider>
|
||||
<div data-testid="app" className="w-full">
|
||||
{!config ? (
|
||||
<div className="flex flex-grow-1 min-h-screen justify-center items-center">
|
||||
@ -48,7 +48,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdvOptionsProvider>
|
||||
</ViewModeProvider>
|
||||
</DrawerProvider>
|
||||
</DarkModeProvider>
|
||||
);
|
||||
|
||||
@ -2,8 +2,7 @@ import { h } from 'preact';
|
||||
import Button from './Button';
|
||||
import MenuIcon from '../icons/Menu';
|
||||
import MoreIcon from '../icons/More';
|
||||
import { About } from '../icons/About';
|
||||
import { useDrawer, useAdvOptions } from '../context';
|
||||
import { useDrawer } from '../context';
|
||||
import { useLayoutEffect, useCallback, useState } from 'preact/hooks';
|
||||
|
||||
// We would typically preserve these in component state
|
||||
@ -14,7 +13,6 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
|
||||
const [show, setShow] = useState(true);
|
||||
const [atZero, setAtZero] = useState(window.scrollY === 0);
|
||||
const { setShowDrawer } = useDrawer();
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
|
||||
const scrollListener = useCallback(() => {
|
||||
const scrollY = window.scrollY;
|
||||
@ -52,19 +50,6 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
|
||||
</div>
|
||||
<Title />
|
||||
<div className="flex-grow-1 flex justify-end w-full">
|
||||
{ !showAdvOptions ?
|
||||
<Button
|
||||
className="rounded-full"
|
||||
type="text"
|
||||
color="gray"
|
||||
aria-label="Advanced Options are hidden via the nearby menu."
|
||||
>
|
||||
<About className="w-6" />
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<div className="flex-grow-1 flex">
|
||||
{overflowRef && onOverflowClick ? (
|
||||
<div className="w-auto" ref={overflowRef}>
|
||||
<Button
|
||||
|
||||
@ -16,7 +16,7 @@ export default function Menu({ className, children, onDismiss, relativeTo, width
|
||||
) : null;
|
||||
}
|
||||
|
||||
export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
|
||||
export function MenuItem({ children, focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect && onSelect(value, label);
|
||||
}, [onSelect, value, label]);
|
||||
@ -39,6 +39,7 @@ export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...a
|
||||
</div>
|
||||
) : null}
|
||||
<div className="whitespace-nowrap">{label}</div>
|
||||
{children}
|
||||
</Element>
|
||||
);
|
||||
}
|
||||
|
||||
12
web/src/components/ViewOption.jsx
Normal file
12
web/src/components/ViewOption.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { h } from 'preact';
|
||||
import { useViewMode } from '../context';
|
||||
import { ViewModeTypes } from './ViewOptionEnum';
|
||||
|
||||
export default function ViewOption({children, requiredmode }) {
|
||||
const { viewMode } = useViewMode();
|
||||
|
||||
return viewMode >= ViewModeTypes[requiredmode] ? (
|
||||
<>{children}</>
|
||||
|
||||
) : null;
|
||||
}
|
||||
5
web/src/components/ViewOptionEnum.tsx
Normal file
5
web/src/components/ViewOptionEnum.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ViewModeTypes {
|
||||
"user",
|
||||
"advanced",
|
||||
"admin",
|
||||
}
|
||||
@ -1,28 +1,29 @@
|
||||
import { h, createContext } from 'preact';
|
||||
import { get as getData, set as setData } from 'idb-keyval';
|
||||
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'preact/hooks';
|
||||
import { ViewModeTypes } from '../components/ViewOptionEnum';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const AdvOptions = createContext(null);
|
||||
const ViewMode = createContext(null);
|
||||
|
||||
export function AdvOptionsProvider({ children }) {
|
||||
const [showAdvOptions, setShowAdvOptions] = usePersistence('show-advanced-options', null);
|
||||
export function ViewModeProvider({ children }) {
|
||||
const [viewMode, setViewMode] = usePersistence('view-mode', null);
|
||||
const { data: config } = useSWR('config');
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const configValue = config.ui.show_advanced_options == true? 1 : 0; //fixes a load error
|
||||
setShowAdvOptions(showAdvOptions || configValue);
|
||||
const configValue = ViewModeTypes[config.ui.viewmode]; //fixes a load error
|
||||
setViewMode(viewMode || configValue);
|
||||
}
|
||||
|
||||
load();
|
||||
}, [setShowAdvOptions, config]);
|
||||
}, [setViewMode, config]);
|
||||
|
||||
return <AdvOptions.Provider value={{ showAdvOptions, setShowAdvOptions }}>{children}</AdvOptions.Provider>;
|
||||
return <ViewMode.Provider value={{ viewMode, setViewMode }}>{children}</ViewMode.Provider>;
|
||||
}
|
||||
|
||||
export function useAdvOptions() {
|
||||
return useContext(AdvOptions);
|
||||
export function useViewMode() {
|
||||
return useContext(ViewMode);
|
||||
}
|
||||
|
||||
const DarkMode = createContext(null);
|
||||
|
||||
@ -9,7 +9,7 @@ import Link from '../components/Link';
|
||||
import SettingsIcon from '../icons/Settings';
|
||||
import Switch from '../components/Switch';
|
||||
import ButtonsTabbed from '../components/ButtonsTabbed';
|
||||
import { usePersistence, useAdvOptions } from '../context';
|
||||
import { usePersistence } from '../context';
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
@ -17,6 +17,7 @@ import WebRtcPlayer from '../components/WebRtcPlayer';
|
||||
import '../components/MsePlayer';
|
||||
import CameraControlPanel from '../components/CameraControlPanel';
|
||||
import { baseUrl } from '../api/baseUrl';
|
||||
import ViewOption from '../components/ViewOption'
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
@ -25,7 +26,6 @@ export default function Camera({ camera }) {
|
||||
const apiHost = useApiHost();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [viewMode, setViewMode] = useState('live');
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
|
||||
const cameraConfig = config?.cameras[camera];
|
||||
const restreamEnabled =
|
||||
@ -109,7 +109,9 @@ export default function Camera({ camera }) {
|
||||
label="Regions"
|
||||
labelPosition="after"
|
||||
/>
|
||||
{showAdvOptions ? <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> : null}
|
||||
<ViewOption requiredmode="admin">
|
||||
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
||||
</ViewOption>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@ import MotionIcon from '../icons/Motion';
|
||||
import SnapshotIcon from '../icons/Snapshot';
|
||||
import { useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import { useAdvOptions } from '../context';
|
||||
import { useViewMode } from '../context'
|
||||
import { ViewModeTypes } from '../components/ViewOptionEnum';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function Cameras() {
|
||||
@ -87,9 +88,9 @@ function Camera({ name, config }) {
|
||||
[config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
|
||||
);
|
||||
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
const { viewMode } = useViewMode();
|
||||
|
||||
return (
|
||||
<Card buttons={buttons} href={href} header={cleanName} icons={showAdvOptions ? icons : []} media={<CameraImage camera={name} stretch />} />
|
||||
<Card buttons={buttons} href={href} header={cleanName} icons={viewMode >= ViewModeTypes["admin"] ? icons : []} media={<CameraImage camera={name} stretch />} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../uti
|
||||
import TimeAgo from '../components/TimeAgo';
|
||||
import Timepicker from '../components/TimePicker';
|
||||
import TimelineSummary from '../components/TimelineSummary';
|
||||
import { useAdvOptions } from '../context';
|
||||
import ViewOption from '../components/ViewOption';
|
||||
|
||||
const API_LIMIT = 25;
|
||||
|
||||
@ -46,7 +46,6 @@ const monthsAgo = (num) => {
|
||||
};
|
||||
|
||||
export default function Events({ path, ...props }) {
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
const apiHost = useApiHost();
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
before: null,
|
||||
@ -662,12 +661,13 @@ export default function Events({ path, ...props }) {
|
||||
)}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Delete
|
||||
className="h-6 w-6 cursor-pointer"
|
||||
stroke={showAdvOptions ? "#f87171" : "lightgrey"}
|
||||
onClick={showAdvOptions ? (e) => onDelete(e, event.id, event.retain_indefinitely) : null}
|
||||
aria-label="Advanced Options are hidden via the nearby menu."
|
||||
/>
|
||||
<ViewOption requiredmode="admin">
|
||||
<Delete
|
||||
className="h-6 w-6 cursor-pointer"
|
||||
stroke="#f87171"
|
||||
onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)}
|
||||
/>
|
||||
</ViewOption>
|
||||
|
||||
<Download
|
||||
className="h-6 w-6 mt-auto"
|
||||
|
||||
@ -12,14 +12,13 @@ import Dialog from '../components/Dialog';
|
||||
import TimeAgo from '../components/TimeAgo';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { About } from '../icons/About';
|
||||
import { useAdvOptions } from '../context';
|
||||
import ViewOption from '../components/ViewOption';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
export default function System() {
|
||||
const [state, setState] = useState({ showFfprobe: false, ffprobe: '' });
|
||||
const { data: config } = useSWR('config');
|
||||
const { showAdvOptions } = useAdvOptions();
|
||||
|
||||
const {
|
||||
value: { payload: stats },
|
||||
@ -100,7 +99,7 @@ export default function System() {
|
||||
{config && (
|
||||
<span class="p-1">
|
||||
go2rtc {go2rtc && `${go2rtc.version} `}
|
||||
{showAdvOptions ?
|
||||
<ViewOption requiredmode="admin">
|
||||
<Link
|
||||
className="text-blue-500 hover:underline"
|
||||
target="_blank"
|
||||
@ -109,8 +108,7 @@ export default function System() {
|
||||
>
|
||||
dashboard
|
||||
</Link>
|
||||
: null
|
||||
}
|
||||
</ViewOption>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user