mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-20 12:06:43 +03:00
Compare commits
13 Commits
40cc33fa37
...
67845d647f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67845d647f | ||
|
|
441d57c76b | ||
|
|
097673b845 | ||
|
|
8e98dff671 | ||
|
|
2d06055bbe | ||
|
|
426699d3d0 | ||
|
|
73b9193a19 | ||
|
|
daa9919966 | ||
|
|
44077ebe43 | ||
|
|
341ffc194b | ||
|
|
29a464f2b5 | ||
|
|
16ac0b3f5e | ||
|
|
b105b3f2b4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ frigate/version.py
|
|||||||
web/build
|
web/build
|
||||||
web/node_modules
|
web/node_modules
|
||||||
web/coverage
|
web/coverage
|
||||||
|
web/.env
|
||||||
core
|
core
|
||||||
!/web/**/*.ts
|
!/web/**/*.ts
|
||||||
.idea/*
|
.idea/*
|
||||||
|
|||||||
1
Makefile
1
Makefile
@ -14,6 +14,7 @@ push-boards: $(BOARDS:%=push-%)
|
|||||||
|
|
||||||
version:
|
version:
|
||||||
echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py
|
echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py
|
||||||
|
echo 'VITE_GIT_COMMIT_HASH=$(COMMIT_HASH)' > web/.env
|
||||||
|
|
||||||
local: version
|
local: version
|
||||||
docker buildx build --target=frigate --file docker/main/Dockerfile . \
|
docker buildx build --target=frigate --file docker/main/Dockerfile . \
|
||||||
|
|||||||
@ -136,6 +136,7 @@ class CameraMaintainer(threading.Thread):
|
|||||||
self.ptz_metrics[name],
|
self.ptz_metrics[name],
|
||||||
self.region_grids[name],
|
self.region_grids[name],
|
||||||
self.stop_event,
|
self.stop_event,
|
||||||
|
self.config.logger,
|
||||||
)
|
)
|
||||||
self.camera_processes[config.name] = camera_process
|
self.camera_processes[config.name] = camera_process
|
||||||
camera_process.start()
|
camera_process.start()
|
||||||
@ -156,7 +157,11 @@ class CameraMaintainer(threading.Thread):
|
|||||||
self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
|
self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
|
||||||
|
|
||||||
capture_process = CameraCapture(
|
capture_process = CameraCapture(
|
||||||
config, count, self.camera_metrics[name], self.stop_event
|
config,
|
||||||
|
count,
|
||||||
|
self.camera_metrics[name],
|
||||||
|
self.stop_event,
|
||||||
|
self.config.logger,
|
||||||
)
|
)
|
||||||
capture_process.daemon = True
|
capture_process.daemon = True
|
||||||
self.capture_processes[name] = capture_process
|
self.capture_processes[name] = capture_process
|
||||||
|
|||||||
@ -132,17 +132,15 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
|
|
||||||
if image_source == ImageSourceEnum.recordings:
|
if image_source == ImageSourceEnum.recordings:
|
||||||
duration = final_data["end_time"] - final_data["start_time"]
|
duration = final_data["end_time"] - final_data["start_time"]
|
||||||
buffer_extension = min(
|
buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
|
||||||
10, max(2, duration * RECORDING_BUFFER_EXTENSION_PERCENT)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure minimum total duration for short review items
|
# Ensure minimum total duration for short review items
|
||||||
# This provides better context for brief events
|
# This provides better context for brief events
|
||||||
total_duration = duration + (2 * buffer_extension)
|
total_duration = duration + (2 * buffer_extension)
|
||||||
if total_duration < MIN_RECORDING_DURATION:
|
if total_duration < MIN_RECORDING_DURATION:
|
||||||
# Expand buffer to reach minimum duration, still respecting max of 10s per side
|
# Expand buffer to reach minimum duration, still respecting max of 5s per side
|
||||||
additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2
|
additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2
|
||||||
buffer_extension = min(10, additional_buffer_per_side)
|
buffer_extension = min(5, additional_buffer_per_side)
|
||||||
|
|
||||||
thumbs = self.get_recording_frames(
|
thumbs = self.get_recording_frames(
|
||||||
camera,
|
camera,
|
||||||
|
|||||||
@ -424,7 +424,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
|
|
||||||
if not res:
|
if not res:
|
||||||
return {
|
return {
|
||||||
"message": "No face was recognized.",
|
"message": "Model is still training, please try again in a few moments.",
|
||||||
"success": False,
|
"success": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from frigate.comms.recordings_updater import (
|
|||||||
RecordingsDataSubscriber,
|
RecordingsDataSubscriber,
|
||||||
RecordingsDataTypeEnum,
|
RecordingsDataTypeEnum,
|
||||||
)
|
)
|
||||||
from frigate.config import CameraConfig, DetectConfig, ModelConfig
|
from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig
|
||||||
from frigate.config.camera.camera import CameraTypeEnum
|
from frigate.config.camera.camera import CameraTypeEnum
|
||||||
from frigate.config.camera.updater import (
|
from frigate.config.camera.updater import (
|
||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
@ -539,6 +539,7 @@ class CameraCapture(FrigateProcess):
|
|||||||
shm_frame_count: int,
|
shm_frame_count: int,
|
||||||
camera_metrics: CameraMetrics,
|
camera_metrics: CameraMetrics,
|
||||||
stop_event: MpEvent,
|
stop_event: MpEvent,
|
||||||
|
log_config: LoggerConfig | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
stop_event,
|
stop_event,
|
||||||
@ -549,9 +550,10 @@ class CameraCapture(FrigateProcess):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.shm_frame_count = shm_frame_count
|
self.shm_frame_count = shm_frame_count
|
||||||
self.camera_metrics = camera_metrics
|
self.camera_metrics = camera_metrics
|
||||||
|
self.log_config = log_config
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self.pre_run_setup()
|
self.pre_run_setup(self.log_config)
|
||||||
camera_watchdog = CameraWatchdog(
|
camera_watchdog = CameraWatchdog(
|
||||||
self.config,
|
self.config,
|
||||||
self.shm_frame_count,
|
self.shm_frame_count,
|
||||||
@ -577,6 +579,7 @@ class CameraTracker(FrigateProcess):
|
|||||||
ptz_metrics: PTZMetrics,
|
ptz_metrics: PTZMetrics,
|
||||||
region_grid: list[list[dict[str, Any]]],
|
region_grid: list[list[dict[str, Any]]],
|
||||||
stop_event: MpEvent,
|
stop_event: MpEvent,
|
||||||
|
log_config: LoggerConfig | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
stop_event,
|
stop_event,
|
||||||
@ -592,9 +595,10 @@ class CameraTracker(FrigateProcess):
|
|||||||
self.camera_metrics = camera_metrics
|
self.camera_metrics = camera_metrics
|
||||||
self.ptz_metrics = ptz_metrics
|
self.ptz_metrics = ptz_metrics
|
||||||
self.region_grid = region_grid
|
self.region_grid = region_grid
|
||||||
|
self.log_config = log_config
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self.pre_run_setup()
|
self.pre_run_setup(self.log_config)
|
||||||
frame_queue = self.camera_metrics.frame_queue
|
frame_queue = self.camera_metrics.frame_queue
|
||||||
frame_shape = self.config.frame_shape
|
frame_shape = self.config.frame_shape
|
||||||
|
|
||||||
|
|||||||
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@ -22,3 +22,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.env
|
||||||
@ -44,11 +44,16 @@ self.addEventListener("notificationclick", (event) => {
|
|||||||
switch (event.action ?? "default") {
|
switch (event.action ?? "default") {
|
||||||
case "markReviewed":
|
case "markReviewed":
|
||||||
if (event.notification.data) {
|
if (event.notification.data) {
|
||||||
fetch("/api/reviews/viewed", {
|
event.waitUntil(
|
||||||
method: "POST",
|
fetch("/api/reviews/viewed", {
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": 1 },
|
method: "POST",
|
||||||
body: JSON.stringify({ ids: [event.notification.data.id] }),
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-TOKEN": 1,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids: [event.notification.data.id] }),
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -58,7 +63,7 @@ self.addEventListener("notificationclick", (event) => {
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
if (clients.openWindow) {
|
if (clients.openWindow) {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
return clients.openWindow(url);
|
event.waitUntil(clients.openWindow(url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -398,11 +398,7 @@ export function GroupedClassificationCard({
|
|||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
selected={false}
|
selected={false}
|
||||||
i18nLibrary={i18nLibrary}
|
i18nLibrary={i18nLibrary}
|
||||||
onClick={(data, meta) => {
|
onClick={() => {}}
|
||||||
if (meta || selectedItems.length > 0) {
|
|
||||||
onClick(data);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{children?.(data)}
|
{children?.(data)}
|
||||||
</ClassificationCard>
|
</ClassificationCard>
|
||||||
|
|||||||
@ -683,6 +683,22 @@ function ObjectDetailsTab({
|
|||||||
|
|
||||||
const mutate = useGlobalMutation();
|
const mutate = useGlobalMutation();
|
||||||
|
|
||||||
|
// Helper to map over SWR cached search results while preserving
|
||||||
|
// either paginated format (SearchResult[][]) or flat format (SearchResult[])
|
||||||
|
const mapSearchResults = useCallback(
|
||||||
|
(
|
||||||
|
currentData: SearchResult[][] | SearchResult[] | undefined,
|
||||||
|
fn: (event: SearchResult) => SearchResult,
|
||||||
|
) => {
|
||||||
|
if (!currentData) return currentData;
|
||||||
|
if (Array.isArray(currentData[0])) {
|
||||||
|
return (currentData as SearchResult[][]).map((page) => page.map(fn));
|
||||||
|
}
|
||||||
|
return (currentData as SearchResult[]).map(fn);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// users
|
// users
|
||||||
|
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
@ -810,17 +826,12 @@ function ObjectDetailsTab({
|
|||||||
(key.includes("events") ||
|
(key.includes("events") ||
|
||||||
key.includes("events/search") ||
|
key.includes("events/search") ||
|
||||||
key.includes("events/explore")),
|
key.includes("events/explore")),
|
||||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||||
if (!currentData) return currentData;
|
mapSearchResults(currentData, (event) =>
|
||||||
// optimistic update
|
event.id === search.id
|
||||||
return currentData
|
? { ...event, data: { ...event.data, description: desc } }
|
||||||
.flat()
|
: event,
|
||||||
.map((event) =>
|
),
|
||||||
event.id === search.id
|
|
||||||
? { ...event, data: { ...event.data, description: desc } }
|
|
||||||
: event,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
optimisticData: true,
|
optimisticData: true,
|
||||||
rollbackOnError: true,
|
rollbackOnError: true,
|
||||||
@ -843,7 +854,7 @@ function ObjectDetailsTab({
|
|||||||
);
|
);
|
||||||
setDesc(search.data.description);
|
setDesc(search.data.description);
|
||||||
});
|
});
|
||||||
}, [desc, search, mutate, t]);
|
}, [desc, search, mutate, t, mapSearchResults]);
|
||||||
|
|
||||||
const regenerateDescription = useCallback(
|
const regenerateDescription = useCallback(
|
||||||
(source: "snapshot" | "thumbnails") => {
|
(source: "snapshot" | "thumbnails") => {
|
||||||
@ -915,9 +926,8 @@ function ObjectDetailsTab({
|
|||||||
(key.includes("events") ||
|
(key.includes("events") ||
|
||||||
key.includes("events/search") ||
|
key.includes("events/search") ||
|
||||||
key.includes("events/explore")),
|
key.includes("events/explore")),
|
||||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||||
if (!currentData) return currentData;
|
mapSearchResults(currentData, (event) =>
|
||||||
return currentData.flat().map((event) =>
|
|
||||||
event.id === search.id
|
event.id === search.id
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
@ -928,8 +938,7 @@ function ObjectDetailsTab({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: event,
|
: event,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
{
|
{
|
||||||
optimisticData: true,
|
optimisticData: true,
|
||||||
rollbackOnError: true,
|
rollbackOnError: true,
|
||||||
@ -963,7 +972,7 @@ function ObjectDetailsTab({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[search, apiHost, mutate, setSearch, t],
|
[search, apiHost, mutate, setSearch, t, mapSearchResults],
|
||||||
);
|
);
|
||||||
|
|
||||||
// recognized plate
|
// recognized plate
|
||||||
@ -992,9 +1001,8 @@ function ObjectDetailsTab({
|
|||||||
(key.includes("events") ||
|
(key.includes("events") ||
|
||||||
key.includes("events/search") ||
|
key.includes("events/search") ||
|
||||||
key.includes("events/explore")),
|
key.includes("events/explore")),
|
||||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||||
if (!currentData) return currentData;
|
mapSearchResults(currentData, (event) =>
|
||||||
return currentData.flat().map((event) =>
|
|
||||||
event.id === search.id
|
event.id === search.id
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
@ -1005,8 +1013,7 @@ function ObjectDetailsTab({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: event,
|
: event,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
{
|
{
|
||||||
optimisticData: true,
|
optimisticData: true,
|
||||||
rollbackOnError: true,
|
rollbackOnError: true,
|
||||||
@ -1040,7 +1047,7 @@ function ObjectDetailsTab({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[search, apiHost, mutate, setSearch, t],
|
[search, apiHost, mutate, setSearch, t, mapSearchResults],
|
||||||
);
|
);
|
||||||
|
|
||||||
// speech transcription
|
// speech transcription
|
||||||
@ -1102,17 +1109,12 @@ function ObjectDetailsTab({
|
|||||||
(key.includes("events") ||
|
(key.includes("events") ||
|
||||||
key.includes("events/search") ||
|
key.includes("events/search") ||
|
||||||
key.includes("events/explore")),
|
key.includes("events/explore")),
|
||||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||||
if (!currentData) return currentData;
|
mapSearchResults(currentData, (event) =>
|
||||||
// optimistic update
|
event.id === search.id
|
||||||
return currentData
|
? { ...event, plus_id: "new_upload" }
|
||||||
.flat()
|
: event,
|
||||||
.map((event) =>
|
),
|
||||||
event.id === search.id
|
|
||||||
? { ...event, plus_id: "new_upload" }
|
|
||||||
: event,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
optimisticData: true,
|
optimisticData: true,
|
||||||
rollbackOnError: true,
|
rollbackOnError: true,
|
||||||
@ -1120,7 +1122,7 @@ function ObjectDetailsTab({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[search, mutate],
|
[search, mutate, mapSearchResults],
|
||||||
);
|
);
|
||||||
|
|
||||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -1503,7 +1505,7 @@ function ObjectDetailsTab({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="text-md h-32"
|
className="text-md h-32 md:text-sm"
|
||||||
placeholder={t("details.description.placeholder")}
|
placeholder={t("details.description.placeholder")}
|
||||||
value={desc}
|
value={desc}
|
||||||
onChange={(e) => setDesc(e.target.value)}
|
onChange={(e) => setDesc(e.target.value)}
|
||||||
@ -1511,25 +1513,7 @@ function ObjectDetailsTab({
|
|||||||
onBlur={handleDescriptionBlur}
|
onBlur={handleDescriptionBlur}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row justify-end gap-4">
|
<div className="mb-10 flex flex-row justify-end gap-5">
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
aria-label={t("button.save", { ns: "common" })}
|
|
||||||
className="text-primary/40 hover:text-primary/80"
|
|
||||||
onClick={() => {
|
|
||||||
setIsEditingDesc(false);
|
|
||||||
updateDescription();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaCheck className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{t("button.save", { ns: "common" })}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
@ -1540,13 +1524,31 @@ function ObjectDetailsTab({
|
|||||||
setDesc(originalDescRef.current ?? "");
|
setDesc(originalDescRef.current ?? "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaTimes className="size-4" />
|
<FaTimes className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("button.cancel", { ns: "common" })}
|
{t("button.cancel", { ns: "common" })}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
aria-label={t("button.save", { ns: "common" })}
|
||||||
|
className="text-primary/40 hover:text-primary/80"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingDesc(false);
|
||||||
|
updateDescription();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaCheck className="size-5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("button.save", { ns: "common" })}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -221,12 +221,26 @@ export function TrackingDetails({
|
|||||||
displaySource,
|
displaySource,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isWithinEventRange =
|
const isWithinEventRange = useMemo(() => {
|
||||||
effectiveTime !== undefined &&
|
if (effectiveTime === undefined || event.start_time === undefined) {
|
||||||
event.start_time !== undefined &&
|
return false;
|
||||||
event.end_time !== undefined &&
|
}
|
||||||
effectiveTime >= event.start_time &&
|
|
||||||
effectiveTime <= event.end_time;
|
// If an event has not ended yet, fall back to last timestamp in eventSequence
|
||||||
|
let eventEnd = event.end_time;
|
||||||
|
if (eventEnd == null && eventSequence && eventSequence.length > 0) {
|
||||||
|
const last = eventSequence[eventSequence.length - 1];
|
||||||
|
if (last && last.timestamp !== undefined) {
|
||||||
|
eventEnd = last.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventEnd == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveTime >= event.start_time && effectiveTime <= eventEnd;
|
||||||
|
}, [effectiveTime, event.start_time, event.end_time, eventSequence]);
|
||||||
|
|
||||||
// Calculate how far down the blue line should extend based on effectiveTime
|
// Calculate how far down the blue line should extend based on effectiveTime
|
||||||
const calculateLineHeight = useCallback(() => {
|
const calculateLineHeight = useCallback(() => {
|
||||||
|
|||||||
@ -318,6 +318,7 @@ export default function HlsVideoPlayer({
|
|||||||
{isDetailMode &&
|
{isDetailMode &&
|
||||||
camera &&
|
camera &&
|
||||||
currentTime &&
|
currentTime &&
|
||||||
|
loadedMetadata &&
|
||||||
videoDimensions.width > 0 &&
|
videoDimensions.width > 0 &&
|
||||||
videoDimensions.height > 0 && (
|
videoDimensions.height > 0 && (
|
||||||
<div className="absolute z-50 size-full">
|
<div className="absolute z-50 size-full">
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
ReviewSummary,
|
ReviewSummary,
|
||||||
SegmentedReviewData,
|
SegmentedReviewData,
|
||||||
} from "@/types/review";
|
} from "@/types/review";
|
||||||
|
import { TimelineType } from "@/types/timeline";
|
||||||
import {
|
import {
|
||||||
getBeginningOfDayTimestamp,
|
getBeginningOfDayTimestamp,
|
||||||
getEndOfDayTimestamp,
|
getEndOfDayTimestamp,
|
||||||
@ -49,6 +50,16 @@ export default function Events() {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [notificationTab, setNotificationTab] =
|
||||||
|
useState<TimelineType>("timeline");
|
||||||
|
|
||||||
|
useSearchEffect("tab", (tab: string) => {
|
||||||
|
if (tab === "timeline" || tab === "events" || tab === "detail") {
|
||||||
|
setNotificationTab(tab as TimelineType);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
useSearchEffect("id", (reviewId: string) => {
|
useSearchEffect("id", (reviewId: string) => {
|
||||||
axios
|
axios
|
||||||
.get(`review/${reviewId}`)
|
.get(`review/${reviewId}`)
|
||||||
@ -66,7 +77,7 @@ export default function Events() {
|
|||||||
camera: resp.data.camera,
|
camera: resp.data.camera,
|
||||||
startTime,
|
startTime,
|
||||||
severity: resp.data.severity,
|
severity: resp.data.severity,
|
||||||
timelineType: "detail",
|
timelineType: notificationTab,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ReviewSeverity } from "./review";
|
import { ReviewSeverity } from "./review";
|
||||||
|
import { TimelineType } from "./timeline";
|
||||||
|
|
||||||
export type Recording = {
|
export type Recording = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -37,7 +38,7 @@ export type RecordingStartingPoint = {
|
|||||||
camera: string;
|
camera: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
severity: ReviewSeverity;
|
severity: ReviewSeverity;
|
||||||
timelineType?: "timeline" | "events" | "detail";
|
timelineType?: TimelineType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RecordingPlayerError = "stalled" | "startup";
|
export type RecordingPlayerError = "stalled" | "startup";
|
||||||
|
|||||||
@ -33,7 +33,7 @@ i18n
|
|||||||
fallbackLng: "en", // use en if detected lng is not available
|
fallbackLng: "en", // use en if detected lng is not available
|
||||||
|
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: "locales/{{lng}}/{{ns}}.json",
|
loadPath: `locales/{{lng}}/{{ns}}.json?v=${import.meta.env.VITE_GIT_COMMIT_HASH || "unknown"}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
ns: [
|
ns: [
|
||||||
|
|||||||
@ -72,8 +72,7 @@ export default function StorageMetrics({
|
|||||||
const earliestDate = useMemo(() => {
|
const earliestDate = useMemo(() => {
|
||||||
const keys = Object.keys(recordingsSummary || {});
|
const keys = Object.keys(recordingsSummary || {});
|
||||||
return keys.length
|
return keys.length
|
||||||
? new TZDate(keys[keys.length - 1] + "T00:00:00", timezone).getTime() /
|
? new TZDate(keys[0] + "T00:00:00", timezone).getTime() / 1000
|
||||||
1000
|
|
||||||
: null;
|
: null;
|
||||||
}, [recordingsSummary, timezone]);
|
}, [recordingsSummary, timezone]);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user