forked from Github/frigate
Compare commits
229 Commits
v0.8.0-rc4
...
v0.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d376f6b1d2 | ||
|
|
45526a7652 | ||
|
|
cc7929932b | ||
|
|
e6516235fa | ||
|
|
40d5a9f890 | ||
|
|
ee3e744cc6 | ||
|
|
b55bd1e027 | ||
|
|
9a96df0319 | ||
|
|
e9b1618364 | ||
|
|
6dc6ed1e94 | ||
|
|
1943a49274 | ||
|
|
a8c00edc94 | ||
|
|
faa8abb2b9 | ||
|
|
f6cd2fc68e | ||
|
|
6482000d6b | ||
|
|
dcf7209706 | ||
|
|
2ec921593e | ||
|
|
75a01f657e | ||
|
|
d4e512c1fc | ||
|
|
26e7d34f18 | ||
|
|
15b5ffddd4 | ||
|
|
f0f3764992 | ||
|
|
2beb44b591 | ||
|
|
27b659dde1 | ||
|
|
630c2ee6f6 | ||
|
|
600477c487 | ||
|
|
d31c295598 | ||
|
|
a7bb0931c4 | ||
|
|
ff99a01423 | ||
|
|
ea6e311318 | ||
|
|
6790467bbc | ||
|
|
d315dbea22 | ||
|
|
8db7ab6724 | ||
|
|
9a2c034ae8 | ||
|
|
2885b80a13 | ||
|
|
4a85156e87 | ||
|
|
1785c69e1b | ||
|
|
a862ba8348 | ||
|
|
633d45d02f | ||
|
|
7f4e042dfa | ||
|
|
507ec13848 | ||
|
|
2132352639 | ||
|
|
11016b8486 | ||
|
|
8615f14407 | ||
|
|
1e84f08018 | ||
|
|
7f663328dc | ||
|
|
ea53068432 | ||
|
|
144aff9b4e | ||
|
|
18db6daf0a | ||
|
|
26ba29b538 | ||
|
|
70167a34b6 | ||
|
|
ccb668a1b6 | ||
|
|
0989c64eab | ||
|
|
c082fc5cb2 | ||
|
|
d39111a294 | ||
|
|
3c072f94b0 | ||
|
|
7f8ae2ce5c | ||
|
|
d84b75168c | ||
|
|
eb0a5e1c55 | ||
|
|
47ac77dbb0 | ||
|
|
ec84847be7 | ||
|
|
e7839bfd40 | ||
|
|
8762da627b | ||
|
|
3fab321045 | ||
|
|
9451048574 | ||
|
|
46c002038b | ||
|
|
d1d833ea9a | ||
|
|
c1f0750526 | ||
|
|
89e02b6956 | ||
|
|
97e8258288 | ||
|
|
39040c1874 | ||
|
|
c709851888 | ||
|
|
b022bec1fa | ||
|
|
bca0531963 | ||
|
|
b2c7fc8f5b | ||
|
|
96ac2c29d6 | ||
|
|
14a5118b4d | ||
|
|
232fa1ffe8 | ||
|
|
d2e91754e9 | ||
|
|
4d9066a58d | ||
|
|
c618867941 | ||
|
|
5ad4017510 | ||
|
|
63e14a98f9 | ||
|
|
25e3fe8eab | ||
|
|
840f046572 | ||
|
|
89e3c2e4b1 | ||
|
|
c770470b58 | ||
|
|
4619836122 | ||
|
|
76403bba8e | ||
|
|
a9afa303a2 | ||
|
|
e5399ae07a | ||
|
|
80a5a7b129 | ||
|
|
9dc97d4b6b | ||
|
|
d8c9169af2 | ||
|
|
ec256f7130 | ||
|
|
19bbfce4ed | ||
|
|
b0b2d9d972 | ||
|
|
6f5f5c9461 | ||
|
|
fc04bc6046 | ||
|
|
9f504253fb | ||
|
|
961997e078 | ||
|
|
363594a9a2 | ||
|
|
247e2677f3 | ||
|
|
5b5159f4dd | ||
|
|
bc8b85860c | ||
|
|
44d45c5880 | ||
|
|
a6d8e4fc3f | ||
|
|
2cc9a15f6a | ||
|
|
151f9fb2ee | ||
|
|
32fb76b3d1 | ||
|
|
8d52e2635a | ||
|
|
f20e1f20a6 | ||
|
|
af8594c5c6 | ||
|
|
899d41f361 | ||
|
|
7dc6382c90 | ||
|
|
e8009c2d26 | ||
|
|
3bc7cdaab6 | ||
|
|
724d8187c6 | ||
|
|
8f68df60c7 | ||
|
|
6af3cb6134 | ||
|
|
2ff0c3907f | ||
|
|
dd102ff01d | ||
|
|
f20b1d75e6 | ||
|
|
a4b88ac4a7 | ||
|
|
93b9d586d2 | ||
|
|
41dd4447cc | ||
|
|
8b9c8a2e80 | ||
|
|
a63ff1bb99 | ||
|
|
d5e3b59245 | ||
|
|
d0470fffcc | ||
|
|
5053305e17 | ||
|
|
9c79392060 | ||
|
|
708c3278bf | ||
|
|
c0249f6e59 | ||
|
|
afd8aefac2 | ||
|
|
3c07767138 | ||
|
|
953c442f13 | ||
|
|
e147852878 | ||
|
|
db7ee6cfb3 | ||
|
|
3b41b6cc33 | ||
|
|
5e79888370 | ||
|
|
a6aa9bdd59 | ||
|
|
d78b7cc110 | ||
|
|
e7cdace0ab | ||
|
|
f60eb4e977 | ||
|
|
7aecf6c6de | ||
|
|
75d62096a6 | ||
|
|
7c44994070 | ||
|
|
f49f3fd9c3 | ||
|
|
5ea86d636c | ||
|
|
4c6e90717a | ||
|
|
d60ca9d783 | ||
|
|
d304718ea0 | ||
|
|
c787c8948e | ||
|
|
62728ef7fb | ||
|
|
47e256f03d | ||
|
|
527db52d5e | ||
|
|
f78b2c48a7 | ||
|
|
90c965a32a | ||
|
|
d4afcde6c9 | ||
|
|
257de89ce4 | ||
|
|
735cc3962b | ||
|
|
feb42181de | ||
|
|
f5c4bfa7b4 | ||
|
|
65ddd91855 | ||
|
|
6d7d838613 | ||
|
|
5edf7b7f00 | ||
|
|
117569830d | ||
|
|
d62aec7287 | ||
|
|
4e0cf3681e | ||
|
|
d98751102a | ||
|
|
1acbeb813e | ||
|
|
b87ec752cf | ||
|
|
753df31fa6 | ||
|
|
bd77b74689 | ||
|
|
810c23d8ee | ||
|
|
34d9b2983e | ||
|
|
63c5c8412a | ||
|
|
60207723d1 | ||
|
|
f4117ad096 | ||
|
|
8bed4e9970 | ||
|
|
f72eaf781c | ||
|
|
e9ecc20a36 | ||
|
|
addfa2a32d | ||
|
|
0dad9bc393 | ||
|
|
5155875a72 | ||
|
|
4ed1217366 | ||
|
|
50e898a684 | ||
|
|
251c7fa982 | ||
|
|
00c75e9f98 | ||
|
|
1b5b02d286 | ||
|
|
946d655cee | ||
|
|
d56710b0b5 | ||
|
|
0cf78277b5 | ||
|
|
ce2a583ff9 | ||
|
|
84bddad30e | ||
|
|
0ff682504a | ||
|
|
5d5984166f | ||
|
|
b825eb44fe | ||
|
|
7015eb66f2 | ||
|
|
494eeb16a5 | ||
|
|
692fdc8d5d | ||
|
|
ec1a8ebd4a | ||
|
|
59daa6597b | ||
|
|
3941ce4ad1 | ||
|
|
aff87d4372 | ||
|
|
373ca87887 | ||
|
|
03c855ecbe | ||
|
|
3a3cb24631 | ||
|
|
4c3fea25a5 | ||
|
|
af303cbf2a | ||
|
|
b7c09a9b38 | ||
|
|
eced06eea8 | ||
|
|
15d989255c | ||
|
|
095566b9c2 | ||
|
|
b77a65d446 | ||
|
|
9778a748fc | ||
|
|
a89dddcafa | ||
|
|
75973fd4c0 | ||
|
|
514036f9d1 | ||
|
|
36fbedab20 | ||
|
|
180baeba50 | ||
|
|
cce82fe2a5 | ||
|
|
5512bb2e06 | ||
|
|
be1fcbbdf8 | ||
|
|
422cd52049 | ||
|
|
d67a56d37e | ||
|
|
070c9721b6 | ||
|
|
0219834dd1 |
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@ default_target: amd64_frigate
|
|||||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||||
|
|
||||||
version:
|
version:
|
||||||
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
|
echo "VERSION='0.8.1-$(COMMIT_HASH)'" > frigate/version.py
|
||||||
|
|
||||||
web:
|
web:
|
||||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -14,9 +14,28 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
|
|||||||
- Uses a very low overhead motion detection to determine where to run object detection
|
- Uses a very low overhead motion detection to determine where to run object detection
|
||||||
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
||||||
- Communicates over MQTT for easy integration into other systems
|
- Communicates over MQTT for easy integration into other systems
|
||||||
|
- Records video clips of detected objects
|
||||||
- 24/7 recording
|
- 24/7 recording
|
||||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
View the documentation at https://blakeblackshear.github.io/frigate
|
View the documentation at https://blakeblackshear.github.io/frigate
|
||||||
|
|
||||||
|
## Donations
|
||||||
|
If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear).
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
Integration into HomeAssistant
|
||||||
|
<div>
|
||||||
|
<a href="docs/static/img/media_browser.png"><img src="docs/static/img/media_browser.png" height=400></a>
|
||||||
|
<a href="docs/static/img/notification.png"><img src="docs/static/img/notification.png" height=400></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Also comes with a builtin UI:
|
||||||
|
<div>
|
||||||
|
<a href="docs/static/img/home-ui.png"><img src="docs/static/img/home-ui.png" height=400></a>
|
||||||
|
<a href="docs/static/img/camera-ui.png"><img src="docs/static/img/camera-ui.png" height=400></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -386,6 +386,29 @@ ffmpeg:
|
|||||||
- '1'
|
- '1'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Reolink 410/520 (possibly others)
|
||||||
|
Several users have reported success with the rtmp video from Reolink cameras.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ffmpeg:
|
||||||
|
input_args:
|
||||||
|
- -avoid_negative_ts
|
||||||
|
- make_zero
|
||||||
|
- -fflags
|
||||||
|
- nobuffer
|
||||||
|
- -flags
|
||||||
|
- low_delay
|
||||||
|
- -strict
|
||||||
|
- experimental
|
||||||
|
- -fflags
|
||||||
|
- +genpts+discardcorrupt
|
||||||
|
- -rw_timeout
|
||||||
|
- '5000000'
|
||||||
|
- -use_wallclock_as_timestamps
|
||||||
|
- '1'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Blue Iris RTSP Cameras
|
### Blue Iris RTSP Cameras
|
||||||
|
|
||||||
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ id: index
|
|||||||
title: Configuration
|
title: Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](/configuration/cameras.md).
|
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](cameras.md).
|
||||||
|
|
||||||
It is recommended to start with a minimal configuration and add to it:
|
It is recommended to start with a minimal configuration and add to it:
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ mqtt:
|
|||||||
|
|
||||||
## `cameras`
|
## `cameras`
|
||||||
|
|
||||||
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras) for a complete list of options.
|
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras.md) for a complete list of options.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cameras:
|
cameras:
|
||||||
@@ -80,7 +80,8 @@ clips:
|
|||||||
max_seconds: 300
|
max_seconds: 300
|
||||||
# Optional: size of tmpfs mount to create for cache files (default: not set)
|
# Optional: size of tmpfs mount to create for cache files (default: not set)
|
||||||
# mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
|
# mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
|
||||||
# Notice: If you have mounted a tmpfs volume through docker, this value should not be set in your config
|
# NOTICE: Addon users must have Protection mode disabled for the addon when using this setting.
|
||||||
|
# Also, if you have mounted a tmpfs volume through docker, this value should not be set in your config.
|
||||||
tmpfs_cache_size: 256m
|
tmpfs_cache_size: 256m
|
||||||
# Optional: Retention settings for clips (default: shown below)
|
# Optional: Retention settings for clips (default: shown below)
|
||||||
retain:
|
retain:
|
||||||
@@ -96,7 +97,7 @@ clips:
|
|||||||
```yaml
|
```yaml
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
# Optional: global ffmpeg args (default: shown below)
|
# Optional: global ffmpeg args (default: shown below)
|
||||||
global_args: -hide_banner -loglevel fatal
|
global_args: -hide_banner -loglevel warning
|
||||||
# Optional: global hwaccel args (default: shown below)
|
# Optional: global hwaccel args (default: shown below)
|
||||||
# NOTE: See hardware acceleration docs for your specific device
|
# NOTE: See hardware acceleration docs for your specific device
|
||||||
hwaccel_args: []
|
hwaccel_args: []
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ HassOS users can install via the addon repository. Frigate requires an MQTT serv
|
|||||||
1. Add https://github.com/blakeblackshear/frigate-hass-addons
|
1. Add https://github.com/blakeblackshear/frigate-hass-addons
|
||||||
1. Setup your configuration in the `Configuration` tab
|
1. Setup your configuration in the `Configuration` tab
|
||||||
1. Start the addon container
|
1. Start the addon container
|
||||||
|
1. If you are using hardware acceleration for ffmpeg, you will need to disable "Protection mode"
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Make sure you choose the right image for your architecture:
|
Make sure you choose the right image for your architecture:
|
||||||
|
|
||||||
|Arch|Image Name|
|
|Arch|Image Name|
|
||||||
|-|-|
|
|-|-|
|
||||||
|amd64|blakeblackshear/frigate:stable-amd64|
|
|amd64|blakeblackshear/frigate:stable-amd64|
|
||||||
@@ -73,15 +75,27 @@ docker run --rm \
|
|||||||
blakeblackshear/frigate:0.8.0-beta2-amd64
|
blakeblackshear/frigate:0.8.0-beta2-amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Calculating shm-size
|
||||||
|
|
||||||
|
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||||
|
|
||||||
|
You can calculate the necessary shm-size for each camera with the following formula:
|
||||||
|
|
||||||
|
```
|
||||||
|
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||||
|
```
|
||||||
|
|
||||||
|
The shm size cannot be set per container for HomeAssistant Addons. You must set `default-shm-size` in `/etc/docker/daemon.json` to increase the default shm size. This will increase the shm size for all of your docker containers. This may or may not cause issues with your setup. https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
|
||||||
|
|
||||||
## Kubernetes
|
## Kubernetes
|
||||||
|
|
||||||
Use the [helm chart](https://github.com/k8s-at-home/charts/tree/master/charts/frigate).
|
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate).
|
||||||
|
|
||||||
## Virtualization
|
## Virtualization
|
||||||
|
|
||||||
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
||||||
|
|
||||||
## Proxmox
|
### Proxmox
|
||||||
|
|
||||||
Some people have had success running Frigate in LXC directly with the following config:
|
Some people have had success running Frigate in LXC directly with the following config:
|
||||||
|
|
||||||
@@ -103,12 +117,6 @@ lxc.cgroup.devices.allow: a
|
|||||||
lxc.cap.drop:
|
lxc.cap.drop:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Calculating shm-size
|
### ESX
|
||||||
|
For details on running Frigate under ESX, see details [here](https://github.com/blakeblackshear/frigate/issues/305).
|
||||||
|
|
||||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
|
||||||
|
|
||||||
You can calculate the necessary shm-size for each camera with the following formula:
|
|
||||||
|
|
||||||
```
|
|
||||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
---
|
---
|
||||||
id: troubleshooting
|
id: troubleshooting
|
||||||
title: Troubleshooting
|
title: Troubleshooting and FAQ
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### How can I get sound or audio in my clips and recordings?
|
||||||
|
By default, Frigate removes audio from clips and recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The default ffmpeg args are shown [here](configuration/index#ffmpeg).
|
||||||
|
|
||||||
### My mjpeg stream or snapshots look green and crazy
|
### My mjpeg stream or snapshots look green and crazy
|
||||||
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
||||||
|
|
||||||
Example:
|
|
||||||

|

|
||||||
|
|
||||||
## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
|
## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
|
||||||
|
|
||||||
These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
|
These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
|
||||||
|
|
||||||
## "ffmpeg didnt return a frame. something is wrong"
|
|
||||||
|
|
||||||
Turn on logging for the ffmpeg process by overriding the global_args and setting the log level to `info` (the default is `fatal`). Note that all ffmpeg logs show up in the Frigate logs as `ERROR` level. This does not mean they are actually errors.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ffmpeg:
|
|
||||||
global_args: -hide_banner -loglevel info
|
|
||||||
```
|
|
||||||
|
|
||||||
## "On connect called"
|
## "On connect called"
|
||||||
|
|
||||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||||
|
|||||||
@@ -135,16 +135,17 @@ Version info
|
|||||||
|
|
||||||
Events from the database. Accepts the following query string parameters:
|
Events from the database. Accepts the following query string parameters:
|
||||||
|
|
||||||
| param | Type | Description |
|
| param | Type | Description |
|
||||||
| -------------- | ---- | --------------------------------------------- |
|
| -------------------- | ---- | --------------------------------------------- |
|
||||||
| `before` | int | Epoch time |
|
| `before` | int | Epoch time |
|
||||||
| `after` | int | Epoch time |
|
| `after` | int | Epoch time |
|
||||||
| `camera` | str | Camera name |
|
| `camera` | str | Camera name |
|
||||||
| `label` | str | Label name |
|
| `label` | str | Label name |
|
||||||
| `zone` | str | Zone name |
|
| `zone` | str | Zone name |
|
||||||
| `limit` | int | Limit the number of events returned |
|
| `limit` | int | Limit the number of events returned |
|
||||||
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
|
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
|
||||||
| `has_clip` | int | Filter to events that have clips (0 or 1) |
|
| `has_clip` | int | Filter to events that have clips (0 or 1) |
|
||||||
|
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
|
||||||
|
|
||||||
### `/api/events/summary`
|
### `/api/events/summary`
|
||||||
|
|
||||||
@@ -159,16 +160,17 @@ Returns data for a single event.
|
|||||||
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||||
|
|
||||||
### `/api/events/<id>/snapshot.jpg`
|
### `/api/events/<id>/snapshot.jpg`
|
||||||
|
|
||||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
||||||
|
|
||||||
Accepts the following query string parameters, but they are only applied when an event is in progress. After the event is completed, the saved snapshot is returned from disk without modification:
|
Accepts the following query string parameters, but they are only applied when an event is in progress. After the event is completed, the saved snapshot is returned from disk without modification:
|
||||||
|
|
||||||
|param|Type|Description|
|
| param | Type | Description |
|
||||||
|----|-----|--|
|
| ----------- | ---- | ------------------------------------------------- |
|
||||||
|`h`|int|Height in pixels|
|
| `h` | int | Height in pixels |
|
||||||
|`bbox`|int|Show bounding boxes for detected objects (0 or 1)|
|
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
|
||||||
|`timestamp`|int|Print the timestamp in the upper left (0 or 1)|
|
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
|
||||||
|`crop`|int|Crop the snapshot to the (0 or 1)|
|
| `crop` | int | Crop the snapshot to the (0 or 1) |
|
||||||
|
|
||||||
### `/clips/<camera>-<id>.mp4`
|
### `/clips/<camera>-<id>.mp4`
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ module.exports = {
|
|||||||
organizationName: 'blakeblackshear',
|
organizationName: 'blakeblackshear',
|
||||||
projectName: 'frigate',
|
projectName: 'frigate',
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
algolia: {
|
||||||
|
apiKey: '81ec882db78f7fed05c51daf973f0362',
|
||||||
|
indexName: 'frigate'
|
||||||
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'Frigate',
|
title: 'Frigate',
|
||||||
logo: {
|
logo: {
|
||||||
|
|||||||
BIN
docs/static/img/camera-ui.png
vendored
Normal file
BIN
docs/static/img/camera-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 944 KiB |
BIN
docs/static/img/events-ui.png
vendored
Normal file
BIN
docs/static/img/events-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/home-ui.png
vendored
Normal file
BIN
docs/static/img/home-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
@@ -10,6 +10,7 @@ import signal
|
|||||||
import yaml
|
import yaml
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||||
@@ -117,13 +118,16 @@ class FrigateApp():
|
|||||||
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
|
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
|
||||||
|
|
||||||
def init_database(self):
|
def init_database(self):
|
||||||
self.db = SqliteExtDatabase(self.config.database.path)
|
migrate_db = SqliteExtDatabase(self.config.database.path)
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
del(logging.getLogger('peewee_migrate').handlers[:])
|
del(logging.getLogger('peewee_migrate').handlers[:])
|
||||||
router = Router(self.db)
|
router = Router(migrate_db)
|
||||||
router.run()
|
router.run()
|
||||||
|
|
||||||
|
migrate_db.close()
|
||||||
|
|
||||||
|
self.db = SqliteQueueDatabase(self.config.database.path)
|
||||||
models = [Event]
|
models = [Event]
|
||||||
self.db.bind(models)
|
self.db.bind(models)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ CLIPS_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
|
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','warning']
|
||||||
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
|
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
|
||||||
'-fflags', '+genpts+discardcorrupt',
|
'-fflags', '+genpts+discardcorrupt',
|
||||||
'-rtsp_transport', 'tcp',
|
'-rtsp_transport', 'tcp',
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def events_summary():
|
|||||||
|
|
||||||
if not has_clip is None:
|
if not has_clip is None:
|
||||||
clauses.append((Event.has_clip == has_clip))
|
clauses.append((Event.has_clip == has_clip))
|
||||||
|
|
||||||
if not has_snapshot is None:
|
if not has_snapshot is None:
|
||||||
clauses.append((Event.has_snapshot == has_snapshot))
|
clauses.append((Event.has_snapshot == has_snapshot))
|
||||||
|
|
||||||
@@ -160,12 +160,14 @@ def events():
|
|||||||
camera = request.args.get('camera')
|
camera = request.args.get('camera')
|
||||||
label = request.args.get('label')
|
label = request.args.get('label')
|
||||||
zone = request.args.get('zone')
|
zone = request.args.get('zone')
|
||||||
after = request.args.get('after', type=int)
|
after = request.args.get('after', type=float)
|
||||||
before = request.args.get('before', type=int)
|
before = request.args.get('before', type=float)
|
||||||
has_clip = request.args.get('has_clip', type=int)
|
has_clip = request.args.get('has_clip', type=int)
|
||||||
has_snapshot = request.args.get('has_snapshot', type=int)
|
has_snapshot = request.args.get('has_snapshot', type=int)
|
||||||
|
include_thumbnails = request.args.get('include_thumbnails', default=1, type=int)
|
||||||
|
|
||||||
clauses = []
|
clauses = []
|
||||||
|
excluded_fields = []
|
||||||
|
|
||||||
if camera:
|
if camera:
|
||||||
clauses.append((Event.camera == camera))
|
clauses.append((Event.camera == camera))
|
||||||
@@ -184,10 +186,13 @@ def events():
|
|||||||
|
|
||||||
if not has_clip is None:
|
if not has_clip is None:
|
||||||
clauses.append((Event.has_clip == has_clip))
|
clauses.append((Event.has_clip == has_clip))
|
||||||
|
|
||||||
if not has_snapshot is None:
|
if not has_snapshot is None:
|
||||||
clauses.append((Event.has_snapshot == has_snapshot))
|
clauses.append((Event.has_snapshot == has_snapshot))
|
||||||
|
|
||||||
|
if not include_thumbnails:
|
||||||
|
excluded_fields.append(Event.thumbnail)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((1 == 1))
|
clauses.append((1 == 1))
|
||||||
|
|
||||||
@@ -196,7 +201,7 @@ def events():
|
|||||||
.order_by(Event.start_time.desc())
|
.order_by(Event.start_time.desc())
|
||||||
.limit(limit))
|
.limit(limit))
|
||||||
|
|
||||||
return jsonify([model_to_dict(e) for e in events])
|
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
|
||||||
|
|
||||||
@bp.route('/config')
|
@bp.route('/config')
|
||||||
def config():
|
def config():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import queue
|
|||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
def listener_configurer():
|
def listener_configurer():
|
||||||
@@ -54,6 +55,7 @@ class LogPipe(threading.Thread):
|
|||||||
self.daemon = False
|
self.daemon = False
|
||||||
self.logger = logging.getLogger(log_name)
|
self.logger = logging.getLogger(log_name)
|
||||||
self.level = level
|
self.level = level
|
||||||
|
self.deque = deque(maxlen=100)
|
||||||
self.fdRead, self.fdWrite = os.pipe()
|
self.fdRead, self.fdWrite = os.pipe()
|
||||||
self.pipeReader = os.fdopen(self.fdRead)
|
self.pipeReader = os.fdopen(self.fdRead)
|
||||||
self.start()
|
self.start()
|
||||||
@@ -67,9 +69,13 @@ class LogPipe(threading.Thread):
|
|||||||
"""Run the thread, logging everything.
|
"""Run the thread, logging everything.
|
||||||
"""
|
"""
|
||||||
for line in iter(self.pipeReader.readline, ''):
|
for line in iter(self.pipeReader.readline, ''):
|
||||||
self.logger.log(self.level, line.strip('\n'))
|
self.deque.append(line.strip('\n'))
|
||||||
|
|
||||||
self.pipeReader.close()
|
self.pipeReader.close()
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
while len(self.deque) > 0:
|
||||||
|
self.logger.log(self.level, self.deque.popleft())
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the write end of the pipe.
|
"""Close the write end of the pipe.
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ class CameraWatchdog(threading.Thread):
|
|||||||
now = datetime.datetime.now().timestamp()
|
now = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
if not self.capture_thread.is_alive():
|
if not self.capture_thread.is_alive():
|
||||||
|
self.logpipe.dump()
|
||||||
self.start_ffmpeg_detect()
|
self.start_ffmpeg_detect()
|
||||||
elif now - self.capture_thread.current_frame.value > 20:
|
elif now - self.capture_thread.current_frame.value > 20:
|
||||||
self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
|
self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
|
||||||
@@ -197,6 +198,7 @@ class CameraWatchdog(threading.Thread):
|
|||||||
poll = p['process'].poll()
|
poll = p['process'].poll()
|
||||||
if poll == None:
|
if poll == None:
|
||||||
continue
|
continue
|
||||||
|
p['logpipe'].dump()
|
||||||
p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
|
p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
|
||||||
|
|
||||||
# wait a bit before checking again
|
# wait a bit before checking again
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ http {
|
|||||||
|
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript image/svg+xml image/x-icon image/bmp image/png image/gif image/jpeg image/jpg;
|
||||||
|
gzip_proxied no-cache no-store private expired auth;
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
upstream frigate_api {
|
upstream frigate_api {
|
||||||
server localhost:5001;
|
server localhost:5001;
|
||||||
keepalive 1024;
|
keepalive 1024;
|
||||||
@@ -98,6 +104,7 @@ http {
|
|||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
add_header 'Access-Control-Allow-Origin' '*';
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
proxy_pass http://frigate_api/;
|
proxy_pass http://frigate_api/;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -105,13 +112,23 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
|
||||||
|
location ~* \.(?:js|css|svg|ico|png)$ {
|
||||||
|
access_log off;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
||||||
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
||||||
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
||||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
||||||
sub_filter_types text/css application/javascript;
|
sub_filter_types text/css application/javascript;
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
|
|
||||||
root /opt/frigate/web;
|
root /opt/frigate/web;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|||||||
289
web/package-lock.json
generated
289
web/package-lock.json
generated
@@ -93,13 +93,6 @@
|
|||||||
"@babel/helper-validator-option": "^7.12.1",
|
"@babel/helper-validator-option": "^7.12.1",
|
||||||
"browserslist": "^4.14.5",
|
"browserslist": "^4.14.5",
|
||||||
"semver": "^5.5.0"
|
"semver": "^5.5.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"semver": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-create-class-features-plugin": {
|
"@babel/helper-create-class-features-plugin": {
|
||||||
@@ -871,13 +864,6 @@
|
|||||||
"@babel/types": "^7.12.11",
|
"@babel/types": "^7.12.11",
|
||||||
"core-js-compat": "^3.8.0",
|
"core-js-compat": "^3.8.0",
|
||||||
"semver": "^5.5.0"
|
"semver": "^5.5.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"semver": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/preset-modules": {
|
"@babel/preset-modules": {
|
||||||
@@ -968,22 +954,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@npmcli/move-file": {
|
"@npmcli/move-file": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.1.tgz",
|
||||||
"integrity": "sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ==",
|
"integrity": "sha512-LtWTicuF2wp7PNTuyCwABx7nNG+DnzSE8gN0iWxkC6mpgm/iOPu0ZMTkXuCxmJxtWFsDxUaixM9COSNJEMUfuQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"rimraf": "^2.7.1"
|
"rimraf": "^3.0.2"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"rimraf": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
|
||||||
"requires": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prefresh/babel-plugin": {
|
"@prefresh/babel-plugin": {
|
||||||
@@ -1013,24 +989,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.1.1.tgz",
|
||||||
"integrity": "sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ=="
|
"integrity": "sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ=="
|
||||||
},
|
},
|
||||||
"@snowpack/plugin-optimize": {
|
|
||||||
"version": "0.2.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@snowpack/plugin-optimize/-/plugin-optimize-0.2.13.tgz",
|
|
||||||
"integrity": "sha512-DW0+iaSc80vWZKDIKbdR5pqDkNDsv3ZbH8eVB8yppK/JlBmOqmsUGdKlZ2eq8XDxE23/V5+gmA9RXLWcTMJ1eQ==",
|
|
||||||
"requires": {
|
|
||||||
"csso": "^4.1.0",
|
|
||||||
"es-module-lexer": "^0.3.25",
|
|
||||||
"esbuild": "^0.8.0",
|
|
||||||
"glob": "^7.1.6",
|
|
||||||
"html-minifier": "^4.0.0",
|
|
||||||
"hypertag": "^0.0.3",
|
|
||||||
"kleur": "^4.1.3",
|
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"node-inject-html": "^0.0.5",
|
|
||||||
"object.fromentries": "^2.0.2",
|
|
||||||
"p-queue": "^6.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@snowpack/plugin-postcss": {
|
"@snowpack/plugin-postcss": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@snowpack/plugin-postcss/-/plugin-postcss-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@snowpack/plugin-postcss/-/plugin-postcss-1.1.0.tgz",
|
||||||
@@ -1061,14 +1019,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||||
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
|
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.14.21",
|
"version": "14.14.22",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
|
||||||
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
|
"integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw=="
|
||||||
},
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -2104,16 +2062,16 @@
|
|||||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
||||||
},
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "3.8.2",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
|
||||||
"integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A=="
|
"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q=="
|
||||||
},
|
},
|
||||||
"core-js-compat": {
|
"core-js-compat": {
|
||||||
"version": "3.8.2",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz",
|
||||||
"integrity": "sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==",
|
"integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"browserslist": "^4.16.0",
|
"browserslist": "^4.16.1",
|
||||||
"semver": "7.0.0"
|
"semver": "7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2824,9 +2782,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entities": {
|
"entities": {
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||||
},
|
},
|
||||||
"errno": {
|
"errno": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
@@ -2863,11 +2821,6 @@
|
|||||||
"string.prototype.trimstart": "^1.0.1"
|
"string.prototype.trimstart": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es-module-lexer": {
|
|
||||||
"version": "0.3.26",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz",
|
|
||||||
"integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA=="
|
|
||||||
},
|
|
||||||
"es-to-primitive": {
|
"es-to-primitive": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
||||||
@@ -2944,11 +2897,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
||||||
},
|
},
|
||||||
"eventemitter3": {
|
|
||||||
"version": "4.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
|
||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
|
||||||
},
|
|
||||||
"events": {
|
"events": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
|
||||||
@@ -3575,11 +3523,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
|
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
|
||||||
},
|
},
|
||||||
"hypertag": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/hypertag/-/hypertag-0.0.3.tgz",
|
|
||||||
"integrity": "sha512-Z/cAsLCihKj+QJGQt5X19L/YY8PW5l8/A9Ju7s4g1ty4kwDdSzGp2QdRoKb266kFglJw6+CSsR/zNpkrOEo54A=="
|
|
||||||
},
|
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -3631,6 +3574,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||||
},
|
},
|
||||||
|
"immer": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
|
||||||
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
|
||||||
@@ -3928,12 +3876,9 @@
|
|||||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
|
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
|
||||||
},
|
},
|
||||||
"is-wsl": {
|
"is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
|
||||||
"requires": {
|
|
||||||
"is-docker": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -4102,11 +4047,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||||
},
|
},
|
||||||
"kleur": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw=="
|
|
||||||
},
|
|
||||||
"last-call-webpack-plugin": {
|
"last-call-webpack-plugin": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
|
||||||
@@ -4227,6 +4167,13 @@
|
|||||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"semver": "^6.0.0"
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map-cache": {
|
"map-cache": {
|
||||||
@@ -4320,23 +4267,15 @@
|
|||||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||||
},
|
},
|
||||||
"mini-css-extract-plugin": {
|
"mini-css-extract-plugin": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz",
|
||||||
"integrity": "sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg==",
|
"integrity": "sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loader-utils": "^2.0.0",
|
"loader-utils": "^2.0.0",
|
||||||
"schema-utils": "^3.0.0",
|
"schema-utils": "^3.0.0",
|
||||||
"webpack-sources": "^1.1.0"
|
"webpack-sources": "^1.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
|
|
||||||
"requires": {
|
|
||||||
"minimist": "^1.2.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
"loader-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||||
@@ -4469,11 +4408,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
|
||||||
"integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw=="
|
"integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw=="
|
||||||
},
|
},
|
||||||
"moo": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
|
|
||||||
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w=="
|
|
||||||
},
|
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
@@ -4560,14 +4494,6 @@
|
|||||||
"lodash.toarray": "^4.4.0"
|
"lodash.toarray": "^4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-inject-html": {
|
|
||||||
"version": "0.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-inject-html/-/node-inject-html-0.0.5.tgz",
|
|
||||||
"integrity": "sha512-YUjHKq6sjD5rru/lqxQ0BHLvgJP33T54/ehsxgi1sShUkA5cU+Kb/eZ8UnbjaZeKxueA5D75vwAmyc+B5Kof1Q==",
|
|
||||||
"requires": {
|
|
||||||
"moo": "^0.5.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-libs-browser": {
|
"node-libs-browser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||||
@@ -4729,17 +4655,6 @@
|
|||||||
"has": "^1.0.3"
|
"has": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"object.fromentries": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==",
|
|
||||||
"requires": {
|
|
||||||
"call-bind": "^1.0.0",
|
|
||||||
"define-properties": "^1.1.3",
|
|
||||||
"es-abstract": "^1.18.0-next.1",
|
|
||||||
"has": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"object.getownpropertydescriptors": {
|
"object.getownpropertydescriptors": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
|
||||||
@@ -4792,6 +4707,16 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0",
|
"is-docker": "^2.0.0",
|
||||||
"is-wsl": "^2.1.1"
|
"is-wsl": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"is-wsl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
|
"requires": {
|
||||||
|
"is-docker": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"optimize-css-assets-webpack-plugin": {
|
"optimize-css-assets-webpack-plugin": {
|
||||||
@@ -4821,11 +4746,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||||
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
|
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
|
||||||
},
|
},
|
||||||
"p-finally": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
|
||||||
},
|
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
@@ -4850,23 +4770,6 @@
|
|||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-queue": {
|
|
||||||
"version": "6.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
|
||||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
|
||||||
"requires": {
|
|
||||||
"eventemitter3": "^4.0.4",
|
|
||||||
"p-timeout": "^3.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"p-timeout": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
|
||||||
"requires": {
|
|
||||||
"p-finally": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"p-try": {
|
"p-try": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
@@ -6671,9 +6574,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "2.36.2",
|
"version": "2.38.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.36.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.1.tgz",
|
||||||
"integrity": "sha512-qjjiuJKb+/8n0EZyQYVW+gFU4bNRBcZaXVzUgSVrGw0HlQBlK2aWyaOMMs1Ufic1jV69b9kW3u3i9B+hISDm3A==",
|
"integrity": "sha512-q07T6vU/V1kqM8rGRRyCgEvIQcIAXoKIE5CpkYAlHhfiWM1Iuh4dIPWpIbqFngCK6lwAB2aYHiUVhIbSWHQWhw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.1.2"
|
"fsevents": "~2.1.2"
|
||||||
},
|
},
|
||||||
@@ -6741,14 +6644,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||||
},
|
},
|
||||||
"serialize-javascript": {
|
"serialize-javascript": {
|
||||||
"version": "4.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
||||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -7013,9 +6916,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ssri": {
|
"ssri": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||||
"integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
|
"integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"minipass": "^3.1.1"
|
"minipass": "^3.1.1"
|
||||||
}
|
}
|
||||||
@@ -7330,11 +7233,6 @@
|
|||||||
"source-map-support": "~0.5.19"
|
"source-map-support": "~0.5.19"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
|
||||||
},
|
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||||
@@ -7375,14 +7273,6 @@
|
|||||||
"ajv": "^6.12.5",
|
"ajv": "^6.12.5",
|
||||||
"ajv-keywords": "^3.5.2"
|
"ajv-keywords": "^3.5.2"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"serialize-javascript": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
|
|
||||||
"requires": {
|
|
||||||
"randombytes": "^2.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7924,26 +7814,6 @@
|
|||||||
"binary-extensions": "^1.0.0"
|
"binary-extensions": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-number": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"kind-of": "^3.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"kind-of": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
|
||||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"is-buffer": "^1.1.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"micromatch": {
|
"micromatch": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||||
@@ -8082,11 +7952,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||||
},
|
},
|
||||||
"commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
|
||||||
},
|
|
||||||
"fill-range": {
|
"fill-range": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||||
@@ -8126,29 +7991,6 @@
|
|||||||
"locate-path": "^3.0.0"
|
"locate-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-number": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
|
||||||
"requires": {
|
|
||||||
"kind-of": "^3.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"kind-of": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
|
||||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
|
||||||
"requires": {
|
|
||||||
"is-buffer": "^1.1.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-wsl": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
|
|
||||||
},
|
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
@@ -8247,10 +8089,13 @@
|
|||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"serialize-javascript": {
|
||||||
"version": "5.7.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||||
|
"requires": {
|
||||||
|
"randombytes": "^2.1.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ssri": {
|
"ssri": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
|
|||||||
@@ -8,17 +8,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prefresh/snowpack": "^3.0.1",
|
"@prefresh/snowpack": "^3.0.1",
|
||||||
"@snowpack/plugin-optimize": "^0.2.13",
|
|
||||||
"@snowpack/plugin-postcss": "^1.1.0",
|
"@snowpack/plugin-postcss": "^1.1.0",
|
||||||
"@snowpack/plugin-webpack": "^2.3.0",
|
"@snowpack/plugin-webpack": "^2.3.0",
|
||||||
"autoprefixer": "^10.2.1",
|
"autoprefixer": "^10.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"immer": "^8.0.1",
|
||||||
"postcss": "^8.2.2",
|
"postcss": "^8.2.2",
|
||||||
"postcss-cli": "^8.3.1",
|
"postcss-cli": "^8.3.1",
|
||||||
"preact": "^10.5.9",
|
"preact": "^10.5.9",
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"snowpack": "^3.0.0",
|
"snowpack": "^3.0.11",
|
||||||
"tailwindcss": "^2.0.2"
|
"tailwindcss": "^2.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
'@snowpack/plugin-postcss',
|
'@snowpack/plugin-postcss',
|
||||||
'@prefresh/snowpack',
|
'@prefresh/snowpack',
|
||||||
[
|
|
||||||
'@snowpack/plugin-optimize',
|
|
||||||
{
|
|
||||||
preloadModules: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'@snowpack/plugin-webpack',
|
'@snowpack/plugin-webpack',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import ActivityIndicator from './components/ActivityIndicator';
|
||||||
import Camera from './Camera';
|
import Camera from './Camera';
|
||||||
import CameraMap from './CameraMap';
|
import CameraMap from './CameraMap';
|
||||||
import Cameras from './Cameras';
|
import Cameras from './Cameras';
|
||||||
@@ -7,37 +8,27 @@ import Event from './Event';
|
|||||||
import Events from './Events';
|
import Events from './Events';
|
||||||
import { Router } from 'preact-router';
|
import { Router } from 'preact-router';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import { ApiHost, Config } from './context';
|
import Api, { FetchStatus, useConfig } from './api';
|
||||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const apiHost = useContext(ApiHost);
|
const { data, status } = useConfig();
|
||||||
const [config, setConfig] = useState(null);
|
return status !== FetchStatus.LOADED ? (
|
||||||
|
<div className="flex flex-grow-1 min-h-screen justify-center items-center">
|
||||||
useEffect(async () => {
|
<ActivityIndicator />
|
||||||
const response = await fetch(`${apiHost}/api/config`);
|
</div>
|
||||||
const data = response.ok ? await response.json() : {};
|
|
||||||
setConfig(data);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return !config ? (
|
|
||||||
<div />
|
|
||||||
) : (
|
) : (
|
||||||
<Config.Provider value={config}>
|
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
<Sidebar />
|
||||||
<Sidebar />
|
<div className="flex-auto p-2 md:p-4 lg:pl-8 lg:pr-8 min-w-0">
|
||||||
<div className="p-4 min-w-0">
|
<Router>
|
||||||
<Router>
|
<CameraMap path="/cameras/:camera/editor" />
|
||||||
<CameraMap path="/cameras/:camera/editor" />
|
<Camera path="/cameras/:camera" />
|
||||||
<Camera path="/cameras/:camera" />
|
<Event path="/events/:eventId" />
|
||||||
<Event path="/events/:eventId" />
|
<Events path="/events" />
|
||||||
<Events path="/events" />
|
<Debug path="/debug" />
|
||||||
<Debug path="/debug" />
|
<Cameras default path="/" />
|
||||||
<Cameras default path="/" />
|
</Router>
|
||||||
</Router>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Config.Provider>
|
</div>
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import Link from './components/Link';
|
|||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useCallback, useContext } from 'preact/hooks';
|
import { useCallback, useContext } from 'preact/hooks';
|
||||||
import { ApiHost, Config } from './context';
|
import { useApiHost, useConfig } from './api';
|
||||||
|
|
||||||
export default function Camera({ camera, url }) {
|
export default function Camera({ camera, url }) {
|
||||||
const config = useContext(Config);
|
const { data: config } = useConfig();
|
||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useApiHost();
|
||||||
|
|
||||||
if (!(camera in config.cameras)) {
|
if (!config) {
|
||||||
return <div>{`No camera named ${camera}`}</div>;
|
return <div>{`No camera named ${camera}`}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import Heading from './components/Heading';
|
|||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { ApiHost, Config } from './context';
|
import { useApiHost, useConfig } from './api';
|
||||||
|
|
||||||
export default function CameraMasks({ camera, url }) {
|
export default function CameraMasks({ camera, url }) {
|
||||||
const config = useContext(Config);
|
const { data: config } = useConfig();
|
||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useApiHost();
|
||||||
const imageRef = useRef(null);
|
const imageRef = useRef(null);
|
||||||
const [imageScale, setImageScale] = useState(1);
|
const [imageScale, setImageScale] = useState(1);
|
||||||
const [snap, setSnap] = useState(true);
|
const [snap, setSnap] = useState(true);
|
||||||
@@ -27,14 +27,26 @@ export default function CameraMasks({ camera, url }) {
|
|||||||
zones,
|
zones,
|
||||||
} = cameraConfig;
|
} = cameraConfig;
|
||||||
|
|
||||||
|
const resizeObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new ResizeObserver((entries) => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (Array.isArray(entries) && entries.length) {
|
||||||
|
const scaledWidth = entries[0].contentRect.width;
|
||||||
|
const scale = scaledWidth / width;
|
||||||
|
setImageScale(scale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[camera, width, setImageScale]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageRef.current) {
|
if (!imageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scaledWidth = imageRef.current.width;
|
resizeObserver.observe(imageRef.current);
|
||||||
const scale = scaledWidth / width;
|
}, [resizeObserver, imageRef.current]);
|
||||||
setImageScale(scale);
|
|
||||||
}, [imageRef.current, setImageScale]);
|
|
||||||
|
|
||||||
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
||||||
Array.isArray(motionMask)
|
Array.isArray(motionMask)
|
||||||
@@ -177,7 +189,7 @@ ${Object.keys(zonePoints)
|
|||||||
const handleAddObjectMask = useCallback(() => {
|
const handleAddObjectMask = useCallback(() => {
|
||||||
const n = Object.keys(objectMaskPoints).filter((name) => name.startsWith('object_')).length;
|
const n = Object.keys(objectMaskPoints).filter((name) => name.startsWith('object_')).length;
|
||||||
const newObjectName = `object_${n}`;
|
const newObjectName = `object_${n}`;
|
||||||
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [] };
|
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [[]] };
|
||||||
setObjectMaskPoints(newObjectMaskPoints);
|
setObjectMaskPoints(newObjectMaskPoints);
|
||||||
setEditing({ set: newObjectMaskPoints, key: newObjectName, subkey: 0, fn: setObjectMaskPoints });
|
setEditing({ set: newObjectMaskPoints, key: newObjectName, subkey: 0, fn: setObjectMaskPoints });
|
||||||
}, [objectMaskPoints, setObjectMaskPoints, setEditing]);
|
}, [objectMaskPoints, setObjectMaskPoints, setEditing]);
|
||||||
@@ -185,7 +197,7 @@ ${Object.keys(zonePoints)
|
|||||||
const handleRemoveObjectMask = useCallback(
|
const handleRemoveObjectMask = useCallback(
|
||||||
(key, subkey) => {
|
(key, subkey) => {
|
||||||
const newObjectMaskPoints = { ...objectMaskPoints };
|
const newObjectMaskPoints = { ...objectMaskPoints };
|
||||||
delete newObjectMaskPoints[key];
|
delete newObjectMaskPoints[key][subkey];
|
||||||
setObjectMaskPoints(newObjectMaskPoints);
|
setObjectMaskPoints(newObjectMaskPoints);
|
||||||
},
|
},
|
||||||
[objectMaskPoints, setObjectMaskPoints]
|
[objectMaskPoints, setObjectMaskPoints]
|
||||||
@@ -205,6 +217,20 @@ ${Object.keys(objectMaskPoints)
|
|||||||
.join('\n')}`);
|
.join('\n')}`);
|
||||||
}, [objectMaskPoints]);
|
}, [objectMaskPoints]);
|
||||||
|
|
||||||
|
const handleAddToObjectMask = useCallback(
|
||||||
|
(key) => {
|
||||||
|
const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
|
||||||
|
setObjectMaskPoints(newObjectMaskPoints);
|
||||||
|
setEditing({
|
||||||
|
set: newObjectMaskPoints,
|
||||||
|
key,
|
||||||
|
subkey: newObjectMaskPoints[key].length - 1,
|
||||||
|
fn: setObjectMaskPoints,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[objectMaskPoints, setObjectMaskPoints, setEditing]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChangeSnap = useCallback(
|
const handleChangeSnap = useCallback(
|
||||||
(id, value) => {
|
(id, value) => {
|
||||||
setSnap(value);
|
setSnap(value);
|
||||||
@@ -226,10 +252,10 @@ ${Object.keys(objectMaskPoints)
|
|||||||
|
|
||||||
<Box className="space-y-4">
|
<Box className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
|
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||||
<EditableMask
|
<EditableMask
|
||||||
onChange={handleUpdateEditable}
|
onChange={handleUpdateEditable}
|
||||||
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
||||||
scale={imageScale}
|
scale={imageScale}
|
||||||
snap={snap}
|
snap={snap}
|
||||||
width={width}
|
width={width}
|
||||||
@@ -268,6 +294,7 @@ ${Object.keys(objectMaskPoints)
|
|||||||
isMulti
|
isMulti
|
||||||
editing={editing}
|
editing={editing}
|
||||||
title="Object masks"
|
title="Object masks"
|
||||||
|
onAdd={handleAddToObjectMask}
|
||||||
onCopy={handleCopyObjectMasks}
|
onCopy={handleCopyObjectMasks}
|
||||||
onCreate={handleAddObjectMask}
|
onCreate={handleAddObjectMask}
|
||||||
onEdit={handleEditObjectMask}
|
onEdit={handleEditObjectMask}
|
||||||
@@ -397,6 +424,7 @@ function MaskValues({
|
|||||||
isMulti = false,
|
isMulti = false,
|
||||||
editing,
|
editing,
|
||||||
title,
|
title,
|
||||||
|
onAdd,
|
||||||
onCopy,
|
onCopy,
|
||||||
onCreate,
|
onCreate,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -438,6 +466,14 @@ function MaskValues({
|
|||||||
[onRemove]
|
[onRemove]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAdd = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const { key } = event.target.dataset;
|
||||||
|
onAdd(key);
|
||||||
|
},
|
||||||
|
[onAdd]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
|
<Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
@@ -454,15 +490,20 @@ function MaskValues({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{` ${mainkey}:\n mask:\n`}
|
{` ${mainkey}:\n mask:\n`}
|
||||||
|
{onAdd && showButtons ? (
|
||||||
|
<Button className="absolute -mt-12 right-0 font-sans" data-key={mainkey} onClick={handleAdd}>
|
||||||
|
{`Add to ${mainkey}`}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{points[mainkey].map((item, subkey) => (
|
{points[mainkey].map((item, subkey) => (
|
||||||
<Item
|
<Item
|
||||||
mainkey={mainkey}
|
mainkey={mainkey}
|
||||||
subkey={subkey}
|
subkey={subkey}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
handleEdit={handleEdit}
|
handleEdit={handleEdit}
|
||||||
|
handleRemove={handleRemove}
|
||||||
points={item}
|
points={item}
|
||||||
showButtons={showButtons}
|
showButtons={showButtons}
|
||||||
handleRemove={handleRemove}
|
|
||||||
yamlKeyPrefix={yamlKeyPrefix}
|
yamlKeyPrefix={yamlKeyPrefix}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -473,10 +514,11 @@ function MaskValues({
|
|||||||
<Item
|
<Item
|
||||||
mainkey={mainkey}
|
mainkey={mainkey}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
|
handleAdd={onAdd ? handleAdd : undefined}
|
||||||
handleEdit={handleEdit}
|
handleEdit={handleEdit}
|
||||||
|
handleRemove={handleRemove}
|
||||||
points={points[mainkey]}
|
points={points[mainkey]}
|
||||||
showButtons={showButtons}
|
showButtons={showButtons}
|
||||||
handleRemove={handleRemove}
|
|
||||||
yamlKeyPrefix={yamlKeyPrefix}
|
yamlKeyPrefix={yamlKeyPrefix}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -487,7 +529,7 @@ function MaskValues({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleRemove, yamlKeyPrefix }) {
|
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleAdd, handleRemove, yamlKeyPrefix }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-key={mainkey}
|
data-key={mainkey}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import ActivityIndicator from './components/ActivityIndicator';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
|
import CameraImage from './components/CameraImage';
|
||||||
import Events from './Events';
|
import Events from './Events';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useContext } from 'preact/hooks';
|
import { useConfig } from './api';
|
||||||
import { ApiHost, Config } from './context';
|
|
||||||
|
|
||||||
export default function Cameras() {
|
export default function Cameras() {
|
||||||
const config = useContext(Config);
|
const { data: config, status } = useConfig();
|
||||||
|
|
||||||
if (!config.cameras) {
|
if (!config) {
|
||||||
return <p>loading…</p>;
|
return <p>loading…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ export default function Cameras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Camera({ name }) {
|
function Camera({ name }) {
|
||||||
const apiHost = useContext(ApiHost);
|
|
||||||
const href = `/cameras/${name}`;
|
const href = `/cameras/${name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +32,7 @@ function Camera({ name }) {
|
|||||||
href={href}
|
href={href}
|
||||||
>
|
>
|
||||||
<Heading size="base">{name}</Heading>
|
<Heading size="base">{name}</Heading>
|
||||||
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
<CameraImage camera={name} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import ActivityIndicator from './components/ActivityIndicator';
|
||||||
|
import Box from './components/Box';
|
||||||
|
import Button from './components/Button';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import { ApiHost, Config } from './context';
|
import { FetchStatus, useConfig, useStats } from './api';
|
||||||
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
|
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
|
||||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Debug() {
|
export default function Debug() {
|
||||||
const apiHost = useContext(ApiHost);
|
const config = useConfig();
|
||||||
const config = useContext(Config);
|
|
||||||
const [stats, setStats] = useState({});
|
|
||||||
const [timeoutId, setTimeoutId] = useState(null);
|
const [timeoutId, setTimeoutId] = useState(null);
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
const forceUpdate = useCallback(async () => {
|
||||||
const statsResponse = await fetch(`${apiHost}/api/stats`);
|
setTimeoutId(setTimeout(forceUpdate, 1000));
|
||||||
const stats = statsResponse.ok ? await statsResponse.json() : {};
|
}, []);
|
||||||
setStats(stats);
|
|
||||||
setTimeoutId(setTimeout(fetchStats, 1000));
|
|
||||||
}, [setStats]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
forceUpdate();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,11 +26,13 @@ export default function Debug() {
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [timeoutId]);
|
}, [timeoutId]);
|
||||||
|
const { data: stats, status } = useStats(null, timeoutId);
|
||||||
|
|
||||||
|
if (stats === null && (status === FetchStatus.LOADING || status === FetchStatus.NONE)) {
|
||||||
|
return <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
const { detectors, detection_fps, service, ...cameras } = stats;
|
const { detectors, detection_fps, service, ...cameras } = stats;
|
||||||
if (!service) {
|
|
||||||
return 'loading…';
|
|
||||||
}
|
|
||||||
|
|
||||||
const detectorNames = Object.keys(detectors);
|
const detectorNames = Object.keys(detectors);
|
||||||
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
|
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
|
||||||
@@ -39,59 +40,73 @@ export default function Debug() {
|
|||||||
const cameraNames = Object.keys(cameras);
|
const cameraNames = Object.keys(cameras);
|
||||||
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
||||||
|
|
||||||
|
const handleCopyConfig = useCallback(async () => {
|
||||||
|
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="space-y-4">
|
||||||
<Heading>
|
<Heading>
|
||||||
Debug <span className="text-sm">{service.version}</span>
|
Debug <span className="text-sm">{service.version}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Table className="w-full">
|
|
||||||
<Thead>
|
<Box>
|
||||||
<Tr>
|
<Table className="w-full">
|
||||||
<Th>detector</Th>
|
<Thead>
|
||||||
{detectorDataKeys.map((name) => (
|
<Tr>
|
||||||
<Th>{name.replace('_', ' ')}</Th>
|
<Th>detector</Th>
|
||||||
))}
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{detectorNames.map((detector, i) => (
|
|
||||||
<Tr index={i}>
|
|
||||||
<Td>{detector}</Td>
|
|
||||||
{detectorDataKeys.map((name) => (
|
{detectorDataKeys.map((name) => (
|
||||||
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
|
<Th>{name.replace('_', ' ')}</Th>
|
||||||
))}
|
))}
|
||||||
</Tr>
|
</Tr>
|
||||||
))}
|
</Thead>
|
||||||
</Tbody>
|
<Tbody>
|
||||||
</Table>
|
{detectorNames.map((detector, i) => (
|
||||||
|
<Tr index={i}>
|
||||||
<Table className="w-full">
|
<Td>{detector}</Td>
|
||||||
<Thead>
|
{detectorDataKeys.map((name) => (
|
||||||
<Tr>
|
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
|
||||||
<Th>camera</Th>
|
))}
|
||||||
{cameraDataKeys.map((name) => (
|
</Tr>
|
||||||
<Th>{name.replace('_', ' ')}</Th>
|
|
||||||
))}
|
))}
|
||||||
</Tr>
|
</Tbody>
|
||||||
</Thead>
|
</Table>
|
||||||
<Tbody>
|
</Box>
|
||||||
{cameraNames.map((camera, i) => (
|
|
||||||
<Tr index={i}>
|
<Box>
|
||||||
<Td>
|
<Table className="w-full">
|
||||||
<Link href={`/cameras/${camera}`}>{camera}</Link>
|
<Thead>
|
||||||
</Td>
|
<Tr>
|
||||||
|
<Th>camera</Th>
|
||||||
{cameraDataKeys.map((name) => (
|
{cameraDataKeys.map((name) => (
|
||||||
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
|
<Th>{name.replace('_', ' ')}</Th>
|
||||||
))}
|
))}
|
||||||
</Tr>
|
</Tr>
|
||||||
))}
|
</Thead>
|
||||||
</Tbody>
|
<Tbody>
|
||||||
</Table>
|
{cameraNames.map((camera, i) => (
|
||||||
|
<Tr index={i}>
|
||||||
|
<Td>
|
||||||
|
<Link href={`/cameras/${camera}`}>{camera}</Link>
|
||||||
|
</Td>
|
||||||
|
{cameraDataKeys.map((name) => (
|
||||||
|
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Heading size="sm">Config</Heading>
|
<Box className="relative">
|
||||||
<pre className="font-mono overflow-y-scroll overflow-x-scroll max-h-96 rounded bg-white dark:bg-gray-900">
|
<Heading size="sm">Config</Heading>
|
||||||
{JSON.stringify(config, null, 2)}
|
<Button className="absolute top-4 right-8" onClick={handleCopyConfig}>
|
||||||
</pre>
|
Copy to Clipboard
|
||||||
|
</Button>
|
||||||
|
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
|
||||||
|
{JSON.stringify(config, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
import { h, Fragment } from 'preact';
|
import { h, Fragment } from 'preact';
|
||||||
import { ApiHost } from './context';
|
import ActivityIndicator from './components/ActivityIndicator';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
|
import { FetchStatus, useApiHost, useEvent } from './api';
|
||||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
||||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
|
||||||
|
|
||||||
export default function Event({ eventId }) {
|
export default function Event({ eventId }) {
|
||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useApiHost();
|
||||||
const [data, setData] = useState(null);
|
const { data, status } = useEvent(eventId);
|
||||||
|
|
||||||
useEffect(async () => {
|
if (status !== FetchStatus.LOADED) {
|
||||||
const response = await fetch(`${apiHost}/api/events/${eventId}`);
|
return <ActivityIndicator />;
|
||||||
const data = response.ok ? await response.json() : null;
|
|
||||||
setData(data);
|
|
||||||
}, [apiHost, eventId]);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Heading>{eventId}</Heading>
|
|
||||||
<p>loading…</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startime = new Date(data.start_time * 1000);
|
const startime = new Date(data.start_time * 1000);
|
||||||
@@ -57,34 +46,36 @@ export default function Event({ eventId }) {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Table>
|
<Box>
|
||||||
<Thead>
|
<Table>
|
||||||
<Th>Key</Th>
|
<Thead>
|
||||||
<Th>Value</Th>
|
<Th>Key</Th>
|
||||||
</Thead>
|
<Th>Value</Th>
|
||||||
<Tbody>
|
</Thead>
|
||||||
<Tr>
|
<Tbody>
|
||||||
<Td>Camera</Td>
|
<Tr>
|
||||||
<Td>
|
<Td>Camera</Td>
|
||||||
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
<Td>
|
||||||
</Td>
|
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||||
</Tr>
|
</Td>
|
||||||
<Tr index={1}>
|
</Tr>
|
||||||
<Td>Timeframe</Td>
|
<Tr index={1}>
|
||||||
<Td>
|
<Td>Timeframe</Td>
|
||||||
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
<Td>
|
||||||
</Td>
|
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||||
</Tr>
|
</Td>
|
||||||
<Tr>
|
</Tr>
|
||||||
<Td>Score</Td>
|
<Tr>
|
||||||
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
<Td>Score</Td>
|
||||||
</Tr>
|
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||||
<Tr index={1}>
|
</Tr>
|
||||||
<Td>Zones</Td>
|
<Tr index={1}>
|
||||||
<Td>{data.zones.join(', ')}</Td>
|
<Td>Zones</Td>
|
||||||
</Tr>
|
<Td>{data.zones.join(', ')}</Td>
|
||||||
</Tbody>
|
</Tr>
|
||||||
</Table>
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,123 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ApiHost } from './context';
|
import ActivityIndicator from './components/ActivityIndicator';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
|
import produce from 'immer';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
|
import { FetchStatus, useApiHost, useConfig, useEvents } from './api';
|
||||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
||||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
|
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Events({ url } = {}) {
|
const API_LIMIT = 25;
|
||||||
const apiHost = useContext(ApiHost);
|
|
||||||
const [events, setEvents] = useState([]);
|
|
||||||
|
|
||||||
const searchParams = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`).searchParams;
|
const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} });
|
||||||
const searchParamsString = searchParams.toString();
|
const reducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'APPEND_EVENTS': {
|
||||||
|
const {
|
||||||
|
meta: { searchString },
|
||||||
|
payload,
|
||||||
|
} = action;
|
||||||
|
return produce(state, (draftState) => {
|
||||||
|
draftState.searchStrings[searchString] = true;
|
||||||
|
draftState.events.push(...payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(async () => {
|
case 'REACHED_END': {
|
||||||
const response = await fetch(`${apiHost}/api/events?${searchParamsString}`);
|
const {
|
||||||
const data = response.ok ? await response.json() : {};
|
meta: { searchString },
|
||||||
setEvents(data);
|
} = action;
|
||||||
}, [searchParamsString]);
|
return produce(state, (draftState) => {
|
||||||
|
draftState.reachedEnd = true;
|
||||||
|
draftState.searchStrings[searchString] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const searchKeys = Array.from(searchParams.keys());
|
case 'RESET':
|
||||||
|
return initialState;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSearchString = `include_thumbnails=0&limit=${API_LIMIT}`;
|
||||||
|
function removeDefaultSearchKeys(searchParams) {
|
||||||
|
searchParams.delete('limit');
|
||||||
|
searchParams.delete('include_thumbnails');
|
||||||
|
searchParams.delete('before');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Events({ path: pathname } = {}) {
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState);
|
||||||
|
const { searchParams: initialSearchParams } = new URL(window.location);
|
||||||
|
const [searchString, setSearchString] = useState(`${defaultSearchString}&${initialSearchParams.toString()}`);
|
||||||
|
const { data, status } = useEvents(searchString);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && !(searchString in searchStrings)) {
|
||||||
|
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
|
||||||
|
}
|
||||||
|
if (Array.isArray(data) && data.length < API_LIMIT) {
|
||||||
|
dispatch({ type: 'REACHED_END', meta: { searchString } });
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const observer = useRef(
|
||||||
|
new IntersectionObserver((entries, observer) => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// under certain edge cases, a ref may be applied / in memory twice
|
||||||
|
// avoid fetching twice by grabbing the last observed entry only
|
||||||
|
const entry = entries[entries.length - 1];
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const { startTime } = entry.target.dataset;
|
||||||
|
const { searchParams } = new URL(window.location);
|
||||||
|
searchParams.set('before', parseFloat(startTime) - 0.0001);
|
||||||
|
|
||||||
|
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastCellRef = useCallback(
|
||||||
|
(node) => {
|
||||||
|
if (node !== null) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
if (!reachedEnd) {
|
||||||
|
observer.current.observe(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[observer.current, reachedEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilter = useCallback(
|
||||||
|
(searchParams) => {
|
||||||
|
dispatch({ type: 'RESET' });
|
||||||
|
removeDefaultSearchKeys(searchParams);
|
||||||
|
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
|
||||||
|
route(`${pathname}?${searchParams.toString()}`);
|
||||||
|
},
|
||||||
|
[pathname, setSearchString]
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 w-full">
|
||||||
<Heading>Events</Heading>
|
<Heading>Events</Heading>
|
||||||
|
|
||||||
{searchKeys.length ? (
|
<Filters onChange={handleFilter} searchParams={searchParams} />
|
||||||
<Box>
|
|
||||||
<Heading size="sm">Filters</Heading>
|
|
||||||
<div className="flex flex-wrap space-x-2">
|
|
||||||
{searchKeys.map((filterKey) => (
|
|
||||||
<UnFilterable
|
|
||||||
paramName={filterKey}
|
|
||||||
searchParams={searchParamsString}
|
|
||||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Box className="min-w-0 overflow-auto">
|
<Box className="min-w-0 overflow-auto">
|
||||||
<Table>
|
<Table className="min-w-full table-fixed">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
@@ -63,25 +138,49 @@ export default function Events({ url } = {}) {
|
|||||||
) => {
|
) => {
|
||||||
const start = new Date(parseInt(startTime * 1000, 10));
|
const start = new Date(parseInt(startTime * 1000, 10));
|
||||||
const end = new Date(parseInt(endTime * 1000, 10));
|
const end = new Date(parseInt(endTime * 1000, 10));
|
||||||
|
const ref = i === events.length - 1 ? lastCellRef : undefined;
|
||||||
return (
|
return (
|
||||||
<Tr key={id} index={i}>
|
<Tr key={id} index={i}>
|
||||||
<Td>
|
<Td className="w-40">
|
||||||
<a href={`/events/${id}`}>
|
<a href={`/events/${id}`} ref={ref} data-start-time={startTime} data-reached-end={reachedEnd}>
|
||||||
<img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
|
<img
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
style="min-height: 48px; min-width: 48px;"
|
||||||
|
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
|
<Filterable
|
||||||
|
onFilter={handleFilter}
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParams}
|
||||||
|
paramName="camera"
|
||||||
|
name={camera}
|
||||||
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
|
<Filterable
|
||||||
|
onFilter={handleFilter}
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParams}
|
||||||
|
paramName="label"
|
||||||
|
name={label}
|
||||||
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<ul>
|
<ul>
|
||||||
{zones.map((zone) => (
|
{zones.map((zone) => (
|
||||||
<li>
|
<li>
|
||||||
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
|
<Filterable
|
||||||
|
onFilter={handleFilter}
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchString}
|
||||||
|
paramName="zone"
|
||||||
|
name={zone}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -94,27 +193,108 @@ export default function Events({ url } = {}) {
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
|
<Tfoot>
|
||||||
|
<Tr>
|
||||||
|
<Td className="text-center p-4" colspan="8">
|
||||||
|
{status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tfoot>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Filterable({ searchParams, paramName, name }) {
|
function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
|
||||||
const params = new URLSearchParams(searchParams);
|
const href = useMemo(() => {
|
||||||
params.set(paramName, name);
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
return <Link href={`?${params.toString()}`}>{name}</Link>;
|
params.set(paramName, name);
|
||||||
}
|
removeDefaultSearchKeys(params);
|
||||||
|
return `${pathname}?${params.toString()}`;
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
route(href, true);
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
params.set(paramName, name);
|
||||||
|
onFilter(params);
|
||||||
|
},
|
||||||
|
[href, searchParams]
|
||||||
|
);
|
||||||
|
|
||||||
function UnFilterable({ searchParams, paramName, name }) {
|
|
||||||
const params = new URLSearchParams(searchParams);
|
|
||||||
params.delete(paramName);
|
|
||||||
return (
|
return (
|
||||||
<a
|
<Link href={href} onclick={handleClick}>
|
||||||
className="bg-gray-700 text-white px-3 py-1 rounded-md hover:bg-gray-300 hover:text-gray-900 dark:bg-gray-300 dark:text-gray-900 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
||||||
href={`?${params.toString()}`}
|
|
||||||
>
|
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Filters({ onChange, searchParams }) {
|
||||||
|
const { data } = useConfig();
|
||||||
|
|
||||||
|
const cameras = useMemo(() => Object.keys(data.cameras), [data]);
|
||||||
|
|
||||||
|
const zones = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(data.cameras)
|
||||||
|
.reduce((memo, camera) => {
|
||||||
|
memo = memo.concat(Object.keys(camera.zones));
|
||||||
|
return memo;
|
||||||
|
}, [])
|
||||||
|
.filter((value, i, self) => self.indexOf(value) === i),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const labels = useMemo(() => {
|
||||||
|
return Object.values(data.cameras)
|
||||||
|
.reduce((memo, camera) => {
|
||||||
|
memo = memo.concat(camera.objects?.track || []);
|
||||||
|
return memo;
|
||||||
|
}, data.objects?.track || [])
|
||||||
|
.filter((value, i, self) => self.indexOf(value) === i);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="flex space-y-0 space-x-8 flex-wrap">
|
||||||
|
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
|
||||||
|
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
|
||||||
|
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Filter({ onChange, searchParams, paramName, options }) {
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const newParams = new URLSearchParams(searchParams.toString());
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value) {
|
||||||
|
newParams.set(paramName, event.target.value);
|
||||||
|
} else {
|
||||||
|
newParams.delete(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(newParams);
|
||||||
|
},
|
||||||
|
[searchParams, paramName, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
<span className="block uppercase text-sm">{paramName}</span>
|
||||||
|
<select className="border-solid border border-gray-500 rounded dark:text-gray-900" onChange={handleSelect}>
|
||||||
|
<option>All</option>
|
||||||
|
{options.map((opt) => {
|
||||||
|
return (
|
||||||
|
<option value={opt} selected={searchParams.get(paramName) === opt}>
|
||||||
|
{opt}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function Sidebar() {
|
|||||||
<hr className="border-solid border-gray-500 mt-2" />
|
<hr className="border-solid border-gray-500 mt-2" />
|
||||||
<NavLink
|
<NavLink
|
||||||
className="self-end"
|
className="self-end"
|
||||||
href="https://github.com/blakeblackshear/frigate/blob/master/README.md"
|
href="https://blakeblackshear.github.io/frigate"
|
||||||
text="Documentation"
|
text="Documentation"
|
||||||
/>
|
/>
|
||||||
<NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
|
<NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
|
||||||
|
|||||||
108
web/src/api/index.jsx
Normal file
108
web/src/api/index.jsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { h, createContext } from 'preact';
|
||||||
|
import produce from 'immer';
|
||||||
|
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
|
||||||
|
|
||||||
|
export const FetchStatus = {
|
||||||
|
NONE: 'none',
|
||||||
|
LOADING: 'loading',
|
||||||
|
LOADED: 'loaded',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = Object.freeze({
|
||||||
|
host: import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '',
|
||||||
|
queries: {},
|
||||||
|
});
|
||||||
|
export const Api = createContext(initialState);
|
||||||
|
export default Api;
|
||||||
|
|
||||||
|
function reducer(state, { type, payload, meta }) {
|
||||||
|
switch (type) {
|
||||||
|
case 'REQUEST': {
|
||||||
|
const { url, request } = payload;
|
||||||
|
const data = state.queries[url]?.data || null;
|
||||||
|
return produce(state, (draftState) => {
|
||||||
|
draftState.queries[url] = { status: FetchStatus.LOADING, data };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RESPONSE': {
|
||||||
|
const { url, ok, data } = payload;
|
||||||
|
return produce(state, (draftState) => {
|
||||||
|
draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiProvider = ({ children }) => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
return <Api.Provider value={{ state, dispatch }}>{children}</Api.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function shouldFetch(state, url, forceRefetch = false) {
|
||||||
|
if (forceRefetch || !(url in state.queries)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { status } = state.queries[url];
|
||||||
|
|
||||||
|
return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetch(url, forceRefetch) {
|
||||||
|
const { state, dispatch } = useContext(Api);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldFetch(state, url, forceRefetch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchConfig() {
|
||||||
|
await dispatch({ type: 'REQUEST', payload: { url } });
|
||||||
|
const response = await fetch(`${state.host}${url}`);
|
||||||
|
const data = await response.json();
|
||||||
|
await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchConfig();
|
||||||
|
}, [url, forceRefetch]);
|
||||||
|
|
||||||
|
if (!(url in state.queries)) {
|
||||||
|
return { data: null, status: FetchStatus.NONE };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = state.queries[url].data || null;
|
||||||
|
const status = state.queries[url].status;
|
||||||
|
|
||||||
|
return { data, status };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApiHost() {
|
||||||
|
const { state, dispatch } = useContext(Api);
|
||||||
|
return state.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvents(searchParams, forceRefetch) {
|
||||||
|
const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||||
|
return useFetch(url, forceRefetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvent(eventId, forceRefetch) {
|
||||||
|
const url = `/api/events/${eventId}`;
|
||||||
|
return useFetch(url, forceRefetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfig(searchParams, forceRefetch) {
|
||||||
|
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||||
|
return useFetch(url, forceRefetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStats(searchParams, forceRefetch) {
|
||||||
|
const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
|
||||||
|
return useFetch(url, forceRefetch);
|
||||||
|
}
|
||||||
15
web/src/components/ActivityIndicator.jsx
Normal file
15
web/src/components/ActivityIndicator.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: 'h-4 w-4 border-2 border-t-2',
|
||||||
|
md: 'h-8 w-8 border-4 border-t-4',
|
||||||
|
lg: 'h-16 w-16 border-8 border-t-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ActivityIndicator({ size = 'md' }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex items-center justify-center" aria-label="Loading…">
|
||||||
|
<div className={`activityindicator ease-in rounded-full border-gray-200 text-blue-500 ${sizes[size]}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ApiHost, Config } from '../context';
|
import CameraImage from './CameraImage';
|
||||||
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
const MIN_LOAD_TIMEOUT_MS = 200;
|
||||||
const config = useContext(Config);
|
|
||||||
const apiHost = useContext(ApiHost);
|
|
||||||
const cameraConfig = config.cameras[camera];
|
|
||||||
|
|
||||||
|
export default function AutoUpdatingCameraImage({ camera, searchParams, showFps = true }) {
|
||||||
const [key, setKey] = useState(Date.now());
|
const [key, setKey] = useState(Date.now());
|
||||||
useEffect(() => {
|
const [fps, setFps] = useState(0);
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
setKey(Date.now());
|
const handleLoad = useCallback(() => {
|
||||||
}, 500);
|
const loadTime = Date.now() - key;
|
||||||
return () => {
|
setFps((1000 / Math.max(loadTime, MIN_LOAD_TIMEOUT_MS)).toFixed(1));
|
||||||
clearTimeout(timeoutId);
|
setTimeout(
|
||||||
};
|
() => {
|
||||||
}, [key, searchParams]);
|
setKey(Date.now());
|
||||||
|
},
|
||||||
|
loadTime > MIN_LOAD_TIMEOUT_MS ? 1 : MIN_LOAD_TIMEOUT_MS
|
||||||
|
);
|
||||||
|
}, [key, searchParams, setFps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<div>
|
||||||
className="w-full"
|
<CameraImage camera={camera} onload={handleLoad} searchParams={`cache=${key}&${searchParams}`} />
|
||||||
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
|
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||||
alt={`Auto-updating ${camera} image`}
|
</div>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default function Box({ children, className = '', hover = false, href, ...
|
|||||||
const Element = href ? 'a' : 'div';
|
const Element = href ? 'a' : 'div';
|
||||||
return (
|
return (
|
||||||
<Element
|
<Element
|
||||||
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 ${
|
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-2 lg:p-4 ${
|
||||||
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
|
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
href={href}
|
href={href}
|
||||||
|
|||||||
65
web/src/components/CameraImage.jsx
Normal file
65
web/src/components/CameraImage.jsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import ActivityIndicator from './ActivityIndicator';
|
||||||
|
import { useApiHost, useConfig } from '../api';
|
||||||
|
import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function CameraImage({ camera, onload, searchParams = '' }) {
|
||||||
|
const { data: config } = useConfig();
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const [availableWidth, setAvailableWidth] = useState(0);
|
||||||
|
const [loadedSrc, setLoadedSrc] = useState(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const { name, width, height } = config.cameras[camera];
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
|
||||||
|
const resizeObserver = useMemo(() => {
|
||||||
|
return new ResizeObserver((entries) => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (Array.isArray(entries) && entries.length) {
|
||||||
|
setAvailableWidth(entries[0].contentRect.width);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [setAvailableWidth, width]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
}, [resizeObserver, containerRef.current]);
|
||||||
|
|
||||||
|
const scaledHeight = useMemo(() => Math.min(Math.ceil(availableWidth / aspectRatio), height), [
|
||||||
|
availableWidth,
|
||||||
|
aspectRatio,
|
||||||
|
height,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const img = useMemo(() => new Image(), [camera]);
|
||||||
|
img.onload = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const src = event.srcElement.currentSrc;
|
||||||
|
setLoadedSrc(src);
|
||||||
|
onload && onload(event);
|
||||||
|
},
|
||||||
|
[searchParams, onload]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scaledHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
|
||||||
|
}, [apiHost, name, img, searchParams, scaledHeight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
{loadedSrc ? (
|
||||||
|
<img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} />
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,13 +2,9 @@ import { h } from 'preact';
|
|||||||
import { useCallback, useState } from 'preact/hooks';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Switch({ checked, label, id, onChange }) {
|
export default function Switch({ checked, label, id, onChange }) {
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(() => {
|
||||||
(event) => {
|
onChange(id, !checked);
|
||||||
console.log(event.target.checked, !checked);
|
}, [id, onChange, checked]);
|
||||||
onChange(id, !checked);
|
|
||||||
},
|
|
||||||
[id, onChange, checked]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label for={id} className="flex items-center cursor-pointer">
|
<label for={id} className="flex items-center cursor-pointer">
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ export function Table({ children, className = '' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Thead({ children, className = '' }) {
|
export function Thead({ children, className }) {
|
||||||
return <thead className={`${className}`}>{children}</thead>;
|
return <thead className={className}>{children}</thead>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tbody({ children, className = '' }) {
|
export function Tbody({ children, className }) {
|
||||||
return <tbody className={`${className}`}>{children}</tbody>;
|
return <tbody className={className}>{children}</tbody>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tfoot({ children, className = '' }) {
|
export function Tfoot({ children, className = '' }) {
|
||||||
@@ -19,13 +19,21 @@ export function Tfoot({ children, className = '' }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Tr({ children, className = '', index }) {
|
export function Tr({ children, className = '', index }) {
|
||||||
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-700' : ''} ${className}`}>{children}</tr>;
|
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-600' : ''} ${className}`}>{children}</tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Th({ children, className = '' }) {
|
export function Th({ children, className = '', colspan }) {
|
||||||
return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>;
|
return (
|
||||||
|
<th className={`border-b-2 border-gray-400 p-1 md:p-2 text-left ${className}`} colspan={colspan}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Td({ children, className = '' }) {
|
export function Td({ children, className = '', colspan }) {
|
||||||
return <td className={`p-4 ${className}`}>{children}</td>;
|
return (
|
||||||
|
<td className={`p-1 md:p-2 ${className}`} colspan={colspan}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createContext } from 'preact';
|
|
||||||
|
|
||||||
export const Config = createContext({});
|
|
||||||
|
|
||||||
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
|
|
||||||
@@ -1,3 +1,27 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.activityindicator {
|
||||||
|
border-top-color: currentColor;
|
||||||
|
-webkit-animation: spinner 0.75s linear infinite;
|
||||||
|
animation: spinner 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes spinner {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { ApiProvider } from './api';
|
||||||
import { h, render } from 'preact';
|
import { h, render } from 'preact';
|
||||||
import 'preact/devtools';
|
import 'preact/devtools';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<App />,
|
<ApiProvider>
|
||||||
|
<App />
|
||||||
|
</ApiProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user