mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
Merge branch 'blakeblackshear:dev' into dev
This commit is contained in:
commit
7440f4b5cf
@ -77,6 +77,8 @@ def events():
|
|||||||
min_length = request.args.get("min_length", type=float)
|
min_length = request.args.get("min_length", type=float)
|
||||||
max_length = request.args.get("max_length", type=float)
|
max_length = request.args.get("max_length", type=float)
|
||||||
|
|
||||||
|
sort = request.args.get("sort", type=str)
|
||||||
|
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
selected_columns = [
|
selected_columns = [
|
||||||
@ -219,10 +221,22 @@ def events():
|
|||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append((True))
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
if sort == "score_asc":
|
||||||
|
order_by = Event.data["score"].asc()
|
||||||
|
elif sort == "score_desc":
|
||||||
|
order_by = Event.data["score"].desc()
|
||||||
|
elif sort == "date_asc":
|
||||||
|
Event.start_time.asc()
|
||||||
|
elif sort == "date_desc":
|
||||||
|
Event.start_time.desc()
|
||||||
|
else:
|
||||||
|
order_by = Event.start_time.desc()
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select(*selected_columns)
|
Event.select(*selected_columns)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(Event.start_time.desc())
|
.order_by(order_by)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"""Configure and control camera via onvif."""
|
"""Configure and control camera via onvif."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import site
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from importlib.util import find_spec
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from onvif import ONVIFCamera, ONVIFError
|
from onvif import ONVIFCamera, ONVIFError
|
||||||
@ -50,10 +51,7 @@ class OnvifController:
|
|||||||
cam.onvif.port,
|
cam.onvif.port,
|
||||||
cam.onvif.user,
|
cam.onvif.user,
|
||||||
cam.onvif.password,
|
cam.onvif.password,
|
||||||
wsdl_dir=site.getsitepackages()[0].replace(
|
wsdl_dir=Path(find_spec("onvif").origin).parent / "../wsdl",
|
||||||
"dist-packages", "site-packages"
|
|
||||||
)
|
|
||||||
+ "/wsdl",
|
|
||||||
),
|
),
|
||||||
"init": False,
|
"init": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
|
|||||||
192
web/package-lock.json
generated
192
web/package-lock.json
generated
@ -34,10 +34,10 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.8",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
"lucide-react": "^0.365.0",
|
"lucide-react": "^0.368.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"react-day-picker": "^8.9.1",
|
"react-day-picker": "^8.9.1",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
@ -68,9 +68,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@types/node": "^20.12.5",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.74",
|
"@types/react": "^18.2.78",
|
||||||
"@types/react-dom": "^18.2.24",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@types/react-icons": "^3.0.0",
|
"@types/react-icons": "^3.0.0",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@types/strftime": "^0.9.8",
|
"@types/strftime": "^0.9.8",
|
||||||
@ -93,9 +93,9 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -715,9 +715,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/object-schema": {
|
"node_modules/@humanwhocodes/object-schema": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
|
||||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/confirm": {
|
"node_modules/@inquirer/confirm": {
|
||||||
@ -2507,9 +2507,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.5",
|
"version": "20.12.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||||
"integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==",
|
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@ -2522,9 +2522,9 @@
|
|||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.74",
|
"version": "18.2.78",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.78.tgz",
|
||||||
"integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==",
|
"integrity": "sha512-qOwdPnnitQY4xKlKayt42q5W5UQrSHjgoXNVEtxeqdITJ99k4VXJOP3vt8Rkm9HmgJpH50UNU+rlqfkfWOqp0A==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -2532,9 +2532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "18.2.24",
|
"version": "18.2.25",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz",
|
||||||
"integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==",
|
"integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
@ -2618,6 +2618,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
|
||||||
|
"version": "7.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
|
||||||
|
"integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/typescript-estree": "7.5.0",
|
||||||
|
"@typescript-eslint/utils": "7.5.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"ts-api-utils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.56.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||||
|
"version": "7.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
|
||||||
|
"integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
|
"@types/json-schema": "^7.0.12",
|
||||||
|
"@types/semver": "^7.5.0",
|
||||||
|
"@typescript-eslint/scope-manager": "7.5.0",
|
||||||
|
"@typescript-eslint/types": "7.5.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "7.5.0",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.56.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
|
||||||
@ -2663,33 +2715,6 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
|
||||||
"version": "7.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
|
|
||||||
"integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/typescript-estree": "7.5.0",
|
|
||||||
"@typescript-eslint/utils": "7.5.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"ts-api-utils": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^8.56.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
|
||||||
@ -2755,31 +2780,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
|
||||||
"version": "7.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
|
|
||||||
"integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
|
||||||
"@types/json-schema": "^7.0.12",
|
|
||||||
"@types/semver": "^7.5.0",
|
|
||||||
"@typescript-eslint/scope-manager": "7.5.0",
|
|
||||||
"@typescript-eslint/types": "7.5.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "7.5.0",
|
|
||||||
"semver": "^7.5.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^8.56.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
|
||||||
@ -2945,9 +2945,9 @@
|
|||||||
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
|
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.2",
|
"version": "8.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@ -4476,9 +4476,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.2.9",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
|
||||||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
@ -4622,9 +4622,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "13.23.0",
|
"version": "13.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||||
"integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
|
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
@ -4703,9 +4703,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/hls.js": {
|
"node_modules/hls.js": {
|
||||||
"version": "1.5.7",
|
"version": "1.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.8.tgz",
|
||||||
"integrity": "sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A=="
|
"integrity": "sha512-hJYMPfLhWO7/7+n4f9pn6bOheCGx0WgvVz7k3ouq3Pp1bja48NN+HeCQu3XCGYzqWQF/wo7Sk6dJAyWVJD8ECA=="
|
||||||
},
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@ -5279,9 +5279,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.365.0",
|
"version": "0.368.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.365.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.368.0.tgz",
|
||||||
"integrity": "sha512-sJYpPyyzGHI4B3pys+XSFnE4qtSWc68rFnDLxbNNKjkLST5XSx9DNn5+1Z3eFgFiw39PphNRiVBSVb+AL3oKwA==",
|
"integrity": "sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
@ -6261,9 +6261,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.51.2",
|
"version": "7.51.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
|
||||||
"integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==",
|
"integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22.0"
|
"node": ">=12.22.0"
|
||||||
},
|
},
|
||||||
@ -7284,9 +7284,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tinypool": {
|
"node_modules/tinypool": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz",
|
||||||
"integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
|
"integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@ -7421,9 +7421,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
@ -39,10 +39,10 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.8",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
"lucide-react": "^0.365.0",
|
"lucide-react": "^0.368.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"react-day-picker": "^8.9.1",
|
"react-day-picker": "^8.9.1",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
@ -73,9 +73,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@types/node": "^20.12.5",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.74",
|
"@types/react": "^18.2.78",
|
||||||
"@types/react-dom": "^18.2.24",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@types/react-icons": "^3.0.0",
|
"@types/react-icons": "^3.0.0",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@types/strftime": "^0.9.8",
|
"@types/strftime": "^0.9.8",
|
||||||
@ -98,8 +98,8 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,7 +209,7 @@ type CameraFilterButtonProps = {
|
|||||||
selectedCameras: string[] | undefined;
|
selectedCameras: string[] | undefined;
|
||||||
updateCameraFilter: (cameras: string[] | undefined) => void;
|
updateCameraFilter: (cameras: string[] | undefined) => void;
|
||||||
};
|
};
|
||||||
function CamerasFilterButton({
|
export function CamerasFilterButton({
|
||||||
allCameras,
|
allCameras,
|
||||||
groups,
|
groups,
|
||||||
selectedCameras,
|
selectedCameras,
|
||||||
@ -227,7 +227,7 @@ function CamerasFilterButton({
|
|||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaVideo
|
<FaVideo
|
||||||
className={`${selectedCameras?.length == 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
className={`${(selectedCameras?.length ?? 0) >= 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
|
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
|||||||
@ -92,6 +92,9 @@ export function ThresholdBarGraph({
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
theme: systemTheme || theme,
|
theme: systemTheme || theme,
|
||||||
|
y: {
|
||||||
|
formatter: (val) => `${val}${unit}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
markers: {
|
markers: {
|
||||||
size: 0,
|
size: 0,
|
||||||
@ -118,7 +121,7 @@ export function ThresholdBarGraph({
|
|||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
} as ApexCharts.ApexOptions;
|
} as ApexCharts.ApexOptions;
|
||||||
}, [graphId, threshold, systemTheme, theme, formatTime]);
|
}, [graphId, threshold, unit, systemTheme, theme, formatTime]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ApexCharts.exec(graphId, "updateOptions", options, true, true);
|
ApexCharts.exec(graphId, "updateOptions", options, true, true);
|
||||||
@ -190,7 +193,7 @@ export function StorageGraph({ graphId, used, total }: StorageGraphProps) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
axisBorder: {
|
axisBorder: {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { LuLoader2 } from "react-icons/lu";
|
import { LuLoader2 } from "react-icons/lu";
|
||||||
|
|
||||||
export default function ActivityIndicator({ size = 30 }) {
|
export default function ActivityIndicator({ className = "w-full", size = 30 }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full flex items-center justify-center"
|
className={`flex items-center justify-center ${className}`}
|
||||||
aria-label="Loading…"
|
aria-label="Loading…"
|
||||||
>
|
>
|
||||||
<LuLoader2 className="animate-spin" size={size} />
|
<LuLoader2 className="animate-spin" size={size} />
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
MutableRefObject,
|
|
||||||
ReactNode,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { isAndroid, isDesktop, isMobile } from "react-device-detect";
|
import { isAndroid, isDesktop, isMobile } from "react-device-detect";
|
||||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
@ -19,7 +13,6 @@ const unsupportedErrorCodes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type HlsVideoPlayerProps = {
|
type HlsVideoPlayerProps = {
|
||||||
children?: ReactNode;
|
|
||||||
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
currentSource: string;
|
currentSource: string;
|
||||||
@ -30,7 +23,6 @@ type HlsVideoPlayerProps = {
|
|||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
};
|
};
|
||||||
export default function HlsVideoPlayer({
|
export default function HlsVideoPlayer({
|
||||||
children,
|
|
||||||
videoRef,
|
videoRef,
|
||||||
visible,
|
visible,
|
||||||
currentSource,
|
currentSource,
|
||||||
@ -83,19 +75,88 @@ export default function HlsVideoPlayer({
|
|||||||
// controls
|
// controls
|
||||||
|
|
||||||
const [isPlaying, setIsPlaying] = useState(true);
|
const [isPlaying, setIsPlaying] = useState(true);
|
||||||
|
const [muted, setMuted] = useState(true);
|
||||||
|
const [volume, setVolume] = useState(1.0);
|
||||||
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
|
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [controls, setControls] = useState(isMobile);
|
const [controls, setControls] = useState(isMobile);
|
||||||
const [controlsOpen, setControlsOpen] = useState(false);
|
const [controlsOpen, setControlsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = (e: MouseEvent) => {
|
||||||
|
if (!videoRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = videoRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.clientX > rect.left &&
|
||||||
|
e.clientX < rect.right &&
|
||||||
|
e.clientY > rect.top &&
|
||||||
|
e.clientY < rect.bottom
|
||||||
|
) {
|
||||||
|
setControls(true);
|
||||||
|
} else {
|
||||||
|
setControls(controlsOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", callback);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", callback);
|
||||||
|
};
|
||||||
|
}, [videoRef, controlsOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransformWrapper minScale={1.0}>
|
<TransformWrapper minScale={1.0}>
|
||||||
|
<VideoControls
|
||||||
|
className="absolute bottom-5 left-1/2 -translate-x-1/2 z-50"
|
||||||
|
video={videoRef.current}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
show={visible && controls}
|
||||||
|
muted={muted}
|
||||||
|
volume={volume}
|
||||||
|
controlsOpen={controlsOpen}
|
||||||
|
setControlsOpen={setControlsOpen}
|
||||||
|
setMuted={setMuted}
|
||||||
|
playbackRate={videoRef.current?.playbackRate ?? 1}
|
||||||
|
hotKeys={hotKeys}
|
||||||
|
onPlayPause={(play) => {
|
||||||
|
if (!videoRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (play) {
|
||||||
|
videoRef.current.play();
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSeek={(diff) => {
|
||||||
|
const currentTime = videoRef.current?.currentTime;
|
||||||
|
|
||||||
|
if (!videoRef.current || !currentTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
||||||
|
}}
|
||||||
|
onSetPlaybackRate={(rate) =>
|
||||||
|
videoRef.current ? (videoRef.current.playbackRate = rate) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
position: "relative",
|
|
||||||
display: visible ? undefined : "none",
|
display: visible ? undefined : "none",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
onClick: isDesktop ? undefined : () => setControls(!controls),
|
||||||
|
}}
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: isMobile ? "100%" : undefined,
|
height: isMobile ? "100%" : undefined,
|
||||||
@ -108,7 +169,8 @@ export default function HlsVideoPlayer({
|
|||||||
autoPlay
|
autoPlay
|
||||||
controls={false}
|
controls={false}
|
||||||
playsInline
|
playsInline
|
||||||
muted
|
muted={muted}
|
||||||
|
onVolumeChange={() => setVolume(videoRef.current?.volume ?? 1.0)}
|
||||||
onPlay={() => {
|
onPlay={() => {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|
||||||
@ -145,61 +207,6 @@ export default function HlsVideoPlayer({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className="absolute inset-0"
|
|
||||||
onMouseOver={
|
|
||||||
isDesktop
|
|
||||||
? () => {
|
|
||||||
setControls(true);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onMouseOut={
|
|
||||||
isDesktop
|
|
||||||
? () => {
|
|
||||||
setControls(controlsOpen);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onClick={isDesktop ? undefined : () => setControls(!controls)}
|
|
||||||
>
|
|
||||||
<div className={`size-full relative ${visible ? "" : "hidden"}`}>
|
|
||||||
<VideoControls
|
|
||||||
className="absolute bottom-5 left-1/2 -translate-x-1/2"
|
|
||||||
video={videoRef.current}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
show={controls}
|
|
||||||
controlsOpen={controlsOpen}
|
|
||||||
setControlsOpen={setControlsOpen}
|
|
||||||
playbackRate={videoRef.current?.playbackRate ?? 1}
|
|
||||||
hotKeys={hotKeys}
|
|
||||||
onPlayPause={(play) => {
|
|
||||||
if (!videoRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (play) {
|
|
||||||
videoRef.current.play();
|
|
||||||
} else {
|
|
||||||
videoRef.current.pause();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSeek={(diff) => {
|
|
||||||
const currentTime = videoRef.current?.currentTime;
|
|
||||||
|
|
||||||
if (!videoRef.current || !currentTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
|
||||||
}}
|
|
||||||
onSetPlaybackRate={(rate) =>
|
|
||||||
videoRef.current ? (videoRef.current.playbackRate = rate) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TransformComponent>
|
</TransformComponent>
|
||||||
</TransformWrapper>
|
</TransformWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { isCurrentHour } from "@/utils/dateUtil";
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { isAndroid, isChrome, isMobile } from "react-device-detect";
|
import { isAndroid, isChrome, isMobile } from "react-device-detect";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
|
import { Skeleton } from "../ui/skeleton";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -143,6 +144,8 @@ function PreviewVideoPlayer({
|
|||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
|
|
||||||
|
const [firstLoad, setFirstLoad] = useState(true);
|
||||||
|
|
||||||
const initialPreview = useMemo(() => {
|
const initialPreview = useMemo(() => {
|
||||||
return cameraPreviews.find(
|
return cameraPreviews.find(
|
||||||
(preview) =>
|
(preview) =>
|
||||||
@ -253,6 +256,10 @@ function PreviewVideoPlayer({
|
|||||||
disableRemotePlayback
|
disableRemotePlayback
|
||||||
onSeeked={onPreviewSeeked}
|
onSeeked={onPreviewSeeked}
|
||||||
onLoadedData={() => {
|
onLoadedData={() => {
|
||||||
|
if (firstLoad) {
|
||||||
|
setFirstLoad(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (controller) {
|
if (controller) {
|
||||||
controller.previewReady();
|
controller.previewReady();
|
||||||
} else {
|
} else {
|
||||||
@ -280,6 +287,7 @@ function PreviewVideoPlayer({
|
|||||||
No Preview Found
|
No Preview Found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{firstLoad && <Skeleton className="absolute size-full aspect-video" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -427,6 +435,8 @@ function PreviewFramesPlayer({
|
|||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
|
|
||||||
|
const [firstLoad, setFirstLoad] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
return;
|
return;
|
||||||
@ -441,6 +451,8 @@ function PreviewFramesPlayer({
|
|||||||
}, [controller]);
|
}, [controller]);
|
||||||
|
|
||||||
const onImageLoaded = useCallback(() => {
|
const onImageLoaded = useCallback(() => {
|
||||||
|
setFirstLoad(false);
|
||||||
|
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -477,6 +489,7 @@ function PreviewFramesPlayer({
|
|||||||
No Preview Found
|
No Preview Found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{firstLoad && <Skeleton className="absolute size-full aspect-video" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import React, {
|
|||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { isCurrentHour } from "@/utils/dateUtil";
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import { Slider } from "../ui/slider-no-thumb";
|
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import TimeAgo from "../dynamic/TimeAgo";
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -23,6 +22,7 @@ import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
|||||||
import useContextMenu from "@/hooks/use-contextmenu";
|
import useContextMenu from "@/hooks/use-contextmenu";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
|
import { NoThumbSlider } from "../ui/slider";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
@ -543,7 +543,7 @@ function VideoPreview({
|
|||||||
>
|
>
|
||||||
<source src={relevantPreview.src} type={relevantPreview.type} />
|
<source src={relevantPreview.src} type={relevantPreview.type} />
|
||||||
</video>
|
</video>
|
||||||
<Slider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
@ -707,7 +707,7 @@ function InProgressPreview({
|
|||||||
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[key]}
|
value={[key]}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import {
|
|||||||
MdVolumeOff,
|
MdVolumeOff,
|
||||||
MdVolumeUp,
|
MdVolumeUp,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
import { Slider } from "../ui/slider-volume";
|
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
|
import { VolumeSlider } from "../ui/slider";
|
||||||
|
|
||||||
type VideoControls = {
|
type VideoControls = {
|
||||||
volume?: boolean;
|
volume?: boolean;
|
||||||
@ -38,11 +38,14 @@ type VideoControlsProps = {
|
|||||||
features?: VideoControls;
|
features?: VideoControls;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
volume?: number;
|
||||||
controlsOpen?: boolean;
|
controlsOpen?: boolean;
|
||||||
playbackRates?: number[];
|
playbackRates?: number[];
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
hotKeys?: boolean;
|
hotKeys?: boolean;
|
||||||
setControlsOpen?: (open: boolean) => void;
|
setControlsOpen?: (open: boolean) => void;
|
||||||
|
setMuted?: (muted: boolean) => void;
|
||||||
onPlayPause: (play: boolean) => void;
|
onPlayPause: (play: boolean) => void;
|
||||||
onSeek: (diff: number) => void;
|
onSeek: (diff: number) => void;
|
||||||
onSetPlaybackRate: (rate: number) => void;
|
onSetPlaybackRate: (rate: number) => void;
|
||||||
@ -53,11 +56,14 @@ export default function VideoControls({
|
|||||||
features = CONTROLS_DEFAULT,
|
features = CONTROLS_DEFAULT,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
show,
|
show,
|
||||||
|
muted,
|
||||||
|
volume,
|
||||||
controlsOpen,
|
controlsOpen,
|
||||||
playbackRates = PLAYBACK_RATE_DEFAULT,
|
playbackRates = PLAYBACK_RATE_DEFAULT,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
hotKeys = true,
|
hotKeys = true,
|
||||||
setControlsOpen,
|
setControlsOpen,
|
||||||
|
setMuted,
|
||||||
onPlayPause,
|
onPlayPause,
|
||||||
onSeek,
|
onSeek,
|
||||||
onSetPlaybackRate,
|
onSetPlaybackRate,
|
||||||
@ -89,18 +95,18 @@ export default function VideoControls({
|
|||||||
// volume control
|
// volume control
|
||||||
|
|
||||||
const VolumeIcon = useMemo(() => {
|
const VolumeIcon = useMemo(() => {
|
||||||
if (!video || video?.muted) {
|
if (!volume || volume == 0.0 || muted) {
|
||||||
return MdVolumeOff;
|
return MdVolumeOff;
|
||||||
} else if (video.volume <= 0.33) {
|
} else if (volume <= 0.33) {
|
||||||
return MdVolumeMute;
|
return MdVolumeMute;
|
||||||
} else if (video.volume <= 0.67) {
|
} else if (volume <= 0.67) {
|
||||||
return MdVolumeDown;
|
return MdVolumeDown;
|
||||||
} else {
|
} else {
|
||||||
return MdVolumeUp;
|
return MdVolumeUp;
|
||||||
}
|
}
|
||||||
// only update when specific fields change
|
// only update when specific fields change
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [video?.volume, video?.muted]);
|
}, [volume, muted]);
|
||||||
|
|
||||||
const onKeyboardShortcut = useCallback(
|
const onKeyboardShortcut = useCallback(
|
||||||
(key: string, down: boolean, repeat: boolean) => {
|
(key: string, down: boolean, repeat: boolean) => {
|
||||||
@ -116,8 +122,8 @@ export default function VideoControls({
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "m":
|
case "m":
|
||||||
if (down && !repeat && video) {
|
if (setMuted && down && !repeat && video) {
|
||||||
video.muted = !video.muted;
|
setMuted(!muted);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case " ":
|
case " ":
|
||||||
@ -150,13 +156,16 @@ export default function VideoControls({
|
|||||||
className="size-5"
|
className="size-5"
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
video.muted = !video.muted;
|
|
||||||
|
if (setMuted) {
|
||||||
|
setMuted(!muted);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{video.muted == false && (
|
{muted == false && (
|
||||||
<Slider
|
<VolumeSlider
|
||||||
className="w-20"
|
className="w-20"
|
||||||
value={[video.volume]}
|
value={[volume ?? 1.0]}
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.02}
|
step={0.02}
|
||||||
@ -193,7 +202,11 @@ export default function VideoControls({
|
|||||||
onValueChange={(rate) => onSetPlaybackRate(parseFloat(rate))}
|
onValueChange={(rate) => onSetPlaybackRate(parseFloat(rate))}
|
||||||
>
|
>
|
||||||
{playbackRates.map((rate) => (
|
{playbackRates.map((rate) => (
|
||||||
<DropdownMenuRadioItem key={rate} value={rate.toString()}>
|
<DropdownMenuRadioItem
|
||||||
|
key={rate}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={rate.toString()}
|
||||||
|
>
|
||||||
{rate}x
|
{rate}x
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import TimelineEventOverlay from "../../overlay/TimelineDataOverlay";
|
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -8,7 +7,8 @@ import { Preview } from "@/types/preview";
|
|||||||
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
|
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
|
||||||
import { DynamicVideoController } from "./DynamicVideoController";
|
import { DynamicVideoController } from "./DynamicVideoController";
|
||||||
import HlsVideoPlayer from "../HlsVideoPlayer";
|
import HlsVideoPlayer from "../HlsVideoPlayer";
|
||||||
import { TimeRange, Timeline } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically switches between video playback and scrubbing preview player.
|
* Dynamically switches between video playback and scrubbing preview player.
|
||||||
@ -45,9 +45,6 @@ export default function DynamicVideoPlayer({
|
|||||||
const playerRef = useRef<HTMLVideoElement | null>(null);
|
const playerRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const [previewController, setPreviewController] =
|
const [previewController, setPreviewController] =
|
||||||
useState<PreviewController | null>(null);
|
useState<PreviewController | null>(null);
|
||||||
const [focusedItem, setFocusedItem] = useState<Timeline | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const controller = useMemo(() => {
|
const controller = useMemo(() => {
|
||||||
if (!config || !playerRef.current || !previewController) {
|
if (!config || !playerRef.current || !previewController) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -59,7 +56,7 @@ export default function DynamicVideoPlayer({
|
|||||||
previewController,
|
previewController,
|
||||||
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
|
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
|
||||||
isScrubbing ? "scrubbing" : "playback",
|
isScrubbing ? "scrubbing" : "playback",
|
||||||
setFocusedItem,
|
() => {},
|
||||||
);
|
);
|
||||||
// we only want to fire once when players are ready
|
// we only want to fire once when players are ready
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -81,6 +78,7 @@ export default function DynamicVideoPlayer({
|
|||||||
// initial state
|
// initial state
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loadingTimeout, setLoadingTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [source, setSource] = useState(
|
const [source, setSource] = useState(
|
||||||
`${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
`${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
||||||
);
|
);
|
||||||
@ -88,8 +86,8 @@ export default function DynamicVideoPlayer({
|
|||||||
// start at correct time
|
// start at correct time
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isScrubbing) {
|
if (!isScrubbing) {
|
||||||
setIsLoading(true);
|
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
||||||
}
|
}
|
||||||
}, [isScrubbing]);
|
}, [isScrubbing]);
|
||||||
|
|
||||||
@ -137,7 +135,7 @@ export default function DynamicVideoPlayer({
|
|||||||
setSource(
|
setSource(
|
||||||
`${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
`${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
||||||
);
|
);
|
||||||
setIsLoading(true);
|
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
||||||
|
|
||||||
controller.newPlayback({
|
controller.newPlayback({
|
||||||
recordings: recordings ?? [],
|
recordings: recordings ?? [],
|
||||||
@ -162,16 +160,13 @@ export default function DynamicVideoPlayer({
|
|||||||
playerRef.current?.pause();
|
playerRef.current?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadingTimeout) {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
{config && focusedItem && (
|
|
||||||
<TimelineEventOverlay
|
|
||||||
timeline={focusedItem}
|
|
||||||
cameraConfig={config.cameras[camera]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</HlsVideoPlayer>
|
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${className}`}
|
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${className}`}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
@ -183,6 +178,9 @@ export default function DynamicVideoPlayer({
|
|||||||
setPreviewController(previewController);
|
setPreviewController(previewController);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{isLoading && (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full">
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-blue-500" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="block h-4 w-16 rounded-full bg-transparent -translate-y-[50%] ring-offset-transparent focus-visible:outline-none focus-visible:ring-transparent disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Slider };
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-muted">
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-white" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full bg-white ring-white focus:ring-white disabled:pointer-events-none disabled:opacity-50" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Slider };
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
const Slider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
@ -11,7 +11,7 @@ const Slider = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none select-none items-center",
|
"relative flex w-full touch-none select-none items-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -20,7 +20,68 @@ const Slider = React.forwardRef<
|
|||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
))
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider }
|
const VolumeSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-muted">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-white" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full bg-white ring-white focus:ring-white disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
VolumeSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const NoThumbSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-16 rounded-full bg-transparent -translate-y-[50%] ring-offset-transparent focus-visible:outline-none focus-visible:ring-transparent disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
NoThumbSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const DualThumbSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-selected/60">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block size-3 rounded-full bg-selected transition-colors cursor-col-resize disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
<SliderPrimitive.Thumb className="block size-3 rounded-full bg-selected transition-colors cursor-col-resize disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
DualThumbSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { DualThumbSlider, Slider, NoThumbSlider, VolumeSlider };
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { FaCopy } from "react-icons/fa6";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
|
||||||
const logTypes = ["frigate", "go2rtc", "nginx"] as const;
|
const logTypes = ["frigate", "go2rtc", "nginx"] as const;
|
||||||
type LogType = (typeof logTypes)[number];
|
type LogType = (typeof logTypes)[number];
|
||||||
@ -278,6 +279,9 @@ function Logs() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
contentRef.current?.scrollBy({
|
||||||
|
top: 10,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (node) startObserver.current.observe(node);
|
if (node) startObserver.current.observe(node);
|
||||||
@ -388,7 +392,7 @@ function Logs() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="size-full flex flex-col my-2 font-mono text-sm sm:p-2 whitespace-pre-wrap bg-background_alt border border-secondary rounded-md overflow-hidden">
|
<div className="relative size-full flex flex-col my-2 font-mono text-sm sm:p-2 whitespace-pre-wrap bg-background_alt border border-secondary rounded-md overflow-hidden">
|
||||||
<div className="grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 *:px-2 *:py-3 *:text-sm *:text-primary/40">
|
<div className="grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 *:px-2 *:py-3 *:text-sm *:text-primary/40">
|
||||||
<div className="p-1 flex items-center capitalize">Type</div>
|
<div className="p-1 flex items-center capitalize">Type</div>
|
||||||
<div className="col-span-2 sm:col-span-1 flex items-center">
|
<div className="col-span-2 sm:col-span-1 flex items-center">
|
||||||
@ -443,6 +447,9 @@ function Logs() {
|
|||||||
})}
|
})}
|
||||||
{logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
|
{logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
|
||||||
</div>
|
</div>
|
||||||
|
{logLines.length == 0 && (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
import {
|
||||||
|
CamerasFilterButton,
|
||||||
|
GeneralFilterContent,
|
||||||
|
} from "@/components/filter/ReviewFilterGroup";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -13,16 +16,25 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { DualThumbSlider } from "@/components/ui/slider";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { FaList, FaVideo } from "react-icons/fa";
|
import {
|
||||||
|
FaList,
|
||||||
|
FaSort,
|
||||||
|
FaSortAmountDown,
|
||||||
|
FaSortAmountUp,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { PiSlidersHorizontalFill } from "react-icons/pi";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
export default function SubmitPlus() {
|
export default function SubmitPlus() {
|
||||||
@ -36,6 +48,11 @@ export default function SubmitPlus() {
|
|||||||
|
|
||||||
const [selectedCameras, setSelectedCameras] = useState<string[]>();
|
const [selectedCameras, setSelectedCameras] = useState<string[]>();
|
||||||
const [selectedLabels, setSelectedLabels] = useState<string[]>();
|
const [selectedLabels, setSelectedLabels] = useState<string[]>();
|
||||||
|
const [scoreRange, setScoreRange] = useState<number[]>();
|
||||||
|
|
||||||
|
// sort
|
||||||
|
|
||||||
|
const [sort, setSort] = useState<string>();
|
||||||
|
|
||||||
// data
|
// data
|
||||||
|
|
||||||
@ -47,6 +64,9 @@ export default function SubmitPlus() {
|
|||||||
is_submitted: 0,
|
is_submitted: 0,
|
||||||
cameras: selectedCameras ? selectedCameras.join(",") : null,
|
cameras: selectedCameras ? selectedCameras.join(",") : null,
|
||||||
labels: selectedLabels ? selectedLabels.join(",") : null,
|
labels: selectedLabels ? selectedLabels.join(",") : null,
|
||||||
|
min_score: scoreRange ? scoreRange[0] : null,
|
||||||
|
max_score: scoreRange ? scoreRange[1] : null,
|
||||||
|
sort: sort ? sort : null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const [upload, setUpload] = useState<Event>();
|
const [upload, setUpload] = useState<Event>();
|
||||||
@ -104,12 +124,17 @@ export default function SubmitPlus() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex flex-col">
|
<div className="size-full flex flex-col">
|
||||||
|
<div className="w-full h-16 px-2 flex items-center justify-between overflow-x-auto">
|
||||||
<PlusFilterGroup
|
<PlusFilterGroup
|
||||||
selectedCameras={selectedCameras}
|
selectedCameras={selectedCameras}
|
||||||
setSelectedCameras={setSelectedCameras}
|
|
||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
|
selectedScoreRange={scoreRange}
|
||||||
|
setSelectedCameras={setSelectedCameras}
|
||||||
setSelectedLabels={setSelectedLabels}
|
setSelectedLabels={setSelectedLabels}
|
||||||
|
setSelectedScoreRange={setScoreRange}
|
||||||
/>
|
/>
|
||||||
|
<PlusSortSelector selectedSort={sort} setSelectedSort={setSort} />
|
||||||
|
</div>
|
||||||
<div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
|
<div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
|
||||||
<div className="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
|
<div className="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -178,15 +203,19 @@ const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
|||||||
|
|
||||||
type PlusFilterGroupProps = {
|
type PlusFilterGroupProps = {
|
||||||
selectedCameras: string[] | undefined;
|
selectedCameras: string[] | undefined;
|
||||||
setSelectedCameras: (cameras: string[] | undefined) => void;
|
|
||||||
selectedLabels: string[] | undefined;
|
selectedLabels: string[] | undefined;
|
||||||
|
selectedScoreRange: number[] | undefined;
|
||||||
|
setSelectedCameras: (cameras: string[] | undefined) => void;
|
||||||
setSelectedLabels: (cameras: string[] | undefined) => void;
|
setSelectedLabels: (cameras: string[] | undefined) => void;
|
||||||
|
setSelectedScoreRange: (range: number[] | undefined) => void;
|
||||||
};
|
};
|
||||||
function PlusFilterGroup({
|
function PlusFilterGroup({
|
||||||
selectedCameras,
|
selectedCameras,
|
||||||
setSelectedCameras,
|
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
|
selectedScoreRange,
|
||||||
|
setSelectedCameras,
|
||||||
setSelectedLabels,
|
setSelectedLabels,
|
||||||
|
setSelectedScoreRange,
|
||||||
}: PlusFilterGroupProps) {
|
}: PlusFilterGroupProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -217,97 +246,28 @@ function PlusFilterGroup({
|
|||||||
return [...labels].sort();
|
return [...labels].sort();
|
||||||
}, [config, selectedCameras]);
|
}, [config, selectedCameras]);
|
||||||
|
|
||||||
const [open, setOpen] = useState<"none" | "camera" | "label">("none");
|
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
||||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
"none",
|
||||||
undefined,
|
|
||||||
);
|
);
|
||||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
const [currentScoreRange, setCurrentScoreRange] = useState<
|
||||||
|
number[] | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const Menu = isMobile ? Drawer : DropdownMenu;
|
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||||
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||||
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-16 flex justify-start gap-2 items-center">
|
<div className="h-full flex justify-start gap-2 items-center">
|
||||||
<Menu
|
<CamerasFilterButton
|
||||||
open={open == "camera"}
|
allCameras={allCameras}
|
||||||
onOpenChange={(open) => {
|
groups={[]}
|
||||||
if (!open) {
|
selectedCameras={selectedCameras}
|
||||||
setCurrentCameras(selectedCameras);
|
updateCameraFilter={setSelectedCameras}
|
||||||
}
|
|
||||||
setOpen(open ? "camera" : "none");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trigger asChild>
|
|
||||||
<Button size="sm" className="mx-1 capitalize">
|
|
||||||
<FaVideo className="md:mr-[10px] text-secondary-foreground" />
|
|
||||||
<div className="hidden md:block text-primary">
|
|
||||||
{selectedCameras == undefined
|
|
||||||
? "All Cameras"
|
|
||||||
: `${selectedCameras.length} Cameras`}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</Trigger>
|
|
||||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
|
||||||
<DropdownMenuLabel className="flex justify-center">
|
|
||||||
Filter Cameras
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<FilterCheckBox
|
|
||||||
isChecked={currentCameras == undefined}
|
|
||||||
label="All Cameras"
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setCurrentCameras(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
|
||||||
{allCameras.map((item) => (
|
|
||||||
<FilterCheckBox
|
|
||||||
key={item}
|
|
||||||
isChecked={currentCameras?.includes(item) ?? false}
|
|
||||||
label={item.replaceAll("_", " ")}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
const updatedCameras = currentCameras
|
|
||||||
? [...currentCameras]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
updatedCameras.push(item);
|
|
||||||
setCurrentCameras(updatedCameras);
|
|
||||||
} else {
|
|
||||||
const updatedCameras = currentCameras
|
|
||||||
? [...currentCameras]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (updatedCameras.length > 1) {
|
|
||||||
updatedCameras.splice(updatedCameras.indexOf(item), 1);
|
|
||||||
setCurrentCameras(updatedCameras);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCameras(currentCameras);
|
|
||||||
setOpen("none");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Content>
|
|
||||||
</Menu>
|
|
||||||
<Menu
|
<Menu
|
||||||
open={open == "label"}
|
open={open == "label"}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@ -318,8 +278,14 @@ function PlusFilterGroup({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<Button size="sm" className="mx-1 capitalize">
|
<Button
|
||||||
<FaList className="md:mr-[10px] text-secondary-foreground" />
|
className="flex items-center gap-2 capitalize"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedLabels == undefined ? "default" : "select"}
|
||||||
|
>
|
||||||
|
<FaList
|
||||||
|
className={`${selectedLabels == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
|
/>
|
||||||
<div className="hidden md:block text-primary">
|
<div className="hidden md:block text-primary">
|
||||||
{selectedLabels == undefined
|
{selectedLabels == undefined
|
||||||
? "All Labels"
|
? "All Labels"
|
||||||
@ -328,60 +294,250 @@ function PlusFilterGroup({
|
|||||||
</Button>
|
</Button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
||||||
<DropdownMenuLabel className="flex justify-center">
|
<GeneralFilterContent
|
||||||
Filter Labels
|
allLabels={allLabels}
|
||||||
</DropdownMenuLabel>
|
selectedLabels={selectedLabels}
|
||||||
<DropdownMenuSeparator />
|
currentLabels={currentLabels}
|
||||||
<FilterCheckBox
|
setCurrentLabels={setCurrentLabels}
|
||||||
isChecked={currentLabels == undefined}
|
updateLabelFilter={setSelectedLabels}
|
||||||
label="All Labels"
|
onClose={() => setOpen("none")}
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setCurrentLabels(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
</Content>
|
||||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
</Menu>
|
||||||
{allLabels.map((item) => (
|
<Menu
|
||||||
<FilterCheckBox
|
open={open == "score"}
|
||||||
key={item}
|
onOpenChange={(open) => {
|
||||||
isChecked={currentLabels?.includes(item) ?? false}
|
setOpen(open ? "score" : "none");
|
||||||
label={item.replaceAll("_", " ")}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
const updatedLabels = currentLabels
|
|
||||||
? [...currentLabels]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
updatedLabels.push(item);
|
|
||||||
setCurrentLabels(updatedLabels);
|
|
||||||
} else {
|
|
||||||
const updatedLabels = currentLabels
|
|
||||||
? [...currentLabels]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (updatedLabels.length > 1) {
|
|
||||||
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
|
||||||
setCurrentLabels(updatedLabels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Trigger asChild>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedScoreRange == undefined ? "default" : "select"}
|
||||||
|
>
|
||||||
|
<PiSlidersHorizontalFill
|
||||||
|
className={`${selectedScoreRange == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div className="hidden md:block text-primary">
|
||||||
|
{selectedScoreRange == undefined
|
||||||
|
? "Score Range"
|
||||||
|
: `${selectedScoreRange[0] * 100}% - ${selectedScoreRange[1] * 100}%`}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content
|
||||||
|
className={`min-w-80 p-2 flex flex-col justify-center ${isMobile ? "gap-2 *:max-h-[75dvh]" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
className="w-12"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={Math.round((currentScoreRange?.at(0) ?? 0.5) * 100)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentScoreRange([
|
||||||
|
parseInt(e.target.value) / 100.0,
|
||||||
|
currentScoreRange?.at(1) ?? 1.0,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DualThumbSlider
|
||||||
|
className="w-full"
|
||||||
|
min={0.5}
|
||||||
|
max={1.0}
|
||||||
|
step={0.01}
|
||||||
|
value={currentScoreRange ?? [0.5, 1.0]}
|
||||||
|
onValueChange={setCurrentScoreRange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="w-12"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={Math.round((currentScoreRange?.at(1) ?? 1.0) * 100)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentScoreRange([
|
||||||
|
currentScoreRange?.at(0) ?? 0.5,
|
||||||
|
parseInt(e.target.value) / 100.0,
|
||||||
|
])
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex justify-center items-center">
|
<div className="p-2 flex justify-evenly items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedLabels(currentLabels);
|
setSelectedScoreRange(currentScoreRange);
|
||||||
setOpen("none");
|
setOpen("none");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentScoreRange(undefined);
|
||||||
|
setSelectedScoreRange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlusSortSelectorProps = {
|
||||||
|
selectedSort?: string;
|
||||||
|
setSelectedSort: (sort: string | undefined) => void;
|
||||||
|
};
|
||||||
|
function PlusSortSelector({
|
||||||
|
selectedSort,
|
||||||
|
setSelectedSort,
|
||||||
|
}: PlusSortSelectorProps) {
|
||||||
|
// menu state
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// sort
|
||||||
|
|
||||||
|
const [currentSort, setCurrentSort] = useState<string>();
|
||||||
|
const [currentDir, setCurrentDir] = useState<string>("desc");
|
||||||
|
|
||||||
|
// components
|
||||||
|
|
||||||
|
const Sort = selectedSort
|
||||||
|
? selectedSort.split("_")[1] == "desc"
|
||||||
|
? FaSortAmountDown
|
||||||
|
: FaSortAmountUp
|
||||||
|
: FaSort;
|
||||||
|
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||||
|
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||||
|
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex justify-start gap-2 items-center">
|
||||||
|
<Menu
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
const parts = selectedSort?.split("_");
|
||||||
|
|
||||||
|
if (parts?.length == 2) {
|
||||||
|
setCurrentSort(parts[0]);
|
||||||
|
setCurrentDir(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trigger asChild>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedSort == undefined ? "default" : "select"}
|
||||||
|
>
|
||||||
|
<Sort
|
||||||
|
className={`${selectedSort == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div className="hidden md:block text-primary">
|
||||||
|
{selectedSort == undefined ? "Sort" : selectedSort.split("_")[0]}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content
|
||||||
|
className={`p-2 flex flex-col justify-center gap-2 ${isMobile ? "max-h-[75dvh]" : ""}`}
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
className={`flex flex-col gap-4 ${isMobile ? "mt-4" : ""}`}
|
||||||
|
onValueChange={(value) => setCurrentSort(value)}
|
||||||
|
>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
currentSort == "date"
|
||||||
|
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||||
|
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||||
|
}
|
||||||
|
id="date"
|
||||||
|
value="date"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className="w-full cursor-pointer capitalize"
|
||||||
|
htmlFor="date"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</Label>
|
||||||
|
{currentSort == "date" ? (
|
||||||
|
currentDir == "desc" ? (
|
||||||
|
<FaSortAmountDown
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("asc")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FaSortAmountUp
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("desc")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="size-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
currentSort == "score"
|
||||||
|
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||||
|
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||||
|
}
|
||||||
|
id="score"
|
||||||
|
value="score"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className="w-full cursor-pointer capitalize"
|
||||||
|
htmlFor="score"
|
||||||
|
>
|
||||||
|
Score
|
||||||
|
</Label>
|
||||||
|
{currentSort == "score" ? (
|
||||||
|
currentDir == "desc" ? (
|
||||||
|
<FaSortAmountDown
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("asc")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FaSortAmountUp
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("desc")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="size-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="p-2 flex justify-evenly items-center">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSort(`${currentSort}_${currentDir}`);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSort(undefined);
|
||||||
|
setCurrentDir("desc");
|
||||||
|
setSelectedSort(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -364,11 +364,11 @@ export function RecordingView({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
key={mainCamera}
|
key={mainCamera}
|
||||||
className={
|
className={`relative ${
|
||||||
isDesktop
|
isDesktop
|
||||||
? `${mainCameraAspect == "tall" ? "xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center`
|
? `${mainCameraAspect == "tall" ? "h-[50%] md:h-[60%] lg:h-[75%] xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center`
|
||||||
: `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}`
|
: `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}`
|
||||||
}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: isDesktop
|
aspectRatio: isDesktop
|
||||||
? mainCameraAspect == "tall"
|
? mainCameraAspect == "tall"
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export default function GeneralMetrics({
|
|||||||
series[key] = { name: key, data: [] };
|
series[key] = { name: key, data: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
series[key].data.push({ x: statsIdx + 1, y: stats.gpu });
|
series[key].data.push({ x: statsIdx + 1, y: stats.gpu.slice(0, -1) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return Object.keys(series).length > 0 ? Object.values(series) : [];
|
return Object.keys(series).length > 0 ? Object.values(series) : [];
|
||||||
@ -215,7 +215,7 @@ export default function GeneralMetrics({
|
|||||||
series[key] = { name: key, data: [] };
|
series[key] = { name: key, data: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
series[key].data.push({ x: statsIdx + 1, y: stats.mem });
|
series[key].data.push({ x: statsIdx + 1, y: stats.mem.slice(0, -1) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return Object.values(series);
|
return Object.values(series);
|
||||||
@ -373,7 +373,7 @@ export default function GeneralMetrics({
|
|||||||
key={series.name}
|
key={series.name}
|
||||||
graphId={`${series.name}-gpu`}
|
graphId={`${series.name}-gpu`}
|
||||||
name={series.name}
|
name={series.name}
|
||||||
unit=""
|
unit="%"
|
||||||
threshold={GPUUsageThreshold}
|
threshold={GPUUsageThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
@ -392,7 +392,7 @@ export default function GeneralMetrics({
|
|||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
key={series.name}
|
key={series.name}
|
||||||
graphId={`${series.name}-mem`}
|
graphId={`${series.name}-mem`}
|
||||||
unit=""
|
unit="%"
|
||||||
name={series.name}
|
name={series.name}
|
||||||
threshold={GPUMemThreshold}
|
threshold={GPUMemThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user