mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-18 17:14:26 +03:00
R1 UI fix
This commit is contained in:
parent
630363ffe6
commit
f4a2021b9c
@ -32,7 +32,7 @@ import {
|
|||||||
MobilePageTitle,
|
MobilePageTitle,
|
||||||
} from "@/components/mobile/MobilePage";
|
} from "@/components/mobile/MobilePage";
|
||||||
|
|
||||||
const LPR_TABS = ["details", "snapshot", "video"] as const;
|
const LPR_TABS = ["details", "snapshot", "video", "raw"] as const;
|
||||||
export type LPRTab = (typeof LPR_TABS)[number];
|
export type LPRTab = (typeof LPR_TABS)[number];
|
||||||
|
|
||||||
type LPRDetailDialogProps = {
|
type LPRDetailDialogProps = {
|
||||||
@ -41,6 +41,7 @@ type LPRDetailDialogProps = {
|
|||||||
event?: Event;
|
event?: Event;
|
||||||
config: FrigateConfig;
|
config: FrigateConfig;
|
||||||
lprImage: string;
|
lprImage: string;
|
||||||
|
rawImage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LPRDetailDialog({
|
export default function LPRDetailDialog({
|
||||||
@ -49,6 +50,7 @@ export default function LPRDetailDialog({
|
|||||||
event,
|
event,
|
||||||
config,
|
config,
|
||||||
lprImage,
|
lprImage,
|
||||||
|
rawImage,
|
||||||
}: LPRDetailDialogProps) {
|
}: LPRDetailDialogProps) {
|
||||||
const [page, setPage] = useState<LPRTab>("details");
|
const [page, setPage] = useState<LPRTab>("details");
|
||||||
|
|
||||||
@ -66,20 +68,21 @@ export default function LPRDetailDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const lprTabs = useMemo(() => {
|
const lprTabs = useMemo(() => {
|
||||||
if (!config || !event) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = [...LPR_TABS];
|
const views = [...LPR_TABS];
|
||||||
|
|
||||||
if (!event.has_snapshot) {
|
if (event) {
|
||||||
const index = views.indexOf("snapshot");
|
if (!event.has_snapshot) {
|
||||||
views.splice(index, 1);
|
const index = views.indexOf("snapshot");
|
||||||
}
|
views.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.has_clip) {
|
if (!event.has_clip) {
|
||||||
const index = views.indexOf("video");
|
const index = views.indexOf("video");
|
||||||
views.splice(index, 1);
|
views.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When no event, show only raw tab
|
||||||
|
return ['raw'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return views;
|
return views;
|
||||||
@ -100,7 +103,7 @@ export default function LPRDetailDialog({
|
|||||||
<Title>License Plate Image</Title>
|
<Title>License Plate Image</Title>
|
||||||
<Description className="sr-only">License plate image details</Description>
|
<Description className="sr-only">License plate image details</Description>
|
||||||
</Header>
|
</Header>
|
||||||
<PlateTab lprImage={lprImage} />
|
<PlateTab lprImage={lprImage} config={config} />
|
||||||
</Content>
|
</Content>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
@ -110,60 +113,68 @@ export default function LPRDetailDialog({
|
|||||||
<Overlay open={open} onOpenChange={setOpen}>
|
<Overlay open={open} onOpenChange={setOpen}>
|
||||||
<Content
|
<Content
|
||||||
className={cn(
|
className={cn(
|
||||||
"scrollbar-container overflow-y-auto min-w-[50vw]",
|
"scrollbar-container overflow-y-auto",
|
||||||
isDesktop &&
|
isDesktop &&
|
||||||
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
||||||
isMobile && "px-4",
|
isMobile && "px-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header>
|
<Header>
|
||||||
<Title>License Plate Event Details</Title>
|
<Title>{event ? "License Plate Event Details" : "License Plate Image"}</Title>
|
||||||
<Description className="sr-only">License plate event details</Description>
|
<Description className="sr-only">License plate details</Description>
|
||||||
</Header>
|
</Header>
|
||||||
<ScrollArea
|
|
||||||
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
{event && (
|
||||||
>
|
<ScrollArea className={cn("w-full whitespace-nowrap", isMobile && "my-2")}>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
className="*:rounded-md *:px-3 *:py-4"
|
className="*:rounded-md *:px-3 *:py-4"
|
||||||
type="single"
|
type="single"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={page}
|
value={page}
|
||||||
onValueChange={(value: LPRTab) => {
|
onValueChange={(value: LPRTab) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
setPage(value);
|
setPage(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(lprTabs).map((item) => (
|
{lprTabs.map((item) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
key={item}
|
key={item}
|
||||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "details" ? "last:mr-20" : ""} ${page == item ? "" : "*:text-muted-foreground"}`}
|
className={`flex scroll-mx-10 items-center justify-between gap-2 ${
|
||||||
value={item}
|
page === item ? "" : "*:text-muted-foreground"
|
||||||
data-nav-item={item}
|
}`}
|
||||||
aria-label={`Select ${item}`}
|
value={item}
|
||||||
>
|
data-nav-item={item}
|
||||||
{item == "details" && <FaRegListAlt className="size-4" />}
|
aria-label={`Select ${item}`}
|
||||||
{item == "snapshot" && <FaImage className="size-4" />}
|
>
|
||||||
{item == "video" && <FaVideo className="size-4" />}
|
{item === "details" && <FaRegListAlt className="size-4" />}
|
||||||
<div className="capitalize">{item}</div>
|
{item === "snapshot" && <FaImage className="size-4" />}
|
||||||
</ToggleGroupItem>
|
{item === "video" && <FaVideo className="size-4" />}
|
||||||
))}
|
{item === "raw" && <FaImage className="size-4" />}
|
||||||
</ToggleGroup>
|
<div className="capitalize">{item}</div>
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
</ToggleGroupItem>
|
||||||
</div>
|
))}
|
||||||
</ScrollArea>
|
</ToggleGroup>
|
||||||
{page === "details" && (
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
<DetailsTab event={event} timestamp={timestamp} />
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
{page === "snapshot" && (
|
|
||||||
<SnapshotTab event={event} />
|
{event ? (
|
||||||
)}
|
<>
|
||||||
{page === "video" && (
|
{page === "details" && <DetailsTab event={event} timestamp={timestamp} />}
|
||||||
<VideoTab event={event} />
|
{page === "snapshot" && <SnapshotTab event={event} />}
|
||||||
)}
|
{page === "video" && <VideoTab event={event} />}
|
||||||
{(page === "details" || !event) && (
|
{page === "raw" && (
|
||||||
<PlateTab lprImage={lprImage} />
|
<PlateTab
|
||||||
|
lprImage={rawImage}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PlateTab lprImage={lprImage} config={config} />
|
||||||
)}
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
@ -333,9 +344,10 @@ function VideoTab({ event }: VideoTabProps) {
|
|||||||
|
|
||||||
type PlateTabProps = {
|
type PlateTabProps = {
|
||||||
lprImage: string;
|
lprImage: string;
|
||||||
|
config: FrigateConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
function PlateTab({ lprImage }: PlateTabProps) {
|
function PlateTab({ lprImage, config }: PlateTabProps) {
|
||||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -346,33 +358,31 @@ function PlateTab({ lprImage }: PlateTabProps) {
|
|||||||
/>
|
/>
|
||||||
<div className={`${imgLoaded ? "visible" : "invisible"} min-h-[60dvh]`}>
|
<div className={`${imgLoaded ? "visible" : "invisible"} min-h-[60dvh]`}>
|
||||||
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
|
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3 h-full">
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
|
||||||
contentStyle={{
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center"
|
justifyContent: "center"
|
||||||
}}
|
}}
|
||||||
|
contentStyle={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative mx-auto w-full h-full flex items-center justify-center">
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className="mx-auto max-h-[60dvh] w-auto h-auto bg-black object-contain"
|
className="max-h-[70vh] w-auto object-contain bg-black"
|
||||||
src={`${baseUrl}clips/lpr/${lprImage}`}
|
src={`${baseUrl}clips/lpr/${lprImage}`}
|
||||||
alt="License plate"
|
alt="License plate"
|
||||||
loading={isSafari ? "eager" : "lazy"}
|
loading={isSafari ? "eager" : "lazy"}
|
||||||
onLoad={() => {
|
onLoad={onImgLoad}
|
||||||
onImgLoad();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className={cn("absolute right-1 top-1 flex items-center gap-2")}>
|
<div className="absolute right-2 top-2 flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<a download href={`${baseUrl}clips/lpr/${lprImage}`}>
|
<a download href={`${baseUrl}clips/lpr/${lprImage}`}>
|
||||||
|
|||||||
@ -31,7 +31,6 @@ export default function LPRDebug() {
|
|||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [sortBy, setSortBy] = useState<SortOption>("time_desc");
|
const [sortBy, setSortBy] = useState<SortOption>("time_desc");
|
||||||
const [selectedCameras, setSelectedCameras] = useState<string[] | undefined>();
|
const [selectedCameras, setSelectedCameras] = useState<string[] | undefined>();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("detected");
|
|
||||||
|
|
||||||
// title
|
// title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -109,40 +108,6 @@ export default function LPRDebug() {
|
|||||||
<div className="relative mb-2 flex h-11 w-full items-center justify-between">
|
<div className="relative mb-2 flex h-11 w-full items-center justify-between">
|
||||||
<ScrollArea className="w-full whitespace-nowrap">
|
<ScrollArea className="w-full whitespace-nowrap">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<ToggleGroup
|
|
||||||
className="*:rounded-md *:px-3 *:py-4"
|
|
||||||
type="single"
|
|
||||||
size="sm"
|
|
||||||
value={viewMode}
|
|
||||||
onValueChange={(value: ViewMode) => {
|
|
||||||
if (value) {
|
|
||||||
setViewMode(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="detected"
|
|
||||||
className={cn(
|
|
||||||
"flex scroll-mx-10 items-center justify-between gap-2",
|
|
||||||
viewMode !== "detected" && "*:text-muted-foreground"
|
|
||||||
)}
|
|
||||||
data-nav-item="detected"
|
|
||||||
aria-label="Select detected"
|
|
||||||
>
|
|
||||||
<div>Detected</div>
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="raw"
|
|
||||||
className={cn(
|
|
||||||
"flex scroll-mx-10 items-center justify-between gap-2",
|
|
||||||
viewMode !== "raw" && "*:text-muted-foreground"
|
|
||||||
)}
|
|
||||||
data-nav-item="raw"
|
|
||||||
aria-label="Select raw"
|
|
||||||
>
|
|
||||||
<div>Raw</div>
|
|
||||||
</ToggleGroupItem>
|
|
||||||
</ToggleGroup>
|
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@ -178,14 +143,13 @@ export default function LPRDebug() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll">
|
<div className="scrollbar-container grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2 overflow-y-auto">
|
||||||
{lprAttempts.map((attempt: string) => (
|
{lprAttempts.map((attempt: string) => (
|
||||||
<LPRAttempt
|
<LPRAttempt
|
||||||
key={attempt}
|
key={attempt}
|
||||||
attempt={attempt}
|
attempt={attempt}
|
||||||
config={config}
|
config={config}
|
||||||
onRefresh={refreshLPR}
|
onRefresh={refreshLPR}
|
||||||
viewMode={viewMode}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -197,10 +161,9 @@ type LPRAttemptProps = {
|
|||||||
attempt: string;
|
attempt: string;
|
||||||
config: FrigateConfig;
|
config: FrigateConfig;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
viewMode: ViewMode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function LPRAttempt({ attempt, config, onRefresh, viewMode }: LPRAttemptProps) {
|
function LPRAttempt({ attempt, config, onRefresh }: LPRAttemptProps) {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const parts = attempt.split("_");
|
const parts = attempt.split("_");
|
||||||
@ -245,6 +208,9 @@ function LPRAttempt({ attempt, config, onRefresh, viewMode }: LPRAttemptProps) {
|
|||||||
});
|
});
|
||||||
}, [attempt, onRefresh]);
|
}, [attempt, onRefresh]);
|
||||||
|
|
||||||
|
// Extract event ID from processed image filename (format: PLATE_SCORE_EVENTID.jpg)
|
||||||
|
const eventId = useMemo(() => attempt.split("_").slice(2).join("_").replace(".jpg", ""), [attempt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LPRDetailDialog
|
<LPRDetailDialog
|
||||||
@ -252,7 +218,8 @@ function LPRAttempt({ attempt, config, onRefresh, viewMode }: LPRAttemptProps) {
|
|||||||
setOpen={setShowDialog}
|
setOpen={setShowDialog}
|
||||||
event={event}
|
event={event}
|
||||||
config={config}
|
config={config}
|
||||||
lprImage={viewMode === "detected" ? attempt : `raw_${data.eventId}.jpg`}
|
lprImage={attempt}
|
||||||
|
rawImage={`raw_${eventId}.jpg`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex flex-col rounded-lg">
|
<div className="relative flex flex-col rounded-lg">
|
||||||
@ -263,7 +230,7 @@ function LPRAttempt({ attempt, config, onRefresh, viewMode }: LPRAttemptProps) {
|
|||||||
<div className="aspect-[2/1] flex items-center justify-center bg-black">
|
<div className="aspect-[2/1] flex items-center justify-center bg-black">
|
||||||
<img
|
<img
|
||||||
className="h-40 max-w-none"
|
className="h-40 max-w-none"
|
||||||
src={`${baseUrl}clips/lpr/${viewMode === "detected" ? attempt : `raw_${data.eventId}.jpg`}`}
|
src={`${baseUrl}clips/lpr/${attempt}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user