mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-19 19:06:16 +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():
|
||||
config_obj: FrigateConfig = current_app.frigate_config
|
||||
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
|
||||
|
@ -1351,11 +1351,12 @@ class FrigateConfig(FrigateBaseModel):
|
||||
"timestamp_style": ...,
|
||||
},
|
||||
exclude_unset=True,
|
||||
warnings="none",
|
||||
)
|
||||
|
||||
for name, camera in config.cameras.items():
|
||||
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(
|
||||
{"name": name, **merged_config}
|
||||
@ -1466,7 +1467,7 @@ class FrigateConfig(FrigateBaseModel):
|
||||
# Set runtime filter to create masks
|
||||
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
||||
frame_shape=camera_config.frame_shape,
|
||||
**filter.model_dump(exclude_unset=True),
|
||||
**filter.model_dump(exclude_unset=True, warnings="none"),
|
||||
)
|
||||
|
||||
# Convert motion configuration
|
||||
@ -1478,7 +1479,9 @@ class FrigateConfig(FrigateBaseModel):
|
||||
camera_config.motion = RuntimeMotionConfig(
|
||||
frame_shape=camera_config.frame_shape,
|
||||
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
|
||||
|
||||
@ -1515,7 +1518,9 @@ class FrigateConfig(FrigateBaseModel):
|
||||
for key, detector in config.detectors.items():
|
||||
adapter = TypeAdapter(DetectorConfig)
|
||||
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)
|
||||
if detector_config.model is None:
|
||||
@ -1536,8 +1541,8 @@ class FrigateConfig(FrigateBaseModel):
|
||||
"Customizing more than a detector model path is unsupported."
|
||||
)
|
||||
merged_model = deep_merge(
|
||||
detector_config.model.model_dump(exclude_unset=True),
|
||||
config.model.model_dump(exclude_unset=True),
|
||||
detector_config.model.model_dump(exclude_unset=True, warnings="none"),
|
||||
config.model.model_dump(exclude_unset=True, warnings="none"),
|
||||
)
|
||||
|
||||
if "path" not in merged_model:
|
||||
|
@ -51,7 +51,9 @@ class OnvifController:
|
||||
cam.onvif.port,
|
||||
cam.onvif.user,
|
||||
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,
|
||||
"active": False,
|
||||
|
@ -22,8 +22,8 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import FilterCheckBox from "./FilterCheckBox";
|
||||
import axios from "axios";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
|
||||
type CameraGroupSelectorProps = {
|
||||
className?: string;
|
||||
@ -305,7 +305,7 @@ function NewGroupDialog({ open, setOpen, currentGroups }: NewGroupDialogProps) {
|
||||
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
||||
...Object.keys(config?.cameras ?? {}),
|
||||
].map((camera) => (
|
||||
<FilterCheckBox
|
||||
<FilterSwitch
|
||||
key={camera}
|
||||
isChecked={cameras.includes(camera)}
|
||||
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 { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
import FilterCheckBox from "./FilterCheckBox";
|
||||
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
|
||||
import MobileReviewSettingsDrawer, {
|
||||
DrawerFeatures,
|
||||
} from "../overlay/MobileReviewSettingsDrawer";
|
||||
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
@ -248,8 +248,8 @@ export function CamerasFilterButton({
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<div className="h-auto overflow-y-auto overflow-x-hidden">
|
||||
<FilterCheckBox
|
||||
<div className="h-auto pt-2 overflow-y-auto overflow-x-hidden">
|
||||
<FilterSwitch
|
||||
isChecked={currentCameras == undefined}
|
||||
label="All Cameras"
|
||||
onCheckedChange={(isChecked) => {
|
||||
@ -260,51 +260,52 @@ export function CamerasFilterButton({
|
||||
/>
|
||||
{groups.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator className="mt-2" />
|
||||
{groups.map(([name, conf]) => {
|
||||
return (
|
||||
<FilterCheckBox
|
||||
<div
|
||||
key={name}
|
||||
label={name}
|
||||
isChecked={false}
|
||||
onCheckedChange={() => {
|
||||
setCurrentCameras([...conf.cameras]);
|
||||
}}
|
||||
/>
|
||||
className="w-full px-2 py-1.5 text-sm text-primary capitalize cursor-pointer rounded-lg hover:bg-muted"
|
||||
onClick={() => setCurrentCameras([...conf.cameras])}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{allCameras.map((item) => (
|
||||
<FilterCheckBox
|
||||
key={item}
|
||||
isChecked={currentCameras?.includes(item) ?? false}
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const updatedCameras = currentCameras
|
||||
? [...currentCameras]
|
||||
: [];
|
||||
<DropdownMenuSeparator className="my-2" />
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{allCameras.map((item) => (
|
||||
<FilterSwitch
|
||||
key={item}
|
||||
isChecked={currentCameras?.includes(item) ?? false}
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const updatedCameras = currentCameras
|
||||
? [...currentCameras]
|
||||
: [];
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator className="my-2" />
|
||||
<div className="p-2 flex justify-evenly items-center">
|
||||
<Button
|
||||
variant="select"
|
||||
|
@ -72,7 +72,7 @@ export function ThresholdBarGraph({
|
||||
} else if (value >= threshold.warning) {
|
||||
return "#FF9966";
|
||||
} else {
|
||||
return (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5";
|
||||
return "#217930";
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -11,8 +11,8 @@ import { IconType } from "react-icons";
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
active: "font-bold text-white bg-selected",
|
||||
inactive: "text-secondary-foreground bg-secondary",
|
||||
active: "font-bold text-white bg-selected hover:bg-selected/80",
|
||||
inactive: "text-secondary-foreground bg-secondary hover:bg-muted",
|
||||
},
|
||||
secondary: {
|
||||
active: "font-bold text-selected",
|
||||
|
@ -32,7 +32,7 @@ function Sidebar() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="flex flex-col items-center gap-4 mb-8">
|
||||
<GeneralSettings />
|
||||
<AccountSettings />
|
||||
</div>
|
||||
|
@ -111,9 +111,13 @@ export default function DynamicVideoPlayer({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
onTimestampUpdate(controller.getProgress(time));
|
||||
},
|
||||
[controller, onTimestampUpdate, isScrubbing],
|
||||
[controller, onTimestampUpdate, isScrubbing, isLoading],
|
||||
);
|
||||
|
||||
// state of playback player
|
||||
|
@ -3,16 +3,18 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { VscAccount } from "react-icons/vsc";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function AccountSettings() {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<VscAccount />
|
||||
</Button>
|
||||
<div
|
||||
className={`flex flex-col justify-center items-center ${isDesktop ? "rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer" : "text-secondary-foreground"}`}
|
||||
>
|
||||
<VscAccount className="size-5 md:m-[6px]" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Account</p>
|
||||
|
@ -118,9 +118,11 @@ export default function GeneralSettings({ className }: GeneralSettings) {
|
||||
<a href="#">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<LuSettings />
|
||||
</Button>
|
||||
<div
|
||||
className={`flex flex-col justify-center items-center ${isDesktop ? "rounded-lg text-secondary-foreground bg-secondary hover:bg-muted cursor-pointer" : "text-secondary-foreground"}`}
|
||||
>
|
||||
<LuSettings className="size-5 md:m-[6px]" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Settings</p>
|
||||
|
@ -31,7 +31,7 @@ function Logs() {
|
||||
const [logService, setLogService] = useState<LogType>("frigate");
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Stats - Frigate`;
|
||||
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Logs - Frigate`;
|
||||
}, [logService]);
|
||||
|
||||
// log data handling
|
||||
@ -366,7 +366,7 @@ function Logs() {
|
||||
size="sm"
|
||||
onClick={handleCopyLogs}
|
||||
>
|
||||
<FaCopy />
|
||||
<FaCopy className="text-secondary-foreground" />
|
||||
<div className="hidden md:block text-primary">
|
||||
Copy to Clipboard
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user