forked from Github/frigate
Compare commits
14 Commits
v0.11.0-be
...
v0.11.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d45b0b351 | ||
|
|
ba119e4f96 | ||
|
|
75c2feb387 | ||
|
|
da637d3c8f | ||
|
|
bc078fcc88 | ||
|
|
5f9d477863 | ||
|
|
ca693240b1 | ||
|
|
468febc434 | ||
|
|
4b81c88794 | ||
|
|
2ac28b93f3 | ||
|
|
3e7ed982d4 | ||
|
|
d8d410802f | ||
|
|
ca7bad8909 | ||
|
|
7b2b5bfa71 |
@@ -23,6 +23,7 @@ services:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
ports:
|
||||
- "1935:1935"
|
||||
- "3000:3000"
|
||||
- "5000:5000"
|
||||
- "5001:5001"
|
||||
- "8080:8080"
|
||||
|
||||
@@ -46,6 +46,7 @@ RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
|
||||
FROM debian:11-slim
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG JELLYFIN_FFMPEG_VERSION=4.3.2-1
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
@@ -72,9 +73,8 @@ RUN apt-get -qq update \
|
||||
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
|
||||
# jellyfin-ffmpeg
|
||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
# enable non-free repo
|
||||
&& sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list \
|
||||
&& apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
# coral drivers
|
||||
@@ -82,8 +82,11 @@ RUN apt-get -qq update \
|
||||
&& pip3 install -U /wheels/*.whl \
|
||||
# arch specific packages
|
||||
&& if [ "${TARGETARCH}" = "amd64" ]; then \
|
||||
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
mesa-va-drivers jellyfin-ffmpeg; else \
|
||||
# jellyfin-ffmpeg
|
||||
wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
|
||||
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
mesa-va-drivers intel-media-va-driver-non-free ./jellyfin.deb \
|
||||
&& rm jellyfin.deb; else \
|
||||
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
ffmpeg; \
|
||||
fi \
|
||||
|
||||
@@ -205,11 +205,13 @@ http {
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
||||
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
||||
sub_filter '"/dist/' '"$http_x_ingress_path/dist/';
|
||||
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
||||
sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
|
||||
sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
|
||||
sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
|
||||
sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
|
||||
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
|
||||
sub_filter '="/BASE_PATH/"' '=window.baseUrl';
|
||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
|
||||
sub_filter_types text/css application/javascript;
|
||||
sub_filter_once off;
|
||||
|
||||
|
||||
@@ -12,33 +12,21 @@ Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_v4l2m2m
|
||||
hwaccel_args: -c:v h264_v4l2m2m
|
||||
```
|
||||
|
||||
### Intel-based CPUs (<10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
- -hwaccel_output_format
|
||||
- yuv420p
|
||||
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
|
||||
```
|
||||
|
||||
### Intel-based CPUs (>=10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- qsv
|
||||
- -qsv_device
|
||||
- /dev/dri/renderD128
|
||||
hwaccel_args: -c:v h264_qsv
|
||||
```
|
||||
|
||||
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
|
||||
@@ -47,11 +35,7 @@ ffmpeg:
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
|
||||
```
|
||||
|
||||
### NVIDIA GPU
|
||||
@@ -69,7 +53,9 @@ services:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [gpu]
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
The decoder you need to pass in the `hwaccel_args` will depend on the input video.
|
||||
@@ -89,13 +75,11 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c
|
||||
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
|
||||
```
|
||||
|
||||
For example, for H265 video (hevc), you'll select `hevc_cuvid`.
|
||||
For example, for H264 video, you'll select `h264_cuvid`.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- hevc_cuvid
|
||||
hwaccel_args: -c:v h264_cuvid
|
||||
```
|
||||
|
||||
If everything is working correctly, you should see a significant improvement in performance.
|
||||
|
||||
@@ -278,10 +278,6 @@ record:
|
||||
mode: all
|
||||
# Optional: Event recording settings
|
||||
events:
|
||||
# Optional: Maximum length of time to retain video during long events. (default: shown below)
|
||||
# NOTE: If an object is being tracked for longer than this amount of time, the retained recordings
|
||||
# will be the last x seconds of the event unless retain->days under record is > 0.
|
||||
max_seconds: 300
|
||||
# Optional: Number of seconds before the event to include (default: shown below)
|
||||
pre_capture: 5
|
||||
# Optional: Number of seconds after the event to include (default: shown below)
|
||||
|
||||
@@ -108,6 +108,18 @@ ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffm
|
||||
ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
**VAAPI**
|
||||
|
||||
```shell
|
||||
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
**QSV**
|
||||
|
||||
```shell
|
||||
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
## Web Interface
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -83,7 +83,6 @@ class RetainConfig(FrigateBaseModel):
|
||||
|
||||
|
||||
class EventsConfig(FrigateBaseModel):
|
||||
max_seconds: int = Field(default=300, title="Maximum event duration.")
|
||||
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
|
||||
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
|
||||
required_zones: List[str] = Field(
|
||||
|
||||
@@ -2,18 +2,14 @@ import base64
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import copy
|
||||
import json
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess as sp
|
||||
import time
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
from flask.helpers import send_file
|
||||
|
||||
import numpy as np
|
||||
from flask import (
|
||||
@@ -26,13 +22,12 @@ from flask import (
|
||||
request,
|
||||
)
|
||||
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.const import CLIPS_DIR, PLUS_ENV_VAR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.stats import stats_snapshot
|
||||
from frigate.util import calculate_region
|
||||
from frigate.version import VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -251,6 +246,20 @@ def set_sub_label(id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/sub_labels")
|
||||
def get_sub_labels():
|
||||
try:
|
||||
events = Event.select(Event.sub_label).distinct()
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{"success": False, "message": f"Failed to get sub_labels: {e}"}, "404"
|
||||
)
|
||||
|
||||
sub_labels = [e.sub_label for e in events]
|
||||
sub_labels.remove(None)
|
||||
return jsonify(sub_labels)
|
||||
|
||||
|
||||
@bp.route("/events/<id>", methods=("DELETE",))
|
||||
def delete_event(id):
|
||||
try:
|
||||
@@ -480,6 +489,7 @@ def events():
|
||||
limit = request.args.get("limit", 100)
|
||||
camera = request.args.get("camera", "all")
|
||||
label = request.args.get("label", "all")
|
||||
sub_label = request.args.get("sub_label", "all")
|
||||
zone = request.args.get("zone", "all")
|
||||
after = request.args.get("after", type=float)
|
||||
before = request.args.get("before", type=float)
|
||||
@@ -511,6 +521,9 @@ def events():
|
||||
if label != "all":
|
||||
clauses.append((Event.label == label))
|
||||
|
||||
if sub_label != "all":
|
||||
clauses.append((Event.sub_label == sub_label))
|
||||
|
||||
if zone != "all":
|
||||
clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
||||
|
||||
|
||||
@@ -377,16 +377,11 @@ class RecordingCleanup(threading.Thread):
|
||||
logger.debug("Start all cameras.")
|
||||
for camera, config in self.config.cameras.items():
|
||||
logger.debug(f"Start camera: {camera}.")
|
||||
# When deleting recordings without events, we have to keep at LEAST the configured max clip duration
|
||||
min_end = (
|
||||
datetime.datetime.now()
|
||||
- datetime.timedelta(seconds=config.record.events.max_seconds)
|
||||
).timestamp()
|
||||
# Get the timestamp for cutoff of retained days
|
||||
expire_days = config.record.retain.days
|
||||
expire_before = (
|
||||
expire_date = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
expire_date = min(min_end, expire_before)
|
||||
|
||||
# Get recordings to check for expiration
|
||||
recordings: Recordings = (
|
||||
|
||||
@@ -20,9 +20,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_latest_version() -> str:
|
||||
request = requests.get(
|
||||
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
|
||||
)
|
||||
try:
|
||||
request = requests.get(
|
||||
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
|
||||
)
|
||||
except:
|
||||
return "unknown"
|
||||
|
||||
response = request.json()
|
||||
|
||||
if request.ok and response and "tag_name" in response:
|
||||
|
||||
@@ -2,7 +2,7 @@ import { rest } from 'msw';
|
||||
import { API_HOST } from '../src/env';
|
||||
|
||||
export const handlers = [
|
||||
rest.get(`${API_HOST}/api/config`, (req, res, ctx) => {
|
||||
rest.get(`${API_HOST}api/config`, (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
@@ -35,7 +35,7 @@ export const handlers = [
|
||||
})
|
||||
);
|
||||
}),
|
||||
rest.get(`${API_HOST}/api/stats`, (req, res, ctx) => {
|
||||
rest.get(`${API_HOST}api/stats`, (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
@@ -54,7 +54,7 @@ export const handlers = [
|
||||
})
|
||||
);
|
||||
}),
|
||||
rest.get(`${API_HOST}/api/events`, (req, res, ctx) => {
|
||||
rest.get(`${API_HOST}api/events`, (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
@@ -72,4 +72,13 @@ export const handlers = [
|
||||
)
|
||||
);
|
||||
}),
|
||||
rest.get(`${API_HOST}api/sub_labels`, (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json([
|
||||
'one',
|
||||
'two',
|
||||
])
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"lint": "eslint ./ --ext .jsx,.js,.tsx,.ts",
|
||||
"build": "tsc && vite build",
|
||||
"build": "tsc && vite build --base=/BASE_PATH/",
|
||||
"preview": "vite preview",
|
||||
"test": "jest"
|
||||
},
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const ENV = 'test';
|
||||
export const API_HOST = 'http://base-url.local:5000';
|
||||
export const API_HOST = 'http://base-url.local:5000/';
|
||||
|
||||
@@ -18,6 +18,6 @@ describe('useApiHost', () => {
|
||||
<Test />
|
||||
</ApiProvider>
|
||||
);
|
||||
expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
|
||||
expect(screen.queryByText('http://base-url.local:5000/')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import { API_HOST } from '../env';
|
||||
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || ''}`;
|
||||
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;
|
||||
|
||||
@@ -4,7 +4,7 @@ import useSWR, { SWRConfig } from 'swr';
|
||||
import { MqttProvider } from './mqtt';
|
||||
import axios from 'axios';
|
||||
|
||||
axios.defaults.baseURL = `${baseUrl}/api/`;
|
||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||
|
||||
export function ApiProvider({ children, options }) {
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ export function MqttProvider({
|
||||
config,
|
||||
children,
|
||||
createWebsocket = defaultCreateWebsocket,
|
||||
mqttUrl = `${baseUrl.replace(/^http/, 'ws')}/ws`,
|
||||
mqttUrl = `${baseUrl.replace(/^http/, 'ws')}ws`,
|
||||
}) {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const wsRef = useRef();
|
||||
|
||||
@@ -5,7 +5,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
|
||||
export default function JSMpegPlayer({ camera, width, height }) {
|
||||
const playerRef = useRef();
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`;
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}live/${camera}`;
|
||||
|
||||
useEffect(() => {
|
||||
const video = new JSMpeg.VideoElement(
|
||||
|
||||
@@ -93,7 +93,8 @@ export default function VideoPlayer({ children, options, seekOptions = {}, onRea
|
||||
|
||||
return (
|
||||
<div data-vjs-player>
|
||||
<video ref={playerRef} className="small-player video-js vjs-default-skin" controls playsinline />
|
||||
{/* Setting an empty data-setup is required to override the default values and allow video to be fit the size of its parent */}
|
||||
<video ref={playerRef} className="small-player video-js vjs-default-skin" data-setup="{}" controls playsinline />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const ENV = import.meta.env.MODE;
|
||||
export const API_HOST = ENV === "production" ? "" : "http://localhost:5000";
|
||||
export const API_HOST = ENV === 'production' ? '' : 'http://localhost:5000/';
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function Events({ path, ...props }) {
|
||||
camera: props.camera ?? 'all',
|
||||
label: props.label ?? 'all',
|
||||
zone: props.zone ?? 'all',
|
||||
sub_label: props.sub_label ?? 'all',
|
||||
});
|
||||
const [state, setState] = useState({
|
||||
showDownloadMenu: false,
|
||||
@@ -59,6 +60,10 @@ export default function Events({ path, ...props }) {
|
||||
has_snapshot: false,
|
||||
plus_id: undefined,
|
||||
});
|
||||
const [deleteFavoriteState, setDeleteFavoriteState] = useState({
|
||||
deletingFavoriteEventId: null,
|
||||
showDeleteFavorite: false,
|
||||
});
|
||||
|
||||
const eventsFetcher = useCallback((path, params) => {
|
||||
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
|
||||
@@ -82,6 +87,8 @@ export default function Events({ path, ...props }) {
|
||||
|
||||
const { data: config } = useSWR('config');
|
||||
|
||||
const { data: allSubLabels } = useSWR('sub_labels')
|
||||
|
||||
const filterValues = useMemo(
|
||||
() => ({
|
||||
cameras: Object.keys(config?.cameras || {}),
|
||||
@@ -97,8 +104,9 @@ export default function Events({ path, ...props }) {
|
||||
return memo;
|
||||
}, config?.objects?.track || [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i),
|
||||
sub_labels: Object.values(allSubLabels || []),
|
||||
}),
|
||||
[config]
|
||||
[config, allSubLabels]
|
||||
);
|
||||
|
||||
const onSave = async (e, eventId, save) => {
|
||||
@@ -114,11 +122,16 @@ export default function Events({ path, ...props }) {
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (e, eventId) => {
|
||||
const onDelete = async (e, eventId, saved) => {
|
||||
e.stopPropagation();
|
||||
const response = await axios.delete(`events/${eventId}`);
|
||||
if (response.status === 200) {
|
||||
mutate();
|
||||
|
||||
if (saved) {
|
||||
setDeleteFavoriteState({ deletingFavoriteEventId: eventId, showDeleteFavorite: true });
|
||||
} else {
|
||||
const response = await axios.delete(`events/${eventId}`);
|
||||
if (response.status === 200) {
|
||||
mutate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,11 +244,11 @@ export default function Events({ path, ...props }) {
|
||||
<Heading>Events</Heading>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<select
|
||||
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
|
||||
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={searchParams.camera}
|
||||
onChange={(e) => onFilter('camera', e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="all">all cameras</option>
|
||||
{filterValues.cameras.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
@@ -243,11 +256,11 @@ export default function Events({ path, ...props }) {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
|
||||
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={searchParams.label}
|
||||
onChange={(e) => onFilter('label', e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="all">all labels</option>
|
||||
{filterValues.labels.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
@@ -255,17 +268,32 @@ export default function Events({ path, ...props }) {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
|
||||
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={searchParams.zone}
|
||||
onChange={(e) => onFilter('zone', e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="all">all zones</option>
|
||||
{filterValues.zones.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{
|
||||
filterValues.sub_labels.length > 0 && (
|
||||
<select
|
||||
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={searchParams.sub_label}
|
||||
onChange={(e) => onFilter('sub_label', e.target.value)}
|
||||
>
|
||||
<option value="all">all sub labels</option>
|
||||
{filterValues.sub_labels.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<div ref={datePicker} className="ml-auto">
|
||||
<CalendarIcon
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
@@ -374,6 +402,19 @@ export default function Events({ path, ...props }) {
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
{deleteFavoriteState.showDeleteFavorite && (
|
||||
<Dialog>
|
||||
<div className="p-4">
|
||||
<Heading size="lg">Delete Saved Event?</Heading>
|
||||
<p className="mb-2">Confirm deletion of saved event.</p>
|
||||
</div>
|
||||
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||
<Button className="ml-2" color="red" onClick={(e) => { setDeleteFavoriteState({ ...state, showDeleteFavorite: false }); onDelete(e, deleteFavoriteState.deletingFavoriteEventId, false) }} type="text">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{eventPages ? (
|
||||
eventPages.map((page, i) => {
|
||||
@@ -441,7 +482,7 @@ export default function Events({ path, ...props }) {
|
||||
)}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
|
||||
<Delete className="h-6 w-6 cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)} />
|
||||
|
||||
<Download
|
||||
className="h-6 w-6 mt-auto"
|
||||
@@ -453,7 +494,7 @@ export default function Events({ path, ...props }) {
|
||||
</div>
|
||||
{viewEvent !== event.id ? null : (
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{event.has_clip ? (
|
||||
<>
|
||||
<Heading size="lg">Clip</Heading>
|
||||
|
||||
Reference in New Issue
Block a user