R1 UI fix

This commit is contained in:
Weitheng Haw 2025-02-04 11:23:14 +00:00
parent 630363ffe6
commit f4a2021b9c
2 changed files with 90 additions and 113 deletions

View File

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

View File

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