mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-20 13:54:36 +01:00
Miscellaneous Fixes (#21050)
* Don't add to history when opening search dialog * Update caniuse * Revamp the history handling for dialog components * clarify audio transcription docs * Use titlecase helper * Allow running object clasasification on stationary objects * small spacing tweaks for tablets * require admin role to delete users * explicitly prevent deletion of admin user --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
|
||||
import { isPWA } from "@/utils/isPWA";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||
|
||||
const MobilePageContext = createContext<{
|
||||
open: boolean;
|
||||
@@ -24,15 +24,16 @@ type MobilePageProps = {
|
||||
children: React.ReactNode;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
enableHistoryBack?: boolean;
|
||||
};
|
||||
|
||||
export function MobilePage({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
enableHistoryBack = true,
|
||||
}: MobilePageProps) {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const open = controlledOpen ?? uncontrolledOpen;
|
||||
const setOpen = useCallback(
|
||||
@@ -46,33 +47,12 @@ export function MobilePage({
|
||||
[onOpenChange, setUncontrolledOpen],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
|
||||
if (open && isActive) {
|
||||
window.history.pushState({ isMobilePage: true }, "", location.pathname);
|
||||
}
|
||||
|
||||
const handlePopState = (event: PopStateEvent) => {
|
||||
if (open && isActive) {
|
||||
event.preventDefault();
|
||||
setOpen(false);
|
||||
// Delay replaceState to ensure state updates are processed
|
||||
setTimeout(() => {
|
||||
if (isActive) {
|
||||
window.history.replaceState(null, "", location.pathname);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
};
|
||||
}, [open, setOpen, location.pathname]);
|
||||
// Handle browser back button to close mobile page
|
||||
useHistoryBack({
|
||||
enabled: enableHistoryBack,
|
||||
open,
|
||||
onClose: () => setOpen(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
||||
|
||||
@@ -113,7 +113,12 @@ export function PlatformAwareSheet({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
modal={false}
|
||||
enableHistoryBack
|
||||
>
|
||||
<SheetTrigger asChild className={triggerClassName}>
|
||||
{trigger}
|
||||
</SheetTrigger>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||
|
||||
// Enhanced Dialog with History Support
|
||||
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
|
||||
@@ -15,51 +16,28 @@ const Dialog = ({
|
||||
...props
|
||||
}: HistoryDialogProps) => {
|
||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||
const historyStateRef = React.useRef<null | {
|
||||
listener: (e: PopStateEvent) => void;
|
||||
}>(null);
|
||||
|
||||
// Sync internal state with controlled open prop
|
||||
React.useEffect(() => {
|
||||
if (open !== undefined) {
|
||||
setInternalOpen(open);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enableHistoryBack) {
|
||||
if (internalOpen) {
|
||||
window.history.pushState({ dialogOpen: true }, "");
|
||||
const handleOpenChange = React.useCallback(
|
||||
(newOpen: boolean) => {
|
||||
setInternalOpen(newOpen);
|
||||
onOpenChange?.(newOpen);
|
||||
},
|
||||
[onOpenChange],
|
||||
);
|
||||
|
||||
const listener = () => {
|
||||
setInternalOpen(false);
|
||||
if (onOpenChange) onOpenChange(false);
|
||||
};
|
||||
|
||||
historyStateRef.current = { listener };
|
||||
window.addEventListener("popstate", listener);
|
||||
|
||||
return () => {
|
||||
if (internalOpen) {
|
||||
window.removeEventListener("popstate", listener);
|
||||
historyStateRef.current = null;
|
||||
}
|
||||
};
|
||||
} else if (historyStateRef.current) {
|
||||
window.removeEventListener(
|
||||
"popstate",
|
||||
historyStateRef.current.listener,
|
||||
);
|
||||
historyStateRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [enableHistoryBack, internalOpen, onOpenChange]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setInternalOpen(open);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
// Handle browser back button to close dialog
|
||||
useHistoryBack({
|
||||
enabled: enableHistoryBack,
|
||||
open: internalOpen,
|
||||
onClose: () => handleOpenChange(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||
|
||||
// Enhanced Sheet with History Support
|
||||
interface HistorySheetProps extends SheetPrimitive.DialogProps {
|
||||
@@ -17,51 +18,28 @@ const Sheet = ({
|
||||
...props
|
||||
}: HistorySheetProps) => {
|
||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||
const historyStateRef = React.useRef<null | {
|
||||
listener: (e: PopStateEvent) => void;
|
||||
}>(null);
|
||||
|
||||
// Sync internal state with controlled open prop
|
||||
React.useEffect(() => {
|
||||
if (open !== undefined) {
|
||||
setInternalOpen(open);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enableHistoryBack) {
|
||||
if (internalOpen) {
|
||||
window.history.pushState({ sheetOpen: true }, "");
|
||||
const handleOpenChange = React.useCallback(
|
||||
(newOpen: boolean) => {
|
||||
setInternalOpen(newOpen);
|
||||
onOpenChange?.(newOpen);
|
||||
},
|
||||
[onOpenChange],
|
||||
);
|
||||
|
||||
const listener = () => {
|
||||
setInternalOpen(false);
|
||||
if (onOpenChange) onOpenChange(false);
|
||||
};
|
||||
|
||||
historyStateRef.current = { listener };
|
||||
window.addEventListener("popstate", listener);
|
||||
|
||||
return () => {
|
||||
if (internalOpen) {
|
||||
window.removeEventListener("popstate", listener);
|
||||
historyStateRef.current = null;
|
||||
}
|
||||
};
|
||||
} else if (historyStateRef.current) {
|
||||
window.removeEventListener(
|
||||
"popstate",
|
||||
historyStateRef.current.listener,
|
||||
);
|
||||
historyStateRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [enableHistoryBack, internalOpen, onOpenChange]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setInternalOpen(open);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
// Handle browser back button to close sheet
|
||||
useHistoryBack({
|
||||
enabled: enableHistoryBack,
|
||||
open: internalOpen,
|
||||
onClose: () => handleOpenChange(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<SheetPrimitive.Root
|
||||
|
||||
57
web/src/hooks/use-history-back.ts
Normal file
57
web/src/hooks/use-history-back.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface UseHistoryBackOptions {
|
||||
enabled: boolean;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that manages browser history for overlay components (dialogs, sheets, etc.)
|
||||
* When enabled, pressing the browser back button will close the overlay instead of navigating away.
|
||||
*/
|
||||
export function useHistoryBack({
|
||||
enabled,
|
||||
open,
|
||||
onClose,
|
||||
}: UseHistoryBackOptions): void {
|
||||
const historyPushedRef = React.useRef(false);
|
||||
const closedByBackRef = React.useRef(false);
|
||||
|
||||
// Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes
|
||||
const onCloseRef = React.useRef(onClose);
|
||||
React.useLayoutEffect(() => {
|
||||
onCloseRef.current = onClose;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
if (open) {
|
||||
// Only push history state if we haven't already (prevents duplicates in strict mode)
|
||||
if (!historyPushedRef.current) {
|
||||
window.history.pushState({ overlayOpen: true }, "");
|
||||
historyPushedRef.current = true;
|
||||
}
|
||||
|
||||
const handlePopState = () => {
|
||||
closedByBackRef.current = true;
|
||||
historyPushedRef.current = false;
|
||||
onCloseRef.current();
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
};
|
||||
} else {
|
||||
// Overlay is closing - clean up history if we pushed and it wasn't via back button
|
||||
if (historyPushedRef.current && !closedByBackRef.current) {
|
||||
window.history.back();
|
||||
}
|
||||
historyPushedRef.current = false;
|
||||
closedByBackRef.current = false;
|
||||
}
|
||||
}, [enabled, open]);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MasksAndZoneViewProps = {
|
||||
selectedCamera: string;
|
||||
@@ -697,7 +698,10 @@ export default function MasksAndZonesView({
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex max-h-[50%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow"
|
||||
className={cn(
|
||||
"flex max-h-[50%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
|
||||
isDesktop && "md:mr-3",
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex size-full flex-row justify-center">
|
||||
{cameraConfig &&
|
||||
|
||||
@@ -23,6 +23,8 @@ import { LuExternalLink } from "react-icons/lu";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
|
||||
type MotionTunerViewProps = {
|
||||
selectedCamera: string;
|
||||
@@ -325,7 +327,12 @@ export default function MotionTunerView({
|
||||
</div>
|
||||
|
||||
{cameraConfig ? (
|
||||
<div className="flex max-h-[70%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow">
|
||||
<div
|
||||
className={cn(
|
||||
"flex max-h-[70%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
|
||||
isDesktop && "md:mr-3",
|
||||
)}
|
||||
>
|
||||
<div className="size-full min-h-10">
|
||||
<AutoUpdatingCameraImage
|
||||
camera={cameraConfig.name}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { useTriggers } from "@/api/ws";
|
||||
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
||||
import { CiCircleAlert } from "react-icons/ci";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
|
||||
type ConfigSetBody = {
|
||||
requires_restart: number;
|
||||
@@ -440,7 +441,12 @@ export default function TriggerView({
|
||||
return (
|
||||
<div className="flex size-full flex-col md:flex-row">
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
||||
<div
|
||||
className={cn(
|
||||
"scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2",
|
||||
isDesktop && "order-none mr-3 mt-0",
|
||||
)}
|
||||
>
|
||||
{!isSemanticSearchEnabled ? (
|
||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -651,7 +657,7 @@ export default function TriggerView({
|
||||
</div>
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:mr-3 md:block">
|
||||
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:block">
|
||||
<div className="h-full overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 bg-muted/50">
|
||||
|
||||
Reference in New Issue
Block a user