mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
485 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5a489198a | ||
|
|
17f54da524 | ||
|
|
b5065673ab | ||
|
|
610b0f63e0 | ||
|
|
73317bda67 | ||
|
|
f690f3acba | ||
|
|
eea9f1f6cb | ||
|
|
d9089e8d1c | ||
|
|
bed5acc9d5 | ||
|
|
f46feff445 | ||
|
|
d46a98c7d5 | ||
|
|
05af60df4f | ||
|
|
5fc266ed81 | ||
|
|
38356c31b0 | ||
|
|
2703cfb81e | ||
|
|
8a226ca473 | ||
|
|
33e6d54fd2 | ||
|
|
d36f37a4ed | ||
|
|
f3ebbb6547 | ||
|
|
28b2f79e5b | ||
|
|
9a77115a30 | ||
|
|
2d899e364d | ||
|
|
32b881891c | ||
|
|
9eefc07c7c | ||
|
|
792accffb6 | ||
|
|
03af88aba5 | ||
|
|
f56b6b4ad0 | ||
|
|
4ce1681e79 | ||
|
|
7570fa71f0 | ||
|
|
8a61c76cd9 | ||
|
|
fbf5381699 | ||
|
|
ff77f0db4f | ||
|
|
2e30abedc9 | ||
|
|
c4cb4f7475 | ||
|
|
7432bb813e | ||
|
|
572a104779 | ||
|
|
f77dc23d92 | ||
|
|
e92fc74dd3 | ||
|
|
9479c3d5c3 | ||
|
|
cfc37a64e1 | ||
|
|
2d5294804c | ||
|
|
6c01a85077 | ||
|
|
cf41e988eb | ||
|
|
d7a161c088 | ||
|
|
379c4040fe | ||
|
|
3f17618ad5 | ||
|
|
d7be64c3d9 | ||
|
|
ef7737e9be | ||
|
|
51ad3184b6 | ||
|
|
efc8fd878a | ||
|
|
6da1e98c83 | ||
|
|
513a06740c | ||
|
|
743a070724 | ||
|
|
5fb0e76669 | ||
|
|
bedeab686e | ||
|
|
9d9fa352ce | ||
|
|
1bfa6ce862 | ||
|
|
755b29c859 | ||
|
|
aab5b0247a | ||
|
|
d7e4b0bd17 | ||
|
|
3bacdadb80 | ||
|
|
1d75ee44ed | ||
|
|
230cc343af | ||
|
|
b318ee165c | ||
|
|
f677646365 | ||
|
|
8db7d820d7 | ||
|
|
adb0632566 | ||
|
|
4d5c8db333 | ||
|
|
01d6a3d5f8 | ||
|
|
fbeadbc32f | ||
|
|
1289be888f | ||
|
|
aa7e3a955c | ||
|
|
2823f3b921 | ||
|
|
ddb2a74540 | ||
|
|
37d8d7a2f8 | ||
|
|
578b715a1f | ||
|
|
f14a811ce9 | ||
|
|
06dd6d2213 | ||
|
|
72471c47f4 | ||
|
|
aec5f7173c | ||
|
|
f7b68789ac | ||
|
|
0672da621e | ||
|
|
a7f9b78533 | ||
|
|
0075429e08 | ||
|
|
43f7ccd166 | ||
|
|
8c64e0f288 | ||
|
|
c91a387833 | ||
|
|
93d5dd88ba | ||
|
|
05427253b9 | ||
|
|
e2bc541089 | ||
|
|
9a959bab16 | ||
|
|
45ca4a15f7 | ||
|
|
ddd2ff53ff | ||
|
|
5c3266b48f | ||
|
|
0da6db9d9f | ||
|
|
adeffbcf71 | ||
|
|
f0ca7b753f | ||
|
|
1bbde65121 | ||
|
|
3acdf041e9 | ||
|
|
fce755f0c4 | ||
|
|
b1cdccc020 | ||
|
|
eee070f1cd | ||
|
|
e36dd56e3b | ||
|
|
aa7d08e93f | ||
|
|
df67896a55 | ||
|
|
3fb790c33c | ||
|
|
850a1a39fe | ||
|
|
942b575c18 | ||
|
|
6dc53052b6 | ||
|
|
7e99b3e505 | ||
|
|
0c474e6b74 | ||
|
|
d5b92478ba | ||
|
|
3b45699e58 | ||
|
|
587a317e91 | ||
|
|
62db38b0d1 | ||
|
|
34ccca6a91 | ||
|
|
bf94d6bf5b | ||
|
|
fc0658574c | ||
|
|
b9e8ee4d0e | ||
|
|
47dc1a3960 | ||
|
|
6796f5cb28 | ||
|
|
f5fb5b32e4 | ||
|
|
3941e7fb1c | ||
|
|
a28051fa16 | ||
|
|
ace1610dfc | ||
|
|
cf2f987fd4 | ||
|
|
1f2639fbb5 | ||
|
|
3c2880e4ba | ||
|
|
db18519c16 | ||
|
|
b520713dc3 | ||
|
|
15a8c4f0d7 | ||
|
|
a7d80fec89 | ||
|
|
d32ecc9080 | ||
|
|
370f156ae0 | ||
|
|
97736d4163 | ||
|
|
a0338beaae | ||
|
|
8bb850d96b | ||
|
|
999e55c7af | ||
|
|
c3533de7fa | ||
|
|
0543f28fd4 | ||
|
|
48f73eab06 | ||
|
|
14c572102e | ||
|
|
c58a52c797 | ||
|
|
8c0c0f1617 | ||
|
|
b154314b79 | ||
|
|
70010d09d6 | ||
|
|
0b24533a13 | ||
|
|
be78d063a4 | ||
|
|
4cee24bd96 | ||
|
|
0a5cdfc57a | ||
|
|
5009f9d3f2 | ||
|
|
f750876425 | ||
|
|
680d488647 | ||
|
|
81af23ecb5 | ||
|
|
d4b05b2612 | ||
|
|
5a284bff26 | ||
|
|
f1a9191e84 | ||
|
|
d876454638 | ||
|
|
06de8dd532 | ||
|
|
70592c2438 | ||
|
|
5acaa31a1f | ||
|
|
79e5ff2fea | ||
|
|
7f91fe59e2 | ||
|
|
b40dad3d3e | ||
|
|
f9f816845f | ||
|
|
193b58d0fc | ||
|
|
b7f490544a | ||
|
|
5d85e3c0e2 | ||
|
|
75214c345a | ||
|
|
6d55e74ae4 | ||
|
|
710f979f94 | ||
|
|
c2a036c526 | ||
|
|
a6c52df4cb | ||
|
|
4d1ad16ea2 | ||
|
|
94c093ea57 | ||
|
|
acd421c617 | ||
|
|
4d2004c8c9 | ||
|
|
6c5bfa466f | ||
|
|
794938c525 | ||
|
|
62188ffdc7 | ||
|
|
34f7bd4341 | ||
|
|
6b45825472 | ||
|
|
b81a5d1e51 | ||
|
|
5d9e90f033 | ||
|
|
55a3e6880b | ||
|
|
beee9ecd84 | ||
|
|
b94d7a4ae8 | ||
|
|
331999c1a4 | ||
|
|
e5db1ec848 | ||
|
|
17d7161374 | ||
|
|
06d4f2b9f3 | ||
|
|
4b69fdefef | ||
|
|
13db31ede0 | ||
|
|
22a073ba1a | ||
|
|
ce9c115f3d | ||
|
|
767aa9b3e1 | ||
|
|
16ddb2461b | ||
|
|
f75827c4c6 | ||
|
|
cf03e60186 | ||
|
|
8ee071769a | ||
|
|
b312183a7b | ||
|
|
5baaf5faec | ||
|
|
d685bfd11d | ||
|
|
cf4b230b7a | ||
|
|
d46f5f4613 | ||
|
|
945ed854a4 | ||
|
|
25f0672c18 | ||
|
|
6f6c8b2ae0 | ||
|
|
7e62410f98 | ||
|
|
f49e8486c7 | ||
|
|
844bc23f8c | ||
|
|
850226b260 | ||
|
|
c5100567d6 | ||
|
|
5344854199 | ||
|
|
d790b17507 | ||
|
|
667d3851ce | ||
|
|
ba4d345f4f | ||
|
|
52816426fc | ||
|
|
38a423cf2a | ||
|
|
75ad7eb7e4 | ||
|
|
533c3b7b1b | ||
|
|
1c98999994 | ||
|
|
b19b4f047e | ||
|
|
95b6ea0e23 | ||
|
|
b3db549a65 | ||
|
|
cd768000e9 | ||
|
|
da6099c29d | ||
|
|
d36e569ede | ||
|
|
eca3757af5 | ||
|
|
7d8634ce5e | ||
|
|
d4f6785946 | ||
|
|
b468045039 | ||
|
|
f3b4f21c2e | ||
|
|
a50ae64397 | ||
|
|
2a5c58e138 | ||
|
|
d21945a6e6 | ||
|
|
0c572cb029 | ||
|
|
9d30b952ee | ||
|
|
e32876d08d | ||
|
|
340b138962 | ||
|
|
7ae0ba31cb | ||
|
|
f566671975 | ||
|
|
b7ff123e44 | ||
|
|
e1c34bc489 | ||
|
|
dedd341e02 | ||
|
|
dc8fc04b57 | ||
|
|
6a85859a35 | ||
|
|
ff1e8d9e8c | ||
|
|
16da998452 | ||
|
|
2fc7c6ab99 | ||
|
|
834f33e5a5 | ||
|
|
90a13a4e83 | ||
|
|
e4343a4f2f | ||
|
|
7852797bab | ||
|
|
4a93d2ba1e | ||
|
|
9287d711dc | ||
|
|
b5538655e0 | ||
|
|
406358aae9 | ||
|
|
a5d59e7e45 | ||
|
|
92a4ad0c5e | ||
|
|
d963bcd0c4 | ||
|
|
2e4125c81c | ||
|
|
5293ff3580 | ||
|
|
7a1349df83 | ||
|
|
de8de8f731 | ||
|
|
6d36382436 | ||
|
|
e31833b649 | ||
|
|
2dce18563d | ||
|
|
aa55c27ab0 | ||
|
|
16e321af54 | ||
|
|
c4edb29ff3 | ||
|
|
1d5cc05941 | ||
|
|
9faae7cb67 | ||
|
|
ea06fbe666 | ||
|
|
cc0b4be50c | ||
|
|
ea55cde043 | ||
|
|
840c88db89 | ||
|
|
8e8c9755a3 | ||
|
|
ba3b48e8ce | ||
|
|
d3806f7d5b | ||
|
|
0c9c1c599f | ||
|
|
af02440c40 | ||
|
|
cd53440eff | ||
|
|
3660140539 | ||
|
|
7bf1bf5369 | ||
|
|
898e30d6de | ||
|
|
a792d213e9 | ||
|
|
ebee953ebc | ||
|
|
200ab220e8 | ||
|
|
2499d25ce6 | ||
|
|
42356166c0 | ||
|
|
80a31c8427 | ||
|
|
867c6f9e97 | ||
|
|
ee9194fce1 | ||
|
|
f6322077a4 | ||
|
|
15a0e6cc54 | ||
|
|
5ee5adbb1e | ||
|
|
1d4b3eee9b | ||
|
|
fe971d23f8 | ||
|
|
34bf49845f | ||
|
|
34468e5bb0 | ||
|
|
d0dd52c5c2 | ||
|
|
98cca4ca8b | ||
|
|
b88463a785 | ||
|
|
6409188de8 | ||
|
|
510973c761 | ||
|
|
4480c26910 | ||
|
|
e778595296 | ||
|
|
f84ff7cedc | ||
|
|
04f98ae7a9 | ||
|
|
8ef3d7c20e | ||
|
|
428fd6cbba | ||
|
|
ee79335eff | ||
|
|
83d7100dd1 | ||
|
|
ccd9049806 | ||
|
|
769f36fa8e | ||
|
|
ffe89b02e9 | ||
|
|
1c158f743c | ||
|
|
4531985032 | ||
|
|
f8aa1ba391 | ||
|
|
9d790894d5 | ||
|
|
eeac1200e7 | ||
|
|
a304d87b8a | ||
|
|
ffbb1f5f0b | ||
|
|
ad53119088 | ||
|
|
fe1c525fb7 | ||
|
|
323375e8e4 | ||
|
|
8af27ea86d | ||
|
|
9335be0049 | ||
|
|
8e0f265080 | ||
|
|
ae357159ef | ||
|
|
387d40910f | ||
|
|
5344485a4f | ||
|
|
21f2f2a215 | ||
|
|
9586fac665 | ||
|
|
4aedda7ba2 | ||
|
|
f79213c9d3 | ||
|
|
eeddcb26a0 | ||
|
|
725922db78 | ||
|
|
1846dcaba9 | ||
|
|
ab96d88ebe | ||
|
|
a3bf28915d | ||
|
|
47920a5f7a | ||
|
|
1c10903823 | ||
|
|
96e4133517 | ||
|
|
8b5167a911 | ||
|
|
45edab5d88 | ||
|
|
b4375fb6fc | ||
|
|
bd2b28a7ac | ||
|
|
53149df5f1 | ||
|
|
bc2025b3ba | ||
|
|
236450f6f1 | ||
|
|
fb9e03b31d | ||
|
|
31ccb9c933 | ||
|
|
6e01a743df | ||
|
|
ed65c89516 | ||
|
|
b8b8dad9fb | ||
|
|
690d17e132 | ||
|
|
a1e9912b36 | ||
|
|
26914bfb09 | ||
|
|
079fdb3011 | ||
|
|
102cdbd53a | ||
|
|
d861264ecf | ||
|
|
9831df1427 | ||
|
|
5e6312fe93 | ||
|
|
e3237b9022 | ||
|
|
3882dd4f5a | ||
|
|
d66326b41d | ||
|
|
ef1c5dbcc9 | ||
|
|
c45c8e93de | ||
|
|
1e93bf3ec4 | ||
|
|
4bff209bd7 | ||
|
|
e5ee937c38 | ||
|
|
c418efe007 | ||
|
|
5677254b46 | ||
|
|
cb76a8165d | ||
|
|
a7a1eca0cd | ||
|
|
85bc078c46 | ||
|
|
5c347d9427 | ||
|
|
6c17efc2ab | ||
|
|
a6e929ba86 | ||
|
|
f9886f7c63 | ||
|
|
ec937f6212 | ||
|
|
b7427c3409 | ||
|
|
5bd9cf46ea | ||
|
|
c340c42ef3 | ||
|
|
27c5b4227d | ||
|
|
914e869778 | ||
|
|
e4ea30becc | ||
|
|
61f91f0e45 | ||
|
|
c6d8668e69 | ||
|
|
3c73b000df | ||
|
|
ed5a5ae86f | ||
|
|
036fbb0f49 | ||
|
|
13779c5618 | ||
|
|
6802fd0c1d | ||
|
|
7b523501ad | ||
|
|
0c8bbdf02b | ||
|
|
0b43f83daa | ||
|
|
0f2f552e87 | ||
|
|
b3bedc7c31 | ||
|
|
32a2a3f484 | ||
|
|
27de7d1c84 | ||
|
|
c31c2a4c84 | ||
|
|
9fb88eb325 | ||
|
|
7cdc2fa89a | ||
|
|
c06dbddcea | ||
|
|
a4b17d9a8f | ||
|
|
7ffea76b9e | ||
|
|
6a6db91dc9 | ||
|
|
9078fd2302 | ||
|
|
39786c5dd4 | ||
|
|
7c35a88483 | ||
|
|
1b885cb189 | ||
|
|
183bbbe6e7 | ||
|
|
562318f2d3 | ||
|
|
797401a7de | ||
|
|
d4dbfebe72 | ||
|
|
575d233078 | ||
|
|
04a78c07a7 | ||
|
|
59856bc753 | ||
|
|
a8e7a5f912 | ||
|
|
c08d4b7b9c | ||
|
|
d25148c8ae | ||
|
|
b0e640fd03 | ||
|
|
7ae07cb1ee | ||
|
|
c0b34faa79 | ||
|
|
81e3b0bd1e | ||
|
|
f74e8b9d32 | ||
|
|
7f041e8303 | ||
|
|
db05a66f3a | ||
|
|
efc6e86991 | ||
|
|
1af867d4b3 | ||
|
|
43da60595c | ||
|
|
c2849744b8 | ||
|
|
9bef2a8fcb | ||
|
|
6ec73362f2 | ||
|
|
f5bef651d8 | ||
|
|
aa77a274c3 | ||
|
|
4e90bd8e46 | ||
|
|
6a238948c2 | ||
|
|
8e2bebcfd9 | ||
|
|
b023801fe4 | ||
|
|
516b812b2b | ||
|
|
8373057758 | ||
|
|
939f5d7c20 | ||
|
|
36d1a9c738 | ||
|
|
fb845c3e03 | ||
|
|
78c52861c7 | ||
|
|
cf39395924 | ||
|
|
6061d9ec65 | ||
|
|
5a8defb478 | ||
|
|
a265038bf6 | ||
|
|
08afa0b747 | ||
|
|
bad436b858 | ||
|
|
7f0345a56a | ||
|
|
97bf174b78 | ||
|
|
975f79f6cc | ||
|
|
e72efe7fd0 | ||
|
|
d94b3e829d | ||
|
|
10c63939dc | ||
|
|
972ede9249 | ||
|
|
8f001ad88a | ||
|
|
229c5dac59 | ||
|
|
0622395ec7 | ||
|
|
66073ed460 | ||
|
|
533f40b536 | ||
|
|
13afe82fa5 | ||
|
|
10c27dfd84 | ||
|
|
057d5eca8f | ||
|
|
e89f3668a9 | ||
|
|
c46306fc1d | ||
|
|
76d534583b | ||
|
|
7b4f360a5e | ||
|
|
992b18c9de | ||
|
|
6291a5422a | ||
|
|
4581c4eeb0 | ||
|
|
d6d93e3c03 | ||
|
|
f40ca1e25c | ||
|
|
2d764ce59b | ||
|
|
a1841f26bb | ||
|
|
c4ab3eb992 | ||
|
|
617cbcaee1 | ||
|
|
a9a28e14df | ||
|
|
169c64f687 |
@@ -1,3 +1,19 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["airbnb", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": ["src"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Configuration**
|
||||
If applicable,
|
||||
```yaml
|
||||
# Please provide your service, widget or otherwise related configuration here
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. This includes things like:
|
||||
- Service version or API version
|
||||
- Docker version
|
||||
- Deployment method
|
||||
- Sample YAML configurations
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a service? Please describe.**
|
||||
A clear and concise description of what you would like to see from this service.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I would like it if [...]
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
20
.github/workflows/docker-publish.yml
vendored
20
.github/workflows/docker-publish.yml
vendored
@@ -54,13 +54,15 @@ jobs:
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
# This step is being disabled because the runner is on a self-hosted machine
|
||||
# where the cache will stick between runs.
|
||||
# - name: Cache Docker layers
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: /tmp/.buildx-cache
|
||||
# key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-buildx-
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
@@ -79,6 +81,8 @@ jobs:
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
@@ -92,7 +96,7 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# https://github.com/docker/setup-qemu-action#about
|
||||
# platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
|
||||
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
ben@phelps.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Contributing to Homepage
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the project
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## We Develop with Github
|
||||
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## Any contributions you make will be under the GNU General Public License v3.0
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [GNU General Public License v3.0](https://choosealicense.com/licenses/gpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/benphelps/homepage/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/benphelps/homepage/issues/new); it's that easy!
|
||||
|
||||
## Write bug reports with detail, background, and sample configurations
|
||||
Homepage includes a lot of configuration options and is often deploying in larger systems. Please include as much information (configurations, deployment method, Docker & API versions, etc) as you can when reporting an issue.
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Give example configurations if you can.
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
## Use a Consistent Coding Style
|
||||
This project follows the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript), please follow it when submitting pull requests.
|
||||
|
||||
## License
|
||||
By contributing, you agree that your contributions will be licensed under its GNU General Public License.
|
||||
|
||||
## References
|
||||
This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/main/CONTRIBUTING.md)
|
||||
78
Dockerfile
78
Dockerfile
@@ -1,4 +1,39 @@
|
||||
FROM node:16-alpine AS base
|
||||
# syntax = docker/dockerfile:latest
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --link package.json pnpm-lock.yaml* ./
|
||||
|
||||
RUN <<EOF
|
||||
set -xe
|
||||
apk add libc6-compat
|
||||
apk add --virtual .gyp python3 make g++
|
||||
yarn global add pnpm
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store"
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY --link --from=deps /app/node_modules ./node_modules/
|
||||
COPY . .
|
||||
|
||||
RUN <<EOF
|
||||
set -xe
|
||||
yarn next telemetry disable
|
||||
mkdir config && echo '-' > config/settings.yaml
|
||||
npm run build
|
||||
EOF
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
LABEL org.opencontainers.image.title "Homepage"
|
||||
LABEL org.opencontainers.image.description "A self-hosted services landing page, with docker and service integrations."
|
||||
LABEL org.opencontainers.image.url="https://github.com/benphelps/homepage"
|
||||
@@ -6,33 +41,22 @@ LABEL org.opencontainers.image.documentation='https://github.com/benphelps/homep
|
||||
LABEL org.opencontainers.image.source='https://github.com/benphelps/homepage'
|
||||
LABEL org.opencontainers.image.licenses='Apache-2.0'
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache --virtual .gyp python3 make g++
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN yarn global add pnpm
|
||||
RUN pnpm install
|
||||
RUN apk del .gyp
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
WORKDIR /app
|
||||
|
||||
# Copy files from context (this allows the files to copy before the builder stage is done).
|
||||
COPY --link package.json next.config.js ./
|
||||
COPY --link /public ./public
|
||||
|
||||
# Copy files from builder
|
||||
COPY --link --from=builder /app/.next/standalone ./
|
||||
COPY --link --from=builder /app/.next/static/ ./.next/static/
|
||||
|
||||
ENV PORT 3000
|
||||
EXPOSE $PORT
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \
|
||||
CMD wget --no-verbose --tries=1 --spider --no-check-certificate http://localhost:$PORT/api/healthcheck || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
70
README.md
70
README.md
@@ -1,21 +1,34 @@
|
||||

|
||||
|
||||
[](https://github.com/benphelps/homepage/actions/workflows/docker-publish.yml)
|
||||
[](https://hosted.weblate.org/engage/homepage/)
|
||||
|
||||
## Features
|
||||
|
||||
* Web Bookmarks
|
||||
* Service Bookmarks
|
||||
- Fast! The entire site is statically generated at build time, so you can expect instant load times
|
||||
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||
- Full i18n support with automatic language detection
|
||||
- Translations for Chinese, Dutch, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
|
||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||
- Service & Web Bookmarks
|
||||
- Docker Integration
|
||||
- Status light + CPU, Memory & Network Reporting *(click on the status light)*
|
||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||
- Automatic service discovery (via labels)
|
||||
- Service Integration
|
||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, NZBGet, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole
|
||||
* Homepage Widgets
|
||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify
|
||||
- Information Providers
|
||||
- Coin Market Cap
|
||||
- Information & Utility Widgets
|
||||
- System Stats (Disk, CPU, Memory)
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap ([thanks to AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
||||
* Customizable
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap
|
||||
- Automatic location detection (with HTTPS), or manual location selection
|
||||
- Search Bar
|
||||
- Customizable
|
||||
- 21 theme colors with light and dark mode support
|
||||
- Background image support
|
||||
|
||||
## Support & Suggestions
|
||||
|
||||
@@ -32,22 +45,22 @@ For configuration options, examples and more, [please check out the Wiki](https:
|
||||
Using docker compose:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
version: "3.3"
|
||||
services:
|
||||
homepage:
|
||||
image: ghcr.io/benphelps/homepage:main
|
||||
image: ghcr.io/benphelps/homepage:latest
|
||||
container_name: homepage
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- /path/to/config:/app/config
|
||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||
```
|
||||
|
||||
or docker run:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:main
|
||||
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:latest
|
||||
```
|
||||
|
||||
### With Node
|
||||
@@ -65,6 +78,8 @@ pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
If this is your first time starting, copy the `src/skeleton` directory to `config/` to populate initial example config files.
|
||||
|
||||
Finally, run the server:
|
||||
|
||||
```bash
|
||||
@@ -78,6 +93,8 @@ Configuration files will be genereted and placed on the first request.
|
||||
Configuration is done in the /config directory using .yaml files. Refer to each config for
|
||||
the specific configuration options.
|
||||
|
||||
You may also check [the wiki](https://github.com/benphelps/homepage/wiki) for detailed configuration instructions, examples and more.
|
||||
|
||||
## Development
|
||||
|
||||
Install NPM packages, this project uses [pnpm](https://pnpm.io/) (and so should you!):
|
||||
@@ -95,3 +112,32 @@ pnpm dev
|
||||
Open [http://localhost:3000](http://localhost:3000) to start.
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) application, see their doucmentation for more information:
|
||||
|
||||
## Contributors
|
||||
|
||||
Huge thanks to the all the contributors who have helped make this project what it is today! In alphabetical order:
|
||||
|
||||
- [aidenpwnz](https://github.com/benphelps/homepage/commits?author=aidenpwnz) - Nginx Proxy Manager, Search Bar Widget
|
||||
- [AlexFullmoon](https://github.com/benphelps/homepage/commits?author=AlexFullmoon) - OpenWeatherMap Widget
|
||||
- [AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves) - Spanish Translation
|
||||
- [boerniee](https://github.com/benphelps/homepage/commits?author=boerniee) - German Translation
|
||||
- [comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu) - Norwegian Bokmål Translation
|
||||
- [deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony) - Dutch Translation
|
||||
- [desolaris](https://github.com/benphelps/homepage/commits?author=desolaris) - Russian Translation
|
||||
- [ilusi0n](https://github.com/benphelps/homepage/commits?author=ilusi0n) - Jellyseerr Integration
|
||||
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
||||
- [jackblk](https://github.com/benphelps/homepage/commits?author=jackblk) - Vietnamese Translation
|
||||
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd, Transmission & qBittorrent Integrations
|
||||
- [juanmanuelbc](https://github.com/benphelps/homepage/commits?author=juanmanuelbc) - Spanish and Catalan Translations
|
||||
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
||||
- [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese Translation
|
||||
- [Nonoss117](https://github.com/benphelps/homepage/commits?author=Nonoss117) - French Translation
|
||||
- [pacoculebras](https://github.com/benphelps/homepage/commits?author=pacoculebras) - Catalan Translation
|
||||
- [psychodracon](https://github.com/benphelps/homepage/commits?author=psychodracon) - Polish Translation
|
||||
- [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
|
||||
- [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
|
||||
- [SuperDOS](https://github.com/benphelps/homepage/commits?author=SuperDOS) - Swedish Translation
|
||||
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration
|
||||
- [andrii-kryvoviaz](https://github.com/benphelps/homepage/commits?author=andrii-kryvoviaz) - Background opacity option
|
||||
- Daniel Varga - German & Hungarian Translation
|
||||
- [ShlomiPorush](https://github.com/benphelps/homepage/commits?author=ShlomiPorush) - Hebrew Translation
|
||||
|
||||
9
docker-entrypoint.sh
Executable file
9
docker-entrypoint.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# This is in attempt to preserve the original behavior of the Dockerfile,
|
||||
# while also supporting the lscr.io /config directory
|
||||
[ ! -d "/app/config" ] && ln -s /config /app/config
|
||||
|
||||
node server.js
|
||||
BIN
images/icons/coinmarketcap.png
Normal file
BIN
images/icons/coinmarketcap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -3,9 +3,9 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
swcMinify: false,
|
||||
experimental: { images: { allowFutureImage: true, unoptimized: true } },
|
||||
images: {
|
||||
domains: ["cdn.jsdelivr.net"],
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
44
package.json
44
package.json
@@ -6,30 +6,48 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"telemetry": "next telemetry disable"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"dockerode": "^3.3.3",
|
||||
"@headlessui/react": "^1.7.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dockerode": "^3.3.4",
|
||||
"follow-redirects": "^1.15.2",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json-rpc-2.0": "^1.4.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "12.2.5",
|
||||
"next": "^12.3.0",
|
||||
"node-os-utils": "^1.3.7",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"raw-body": "^2.5.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-icons": "^4.4.0",
|
||||
"rutorrent-promise": "^2.0.0",
|
||||
"swr": "^1.3.0"
|
||||
"shvl": "^3.0.0",
|
||||
"swr": "^1.3.0",
|
||||
"tough-cookie": "^4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"autoprefixer": "^10.4.9",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^12.3.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
727
pnpm-lock.yaml
generated
727
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
161
public/locales/ca/common.json
Normal file
161
public/locales/ca/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Falta el tipus de widget: {{type}}",
|
||||
"api_error": "Error d'API",
|
||||
"status": "Estat"
|
||||
},
|
||||
"weather": {
|
||||
"allow": "Feu clic per permetre",
|
||||
"updating": "Actualitzant",
|
||||
"wait": "Si us plau, espereu",
|
||||
"current": "Localització actual"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cercar…"
|
||||
},
|
||||
"transmission": {
|
||||
"seed": "Llavors",
|
||||
"download": "Descàrrega",
|
||||
"upload": "Càrrega",
|
||||
"leech": "Companys"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Volgut",
|
||||
"queued": "En cua",
|
||||
"series": "Sèries"
|
||||
},
|
||||
"speedtest": {
|
||||
"ping": "Ping",
|
||||
"upload": "Càrrega",
|
||||
"download": "Descàrrega"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Lliure",
|
||||
"used": "Usat",
|
||||
"load": "Càrrega"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Fora de línia"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Reproduint",
|
||||
"transcoding": "Transcodificant",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "Sense transmissions actives"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Reproduint",
|
||||
"transcoding": "Transcodificant",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "Sense transmissions actives"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Taxa",
|
||||
"remaining": "Restant",
|
||||
"downloaded": "Descarregat"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Taxa",
|
||||
"queue": "Cua",
|
||||
"timeleft": "Temps restant"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Actiu",
|
||||
"upload": "Càrrega",
|
||||
"download": "Descàrrega"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Volgut",
|
||||
"queued": "En cua",
|
||||
"movies": "Pel·lícules"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Volgut",
|
||||
"queued": "En cua",
|
||||
"books": "Llibres"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendent",
|
||||
"approved": "Aprovat",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pendent",
|
||||
"approved": "Aprovat",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pendent",
|
||||
"approved": "Aprovat",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultes",
|
||||
"blocked": "Bloquejat",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Executant",
|
||||
"stopped": "Aturat",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Encaminadors",
|
||||
"services": "Serveis",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"total": "Total",
|
||||
"enabled": "Activat",
|
||||
"disabled": "Desactivat"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configura una o més criptomonedes per fer el seguiment",
|
||||
"1hour": "1 Hora",
|
||||
"1day": "1 Dia",
|
||||
"7days": "7 Dies",
|
||||
"30days": "30 Dies"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplicacions",
|
||||
"clients": "Clients",
|
||||
"messages": "Missatges"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexadors",
|
||||
"numberOfGrabs": "Captures",
|
||||
"numberOfQueries": "Consultes",
|
||||
"numberOfFailGrabs": "Captures fallides",
|
||||
"numberOfFailQueries": "Consultes fallides"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configurat",
|
||||
"errored": "Amb errors"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Episodis que falten",
|
||||
"missingMovies": "Pel·lícules que falten"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Volgut",
|
||||
"queued": "En cua",
|
||||
"albums": "Àlbums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Consultes",
|
||||
"blocked": "Bloquejat",
|
||||
"filtered": "Filtrat",
|
||||
"latency": "Latència"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Descàrrega",
|
||||
"upload": "Càrrega",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/de/common.json
Normal file
161
public/locales/de/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Fehlender Widget-Typ: {{type}}",
|
||||
"api_error": "API-Fehler",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Suche…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Gesamt",
|
||||
"free": "Frei",
|
||||
"used": "Gebraucht",
|
||||
"load": "Belastung"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mem",
|
||||
"cpu": "Prozessor",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Keine aktiven streamen"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Keine aktiven streamen"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
"upload": "Hochladen",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"series": "Serie"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"movies": "Filme"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Abfragen",
|
||||
"blocked": "Blockiert",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Betrieb",
|
||||
"stopped": "Gestoppt",
|
||||
"total": "Gesamt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Router",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"total": "Gesamt"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Aktueller Standort",
|
||||
"allow": "Zum Zulassen anklicken",
|
||||
"updating": "Aktualisieren",
|
||||
"wait": "Bitte warten"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
172
public/locales/en/common.json
Normal file
172
public/locales/en/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"common": {
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"byterate": "{{value, rate}}",
|
||||
"bitrate": "{{value, rate(bits: true)}}",
|
||||
"percent": "{{value, percent}}",
|
||||
"number": "{{value, number}}",
|
||||
"ms": "{{value, number}}"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr":{
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
}
|
||||
}
|
||||
161
public/locales/es/common.json
Normal file
161
public/locales/es/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Falta el tipo de widget: {{type}}",
|
||||
"api_error": "Error de API",
|
||||
"status": "Estado"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Libre",
|
||||
"used": "Usado",
|
||||
"load": "Carga"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Recibido",
|
||||
"tx": "Transmitido",
|
||||
"mem": "Memoria",
|
||||
"cpu": "Procesador",
|
||||
"offline": "Desconectado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Activo",
|
||||
"upload": "Subida",
|
||||
"download": "Descarga"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "En cola",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "En cola",
|
||||
"movies": "Películas"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "En cola",
|
||||
"books": "Libros"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"gravity": "Gravedad"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Subida",
|
||||
"download": "Descarga",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "En ejecución",
|
||||
"stopped": "Detenido",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Enrutadores",
|
||||
"services": "Servicios",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Ubicación actual",
|
||||
"allow": "Haga clic para permitir",
|
||||
"updating": "Actualizando",
|
||||
"wait": "Espere, por favor"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Tasa",
|
||||
"queue": "En cola",
|
||||
"timeleft": "Tiempo restante"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Tasa",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Descargado"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configurar una o más criptomonedas para rastrear",
|
||||
"1hour": "1 Hora",
|
||||
"1day": "1 Día",
|
||||
"7days": "7 Días",
|
||||
"30days": "30 Días"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplicaciones",
|
||||
"clients": "Clientes",
|
||||
"messages": "Mensajes"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexadores",
|
||||
"numberOfGrabs": "Capturas",
|
||||
"numberOfQueries": "Consultas",
|
||||
"numberOfFailGrabs": "Capturas fallidas",
|
||||
"numberOfFailQueries": "Consultas fallidas"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Compañeros",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configurado",
|
||||
"errored": "Con errores"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Episodios perdidos",
|
||||
"missingMovies": "Películas perdidas"
|
||||
},
|
||||
"lidarr": {
|
||||
"queued": "En cola",
|
||||
"wanted": "Más deseado",
|
||||
"albums": "Álbumes"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"filtered": "Filtrado",
|
||||
"latency": "Latencia"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Compañeros",
|
||||
"seed": "Semillas"
|
||||
}
|
||||
}
|
||||
172
public/locales/fr/common.json
Normal file
172
public/locales/fr/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Type de widget manquant: {{type}}",
|
||||
"api_error": "Erreur de l'API",
|
||||
"status": "Statut"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Recherche…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Libre",
|
||||
"used": "Utilisé",
|
||||
"load": "Charge"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mém",
|
||||
"cpu": "Cpu",
|
||||
"offline": "Hors ligne"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "En lecture",
|
||||
"transcoding": "Transcodage",
|
||||
"bitrate": "Débit",
|
||||
"no_active": "Aucun flux actif"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "En lecture",
|
||||
"transcoding": "Transcodage",
|
||||
"bitrate": "Débit",
|
||||
"no_active": "Aucun flux actif"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Actif",
|
||||
"upload": "Envoi",
|
||||
"download": "Réception"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Demandé",
|
||||
"queued": "En queue",
|
||||
"series": "Séries"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Demandé",
|
||||
"queued": "En queue",
|
||||
"movies": "Films"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Demandé",
|
||||
"queued": "En Queue",
|
||||
"books": "Livres"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "En attente",
|
||||
"approved": "Validé",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Validé",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Requêtes",
|
||||
"blocked": "Bloqué",
|
||||
"gravity": "Listes dom. bloqués"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Envoi",
|
||||
"download": "Récept.",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Démarré",
|
||||
"stopped": "Arrêté",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routeurs",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"total": "Total"
|
||||
},
|
||||
"common": {
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"number": "{{value, number}}",
|
||||
"byterate": "{{value, bytes}}",
|
||||
"bitrate": "{{value, bytes(bits: true)}}",
|
||||
"percent": "{{value, percent}}",
|
||||
"ms": "{{value, number}}"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Localisation actuelle",
|
||||
"allow": "Cliquez pour autoriser",
|
||||
"updating": "Mise à jour",
|
||||
"wait": "Veuillez patienter"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Demande",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Débit",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Temps restant"
|
||||
},
|
||||
"nzbget": {
|
||||
"remaining": "Restant",
|
||||
"downloaded": "Téléchargé",
|
||||
"rate": "Débit"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configurer une ou plusieurs crypto-monnaies à suivre",
|
||||
"1hour": "1 Heure",
|
||||
"1day": "1 Jour",
|
||||
"7days": "7 Jours",
|
||||
"30days": "30 Jours"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applis",
|
||||
"clients": "Clients",
|
||||
"messages": "Msg"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexeurs",
|
||||
"numberOfGrabs": "Capture",
|
||||
"numberOfQueries": "Demandes",
|
||||
"numberOfFailGrabs": "Capture échouée",
|
||||
"numberOfFailQueries": "Demande échouée"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Réception",
|
||||
"upload": "Envoi",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configuré",
|
||||
"errored": "En erreur"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Épisodes manquants",
|
||||
"missingMovies": "Films manquants"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Demandé",
|
||||
"queued": "En queue",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Requêtes",
|
||||
"blocked": "Bloquées",
|
||||
"filtered": "Filtrées",
|
||||
"latency": "Latence"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Réception",
|
||||
"upload": "Envoi",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/he/common.json
Normal file
161
public/locales/he/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "סוג ווידג'ט חסר: {{type}}",
|
||||
"api_error": "שגיאת API",
|
||||
"status": "סטטוס"
|
||||
},
|
||||
"weather": {
|
||||
"current": "מיקום נוכחי",
|
||||
"allow": "יש ללחוץ כדי לאשר",
|
||||
"updating": "מעדכן",
|
||||
"wait": "המתן בבקשה"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "חיפוש…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "סה\"כ",
|
||||
"free": "פנוי",
|
||||
"used": "בשימוש",
|
||||
"load": "עומס"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "זיכרון",
|
||||
"cpu": "מעבד",
|
||||
"offline": "כבוי"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "מנגן",
|
||||
"transcoding": "מקודד",
|
||||
"bitrate": "סיביות",
|
||||
"no_active": "אין הזרמות פעילות"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "מנגן",
|
||||
"transcoding": "מקודד",
|
||||
"bitrate": "סיביות",
|
||||
"no_active": "אין הזרמות פעילות"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "יחס",
|
||||
"remaining": "נותר",
|
||||
"downloaded": "הורד"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "יחס",
|
||||
"queue": "תור",
|
||||
"timeleft": "זמן שנותר"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "פעיל",
|
||||
"upload": "העלאה",
|
||||
"download": "הורדה"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "הורדה",
|
||||
"upload": "העלאה",
|
||||
"leech": "בהורדה",
|
||||
"seed": "בשיתוף"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "הורדה",
|
||||
"upload": "העלאה",
|
||||
"leech": "בהורדה",
|
||||
"seed": "בשיתוף"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "מבוקש",
|
||||
"queued": "בתור",
|
||||
"series": "סדרות"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "מבוקש",
|
||||
"queued": "בתור",
|
||||
"movies": "סרטים"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "מבוקש",
|
||||
"queued": "בתור",
|
||||
"albums": "אלבומים"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "מבוקש",
|
||||
"queued": "בתור",
|
||||
"books": "ספרים"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "פרקים חסרים",
|
||||
"missingMovies": "סרטים חסרים"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "ממתין",
|
||||
"approved": "מאושר",
|
||||
"available": "זמין"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "ממתין",
|
||||
"approved": "מאושר",
|
||||
"available": "זמין"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "ממתין",
|
||||
"approved": "מאושר",
|
||||
"available": "זמין"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "שאילתות",
|
||||
"blocked": "נחסם",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "שאילתות",
|
||||
"blocked": "נחסם",
|
||||
"filtered": "מסונן",
|
||||
"latency": "השהיה"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "העלאה",
|
||||
"download": "הורדה",
|
||||
"ping": "פינג"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "פעיל",
|
||||
"stopped": "נעצר",
|
||||
"total": "סה\"כ"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "ניתובים",
|
||||
"services": "שירותים",
|
||||
"middleware": "מתווך"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "מופעל",
|
||||
"disabled": "מבוטל",
|
||||
"total": "סה\"כ"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "קבע את התצורה של מטבע קריפטו אחד או יותר למעקב",
|
||||
"1hour": "שעה אחת",
|
||||
"1day": "יום 1",
|
||||
"7days": "7 יום",
|
||||
"30days": "30 יום"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "אפליקציות",
|
||||
"clients": "לקוחות",
|
||||
"messages": "הודעות"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "אינדקסים",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "שאילתות",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "מוגדר",
|
||||
"errored": "שגיאה"
|
||||
}
|
||||
}
|
||||
161
public/locales/hr/common.json
Normal file
161
public/locales/hr/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"overseerr": {
|
||||
"available": "Available",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"latency": "Latency",
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered"
|
||||
},
|
||||
"npm": {
|
||||
"total": "Total",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"rutorrent": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"active": "Active"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"gotify": {
|
||||
"clients": "Clients",
|
||||
"messages": "Messages",
|
||||
"apps": "Applications"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/hu/common.json
Normal file
161
public/locales/hu/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"resources": {
|
||||
"total": "Összes",
|
||||
"free": "Szabad",
|
||||
"used": "Használt",
|
||||
"load": "Terhelés"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"lidarr": {
|
||||
"albums": "Albumok",
|
||||
"wanted": "Keresett",
|
||||
"queued": "Sorban áll"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Keresett",
|
||||
"queued": "Sorban áll",
|
||||
"books": "Könyvek"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Hiányzó epizódok",
|
||||
"missingMovies": "Hiányzó filmek"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Hiányzó Widget Típus: {{type}}",
|
||||
"api_error": "API Hiba",
|
||||
"status": "Státusz"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Aktuális hely",
|
||||
"allow": "Kattints az engedélyezéshez",
|
||||
"updating": "Frissítés",
|
||||
"wait": "Kérlek várj"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Keresés…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Lejátszás",
|
||||
"transcoding": "Átkódolás",
|
||||
"bitrate": "Bitráta",
|
||||
"no_active": "Nincs aktív lejátszás"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Lejátszás folyamatban",
|
||||
"transcoding": "Átkódolás",
|
||||
"bitrate": "Bitráta",
|
||||
"no_active": "Nincs aktív lejátszás"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Ráta",
|
||||
"remaining": "Hátralévő",
|
||||
"downloaded": "Letöltött"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Ráta",
|
||||
"queue": "Sor",
|
||||
"timeleft": "Hátralévő idő"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktív",
|
||||
"upload": "Feltöltés",
|
||||
"download": "Letöltés"
|
||||
},
|
||||
"transmission": {
|
||||
"leech": "Leechelés",
|
||||
"seed": "Seedelés",
|
||||
"download": "Letöltés",
|
||||
"upload": "Feltöltés"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Letöltés",
|
||||
"upload": "Feltöltés",
|
||||
"leech": "Leechelés",
|
||||
"seed": "Seedelés"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Keresett",
|
||||
"queued": "Sorban áll",
|
||||
"series": "Sorozat"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Keresett",
|
||||
"queued": "Sorban áll",
|
||||
"movies": "Filmek"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Függőben",
|
||||
"approved": "Engedélyezett",
|
||||
"available": "Elérhető"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Függőben",
|
||||
"approved": "Engedélyezett",
|
||||
"available": "Elérhető"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Függőben",
|
||||
"approved": "Engedélyezett",
|
||||
"available": "Elérhető"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Lekérdezések",
|
||||
"blocked": "Blokkolt",
|
||||
"gravity": "Gravitáció"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Lekérdezések",
|
||||
"blocked": "Blokkolt",
|
||||
"filtered": "Szűrt",
|
||||
"latency": "Késleltetés"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Feltöltés",
|
||||
"download": "Letöltés",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Futó",
|
||||
"stopped": "Megállított",
|
||||
"total": "Összes"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routerek",
|
||||
"services": "Folyamatok",
|
||||
"middleware": "Közvetítő"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Bekapcsolva",
|
||||
"disabled": "Kikapcsolva",
|
||||
"total": "Összes"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Állíts be egy vagy több Cryptovalutát a követéshez",
|
||||
"1hour": "1 Óra",
|
||||
"1day": "1 Nap",
|
||||
"7days": "7 Nap",
|
||||
"30days": "30 Nap"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applikációk",
|
||||
"clients": "Kliensek",
|
||||
"messages": "Üzenetek"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexerek",
|
||||
"numberOfGrabs": "Fogott",
|
||||
"numberOfFailGrabs": "Hibás fogások",
|
||||
"numberOfQueries": "Lekérdezések",
|
||||
"numberOfFailQueries": "Hibás lekérdezések"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Beállított",
|
||||
"errored": "Hibás"
|
||||
}
|
||||
}
|
||||
161
public/locales/it/common.json
Normal file
161
public/locales/it/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"docker": {
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline",
|
||||
"rx": "RX"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "In riproduzione",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Nessuno Stream Attivo"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "In riproduzione",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Nessuno Stream Attivo"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "In esecuzione",
|
||||
"stopped": "Fermati",
|
||||
"total": "Totali"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Servizi",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "Errore API",
|
||||
"status": "Stato"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cerca…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totale",
|
||||
"free": "Libero",
|
||||
"used": "In utilizzo",
|
||||
"load": "Load"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Attivo",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"series": "Serie",
|
||||
"wanted": "Rchiesti",
|
||||
"queued": "In coda"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Richiesti",
|
||||
"queued": "In coda",
|
||||
"movies": "Film"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "In attesa",
|
||||
"approved": "Approvati",
|
||||
"available": "Disponibili"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "In attesa",
|
||||
"approved": "Approvati",
|
||||
"available": "Disponibili"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Richieste",
|
||||
"blocked": "Bloccati",
|
||||
"gravity": "Severità"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Attivi",
|
||||
"disabled": "Disabilitati",
|
||||
"total": "Totali"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Posizione Attuale",
|
||||
"allow": "Clicca per consentire",
|
||||
"updating": "Aggiornamento in corso",
|
||||
"wait": "Attendi per favore"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "In attesa",
|
||||
"approved": "Approvati",
|
||||
"available": "Disponibili"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"1hour": "1 Hour",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"leech": "Leech",
|
||||
"upload": "Upload",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/nb-NO/common.json
Normal file
161
public/locales/nb-NO/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Manglende miniprogramstype: {{type}}",
|
||||
"api_error": "API-feil",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Søk …"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totalt",
|
||||
"free": "Ledig",
|
||||
"used": "Brukt",
|
||||
"load": "Last inn"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Mottatt",
|
||||
"tx": "Sendt",
|
||||
"mem": "Minne",
|
||||
"cpu": "Prosessor",
|
||||
"offline": "Frakoblet"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Ingen aktive strømmer"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Ingen aktive strømmer"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
"upload": "Opplasting",
|
||||
"download": "Nedlasting"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Ønsket",
|
||||
"queued": "I kø",
|
||||
"series": "Serie"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Ønsket",
|
||||
"queued": "I kø",
|
||||
"movies": "Filmer"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Spørringer",
|
||||
"blocked": "Blokkert",
|
||||
"gravity": "Gravitet"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Opplasting",
|
||||
"download": "Nedlasting",
|
||||
"ping": "Ekkoforespørsel"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Kjører",
|
||||
"stopped": "Stoppet",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Rutere",
|
||||
"services": "Tjenester",
|
||||
"middleware": "Midtvare"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Påskrudd",
|
||||
"disabled": "Avskrudd",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"weather": {
|
||||
"allow": "Klikk for å tillate",
|
||||
"updating": "Oppdaterer …",
|
||||
"wait": "Vent litt …",
|
||||
"current": "Nåværende posisjon"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Takt",
|
||||
"queue": "Kø",
|
||||
"timeleft": "Gjenstående tid"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Takt",
|
||||
"downloaded": "Nedlastet",
|
||||
"remaining": "Gjenstående"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Sett opp én eller flere kryptovalutaer å holde øye med",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Programmer",
|
||||
"clients": "Klienter",
|
||||
"messages": "Meldinger"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indekserere",
|
||||
"numberOfGrabs": "Hentninger",
|
||||
"numberOfQueries": "Spørringer",
|
||||
"numberOfFailGrabs": "Mislykkede hentinger",
|
||||
"numberOfFailQueries": "Mislykkede spørringer"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/nl/common.json
Normal file
161
public/locales/nl/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Totaal",
|
||||
"free": "Vrij",
|
||||
"used": "Gebruikt",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Draaiend",
|
||||
"stopped": "Gestopt",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"weather": {
|
||||
"updating": "Updaten",
|
||||
"wait": "Even geduld",
|
||||
"current": "Huidige Locatie",
|
||||
"allow": "Klik om toe te staan"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Zoeken…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Afspelen",
|
||||
"transcoding": "Transcodering",
|
||||
"bitrate": "Bitsnelheid",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Afspelen",
|
||||
"transcoding": "Transcodering",
|
||||
"bitrate": "Bitsnelheid",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Actief",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Gezocht",
|
||||
"queued": "In de wachtrij",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"movies": "Films",
|
||||
"wanted": "Gezocht",
|
||||
"queued": "In de wachtrij"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "In afwachting",
|
||||
"approved": "Goedgekeurd",
|
||||
"available": "Beschikbaar"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "In afwachting",
|
||||
"approved": "Goedgekeurd",
|
||||
"available": "Beschikbaar"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Geblokkeerd",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"7days": "7 Days",
|
||||
"1day": "1 Day",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/pl/common.json
Normal file
161
public/locales/pl/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"weather": {
|
||||
"allow": "Kliknij, aby zezwolić",
|
||||
"updating": "Aktualizacja",
|
||||
"wait": "Proszę czekać",
|
||||
"current": "Aktualna lokalizacja"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Szukaj…"
|
||||
},
|
||||
"resources": {
|
||||
"used": "Użyte",
|
||||
"load": "Obciążenie",
|
||||
"total": "Całkowite",
|
||||
"free": "Wolne"
|
||||
},
|
||||
"emby": {
|
||||
"no_active": "Brak aktywnych strumieni",
|
||||
"playing": "Odtwarzanie",
|
||||
"transcoding": "Transkodowanie",
|
||||
"bitrate": "Bitrate"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Odtwarzanie",
|
||||
"transcoding": "Transkodowanie",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Brak aktywnych strumieni"
|
||||
},
|
||||
"speedtest": {
|
||||
"download": "Pobieranie",
|
||||
"ping": "Ping",
|
||||
"upload": "Wysyłanie"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Działające",
|
||||
"stopped": "Zatrzymane",
|
||||
"total": "Ogólnie"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"1day": "1 dzień",
|
||||
"7days": "7 dni",
|
||||
"30days": "30 dni",
|
||||
"1hour": "1 godzina",
|
||||
"configure": "Wybierz jedną lub więcej kryptowalut do śledzenia"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplikacje",
|
||||
"clients": "Klienci",
|
||||
"messages": "Wiadomości"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Brakujący typ widżetu: {{type}}",
|
||||
"api_error": "Błąd API",
|
||||
"status": "Stan"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Szybkość",
|
||||
"remaining": "Pozostało",
|
||||
"downloaded": "Pobrano"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Szybkość",
|
||||
"queue": "Kolejka",
|
||||
"timeleft": "Pozostało"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktywny",
|
||||
"upload": "Wysyłanie",
|
||||
"download": "Pobieranie"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Pobieranie",
|
||||
"upload": "Wysyłanie",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"series": "Seriale"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"movies": "Filmy"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"albums": "Albumy"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"books": "Książki"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Brakujące odcinki",
|
||||
"missingMovies": "Brakujące filmy"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Zapytania",
|
||||
"blocked": "Zablokowane",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routery",
|
||||
"services": "Serwisy",
|
||||
"middleware": "Pośrednicy"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
"total": "Ogólnie"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indeksery",
|
||||
"numberOfGrabs": "Pochwycenia",
|
||||
"numberOfQueries": "Zapytania",
|
||||
"numberOfFailGrabs": "Nieudane pochwycenia",
|
||||
"numberOfFailQueries": "Nieudane zapytania"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Skonfigurowane",
|
||||
"errored": "Błędne"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
172
public/locales/pt/common.json
Normal file
172
public/locales/pt/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Widget ausente: {{type}}",
|
||||
"api_error": "Erro da API",
|
||||
"status": "Status"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Pesquisar…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Livre",
|
||||
"used": "Usado",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mem",
|
||||
"cpu": "CPU",
|
||||
"offline": "Desligado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "A reproduzir",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Sem streams ativas"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Reproduzindo",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "Sem streams ativas"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Ativo",
|
||||
"upload": "Envio",
|
||||
"download": "ReceçãoDownload"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Desejada",
|
||||
"queued": "Em fila",
|
||||
"series": "Séries"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Desejado",
|
||||
"queued": "Fila",
|
||||
"movies": "Filmes"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Em fila",
|
||||
"books": "Livros"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovada",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovada",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Envio",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "A correr",
|
||||
"stopped": "Parado",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Serviços",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Ativo",
|
||||
"disabled": "Desabilitado",
|
||||
"total": "Total"
|
||||
},
|
||||
"common": {
|
||||
"bytes": "{{value, bytes}}",
|
||||
"bbytes": "{{value, bytes(binary: true)}}",
|
||||
"bits": "{{value, bytes(bits: true)}}",
|
||||
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||
"number": "{{value, number}}",
|
||||
"byterate": "{{value, bytes}}",
|
||||
"ms": "{{value, number}}",
|
||||
"bitrate": "{{value, bytes(bits: true)}}",
|
||||
"percent": "{{value, percent}}"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Localização atual",
|
||||
"allow": "Clicar para permitir",
|
||||
"updating": "A atualizar",
|
||||
"wait": "Por favor aguarde"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovado",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Fila",
|
||||
"timeleft": "Tempo restante"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configurar uma ou mais moedas",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplicações",
|
||||
"clients": "Clientes",
|
||||
"messages": "Mensagens"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Falhados",
|
||||
"numberOfFailQueries": "Pesquisas falhadas"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Envio",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"queued": "Queued",
|
||||
"wanted": "Wanted",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/ru/common.json
Normal file
161
public/locales/ru/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Отсутствует тип виджета: {{type}}",
|
||||
"api_error": "Ошибка API",
|
||||
"status": "Статус"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Всего",
|
||||
"free": "Свободно",
|
||||
"used": "Использовано",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Тx",
|
||||
"mem": "Память",
|
||||
"cpu": "Процессор",
|
||||
"offline": "Не в сети"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Воспроизведение",
|
||||
"transcoding": "Транскодирование",
|
||||
"bitrate": "Битрейт",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Воспроизведение",
|
||||
"transcoding": "Транскодирование",
|
||||
"bitrate": "Битрейт",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Активный",
|
||||
"upload": "Загрузить",
|
||||
"download": "Скачать"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Хотел",
|
||||
"queued": "В очереди",
|
||||
"series": "Серии"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Хотел",
|
||||
"queued": "В очереди",
|
||||
"movies": "Фильмы"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Ожидание",
|
||||
"approved": "Одобрено",
|
||||
"available": "Доступно"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Ожидание",
|
||||
"approved": "Одобрено",
|
||||
"available": "Доступно"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Запросы",
|
||||
"blocked": "Заблокировано",
|
||||
"gravity": "Сила тяжести"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Загрузка",
|
||||
"download": "Скачать",
|
||||
"ping": "пинг"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Запущено",
|
||||
"stopped": "Остановлено",
|
||||
"total": "Всего"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Маршрутизаторы",
|
||||
"services": "Сервисы",
|
||||
"middleware": "Промежуточное программное обеспечение"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"total": "Всего"
|
||||
},
|
||||
"weather": {
|
||||
"wait": "Пожалуйста подождите",
|
||||
"current": "Текущая локация",
|
||||
"allow": "Нажмите, чтобы разрешить",
|
||||
"updating": "Обновление"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/sv/common.json
Normal file
161
public/locales/sv/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Saknar Widget-typ: {{type}}",
|
||||
"api_error": "API-fel",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Nuvarande plats",
|
||||
"allow": "Klicka för att tillåta",
|
||||
"updating": "Uppdaterar",
|
||||
"wait": "Vänligen vänta"
|
||||
},
|
||||
"resources": {
|
||||
"load": "Laddar",
|
||||
"total": "Total",
|
||||
"free": "Ledigt",
|
||||
"used": "Använt"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Sök…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spelar",
|
||||
"transcoding": "Omkodning",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Inga aktiva strömmar"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spelar",
|
||||
"transcoding": "Omkodning",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Inga aktiva strömmar"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Hastighet",
|
||||
"remaining": "Återstående",
|
||||
"downloaded": "Nedladdat"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Hastighet",
|
||||
"queue": "Kö",
|
||||
"timeleft": "Tid kvar"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiva",
|
||||
"upload": "Uppladdning",
|
||||
"download": "Nedladdning"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Nedladdning",
|
||||
"upload": "Uppladdning",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"series": "Serier"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"movies": "Filmer"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"albums": "Album"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Eftersökt",
|
||||
"queued": "I kö",
|
||||
"books": "Böcker"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Saknade program",
|
||||
"missingMovies": "Saknade filmer"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"pihole": {
|
||||
"blocked": "Blockerad",
|
||||
"queries": "Förfrågningar",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Uppladdning",
|
||||
"download": "Nedladdning",
|
||||
"ping": "Svarstid"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Körs",
|
||||
"stopped": "Stoppade",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Tjänster",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Aktiverad",
|
||||
"disabled": "Inaktiverad",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Konfigurera en eller flera kryptovalutor att följa",
|
||||
"1hour": "1 timme",
|
||||
"1day": "1 dag",
|
||||
"7days": "7 dagar",
|
||||
"30days": "30 dagar"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Program",
|
||||
"clients": "Klienter",
|
||||
"messages": "Meddelande"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexerare",
|
||||
"numberOfGrabs": "Hämtningar",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Misslyckade hämtningar",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Konfigurerade",
|
||||
"errored": "Felaktiga"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/vi/common.json
Normal file
161
public/locales/vi/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Thiếu loại Widget: {{type}}",
|
||||
"api_error": "Lỗi API",
|
||||
"status": "Trạng thái"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Tìm kiếm…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Tổng",
|
||||
"free": "Dư",
|
||||
"used": "Đã dùng",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "BỘ NHỚ",
|
||||
"cpu": "CPU",
|
||||
"offline": "Ngoại tuyến"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Đang chơi",
|
||||
"transcoding": "Chuyển định dạng",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Đang chơi",
|
||||
"transcoding": "Chuyển định dạng",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Hoạt động",
|
||||
"upload": "Tải lên",
|
||||
"download": "Tải xuống"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Phim"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Đang tìm",
|
||||
"queued": "Đang chờ",
|
||||
"books": "Sách"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Đang xử lý",
|
||||
"approved": "Đã duyệt",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Vị trí hiện tại",
|
||||
"allow": "Bấm để đồng ý",
|
||||
"updating": "Đang cập nhật",
|
||||
"wait": "Vui lòng chờ"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Đã duyệt",
|
||||
"available": "Available"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Hàng chờ",
|
||||
"timeleft": "Thời gian còn lại"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Đã tải"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/zh-CN/common.json
Normal file
161
public/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "缺少小部件类型:{{type}}",
|
||||
"api_error": "API错误",
|
||||
"status": "状态"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "共",
|
||||
"free": "空闲",
|
||||
"used": "已用",
|
||||
"load": "负载"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "接收",
|
||||
"tx": "发送",
|
||||
"mem": "内存",
|
||||
"cpu": "处理器",
|
||||
"offline": "离线"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "正在播放",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "暂无播放"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "正在播放",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "暂无播放"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "活动中",
|
||||
"upload": "上传",
|
||||
"download": "下载"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "通缉",
|
||||
"queued": "排队",
|
||||
"series": "系列"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "订阅",
|
||||
"queued": "队列",
|
||||
"movies": "电影"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "订阅",
|
||||
"queued": "队列",
|
||||
"books": "书籍"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "待办的",
|
||||
"approved": "已批准",
|
||||
"available": "可用的"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "待办的",
|
||||
"approved": "得到正式认可的",
|
||||
"available": "可用的"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "查询",
|
||||
"blocked": "阻止",
|
||||
"gravity": "重力"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"ping": "ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "运行中",
|
||||
"stopped": "已停止",
|
||||
"total": "总计"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "路由器",
|
||||
"services": "服务",
|
||||
"middleware": "中间件"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "禁用",
|
||||
"total": "全部的"
|
||||
},
|
||||
"weather": {
|
||||
"current": "当前定位",
|
||||
"allow": "点击并允许",
|
||||
"updating": "更新中",
|
||||
"wait": "请等待"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "待办",
|
||||
"approved": "已批准",
|
||||
"available": "可用"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "速率",
|
||||
"queue": "队列",
|
||||
"timeleft": "剩余时间"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "速率",
|
||||
"remaining": "剩余",
|
||||
"downloaded": "下载"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "配置一个或多个需要追踪的加密",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"7days": "7天",
|
||||
"30days": "30天"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "应用",
|
||||
"clients": "客户端",
|
||||
"messages": "信息"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "索引器",
|
||||
"numberOfGrabs": "抓取",
|
||||
"numberOfQueries": "查询",
|
||||
"numberOfFailGrabs": "抓取失败",
|
||||
"numberOfFailQueries": "查询失败"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"leech": "吸血",
|
||||
"seed": "做种"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "已配置",
|
||||
"errored": "出错了"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "缺少的剧集",
|
||||
"missingMovies": "缺少的电影"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "订阅",
|
||||
"queued": "队列",
|
||||
"albums": "相册"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "查询",
|
||||
"blocked": "阻止",
|
||||
"filtered": "过滤",
|
||||
"latency": "延迟"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"leech": "吸血",
|
||||
"seed": "做种"
|
||||
}
|
||||
}
|
||||
161
public/locales/zh-Hant/common.json
Normal file
161
public/locales/zh-Hant/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"offline": "Offline",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"radarr": {
|
||||
"movies": "Movies",
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"gotify": {
|
||||
"clients": "Clients",
|
||||
"apps": "Applications",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,22 @@ export default function Item({ bookmark }) {
|
||||
const { hostname } = new URL(bookmark.href);
|
||||
|
||||
return (
|
||||
<li
|
||||
onClick={() => {
|
||||
window.open(bookmark.href, "_blank").focus();
|
||||
}}
|
||||
key={bookmark.name}
|
||||
className="mb-3 cursor-pointer flex rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/50 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10"
|
||||
<li key={bookmark.name}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(bookmark.href, "_blank").focus()}
|
||||
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
{bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div>
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-400 opacity-50 text-xs">{hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Item from "components/bookmarks/item";
|
||||
|
||||
export default function List({ bookmarks }) {
|
||||
return (
|
||||
<ul role="list" className="mt-3 flex flex-col">
|
||||
<ul className="mt-3 flex flex-col">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<Item key={bookmark.name} bookmark={bookmark} />
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
import { useContext, Fragment } from "react";
|
||||
import { IoColorPalette } from "react-icons/io5";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { ColorContext } from "utils/color-context";
|
||||
|
||||
@@ -27,6 +27,7 @@ const colors = [
|
||||
"pink",
|
||||
"rose",
|
||||
"red",
|
||||
"white",
|
||||
];
|
||||
|
||||
export default function ColorToggle() {
|
||||
@@ -39,8 +40,6 @@ export default function ColorToggle() {
|
||||
return (
|
||||
<div className="w-full self-center">
|
||||
<Popover className="relative flex items-center">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button className="outline-none">
|
||||
<IoColorPalette
|
||||
className="h-5 w-5 text-theme-800 dark:text-theme-200 transition duration-150 ease-in-out"
|
||||
@@ -57,15 +56,16 @@ export default function ColorToggle() {
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-[75px] left-0">
|
||||
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<div className="relative grid gap-2 p-2 grid-cols-11 shadow-theme-900/10 dark:shadow-theme-900 rounded-md shadow-md">
|
||||
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5 w-[85vw] sm:w-full">
|
||||
<div className="relative grid gap-2 p-2 grid-cols-11 bg-white/50 dark:bg-white/10 shadow-black/10 dark:shadow-black/20 rounded-md shadow-md">
|
||||
{colors.map((color) => (
|
||||
<button role="button" onClick={() => setColor(color)} key={color}>
|
||||
<button type="button" onClick={() => setColor(color)} key={color}>
|
||||
<div
|
||||
className={
|
||||
(active == color ? "border-2" : "border-0") +
|
||||
` rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-500`
|
||||
}
|
||||
title={color}
|
||||
className={classNames(
|
||||
active === color ? "border-2" : "border-0",
|
||||
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -73,8 +73,6 @@ export default function ColorToggle() {
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export default function Greeting() {
|
||||
const name = process.env.NEXT_PUBLIC_DISPLAY_NAME;
|
||||
const hour = new Date().getHours();
|
||||
|
||||
let day = "day";
|
||||
|
||||
if (hour < 12) {
|
||||
day = "morning";
|
||||
} else if (hour < 17) {
|
||||
day = "afternoon";
|
||||
} else {
|
||||
day = "evening";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="self-end grow text-2xl font-thin text-theme-800 dark:text-theme-200">
|
||||
Good {day}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Fragment, useRef, useState, Children } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const Modal = ({ Toggle, Content }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toggle open={open} setOpen={setOpen} />
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={setOpen}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-theme-900/90 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-full">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative rounded-lg shadow-xl transform transition-all my-8 max-w-lg w-full">
|
||||
<Content open={open} setOpen={setOpen} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalToggle = ({ open, setOpen, children }) => (
|
||||
<div onClick={() => setOpen(!open)}>{children}</div>
|
||||
);
|
||||
|
||||
const ModalContent = ({ open, setOpen, children }) => (
|
||||
<div className="body">{children}</div>
|
||||
);
|
||||
|
||||
Modal.Toggle = ModalToggle;
|
||||
Modal.Content = ModalContent;
|
||||
|
||||
export default Modal;
|
||||
17
src/components/revalidate.jsx
Normal file
17
src/components/revalidate.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MdRefresh } from "react-icons/md";
|
||||
|
||||
export default function Revalidate() {
|
||||
const revalidate = () => {
|
||||
fetch("/api/revalidate").then((res) => {
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-full flex align-middle self-center mr-3">
|
||||
<MdRefresh onClick={() => revalidate()} className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/components/services/dropdown.jsx
Normal file
48
src/components/services/dropdown.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Fragment } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { BiCog } from "react-icons/bi";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default function Dropdown({ options, value, setValue }) {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="text-xs inline-flex w-full items-center rounded bg-theme-200/50 dark:bg-theme-900/20 px-3 py-1.5">
|
||||
{options.find((option) => option.value === value).label}
|
||||
<BiCog className="-mr-1 ml-2 h-4 w-4" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-theme-200/50 dark:bg-theme-900/50 backdrop-blur shadow-md focus:outline-none text-theme-700 dark:text-theme-200">
|
||||
<div className="py-1">
|
||||
{options.map((option) => (
|
||||
<Menu.Item key={option.value} as={Fragment}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setValue(option.value);
|
||||
}}
|
||||
type="button"
|
||||
className={classNames(
|
||||
value === option.value ? "bg-theme-300/40 dark:bg-theme-900/40" : "",
|
||||
"w-full block px-3 py-1.5 text-sm hover:bg-theme-300/70 hover:dark:bg-theme-900/70 text-left"
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import List from "components/services/list";
|
||||
|
||||
export default function ServicesGroup({ services }) {
|
||||
export default function ServicesGroup({ services, layout }) {
|
||||
return (
|
||||
<div
|
||||
key={services.name}
|
||||
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
|
||||
className={classNames(
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
|
||||
"flex-1 p-1"
|
||||
)}
|
||||
>
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
|
||||
{services.name}
|
||||
</h2>
|
||||
<List services={services.services} />
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
<List services={services.services} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,47 +8,67 @@ import Docker from "./widgets/service/docker";
|
||||
function resolveIcon(icon) {
|
||||
if (icon.startsWith("http")) {
|
||||
return `/api/proxy?url=${encodeURIComponent(icon)}`;
|
||||
} else if (icon.startsWith("/")) {
|
||||
}
|
||||
|
||||
if (icon.startsWith("/")) {
|
||||
return icon;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (icon.endsWith(".png")) {
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
|
||||
} else {
|
||||
}
|
||||
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Item({ service }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
|
||||
return (
|
||||
<li key={service.name} className="">
|
||||
<li key={service.name}>
|
||||
<Disclosure>
|
||||
<div className="transition-all h-15 overflow-hidden mb-3 cursor-pointer p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10">
|
||||
<div className="flex">
|
||||
{service.icon && (
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(service.href, "_blank").focus();
|
||||
}}
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
className={`${
|
||||
hasLink ? "cursor-pointer " : " "
|
||||
}transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-700/70 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/20 dark:bg-white/10 dark:hover:bg-white/20`}
|
||||
>
|
||||
<div className="flex select-none">
|
||||
{service.icon &&
|
||||
(hasLink ? (
|
||||
<a type="button" href={service.href} className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(service.href, "_blank").focus();
|
||||
}}
|
||||
{hasLink ? (
|
||||
<button
|
||||
type="button"
|
||||
href={service.href}
|
||||
className="flex-1 flex items-center justify-between rounded-r-md "
|
||||
>
|
||||
<div className="flex-1 px-2 py-2 text-sm">
|
||||
<div className="flex-1 px-2 py-2 text-sm text-left">
|
||||
{service.name}
|
||||
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 px-2 py-2 text-sm text-left">
|
||||
{service.name}
|
||||
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.container && (
|
||||
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
|
||||
>
|
||||
<Status service={service} />
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ services }) {
|
||||
const columnMap = [
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
|
||||
];
|
||||
|
||||
export default function List({ services, layout }) {
|
||||
return (
|
||||
<ul role="list" className="mt-3 flex flex-col">
|
||||
<ul
|
||||
className={classNames(
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
|
||||
"mt-3"
|
||||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item key={service.name} service={service} />
|
||||
))}
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Status({ service }) {
|
||||
const { data, error } = useSWR(
|
||||
`/api/docker/status/${service.container}/${service.server || ""}`
|
||||
);
|
||||
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />
|
||||
);
|
||||
return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "running") {
|
||||
return (
|
||||
<div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />
|
||||
);
|
||||
return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "not found") {
|
||||
return (
|
||||
<>
|
||||
<div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45"></div>
|
||||
</>
|
||||
);
|
||||
return <div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45" />;
|
||||
}
|
||||
|
||||
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
|
||||
|
||||
@@ -1,32 +1,66 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Sonarr from "./widgets/service/sonarr";
|
||||
import Radarr from "./widgets/service/radarr";
|
||||
import Lidarr from "./widgets/service/lidarr";
|
||||
import Readarr from "./widgets/service/readarr";
|
||||
import Bazarr from "./widgets/service/bazarr";
|
||||
import Ombi from "./widgets/service/ombi";
|
||||
import Portainer from "./widgets/service/portainer";
|
||||
import Emby from "./widgets/service/emby";
|
||||
import Nzbget from "./widgets/service/nzbget";
|
||||
import SABnzbd from "./widgets/service/sabnzbd";
|
||||
import Transmission from "./widgets/service/transmission";
|
||||
import QBittorrent from "./widgets/service/qbittorrent";
|
||||
import Docker from "./widgets/service/docker";
|
||||
import Pihole from "./widgets/service/pihole";
|
||||
import Rutorrent from "./widgets/service/rutorrent";
|
||||
import Jellyfin from "./widgets/service/jellyfin";
|
||||
import Speedtest from "./widgets/service/speedtest";
|
||||
import Traefik from "./widgets/service/traefik";
|
||||
import Jellyseerr from "./widgets/service/jellyseerr";
|
||||
import Overseerr from "./widgets/service/overseerr";
|
||||
import Npm from "./widgets/service/npm";
|
||||
import Tautulli from "./widgets/service/tautulli";
|
||||
import CoinMarketCap from "./widgets/service/coinmarketcap";
|
||||
import Gotify from "./widgets/service/gotify";
|
||||
import Prowlarr from "./widgets/service/prowlarr";
|
||||
import Jackett from "./widgets/service/jackett";
|
||||
import AdGuard from "./widgets/service/adguard";
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
sonarr: Sonarr,
|
||||
radarr: Radarr,
|
||||
lidarr: Lidarr,
|
||||
readarr: Readarr,
|
||||
bazarr: Bazarr,
|
||||
ombi: Ombi,
|
||||
portainer: Portainer,
|
||||
emby: Emby,
|
||||
jellyfin: Jellyfin,
|
||||
nzbget: Nzbget,
|
||||
sabnzbd: SABnzbd,
|
||||
transmission: Transmission,
|
||||
qbittorrent: QBittorrent,
|
||||
pihole: Pihole,
|
||||
rutorrent: Rutorrent,
|
||||
speedtest: Speedtest,
|
||||
traefik: Traefik,
|
||||
jellyseerr: Jellyseerr,
|
||||
overseerr: Overseerr,
|
||||
coinmarketcap: CoinMarketCap,
|
||||
npm: Npm,
|
||||
tautulli: Tautulli,
|
||||
gotify: Gotify,
|
||||
prowlarr: Prowlarr,
|
||||
jackett: Jackett,
|
||||
adguard: AdGuard,
|
||||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const ServiceWidget = widgetMappings[service.widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
@@ -35,9 +69,7 @@ export default function Widget({ service }) {
|
||||
|
||||
return (
|
||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
|
||||
<div className="font-thin text-sm">
|
||||
Missing Widget Type: <strong>{service.widget.type}</strong>
|
||||
</div>
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
45
src/components/services/widgets/service/adguard.jsx
Normal file
45
src/components/services/widgets/service/adguard.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function AdGuard({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: adguardData, error: adguardError } = useSWR(formatApiUrl(config, "stats"));
|
||||
|
||||
if (adguardError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!adguardData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} />
|
||||
<Block label={t("adguard.blocked")} />
|
||||
<Block label={t("adguard.filtered")} />
|
||||
<Block label={t("adguard.latency")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const filtered =
|
||||
adguardData.num_replaced_safebrowsing + adguardData.num_replaced_safesearch + adguardData.num_replaced_parental;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} value={t("common.number", { value: adguardData.num_dns_queries })} />
|
||||
<Block label={t("adguard.blocked")} value={t("common.number", { value: adguardData.num_blocked_filtering })} />
|
||||
<Block label={t("adguard.filtered")} value={t("common.number", { value: filtered })} />
|
||||
<Block
|
||||
label={t("adguard.latency")}
|
||||
value={t("common.ms", { value: adguardData.avg_processing_time * 1000, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
36
src/components/services/widgets/service/bazarr.jsx
Normal file
36
src/components/services/widgets/service/bazarr.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Bazarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: episodesData, error: episodesError } = useSWR(formatApiUrl(config, "episodes"));
|
||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movies"));
|
||||
|
||||
if (episodesError || moviesError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!episodesData || !moviesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("bazarr.missingEpisodes")} />
|
||||
<Block label={t("bazarr.missingMovies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
|
||||
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
90
src/components/services/widgets/service/coinmarketcap.jsx
Normal file
90
src/components/services/widgets/service/coinmarketcap.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import Dropdown from "components/services/dropdown";
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function CoinMarketCap({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dateRangeOptions = [
|
||||
{ label: t("coinmarketcap.1hour"), value: "1h" },
|
||||
{ label: t("coinmarketcap.1day"), value: "24h" },
|
||||
{ label: t("coinmarketcap.7days"), value: "7d" },
|
||||
{ label: t("coinmarketcap.30days"), value: "30d" },
|
||||
];
|
||||
|
||||
const [dateRange, setDateRange] = useState(dateRangeOptions[0].value);
|
||||
|
||||
const config = service.widget;
|
||||
const currencyCode = config.currency ?? "USD";
|
||||
const { symbols } = config;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
formatApiUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`)
|
||||
);
|
||||
|
||||
if (!symbols || symbols.length === 0) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block value={t("coinmarketcap.configure")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData || !dateRange) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block value={t("coinmarketcap.configure")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const { data } = statsData;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1")}>
|
||||
<Dropdown options={dateRangeOptions} value={dateRange} setValue={setDateRange} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{symbols.map((symbol) => (
|
||||
<div
|
||||
key={data[symbol].symbol}
|
||||
className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs"
|
||||
>
|
||||
<div className="font-thin pl-2">{data[symbol].name}</div>
|
||||
<div className="flex flex-row text-right">
|
||||
<div className="font-bold mr-2">
|
||||
{t("common.number", {
|
||||
value: data[symbol].quote[currencyCode].price,
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`font-bold w-10 mr-2 ${
|
||||
data[symbol].quote[currencyCode][`percent_change_${dateRange}`] > 0
|
||||
? "text-emerald-300"
|
||||
: "text-rose-300"
|
||||
}`}
|
||||
>
|
||||
{data[symbol].quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import calculateCPUPercent from "utils/stats-helpers";
|
||||
|
||||
export default function Docker({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`/api/docker/status/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
`/api/docker/stats/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Widget error="Error Fetching Data" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Status" value="Offline" />
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -37,20 +40,24 @@ export default function Docker({ service }) {
|
||||
if (!statsData || !statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" />
|
||||
<Block label="MEM" />
|
||||
<Block label="RX" />
|
||||
<Block label="TX" />
|
||||
<Block label={t("docker.cpu")} />
|
||||
<Block label={t("docker.mem")} />
|
||||
<Block label={t("docker.rx")} />
|
||||
<Block label={t("docker.tx")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
<Block label={t("docker.cpu")} value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
<Block label={t("docker.mem")} value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label={t("docker.rx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
||||
<Block label={t("docker.tx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,239 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||
import { MdOutlineSmartDisplay } from "react-icons/md";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
export default function Emby({ service, title = "Emby" }) {
|
||||
const config = service.widget;
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/emby/${endpoint}?api_key=${key}`;
|
||||
function ticksToTime(ticks) {
|
||||
const milliseconds = ticks / 10000;
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
return { hours, minutes, seconds };
|
||||
}
|
||||
|
||||
const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
|
||||
refreshInterval: 1000,
|
||||
function ticksToString(ticks) {
|
||||
const { hours, minutes, seconds } = ticksToTime(ticks);
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours);
|
||||
}
|
||||
parts.push(minutes);
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ playCommand, session }) {
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||
IsVideoDirect: true,
|
||||
VideoDecoderIsHardware: true,
|
||||
VideoEncoderIsHardware: true,
|
||||
};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="grow text-xs z-10 self-center ml-2 relative w-full h-4 mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
|
||||
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && (
|
||||
<BsFillCpuFill className="opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{ticksToString(PositionTicks)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{ticksToString(RunTimeTicks)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ playCommand, session }) {
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
}}
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grow text-xs z-10 self-center relative w-full h-4">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{ticksToString(PositionTicks)}</div>
|
||||
<div className="self-center items-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
|
||||
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && <BsFillCpuFill className="opacity-50" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Emby({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const {
|
||||
data: sessionsData,
|
||||
error: sessionsError,
|
||||
mutate: sessionMutate,
|
||||
} = useSWR(formatApiUrl(config, "Sessions"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
async function handlePlayCommand(session, command) {
|
||||
const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`);
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
}).then(() => {
|
||||
sessionMutate();
|
||||
});
|
||||
}
|
||||
|
||||
if (sessionsError) {
|
||||
return <Widget error={`${title} API Error`} />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!sessionsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
</Widget>
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playing = sessionsData.filter((session) => session.hasOwnProperty("NowPlayingItem"));
|
||||
const transcoding = sessionsData.filter(
|
||||
(session) => session.hasOwnProperty("PlayState") && session.PlayState.PlayMethod === "Transcode"
|
||||
const playing = sessionsData
|
||||
.filter((session) => session?.NowPlayingItem)
|
||||
.sort((a, b) => {
|
||||
if (a.PlayState.PositionTicks > b.PlayState.PositionTicks) {
|
||||
return 1;
|
||||
}
|
||||
if (a.PlayState.PositionTicks < b.PlayState.PositionTicks) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
|
||||
}
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<SingleSessionEntry
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={playing.length} />
|
||||
<Block label="Transcoding" value={transcoding.length} />
|
||||
<Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
|
||||
</Widget>
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
29
src/components/services/widgets/service/gotify.jsx
Normal file
29
src/components/services/widgets/service/gotify.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Gotify({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: appsData, error: appsError } = useSWR(formatApiUrl(config, `application`));
|
||||
const { data: messagesData, error: messagesError } = useSWR(formatApiUrl(config, `message`));
|
||||
const { data: clientsData, error: clientsError } = useSWR(formatApiUrl(config, `client`));
|
||||
|
||||
if (appsError || messagesError || clientsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("gotify.apps")} value={appsData?.length} />
|
||||
<Block label={t("gotify.clients")} value={clientsData?.length} />
|
||||
<Block label={t("gotify.messages")} value={messagesData?.messages?.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
37
src/components/services/widgets/service/jackett.jsx
Normal file
37
src/components/services/widgets/service/jackett.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Jackett({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexers"));
|
||||
|
||||
if (indexersError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!indexersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jackett.configured")} />
|
||||
<Block label={t("jackett.errored")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const errored = indexersData.filter((indexer) => indexer.last_error);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jackett.configured")} value={t("common.number", { value: indexersData.length })} />
|
||||
<Block label={t("jackett.errored")} value={t("common.number", { value: errored.length })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import Emby from "./emby";
|
||||
|
||||
// Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin.
|
||||
export default function Jellyfin({ service }) {
|
||||
return <Emby service={service} title="Jellyfin" />;
|
||||
return <Emby service={service} />;
|
||||
}
|
||||
|
||||
37
src/components/services/widgets/service/jellyseerr.jsx
Normal file
37
src/components/services/widgets/service/jellyseerr.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Jellyseerr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jellyseerr.pending")} />
|
||||
<Block label={t("jellyseerr.approved")} />
|
||||
<Block label={t("jellyseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jellyseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("jellyseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("jellyseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
39
src/components/services/widgets/service/lidarr.jsx
Normal file
39
src/components/services/widgets/service/lidarr.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Lidarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: albumsData, error: albumsError } = useSWR(formatApiUrl(config, "album"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (albumsError || wantedError || queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!albumsData || !wantedData || !queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("lidarr.wanted")} />
|
||||
<Block label={t("lidarr.queued")} />
|
||||
<Block label={t("lidarr.albums")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("lidarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label={t("lidarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label={t("lidarr.albums")} value={t("common.number", { value: albumsData.have })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
41
src/components/services/widgets/service/npm.jsx
Normal file
41
src/components/services/widgets/service/npm.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Npm({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
||||
|
||||
if (infoError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("npm.enabled")} />
|
||||
<Block label={t("npm.disabled")} />
|
||||
<Block label={t("npm.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const enabled = infoData.filter((c) => c.enabled === 1).length;
|
||||
const disabled = infoData.filter((c) => c.enabled === 0).length;
|
||||
const total = infoData.length;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("npm.enabled")} value={enabled} />
|
||||
<Block label={t("npm.disabled")} value={disabled} />
|
||||
<Block label={t("npm.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +1,43 @@
|
||||
import useSWR from "swr";
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Nzbget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const constructedUrl = new URL(config.url);
|
||||
constructedUrl.pathname = "jsonrpc";
|
||||
|
||||
const client = new JSONRPCClient((jsonRPCRequest) =>
|
||||
fetch(constructedUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
|
||||
},
|
||||
body: JSON.stringify(jsonRPCRequest),
|
||||
}).then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
const jsonRPCResponse = await response.json();
|
||||
return client.receive(jsonRPCResponse);
|
||||
} else if (jsonRPCRequest.id !== undefined) {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
"status",
|
||||
(resource) => {
|
||||
return client.request(resource).then((response) => response);
|
||||
},
|
||||
{
|
||||
refreshInterval: 1000,
|
||||
}
|
||||
);
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" />
|
||||
<Block label="Remaining" />
|
||||
<Block label="Downloaded" />
|
||||
<Block label={t("nzbget.rate")} />
|
||||
<Block label={t("nzbget.remaining")} />
|
||||
<Block label={t("nzbget.downloaded")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
|
||||
<Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label={t("nzbget.rate")} value={t("common.bitrate", { value: statusData.DownloadRate })} />
|
||||
<Block
|
||||
label={t("nzbget.remaining")}
|
||||
value={t("common.bytes", { value: statusData.RemainingSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("nzbget.downloaded")}
|
||||
value={t("common.bytes", { value: statusData.DownloadedSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Ombi({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/v1/${endpoint}`;
|
||||
}
|
||||
|
||||
const fetcher = (url) => {
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
ApiKey: `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
};
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Ombi API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("ombi.pending")} />
|
||||
<Block label={t("ombi.approved")} />
|
||||
<Block label={t("ombi.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("ombi.pending")} value={statsData.pending} />
|
||||
<Block label={t("ombi.approved")} value={statsData.approved} />
|
||||
<Block label={t("ombi.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
37
src/components/services/widgets/service/overseerr.jsx
Normal file
37
src/components/services/widgets/service/overseerr.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Overseerr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} />
|
||||
<Block label={t("overseerr.approved")} />
|
||||
<Block label={t("overseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("overseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("overseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Pihole({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, proxy } = config;
|
||||
|
||||
if (proxy) {
|
||||
const fullUrl = `${url}/admin/${endpoint}`;
|
||||
return "/api/proxy?url=" + encodeURIComponent(fullUrl);
|
||||
}
|
||||
|
||||
return `${url}/admin/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php"));
|
||||
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
||||
|
||||
if (piholeError) {
|
||||
return <Widget error="PiHole API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!piholeData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" />
|
||||
<Block label="Blocked" />
|
||||
<Block label="Gravity" />
|
||||
<Block label={t("pihole.queries")} />
|
||||
<Block label={t("pihole.blocked")} />
|
||||
<Block label={t("pihole.gravity")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" value={piholeData.dns_queries_today.toLocaleString()} />
|
||||
<Block label="Blocked" value={piholeData.ads_blocked_today.toLocaleString()} />
|
||||
<Block label="Gravity" value={piholeData.domains_being_blocked.toLocaleString()} />
|
||||
<Block label={t("pihole.queries")} value={t("common.number", { value: piholeData.dns_queries_today })} />
|
||||
<Block label={t("pihole.blocked")} value={t("common.number", { value: piholeData.ads_blocked_today })} />
|
||||
<Block label={t("pihole.gravity")} value={t("common.number", { value: piholeData.domains_being_blocked })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Portainer({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, env } = config;
|
||||
const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
|
||||
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
|
||||
}
|
||||
|
||||
const fetcher = async (url) => {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-API-Key": `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher);
|
||||
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
||||
|
||||
if (containersError) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" />
|
||||
<Block label="Stopped" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("portainer.running")} />
|
||||
<Block label={t("portainer.stopped")} />
|
||||
<Block label={t("portainer.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (containersData.error) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
@@ -51,9 +37,9 @@ export default function Portainer({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" value={running} />
|
||||
<Block label="Stopped" value={stopped} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("portainer.running")} value={running} />
|
||||
<Block label={t("portainer.stopped")} value={stopped} />
|
||||
<Block label={t("portainer.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
55
src/components/services/widgets/service/prowlarr.jsx
Normal file
55
src/components/services/widgets/service/prowlarr.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Prowlarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexer"));
|
||||
const { data: grabsData, error: grabsError } = useSWR(formatApiUrl(config, "indexerstats"));
|
||||
|
||||
if (indexersError || grabsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!indexersData || !grabsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("prowlarr.enableIndexers")} />
|
||||
<Block label={t("prowlarr.numberOfGrabs")} />
|
||||
<Block label={t("prowlarr.numberOfQueries")} />
|
||||
<Block label={t("prowlarr.numberOfFailGrabs")} />
|
||||
<Block label={t("prowlarr.numberOfFailQueries")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const indexers = indexersData?.filter((indexer) => indexer.enable === true);
|
||||
|
||||
let numberOfGrabs = 0
|
||||
let numberOfQueries = 0
|
||||
let numberOfFailedGrabs = 0
|
||||
let numberOfFailedQueries = 0
|
||||
grabsData?.indexers?.forEach(element => {
|
||||
numberOfGrabs += element.numberOfGrabs;
|
||||
numberOfQueries += element.numberOfQueries;
|
||||
numberOfFailedGrabs += numberOfFailedGrabs + element.numberOfFailedGrabs;
|
||||
numberOfFailedQueries += numberOfFailedQueries + element.numberOfFailedQueries;
|
||||
});
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("prowlarr.enableIndexers")} value={indexers.length} />
|
||||
<Block label={t("prowlarr.numberOfGrabs")} value={numberOfGrabs} />
|
||||
<Block label={t("prowlarr.numberOfQueries")} value={numberOfQueries} />
|
||||
<Block label={t("prowlarr.numberOfFailGrabs")} value={numberOfFailedGrabs} />
|
||||
<Block label={t("prowlarr.numberOfFailQueries")} value={numberOfFailedQueries} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function QBittorrent ({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info"));
|
||||
|
||||
if (torrentError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} />
|
||||
<Block label={t("qbittorrent.download")} />
|
||||
<Block label={t("qbittorrent.seed")} />
|
||||
<Block label={t("qbittorrent.upload")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (let i = 0; i < torrentData.length; i += 1) {
|
||||
const torrent = torrentData[i];
|
||||
rateDl += torrent.dlspeed;
|
||||
rateUl += torrent.upspeed;
|
||||
if (torrent.progress === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrentData.length - completed;
|
||||
|
||||
let unitsDl = "KB/s";
|
||||
let unitsUl = "KB/s";
|
||||
rateDl /= 1024;
|
||||
rateUl /= 1024;
|
||||
|
||||
if (rateDl > 1024) {
|
||||
rateDl /= 1024;
|
||||
unitsDl = "MB/s";
|
||||
}
|
||||
|
||||
if (rateUl > 1024) {
|
||||
rateUl /= 1024;
|
||||
unitsUl = "MB/s";
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("qbittorrent.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("qbittorrent.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("qbittorrent.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Radarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
|
||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (moviesError || queuedError) {
|
||||
return <Widget error="Radarr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!moviesData || !queuedData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Movies" />
|
||||
<Block label={t("radarr.wanted")} />
|
||||
<Block label={t("radarr.queued")} />
|
||||
<Block label={t("radarr.movies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const wanted = moviesData.filter((movie) => movie.isAvailable === false);
|
||||
const have = moviesData.filter((movie) => movie.isAvailable === true);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wanted.length} />
|
||||
<Block label="Queued" value={queuedData.totalCount} />
|
||||
<Block label="Movies" value={moviesData.length} />
|
||||
<Block label={t("radarr.wanted")} value={moviesData.wanted} />
|
||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||
<Block label={t("radarr.movies")} value={moviesData.have} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
39
src/components/services/widgets/service/readarr.jsx
Normal file
39
src/components/services/widgets/service/readarr.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Readarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: booksData, error: booksError } = useSWR(formatApiUrl(config, "book"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (booksError || wantedError || queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!booksData || !wantedData || !queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} />
|
||||
<Block label={t("readarr.queued")} />
|
||||
<Block label={t("readarr.books")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label={t("readarr.books")} value={t("common.number", { value: booksData.have })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,43 @@
|
||||
import useSWR from "swr";
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Rutorrent({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl() {
|
||||
const { url, username, password } = config;
|
||||
|
||||
const options = {
|
||||
url: `${url}/plugins/httprpc/action.php`,
|
||||
};
|
||||
|
||||
if (username && password) {
|
||||
options.username = username;
|
||||
options.password = password;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(options);
|
||||
|
||||
return `/api/widgets/rutorrent?${params.toString()}`;
|
||||
}
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(buildApiUrl());
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Download" />
|
||||
<Block label={t("rutorrent.active")} />
|
||||
<Block label={t("rutorrent.upload")} />
|
||||
<Block label={t("rutorrent.download")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const upload = statusData.reduce((acc, torrent) => {
|
||||
return acc + parseInt(torrent["d.get_up_rate"]);
|
||||
}, 0);
|
||||
const upload = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_up_rate"], 10), 0);
|
||||
|
||||
const download = statusData.reduce((acc, torrent) => {
|
||||
return acc + parseInt(torrent["d.get_down_rate"]);
|
||||
}, 0);
|
||||
const download = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_down_rate"], 10), 0);
|
||||
|
||||
const active = statusData.filter((torrent) => torrent["d.get_state"] === "1");
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" value={active.length} />
|
||||
<Block label="Upload" value={`${formatBytes(upload)}/s`} />
|
||||
<Block label="Download" value={`${formatBytes(download)}/s`} />
|
||||
<Block label={t("rutorrent.active")} value={active.length} />
|
||||
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
37
src/components/services/widgets/service/sabnzbd.jsx
Normal file
37
src/components/services/widgets/service/sabnzbd.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function SABnzbd({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue"));
|
||||
|
||||
if (queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} />
|
||||
<Block label={t("sabnzbd.queue")} />
|
||||
<Block label={t("sabnzbd.timeleft")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
|
||||
<Block label={t("sabnzbd.queue")} value={t("common.number", { value: queueData.queue.noofslots })} />
|
||||
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1,39 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Sonarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
|
||||
|
||||
if (wantedError || queuedError || seriesError) {
|
||||
return <Widget error="Sonar API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!wantedData || !queuedData || !seriesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Series" />
|
||||
<Block label={t("sonarr.wanted")} />
|
||||
<Block label={t("sonarr.queued")} />
|
||||
<Block label={t("sonarr.series")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wantedData.totalRecords} />
|
||||
<Block label="Queued" value={queuedData.totalRecords} />
|
||||
<Block label="Series" value={seriesData.length} />
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatBits } from "utils/stats-helpers";
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Speedtest({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest"));
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
||||
|
||||
if (speedtestError) {
|
||||
return <Widget error="Speedtest API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!speedtestData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Ping" />
|
||||
<Block label={t("speedtest.download")} />
|
||||
<Block label={t("speedtest.upload")} />
|
||||
<Block label={t("speedtest.ping")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024)}ps`} />
|
||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
||||
<Block
|
||||
label={t("speedtest.download")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.upload")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.ping")}
|
||||
value={t("common.ms", { value: speedtestData.data.ping, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
183
src/components/services/widgets/service/tautulli.jsx
Normal file
183
src/components/services/widgets/service/tautulli.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable camelcase */
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
|
||||
|
||||
import Widget from "../widget";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
function millisecondsToTime(milliseconds) {
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
return { hours, minutes, seconds };
|
||||
}
|
||||
|
||||
function millisecondsToString(milliseconds) {
|
||||
const { hours, minutes, seconds } = millisecondsToTime(milliseconds);
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours);
|
||||
}
|
||||
parts.push(minutes);
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ session }) {
|
||||
const { full_title, duration, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{millisecondsToString(view_offset)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{millisecondsToString(duration)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ session }) {
|
||||
const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
|
||||
style={{
|
||||
width: `${progress_percent}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{state === "paused" && (
|
||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">{millisecondsToString(view_offset)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Tautulli({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (activityError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!activityData) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playing = activityData.response.data.sessions.sort((a, b) => {
|
||||
if (a.view_offset > b.view_offset) {
|
||||
return 1;
|
||||
}
|
||||
if (a.view_offset < b.view_offset) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("tautulli.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<SingleSessionEntry session={session} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry key={session.Id} session={session} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Traefik({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
const fullUrl = `${url}/api/${endpoint}`;
|
||||
return `/api/proxy?url=${encodeURIComponent(fullUrl)}`;
|
||||
}
|
||||
|
||||
const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview"));
|
||||
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
||||
|
||||
if (traefikError) {
|
||||
return <Widget error="Traefik API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!traefikData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" />
|
||||
<Block label="Services" />
|
||||
<Block label="Middleware" />
|
||||
<Block label={t("traefik.routers")} />
|
||||
<Block label={t("traefik.services")} />
|
||||
<Block label={t("traefik.middleware")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" value={traefikData.http.routers.total} />
|
||||
<Block label="Services" value={traefikData.http.services.total} />
|
||||
<Block label="Middleware" value={traefikData.http.middlewares.total} />
|
||||
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/components/services/widgets/service/transmission.jsx
Normal file
70
src/components/services/widgets/service/transmission.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Transmission({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config));
|
||||
|
||||
if (torrentError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("transmission.leech")} />
|
||||
<Block label={t("transmission.download")} />
|
||||
<Block label={t("transmission.seed")} />
|
||||
<Block label={t("transmission.upload")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const { torrents } = torrentData.arguments;
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (let i = 0; i < torrents.length; i += 1) {
|
||||
const torrent = torrents[i];
|
||||
rateDl += torrent.rateDownload;
|
||||
rateUl += torrent.rateUpload;
|
||||
if (torrent.percentDone === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrents.length - completed;
|
||||
|
||||
let unitsDl = "KB/s";
|
||||
let unitsUl = "KB/s";
|
||||
rateDl /= 1024;
|
||||
rateUl /= 1024;
|
||||
|
||||
if (rateDl > 1024) {
|
||||
rateDl /= 1024;
|
||||
unitsDl = "MB/s";
|
||||
}
|
||||
|
||||
if (rateUl > 1024) {
|
||||
rateUl /= 1024;
|
||||
unitsUl = "MB/s";
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("transmission.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -7,5 +7,5 @@ export default function Widget({ error = false, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex flex-row w-full">{children}</div>;
|
||||
return <div className="relative flex flex-row w-full">{children}</div>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import {
|
||||
MdDarkMode,
|
||||
MdLightMode,
|
||||
MdToggleOff,
|
||||
MdToggleOn,
|
||||
} from "react-icons/md";
|
||||
import { MdDarkMode, MdLightMode, MdToggleOff, MdToggleOn } from "react-icons/md";
|
||||
|
||||
import { ThemeContext } from "utils/theme-context";
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import WeatherApi from "components/widgets/weather/weather";
|
||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||
import Resources from "components/widgets/resources/resources";
|
||||
import Search from "components/widgets/search/search";
|
||||
import Greeting from "components/widgets/greeting/greeting";
|
||||
import DateTime from "components/widgets/datetime/datetime";
|
||||
|
||||
const widgetMappings = {
|
||||
weather: WeatherApi, // This key will be deprecated in the future
|
||||
weatherapi: WeatherApi,
|
||||
openweathermap: OpenWeatherMap,
|
||||
resources: Resources,
|
||||
search: Search,
|
||||
greeting: Greeting,
|
||||
datetime: DateTime,
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
const ServiceWidget = widgetMappings[widget.type];
|
||||
const InfoWidget = widgetMappings[widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
return <ServiceWidget options={widget.options} />;
|
||||
if (InfoWidget) {
|
||||
return <InfoWidget options={widget.options} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
36
src/components/widgets/datetime/datetime.jsx
Normal file
36
src/components/widgets/datetime/datetime.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function DateTime({ options }) {
|
||||
const { text_size: textSize, format } = options;
|
||||
const { i18n } = useTranslation();
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [setDate]);
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center grow justify-end">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
|
||||
{dateFormat.format(date)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/widgets/greeting/greeting.jsx
Normal file
22
src/components/widgets/greeting/greeting.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function Greeting({ options }) {
|
||||
if (options.text) {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-start">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[options.text_size || "xl"]}`}>
|
||||
{options.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import mapIcon from "utils/owm-condition-map";
|
||||
|
||||
export default function Icon({ condition, timeOfDay }) {
|
||||
const Icon = mapIcon(condition, timeOfDay);
|
||||
const IconComponent = mapIcon(condition, timeOfDay);
|
||||
|
||||
return <Icon className="w-10 h-10 text-theme-800 dark:text-theme-200"></Icon>;
|
||||
return <IconComponent className="w-10 h-10 text-theme-800 dark:text-theme-200" />;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
export default function OpenWeatherMap({ options }) {
|
||||
function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(
|
||||
`/api/widgets/openweathermap?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}&units=${options.units}`
|
||||
`/api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||
);
|
||||
|
||||
if (error || data?.cod == 401) {
|
||||
if (error || data?.cod === 401 || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">API</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,15 +31,25 @@ export default function OpenWeatherMap({ options }) {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="flex flex-row items-center"></div>;
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div className="flex flex-row items-center"></div>;
|
||||
}
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon
|
||||
@@ -44,13 +60,63 @@ export default function OpenWeatherMap({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{data.main.temp.toFixed(1)}°
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{data.weather[0].description.charAt(0).toUpperCase() + data.weather[0].description.slice(1)}
|
||||
{t("common.number", { value: data.main.temp, style: "unit", unit })}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OpenWeatherMap({ options }) {
|
||||
const { t } = useTranslation();
|
||||
const [location, setLocation] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
if (!location && options.latitude && options.longitude) {
|
||||
setLocation({ latitude: options.latitude, longitude: options.longitude });
|
||||
}
|
||||
|
||||
const requestLocation = () => {
|
||||
setRequesting(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude });
|
||||
setRequesting(false);
|
||||
},
|
||||
() => {
|
||||
setRequesting(false);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 1000 * 60 * 60 * 3,
|
||||
timeout: 1000 * 30,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FiCpu } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Cpu({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
export default function Cpu() {
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -21,26 +25,44 @@ export default function Cpu() {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Usage</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Load</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = data.cpu.usage;
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
<span className="whitespace-pre">{`${Math.round(data.cpu.usage)}%`.padEnd(3, " ")} Usage</span>
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{`${(Math.round(data.cpu.load * 100) / 100).toFixed(1)}`.padEnd(3, " ")} Load
|
||||
</span>
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.usage,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("docker.cpu")}</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.load,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.load")}</div>
|
||||
</div>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Disk({ options, expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
export default function Disk({ options }) {
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -22,26 +25,32 @@ export default function Disk({ options }) {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Free</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Used</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.drive.freeGb * 1024 * 1024 * 1024)} Free
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })}</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.drive.usedGb * 1024 * 1024 * 1024)} Used
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })}</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import useSWR from "swr";
|
||||
import { FaMemory } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Memory({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
export default function Memory() {
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Resources</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -22,26 +25,40 @@ export default function Memory() {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- GB Used</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- GB Free</span>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.memory.usedMemMb * 1024 * 1024)} Used
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 0, binary: true })}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", {
|
||||
value: data.memory.totalMemMb * 1024 * 1024,
|
||||
maximumFractionDigits: 0,
|
||||
binary: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,20 +3,19 @@ import Cpu from "./cpu";
|
||||
import Memory from "./memory";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
const { expanded } = options;
|
||||
return (
|
||||
<>
|
||||
<div className="pr-2 flex flex-col">
|
||||
<div className="flex flex-row space-x-4">
|
||||
{options.disk && <Disk options={options} />}
|
||||
{options.cpu && <Cpu />}
|
||||
{options.memory && <Memory />}
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center m-auto flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{options.cpu && <Cpu expanded={expanded} />}
|
||||
{options.memory && <Memory expanded={expanded} />}
|
||||
{Array.isArray(options.disk)
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} />)
|
||||
: options.disk && <Disk options={options} expanded={expanded} />}
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="border-t-2 border-theme-800 dark:border-theme-200 mt-1 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">
|
||||
{options.label}
|
||||
</div>
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/widgets/resources/usage-bar.jsx
Normal file
12
src/components/widgets/resources/usage-bar.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function UsageBar({ percent }) {
|
||||
return (
|
||||
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20">
|
||||
<div
|
||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
src/components/widgets/search/search.jsx
Normal file
87
src/components/widgets/search/search.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||
|
||||
const providers = {
|
||||
google: {
|
||||
name: "Google",
|
||||
url: "https://www.google.com/search?q=",
|
||||
icon: SiGoogle,
|
||||
},
|
||||
duckduckgo: {
|
||||
name: "DuckDuckGo",
|
||||
url: "https://duckduckgo.com/?q=",
|
||||
icon: SiDuckduckgo,
|
||||
},
|
||||
bing: {
|
||||
name: "Bing",
|
||||
url: "https://www.bing.com/search?q=",
|
||||
icon: SiMicrosoftbing,
|
||||
},
|
||||
custom: {
|
||||
name: "Custom",
|
||||
url: false,
|
||||
icon: FiSearch,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Search({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const provider = providers[options.provider];
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const q = encodeURIComponent(query);
|
||||
|
||||
if (provider.url) {
|
||||
window.open(`${provider.url}${q}`, options.target || "_blank");
|
||||
} else {
|
||||
window.open(`${options.url}${q}`, options.target || "_blank");
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.target.reset();
|
||||
setQuery("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
||||
<input
|
||||
type="text"
|
||||
className="
|
||||
overflow-hidden w-full h-full rounded-md
|
||||
text-xs text-theme-900 dark:text-white
|
||||
placeholder-theme-900 dark:placeholder-white/80
|
||||
bg-white/50 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50
|
||||
focus:border-theme-500 dark:focus:border-white/50
|
||||
border border-theme-300 dark:border-theme-200/50"
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={options.focus}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="
|
||||
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
|
||||
text-white font-medium text-sm
|
||||
bg-theme-600/40 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50"
|
||||
>
|
||||
<provider.icon className="text-white w-3 h-3" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import mapIcon from "utils/condition-map";
|
||||
|
||||
export default function Icon({ condition, timeOfDay }) {
|
||||
const Icon = mapIcon(condition, timeOfDay);
|
||||
const IconComponent = mapIcon(condition, timeOfDay);
|
||||
|
||||
return <Icon className="w-10 h-10 text-theme-800 dark:text-theme-200"></Icon>;
|
||||
return <IconComponent className="w-10 h-10 text-theme-800 dark:text-theme-200" />;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
export default function WeatherApi({ options }) {
|
||||
function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(
|
||||
`/api/widgets/weather?lat=${options.latitude}&lon=${options.longitude}&apiKey=${options.apiKey}&duration=${options.cache}`
|
||||
`/api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">API</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,15 +31,25 @@ export default function WeatherApi({ options }) {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="flex flex-row items-center justify-end"></div>;
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div className="flex flex-row items-center justify-end"></div>;
|
||||
}
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
||||
@@ -41,7 +57,11 @@ export default function WeatherApi({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{options.units === "metric" ? data.current.temp_c : data.current.temp_f}°
|
||||
{t("common.number", {
|
||||
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||
</div>
|
||||
@@ -49,3 +69,55 @@ export default function WeatherApi({ options }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WeatherApi({ options }) {
|
||||
const { t } = useTranslation();
|
||||
const [location, setLocation] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
if (!location && options.latitude && options.longitude) {
|
||||
setLocation({ latitude: options.latitude, longitude: options.longitude });
|
||||
}
|
||||
|
||||
const requestLocation = () => {
|
||||
setRequesting(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude });
|
||||
setRequesting(false);
|
||||
},
|
||||
() => {
|
||||
setRequesting(false);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 1000 * 60 * 60 * 3,
|
||||
timeout: 1000 * 30,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
import "styles/globals.css";
|
||||
import "styles/weather-icons.css";
|
||||
import "styles/theme.css";
|
||||
|
||||
import "utils/i18n";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<SWRConfig
|
||||
@@ -10,7 +16,11 @@ function MyApp({ Component, pageProps }) {
|
||||
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
||||
}}
|
||||
>
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ColorProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default function Document() {
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<body className="w-full h-full bg-theme-50 dark:bg-theme-800 transition duration-150 ease-in-out">
|
||||
<body className="relative w-full h-full bg-theme-50 dark:bg-theme-800 transition duration-150 ease-in-out">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
@@ -1,6 +1,8 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -11,17 +13,13 @@ export default async function handler(req, res) {
|
||||
const bookmarks = yaml.load(fileContents);
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const bookmarksArray = bookmarks.map((group) => {
|
||||
return {
|
||||
const bookmarksArray = bookmarks.map((group) => ({
|
||||
name: Object.keys(group)[0],
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]][0],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
})),
|
||||
}));
|
||||
|
||||
res.send(bookmarksArray);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
import getDockerArguments from "utils/docker";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -13,7 +14,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const docker = new Docker(await getDockerArguments(containerServer));
|
||||
const docker = new Docker(getDockerArguments(containerServer));
|
||||
const containers = await docker.listContainers({
|
||||
all: true,
|
||||
});
|
||||
@@ -21,30 +22,30 @@ export default async function handler(req, res) {
|
||||
// bad docker connections can result in a <Buffer ...> object?
|
||||
// in any case, this ensures the result is the expected array
|
||||
if (!Array.isArray(containers)) {
|
||||
return res.status(500).send({
|
||||
res.status(500).send({
|
||||
error: "query failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerNames = containers.map((container) => {
|
||||
return container.Names[0].replace(/^\//, "");
|
||||
});
|
||||
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
|
||||
const containerExists = containerNames.includes(containerName);
|
||||
|
||||
if (!containerExists) {
|
||||
return res.status(200).send({
|
||||
res.status(200).send({
|
||||
error: "not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const container = docker.getContainer(containerName);
|
||||
const stats = await container.stats({ stream: false });
|
||||
|
||||
return res.status(200).json({
|
||||
stats: stats,
|
||||
res.status(200).json({
|
||||
stats,
|
||||
});
|
||||
} catch {
|
||||
return res.status(500).send({
|
||||
res.status(500).send({
|
||||
error: "unknown error",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
import getDockerArguments from "utils/docker";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -12,7 +13,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const docker = new Docker(await getDockerArguments(containerServer));
|
||||
const docker = new Docker(getDockerArguments(containerServer));
|
||||
const containers = await docker.listContainers({
|
||||
all: true,
|
||||
});
|
||||
@@ -25,9 +26,7 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
const containerNames = containers.map((container) => {
|
||||
return container.Names[0].replace(/^\//, "");
|
||||
});
|
||||
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
|
||||
const containerExists = containerNames.includes(containerName);
|
||||
|
||||
if (!containerExists) {
|
||||
|
||||
3
src/pages/api/healthcheck.js
Normal file
3
src/pages/api/healthcheck.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.send("up");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import https from "https";
|
||||
|
||||
import getRawBody from "raw-body";
|
||||
|
||||
import { httpRequest, httpsRequest } from "utils/http";
|
||||
@@ -11,7 +12,8 @@ export const config = {
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const headers = ["X-API-Key", "Authorization"].reduce((obj, key) => {
|
||||
if (req.headers && req.headers.hasOwnProperty(key.toLowerCase())) {
|
||||
if (req.headers && Object.prototype.hasOwnProperty.call(req.headers, key.toLowerCase())) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = req.headers[key.toLowerCase()];
|
||||
}
|
||||
return obj;
|
||||
@@ -29,9 +31,9 @@ export default async function handler(req, res) {
|
||||
const [status, contentType, data] = await httpsRequest(url, {
|
||||
agent: httpsAgent,
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
headers,
|
||||
body:
|
||||
req.method == "GET" || req.method == "HEAD"
|
||||
req.method === "GET" || req.method === "HEAD"
|
||||
? null
|
||||
: await getRawBody(req, {
|
||||
encoding: "utf8",
|
||||
@@ -40,12 +42,12 @@ export default async function handler(req, res) {
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
} else {
|
||||
}
|
||||
const [status, contentType, data] = await httpRequest(url, {
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
headers,
|
||||
body:
|
||||
req.method == "GET" || req.method == "HEAD"
|
||||
req.method === "GET" || req.method === "HEAD"
|
||||
? null
|
||||
: await getRawBody(req, {
|
||||
encoding: "utf8",
|
||||
@@ -55,4 +57,3 @@ export default async function handler(req, res) {
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
8
src/pages/api/revalidate.js
Normal file
8
src/pages/api/revalidate.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default async function handler(req, res) {
|
||||
try {
|
||||
await res.revalidate("/");
|
||||
return res.json({ revalidated: true });
|
||||
} catch (err) {
|
||||
return res.status(500).send("Error revalidating");
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,43 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import yaml from "js-yaml";
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
/* eslint-disable no-console */
|
||||
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/service-helpers";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
checkAndCopyConfig("services.yaml");
|
||||
let discoveredServices;
|
||||
let configuredServices;
|
||||
|
||||
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
|
||||
const fileContents = await fs.readFile(servicesYaml, "utf8");
|
||||
const services = yaml.load(fileContents);
|
||||
try {
|
||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||
console.error(e);
|
||||
discoveredServices = [];
|
||||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const servicesArray = services.map((group) => {
|
||||
return {
|
||||
name: Object.keys(group)[0],
|
||||
services: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
};
|
||||
}),
|
||||
try {
|
||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||
} catch (e) {
|
||||
console.error("Failed to load services.yaml, please check for errors");
|
||||
console.error(e);
|
||||
configuredServices = [];
|
||||
}
|
||||
|
||||
const mergedGroupsNames = [
|
||||
...new Set([discoveredServices.map((group) => group.name), configuredServices.map((group) => group.name)].flat()),
|
||||
];
|
||||
|
||||
const mergedGroups = [];
|
||||
|
||||
mergedGroupsNames.forEach((groupName) => {
|
||||
const discoveredGroup = discoveredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
|
||||
const mergedGroup = {
|
||||
name: groupName,
|
||||
services: [...discoveredGroup.services, ...configuredGroup.services].filter((service) => service),
|
||||
};
|
||||
|
||||
mergedGroups.push(mergedGroup);
|
||||
});
|
||||
|
||||
res.send(servicesArray);
|
||||
res.send(mergedGroups);
|
||||
}
|
||||
|
||||
117
src/pages/api/services/proxy.js
Normal file
117
src/pages/api/services/proxy.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import credentialedProxyHandler from "utils/proxies/credentialed";
|
||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||
import npmProxyHandler from "utils/proxies/npm";
|
||||
import transmissionProxyHandler from "utils/proxies/transmission";
|
||||
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
|
||||
|
||||
function asJson(data) {
|
||||
if (data?.length > 0) {
|
||||
const json = JSON.parse(data.toString());
|
||||
return json;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function jsonArrayTransform(data, transform) {
|
||||
const json = asJson(data);
|
||||
if (json instanceof Array) {
|
||||
return transform(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function jsonArrayFilter(data, filter) {
|
||||
return jsonArrayTransform(data, (items) => items.filter(filter));
|
||||
}
|
||||
|
||||
const serviceProxyHandlers = {
|
||||
// uses query param auth
|
||||
emby: genericProxyHandler,
|
||||
jellyfin: genericProxyHandler,
|
||||
pihole: genericProxyHandler,
|
||||
radarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movie: (data) => ({
|
||||
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
|
||||
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
sonarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
series: (data) => ({
|
||||
total: asJson(data).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
lidarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
album: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
readarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
book: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
bazarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movies: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
episodes: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
},
|
||||
},
|
||||
speedtest: genericProxyHandler,
|
||||
tautulli: genericProxyHandler,
|
||||
traefik: genericProxyHandler,
|
||||
sabnzbd: genericProxyHandler,
|
||||
jackett: genericProxyHandler,
|
||||
adguard: genericProxyHandler,
|
||||
// uses X-API-Key (or similar) header auth
|
||||
gotify: credentialedProxyHandler,
|
||||
portainer: credentialedProxyHandler,
|
||||
jellyseerr: credentialedProxyHandler,
|
||||
overseerr: credentialedProxyHandler,
|
||||
ombi: credentialedProxyHandler,
|
||||
coinmarketcap: credentialedProxyHandler,
|
||||
prowlarr: credentialedProxyHandler,
|
||||
// super specific handlers
|
||||
rutorrent: rutorrentProxyHandler,
|
||||
nzbget: nzbgetProxyHandler,
|
||||
npm: npmProxyHandler,
|
||||
transmission: transmissionProxyHandler,
|
||||
qbittorrent: qbittorrentProxyHandler,
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
const { proxy, maps } = serviceProxyHandler;
|
||||
if (proxy) {
|
||||
return proxy(req, res, maps);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
9
src/pages/api/validate.js
Normal file
9
src/pages/api/validate.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"];
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
||||
|
||||
res.send(errors);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -11,12 +13,10 @@ export default async function handler(req, res) {
|
||||
const widgets = yaml.load(fileContents);
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const widgetsArray = widgets.map((group) => {
|
||||
return {
|
||||
const widgetsArray = widgets.map((group) => ({
|
||||
type: Object.keys(group)[0],
|
||||
options: { ...group[Object.keys(group)[0]] },
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
res.send(widgetsArray);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import cachedFetch from "utils/cached-fetch";
|
||||
import { getSettings } from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { lat, lon, apiKey, duration, units } = req.query;
|
||||
const { latitude, longitude, units, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
|
||||
const api_url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${units}`;
|
||||
|
||||
res.send(await cachedFetch(api_url, duration));
|
||||
if (!apiKey && !provider) {
|
||||
return res.status(400).json({ error: "Missing API key or provider" });
|
||||
}
|
||||
|
||||
if (!apiKey && provider !== "openweathermap") {
|
||||
return res.status(400).json({ error: "Invalid provider for endpoint" });
|
||||
}
|
||||
|
||||
if (!apiKey && provider) {
|
||||
const settings = getSettings();
|
||||
apiKey = settings?.providers?.openweathermap;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ error: "Missing API key" });
|
||||
}
|
||||
|
||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
||||
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user