mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	UI Fixes (#11742)
* Allow deleting failed in progress exports * Fix comparison and preview retrieval * Fix stretching of event cards * Reset edit state when group changes * Allow specifying group
This commit is contained in:
		
							parent
							
								
									7917bf55ff
								
							
						
					
					
						commit
						2875e84cb5
					
				@ -4,6 +4,7 @@ import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import psutil
 | 
			
		||||
from flask import (
 | 
			
		||||
    Blueprint,
 | 
			
		||||
    current_app,
 | 
			
		||||
@ -14,6 +15,7 @@ from flask import (
 | 
			
		||||
from peewee import DoesNotExist
 | 
			
		||||
from werkzeug.utils import secure_filename
 | 
			
		||||
 | 
			
		||||
from frigate.const import EXPORT_DIR
 | 
			
		||||
from frigate.models import Export, Recordings
 | 
			
		||||
from frigate.record.export import PlaybackFactorEnum, RecordingExporter
 | 
			
		||||
 | 
			
		||||
@ -140,6 +142,27 @@ def export_delete(id: str):
 | 
			
		||||
            404,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    files_in_use = []
 | 
			
		||||
    for process in psutil.process_iter():
 | 
			
		||||
        try:
 | 
			
		||||
            if process.name() != "ffmpeg":
 | 
			
		||||
                continue
 | 
			
		||||
            flist = process.open_files()
 | 
			
		||||
            if flist:
 | 
			
		||||
                for nt in flist:
 | 
			
		||||
                    if nt.path.startswith(EXPORT_DIR):
 | 
			
		||||
                        files_in_use.append(nt.path.split("/")[-1])
 | 
			
		||||
        except psutil.Error:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
    if export.video_path.split("/")[-1] in files_in_use:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            jsonify(
 | 
			
		||||
                {"success": False, "message": "Can not delete in progress export."}
 | 
			
		||||
            ),
 | 
			
		||||
            400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    Path(export.video_path).unlink(missing_ok=True)
 | 
			
		||||
 | 
			
		||||
    if export.thumb_path:
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,8 @@ import threading
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from peewee import DoesNotExist
 | 
			
		||||
 | 
			
		||||
from frigate.config import FrigateConfig
 | 
			
		||||
from frigate.const import (
 | 
			
		||||
    CACHE_DIR,
 | 
			
		||||
@ -72,8 +74,11 @@ class RecordingExporter(threading.Thread):
 | 
			
		||||
 | 
			
		||||
        if datetime.datetime.fromtimestamp(
 | 
			
		||||
            self.start_time
 | 
			
		||||
        ) < datetime.datetime.now().replace(minute=0, second=0):
 | 
			
		||||
        ) < datetime.datetime.now().astimezone(datetime.timezone.dst).replace(
 | 
			
		||||
            minute=0, second=0, microsecond=0
 | 
			
		||||
        ):
 | 
			
		||||
            # has preview mp4
 | 
			
		||||
            try:
 | 
			
		||||
                preview: Previews = (
 | 
			
		||||
                    Previews.select(
 | 
			
		||||
                        Previews.camera,
 | 
			
		||||
@ -94,8 +99,7 @@ class RecordingExporter(threading.Thread):
 | 
			
		||||
                    .limit(1)
 | 
			
		||||
                    .get()
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if not preview:
 | 
			
		||||
            except DoesNotExist:
 | 
			
		||||
                return ""
 | 
			
		||||
 | 
			
		||||
            diff = self.start_time - preview.start_time
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,12 @@ import { baseUrl } from "@/api/baseUrl";
 | 
			
		||||
 | 
			
		||||
type AnimatedEventCardProps = {
 | 
			
		||||
  event: ReviewSegment;
 | 
			
		||||
  selectedGroup?: string;
 | 
			
		||||
};
 | 
			
		||||
export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
 | 
			
		||||
export function AnimatedEventCard({
 | 
			
		||||
  event,
 | 
			
		||||
  selectedGroup,
 | 
			
		||||
}: AnimatedEventCardProps) {
 | 
			
		||||
  const { data: config } = useSWR<FrigateConfig>("config");
 | 
			
		||||
 | 
			
		||||
  const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]);
 | 
			
		||||
@ -53,7 +57,8 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
 | 
			
		||||
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const onOpenReview = useCallback(() => {
 | 
			
		||||
    navigate("review", {
 | 
			
		||||
    const url = selectedGroup ? `review?group=${selectedGroup}` : "review";
 | 
			
		||||
    navigate(url, {
 | 
			
		||||
      state: {
 | 
			
		||||
        severity: event.severity,
 | 
			
		||||
        recording: {
 | 
			
		||||
@ -64,7 +69,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    axios.post(`reviews/viewed`, { ids: [event.id] });
 | 
			
		||||
  }, [navigate, event]);
 | 
			
		||||
  }, [navigate, selectedGroup, event]);
 | 
			
		||||
 | 
			
		||||
  // image behavior
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -109,26 +109,15 @@ export default function ExportCard({
 | 
			
		||||
          "relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl",
 | 
			
		||||
          className,
 | 
			
		||||
        )}
 | 
			
		||||
        onMouseEnter={
 | 
			
		||||
          isDesktop && !exportedRecording.in_progress
 | 
			
		||||
            ? () => setHovered(true)
 | 
			
		||||
            : undefined
 | 
			
		||||
        }
 | 
			
		||||
        onMouseLeave={
 | 
			
		||||
          isDesktop && !exportedRecording.in_progress
 | 
			
		||||
            ? () => setHovered(false)
 | 
			
		||||
            : undefined
 | 
			
		||||
        }
 | 
			
		||||
        onClick={
 | 
			
		||||
          isDesktop || exportedRecording.in_progress
 | 
			
		||||
            ? undefined
 | 
			
		||||
            : () => setHovered(!hovered)
 | 
			
		||||
        }
 | 
			
		||||
        onMouseEnter={isDesktop ? () => setHovered(true) : undefined}
 | 
			
		||||
        onMouseLeave={isDesktop ? () => setHovered(false) : undefined}
 | 
			
		||||
        onClick={isDesktop ? undefined : () => setHovered(!hovered)}
 | 
			
		||||
      >
 | 
			
		||||
        {hovered && (
 | 
			
		||||
          <>
 | 
			
		||||
            <div className="absolute inset-0 z-10 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" />
 | 
			
		||||
            <div className="absolute right-1 top-1 flex items-center gap-2">
 | 
			
		||||
              {!exportedRecording.in_progress && (
 | 
			
		||||
                <a
 | 
			
		||||
                  className="z-20"
 | 
			
		||||
                  download
 | 
			
		||||
@ -138,14 +127,20 @@ export default function ExportCard({
 | 
			
		||||
                    <FaDownload className="size-4 text-white" />
 | 
			
		||||
                  </Chip>
 | 
			
		||||
                </a>
 | 
			
		||||
              )}
 | 
			
		||||
              {!exportedRecording.in_progress && (
 | 
			
		||||
                <Chip
 | 
			
		||||
                  className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                  setEditName({ original: exportedRecording.name, update: "" })
 | 
			
		||||
                    setEditName({
 | 
			
		||||
                      original: exportedRecording.name,
 | 
			
		||||
                      update: "",
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  <MdEditSquare className="size-4 text-white" />
 | 
			
		||||
                </Chip>
 | 
			
		||||
              )}
 | 
			
		||||
              <Chip
 | 
			
		||||
                className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
@ -159,6 +154,7 @@ export default function ExportCard({
 | 
			
		||||
              </Chip>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {!exportedRecording.in_progress && (
 | 
			
		||||
              <Button
 | 
			
		||||
                className="absolute left-1/2 top-1/2 z-20 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
 | 
			
		||||
                variant="ghost"
 | 
			
		||||
@ -168,6 +164,7 @@ export default function ExportCard({
 | 
			
		||||
              >
 | 
			
		||||
                <FaPlay />
 | 
			
		||||
              </Button>
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        {exportedRecording.in_progress ? (
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,19 @@ export default function Events() {
 | 
			
		||||
  const [reviewFilter, setReviewFilter, reviewSearchParams] =
 | 
			
		||||
    useApiFilter<ReviewFilter>();
 | 
			
		||||
 | 
			
		||||
  useSearchEffect("group", (reviewGroup) => {
 | 
			
		||||
    if (config && reviewGroup) {
 | 
			
		||||
      const group = config.camera_groups[reviewGroup];
 | 
			
		||||
 | 
			
		||||
      if (group) {
 | 
			
		||||
        setReviewFilter({
 | 
			
		||||
          ...reviewFilter,
 | 
			
		||||
          cameras: group.cameras,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const onUpdateFilter = useCallback(
 | 
			
		||||
    (newFilter: ReviewFilter) => {
 | 
			
		||||
      setReviewFilter(newFilter);
 | 
			
		||||
 | 
			
		||||
@ -668,8 +668,12 @@ function Timeline({
 | 
			
		||||
          <Skeleton className="size-full" />
 | 
			
		||||
        )
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className="h-full overflow-auto bg-secondary">
 | 
			
		||||
          <div
 | 
			
		||||
          className={`grid h-full grid-cols-1 gap-4 overflow-auto bg-secondary p-4 ${isDesktop ? "" : "sm:grid-cols-2"}`}
 | 
			
		||||
            className={cn(
 | 
			
		||||
              "grid h-auto grid-cols-1 gap-4 overflow-auto p-4",
 | 
			
		||||
              isMobile && "sm:grid-cols-2",
 | 
			
		||||
            )}
 | 
			
		||||
          >
 | 
			
		||||
            {mainCameraReviewItems.map((review) => {
 | 
			
		||||
              if (review.severity == "significant_motion") {
 | 
			
		||||
@ -690,6 +694,7 @@ function Timeline({
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -91,8 +91,16 @@ export default function DraggableGridLayout({
 | 
			
		||||
    );
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  // editing
 | 
			
		||||
 | 
			
		||||
  const [editGroup, setEditGroup] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setEditGroup(false);
 | 
			
		||||
  }, [cameraGroup]);
 | 
			
		||||
 | 
			
		||||
  // camera state
 | 
			
		||||
 | 
			
		||||
  const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>();
 | 
			
		||||
  const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
 | 
			
		||||
    useState<boolean>();
 | 
			
		||||
 | 
			
		||||
@ -224,7 +224,13 @@ export default function LiveDashboardView({
 | 
			
		||||
          <TooltipProvider>
 | 
			
		||||
            <div className="flex items-center gap-2 px-1">
 | 
			
		||||
              {events.map((event) => {
 | 
			
		||||
                return <AnimatedEventCard key={event.id} event={event} />;
 | 
			
		||||
                return (
 | 
			
		||||
                  <AnimatedEventCard
 | 
			
		||||
                    key={event.id}
 | 
			
		||||
                    event={event}
 | 
			
		||||
                    selectedGroup={cameraGroup}
 | 
			
		||||
                  />
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
            </div>
 | 
			
		||||
          </TooltipProvider>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user