* 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:
Nicolas Mowen 2024-04-16 14:55:24 -06:00 committed by GitHub
parent a823a18496
commit 9be5951076
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 106 additions and 95 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
);
}

View 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>
);
}

View File

@ -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,51 +260,52 @@ 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" />
{allCameras.map((item) => ( <div className="flex flex-col gap-2.5">
<FilterCheckBox {allCameras.map((item) => (
key={item} <FilterSwitch
isChecked={currentCameras?.includes(item) ?? false} key={item}
label={item.replaceAll("_", " ")} isChecked={currentCameras?.includes(item) ?? false}
onCheckedChange={(isChecked) => { label={item.replaceAll("_", " ")}
if (isChecked) { onCheckedChange={(isChecked) => {
const updatedCameras = currentCameras if (isChecked) {
? [...currentCameras] const updatedCameras = currentCameras
: []; ? [...currentCameras]
: [];
updatedCameras.push(item); updatedCameras.push(item);
setCurrentCameras(updatedCameras);
} else {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
// can not deselect the last item
if (updatedCameras.length > 1) {
updatedCameras.splice(updatedCameras.indexOf(item), 1);
setCurrentCameras(updatedCameras); setCurrentCameras(updatedCameras);
} else {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
// can not deselect the last item
if (updatedCameras.length > 1) {
updatedCameras.splice(updatedCameras.indexOf(item), 1);
setCurrentCameras(updatedCameras);
}
} }
} }}
}} />
/> ))}
))} </div>
</div> </div>
<DropdownMenuSeparator /> <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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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