mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
UI tweaks (#10998)
* Make buttons consistent and have hover state * Use switch for camera to be consistent * Use everywhere and remove unused * Use green for normal stats color * Fix logs copy icon * Remove warnings from pydantic serialization * Ignore warnings * Fix wsdl resolution * Fix loading on switch
This commit is contained in:
parent
a823a18496
commit
9be5951076
@ -139,7 +139,7 @@ def stats_history():
|
|||||||
def config():
|
def config():
|
||||||
config_obj: FrigateConfig = current_app.frigate_config
|
config_obj: FrigateConfig = current_app.frigate_config
|
||||||
config: dict[str, dict[str, any]] = config_obj.model_dump(
|
config: dict[str, dict[str, any]] = config_obj.model_dump(
|
||||||
mode="json", exclude_none=True
|
mode="json", warnings="none", exclude_none=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove the mqtt password
|
# remove the mqtt password
|
||||||
|
@ -1351,11 +1351,12 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
"timestamp_style": ...,
|
"timestamp_style": ...,
|
||||||
},
|
},
|
||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
|
warnings="none",
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, camera in config.cameras.items():
|
for name, camera in config.cameras.items():
|
||||||
merged_config = deep_merge(
|
merged_config = deep_merge(
|
||||||
camera.model_dump(exclude_unset=True), global_config
|
camera.model_dump(exclude_unset=True, warnings="none"), global_config
|
||||||
)
|
)
|
||||||
camera_config: CameraConfig = CameraConfig.model_validate(
|
camera_config: CameraConfig = CameraConfig.model_validate(
|
||||||
{"name": name, **merged_config}
|
{"name": name, **merged_config}
|
||||||
@ -1466,7 +1467,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
# Set runtime filter to create masks
|
# Set runtime filter to create masks
|
||||||
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
||||||
frame_shape=camera_config.frame_shape,
|
frame_shape=camera_config.frame_shape,
|
||||||
**filter.model_dump(exclude_unset=True),
|
**filter.model_dump(exclude_unset=True, warnings="none"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert motion configuration
|
# Convert motion configuration
|
||||||
@ -1478,7 +1479,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
camera_config.motion = RuntimeMotionConfig(
|
camera_config.motion = RuntimeMotionConfig(
|
||||||
frame_shape=camera_config.frame_shape,
|
frame_shape=camera_config.frame_shape,
|
||||||
raw_mask=camera_config.motion.mask,
|
raw_mask=camera_config.motion.mask,
|
||||||
**camera_config.motion.model_dump(exclude_unset=True),
|
**camera_config.motion.model_dump(
|
||||||
|
exclude_unset=True, warnings="none"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
camera_config.motion.enabled_in_config = camera_config.motion.enabled
|
camera_config.motion.enabled_in_config = camera_config.motion.enabled
|
||||||
|
|
||||||
@ -1515,7 +1518,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
for key, detector in config.detectors.items():
|
for key, detector in config.detectors.items():
|
||||||
adapter = TypeAdapter(DetectorConfig)
|
adapter = TypeAdapter(DetectorConfig)
|
||||||
model_dict = (
|
model_dict = (
|
||||||
detector if isinstance(detector, dict) else detector.model_dump()
|
detector
|
||||||
|
if isinstance(detector, dict)
|
||||||
|
else detector.model_dump(warnings="none")
|
||||||
)
|
)
|
||||||
detector_config: DetectorConfig = adapter.validate_python(model_dict)
|
detector_config: DetectorConfig = adapter.validate_python(model_dict)
|
||||||
if detector_config.model is None:
|
if detector_config.model is None:
|
||||||
@ -1536,8 +1541,8 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
"Customizing more than a detector model path is unsupported."
|
"Customizing more than a detector model path is unsupported."
|
||||||
)
|
)
|
||||||
merged_model = deep_merge(
|
merged_model = deep_merge(
|
||||||
detector_config.model.model_dump(exclude_unset=True),
|
detector_config.model.model_dump(exclude_unset=True, warnings="none"),
|
||||||
config.model.model_dump(exclude_unset=True),
|
config.model.model_dump(exclude_unset=True, warnings="none"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if "path" not in merged_model:
|
if "path" not in merged_model:
|
||||||
|
@ -51,7 +51,9 @@ class OnvifController:
|
|||||||
cam.onvif.port,
|
cam.onvif.port,
|
||||||
cam.onvif.user,
|
cam.onvif.user,
|
||||||
cam.onvif.password,
|
cam.onvif.password,
|
||||||
wsdl_dir=Path(find_spec("onvif").origin).parent / "../wsdl",
|
wsdl_dir=str(
|
||||||
|
Path(find_spec("onvif").origin).parent / "wsdl"
|
||||||
|
).replace("dist-packages/onvif", "site-packages"),
|
||||||
),
|
),
|
||||||
"init": False,
|
"init": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
|
@ -22,8 +22,8 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import FilterCheckBox from "./FilterCheckBox";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import FilterSwitch from "./FilterSwitch";
|
||||||
|
|
||||||
type CameraGroupSelectorProps = {
|
type CameraGroupSelectorProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -305,7 +305,7 @@ function NewGroupDialog({ open, setOpen, currentGroups }: NewGroupDialogProps) {
|
|||||||
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
||||||
...Object.keys(config?.cameras ?? {}),
|
...Object.keys(config?.cameras ?? {}),
|
||||||
].map((camera) => (
|
].map((camera) => (
|
||||||
<FilterCheckBox
|
<FilterSwitch
|
||||||
key={camera}
|
key={camera}
|
||||||
isChecked={cameras.includes(camera)}
|
isChecked={cameras.includes(camera)}
|
||||||
label={camera.replaceAll("_", " ")}
|
label={camera.replaceAll("_", " ")}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import { LuCheck } from "react-icons/lu";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import { IconType } from "react-icons";
|
|
||||||
|
|
||||||
type FilterCheckBoxProps = {
|
|
||||||
label: string;
|
|
||||||
CheckIcon?: IconType;
|
|
||||||
iconClassName?: string;
|
|
||||||
isChecked: boolean;
|
|
||||||
onCheckedChange: (isChecked: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FilterCheckBox({
|
|
||||||
label,
|
|
||||||
CheckIcon = LuCheck,
|
|
||||||
iconClassName = "size-6",
|
|
||||||
isChecked,
|
|
||||||
onCheckedChange,
|
|
||||||
}: FilterCheckBoxProps) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="capitalize flex justify-between items-center cursor-pointer w-full text-primary"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onCheckedChange(!isChecked)}
|
|
||||||
>
|
|
||||||
{isChecked ? (
|
|
||||||
<CheckIcon className={iconClassName} />
|
|
||||||
) : (
|
|
||||||
<div className={iconClassName} />
|
|
||||||
)}
|
|
||||||
<div className="ml-1 w-full flex justify-start">{label}</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
29
web/src/components/filter/FilterSwitch.tsx
Normal file
29
web/src/components/filter/FilterSwitch.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
|
||||||
|
type FilterSwitchProps = {
|
||||||
|
label: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
onCheckedChange: (checked: boolean) => void;
|
||||||
|
};
|
||||||
|
export default function FilterSwitch({
|
||||||
|
label,
|
||||||
|
isChecked,
|
||||||
|
onCheckedChange,
|
||||||
|
}: FilterSwitchProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center gap-1">
|
||||||
|
<Label
|
||||||
|
className="w-full mx-2 text-primary capitalize cursor-pointer"
|
||||||
|
htmlFor={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id={label}
|
||||||
|
checked={isChecked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -24,12 +24,12 @@ import { isDesktop, isMobile } from "react-device-detect";
|
|||||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import FilterCheckBox from "./FilterCheckBox";
|
|
||||||
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
|
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
|
||||||
import MobileReviewSettingsDrawer, {
|
import MobileReviewSettingsDrawer, {
|
||||||
DrawerFeatures,
|
DrawerFeatures,
|
||||||
} from "../overlay/MobileReviewSettingsDrawer";
|
} from "../overlay/MobileReviewSettingsDrawer";
|
||||||
import useOptimisticState from "@/hooks/use-optimistic-state";
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
|
import FilterSwitch from "./FilterSwitch";
|
||||||
|
|
||||||
const REVIEW_FILTERS = [
|
const REVIEW_FILTERS = [
|
||||||
"cameras",
|
"cameras",
|
||||||
@ -248,8 +248,8 @@ export function CamerasFilterButton({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="h-auto overflow-y-auto overflow-x-hidden">
|
<div className="h-auto pt-2 overflow-y-auto overflow-x-hidden">
|
||||||
<FilterCheckBox
|
<FilterSwitch
|
||||||
isChecked={currentCameras == undefined}
|
isChecked={currentCameras == undefined}
|
||||||
label="All Cameras"
|
label="All Cameras"
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
@ -260,24 +260,24 @@ export function CamerasFilterButton({
|
|||||||
/>
|
/>
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="mt-2" />
|
||||||
{groups.map(([name, conf]) => {
|
{groups.map(([name, conf]) => {
|
||||||
return (
|
return (
|
||||||
<FilterCheckBox
|
<div
|
||||||
key={name}
|
key={name}
|
||||||
label={name}
|
className="w-full px-2 py-1.5 text-sm text-primary capitalize cursor-pointer rounded-lg hover:bg-muted"
|
||||||
isChecked={false}
|
onClick={() => setCurrentCameras([...conf.cameras])}
|
||||||
onCheckedChange={() => {
|
>
|
||||||
setCurrentCameras([...conf.cameras]);
|
{name}
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="my-2" />
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
{allCameras.map((item) => (
|
{allCameras.map((item) => (
|
||||||
<FilterCheckBox
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
isChecked={currentCameras?.includes(item) ?? false}
|
isChecked={currentCameras?.includes(item) ?? false}
|
||||||
label={item.replaceAll("_", " ")}
|
label={item.replaceAll("_", " ")}
|
||||||
@ -304,7 +304,8 @@ export function CamerasFilterButton({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
</div>
|
||||||
|
<DropdownMenuSeparator className="my-2" />
|
||||||
<div className="p-2 flex justify-evenly items-center">
|
<div className="p-2 flex justify-evenly items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
|
@ -72,7 +72,7 @@ export function ThresholdBarGraph({
|
|||||||
} else if (value >= threshold.warning) {
|
} else if (value >= threshold.warning) {
|
||||||
return "#FF9966";
|
return "#FF9966";
|
||||||
} else {
|
} else {
|
||||||
return (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5";
|
return "#217930";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -11,8 +11,8 @@ import { IconType } from "react-icons";
|
|||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: {
|
primary: {
|
||||||
active: "font-bold text-white bg-selected",
|
active: "font-bold text-white bg-selected hover:bg-selected/80",
|
||||||
inactive: "text-secondary-foreground bg-secondary",
|
inactive: "text-secondary-foreground bg-secondary hover:bg-muted",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
active: "font-bold text-selected",
|
active: "font-bold text-selected",
|
||||||
|
@ -32,7 +32,7 @@ function Sidebar() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center mb-8">
|
<div className="flex flex-col items-center gap-4 mb-8">
|
||||||
<GeneralSettings />
|
<GeneralSettings />
|
||||||
<AccountSettings />
|
<AccountSettings />
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,9 +111,13 @@ export default function DynamicVideoPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
onTimestampUpdate(controller.getProgress(time));
|
onTimestampUpdate(controller.getProgress(time));
|
||||||
},
|
},
|
||||||
[controller, onTimestampUpdate, isScrubbing],
|
[controller, onTimestampUpdate, isScrubbing, isLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
// state of playback player
|
// state of playback player
|
||||||
|
@ -3,16 +3,18 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
import { VscAccount } from "react-icons/vsc";
|
import { VscAccount } from "react-icons/vsc";
|
||||||
import { Button } from "../ui/button";
|
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button size="icon" variant="ghost">
|
<div
|
||||||
<VscAccount />
|
className={`flex flex-col justify-center items-center ${isDesktop ? "rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer" : "text-secondary-foreground"}`}
|
||||||
</Button>
|
>
|
||||||
|
<VscAccount className="size-5 md:m-[6px]" />
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>Account</p>
|
<p>Account</p>
|
||||||
|
@ -118,9 +118,11 @@ export default function GeneralSettings({ className }: GeneralSettings) {
|
|||||||
<a href="#">
|
<a href="#">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button size="icon" variant="ghost">
|
<div
|
||||||
<LuSettings />
|
className={`flex flex-col justify-center items-center ${isDesktop ? "rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer" : "text-secondary-foreground"}`}
|
||||||
</Button>
|
>
|
||||||
|
<LuSettings className="size-5 md:m-[6px]" />
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>Settings</p>
|
<p>Settings</p>
|
||||||
|
@ -31,7 +31,7 @@ function Logs() {
|
|||||||
const [logService, setLogService] = useState<LogType>("frigate");
|
const [logService, setLogService] = useState<LogType>("frigate");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Stats - Frigate`;
|
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Logs - Frigate`;
|
||||||
}, [logService]);
|
}, [logService]);
|
||||||
|
|
||||||
// log data handling
|
// log data handling
|
||||||
@ -366,7 +366,7 @@ function Logs() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCopyLogs}
|
onClick={handleCopyLogs}
|
||||||
>
|
>
|
||||||
<FaCopy />
|
<FaCopy className="text-secondary-foreground" />
|
||||||
<div className="hidden md:block text-primary">
|
<div className="hidden md:block text-primary">
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user