mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
325 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 | ||
|
|
9831df1427 | ||
|
|
5e6312fe93 |
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.
|
||||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
# https://github.com/docker/setup-qemu-action#about
|
# 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/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-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
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)
|
||||||
97
README.md
97
README.md
@@ -5,28 +5,30 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Fast! The entire site is statically generated at build time, so you can expect instant load times.
|
- 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 ([schklom](https://github.com/benphelps/homepage/pull/3) and [modem7](https://github.com/benphelps/homepage/pull/62))
|
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||||
* Full i18n support with automatic language detection.
|
- Full i18n support with automatic language detection
|
||||||
- Human translations for English, Norwegian Bokmål ([comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu)), Spanish ([AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves)), French (J. Lavoie), Dutch ([deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony)), Chinese ([nicedc](https://github.com/nicedc)) and Russian ([desolaris](https://github.com/benphelps/homepage/commits?author=desolaris)).
|
- Translations for Chinese, Dutch, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
|
||||||
- Machine translations for Portuguese and German.
|
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/).
|
- Service & Web Bookmarks
|
||||||
* Service & Web Bookmarks
|
- Docker Integration
|
||||||
* Docker Integration
|
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
- Automatic service discovery (via labels)
|
||||||
- Automatic service discovery (via labels)
|
- Service Integration
|
||||||
* Service Integration
|
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Tautulli (Plex), Overseerr, Jellyseerr ([ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
|
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
||||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify
|
||||||
* Information & Utility Widgets
|
- Information Providers
|
||||||
- System Stats (Disk, CPU, Memory)
|
- Coin Market Cap
|
||||||
- Weather via WeatherAPI.com or OpenWeatherMap ([AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
- Information & Utility Widgets
|
||||||
- Automatic location detection (with HTTPS), or manual location selection
|
- System Stats (Disk, CPU, Memory)
|
||||||
- Search Bar ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
- Weather via WeatherAPI.com or OpenWeatherMap
|
||||||
* Customizable
|
- Automatic location detection (with HTTPS), or manual location selection
|
||||||
- 21 theme colors with light and dark mode support
|
- Search Bar
|
||||||
- Background image support
|
- Customizable
|
||||||
|
- 21 theme colors with light and dark mode support
|
||||||
|
- Background image support
|
||||||
|
|
||||||
## Support & Suggestions
|
## Support & Suggestions
|
||||||
|
|
||||||
@@ -43,16 +45,16 @@ For configuration options, examples and more, [please check out the Wiki](https:
|
|||||||
Using docker compose:
|
Using docker compose:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.3'
|
version: "3.3"
|
||||||
services:
|
services:
|
||||||
homepage:
|
homepage:
|
||||||
image: ghcr.io/benphelps/homepage:latest
|
image: ghcr.io/benphelps/homepage:latest
|
||||||
container_name: homepage
|
container_name: homepage
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
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
|
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||||
```
|
```
|
||||||
|
|
||||||
or docker run:
|
or docker run:
|
||||||
@@ -76,6 +78,8 @@ pnpm install
|
|||||||
pnpm build
|
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:
|
Finally, run the server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -86,7 +90,7 @@ pnpm start
|
|||||||
|
|
||||||
Configuration files will be genereted and placed on the first request.
|
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
|
Configuration is done in the /config directory using .yaml files. Refer to each config for
|
||||||
the specific configuration options.
|
the specific configuration options.
|
||||||
|
|
||||||
You may also check [the wiki](https://github.com/benphelps/homepage/wiki) for detailed configuration instructions, examples and more.
|
You may also check [the wiki](https://github.com/benphelps/homepage/wiki) for detailed configuration instructions, examples and more.
|
||||||
@@ -108,3 +112,32 @@ pnpm dev
|
|||||||
Open [http://localhost:3000](http://localhost:3000) to start.
|
Open [http://localhost:3000](http://localhost:3000) to start.
|
||||||
|
|
||||||
This is a [Next.js](https://nextjs.org/) application, see their doucmentation for more information:
|
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,
|
reactStrictMode: true,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
swcMinify: false,
|
swcMinify: false,
|
||||||
experimental: { images: { allowFutureImage: true, unoptimized: true } },
|
|
||||||
images: {
|
images: {
|
||||||
domains: ["cdn.jsdelivr.net"],
|
domains: ["cdn.jsdelivr.net"],
|
||||||
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -6,45 +6,48 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"telemetry": "next telemetry disable"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.6.6",
|
"@headlessui/react": "^1.7.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dockerode": "^3.3.4",
|
"dockerode": "^3.3.4",
|
||||||
|
"follow-redirects": "^1.15.2",
|
||||||
"i18next": "^21.9.1",
|
"i18next": "^21.9.1",
|
||||||
"i18next-browser-languagedetector": "^6.1.5",
|
"i18next-browser-languagedetector": "^6.1.5",
|
||||||
"i18next-http-backend": "^1.4.1",
|
"i18next-http-backend": "^1.4.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-rpc-2.0": "^1.4.1",
|
"json-rpc-2.0": "^1.4.1",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "12.2.5",
|
"next": "^12.3.0",
|
||||||
"node-os-utils": "^1.3.7",
|
"node-os-utils": "^1.3.7",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"raw-body": "^2.5.1",
|
"raw-body": "^2.5.1",
|
||||||
"react": "18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^11.18.5",
|
"react-i18next": "^11.18.6",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"rutorrent-promise": "^2.0.0",
|
"rutorrent-promise": "^2.0.0",
|
||||||
"shvl": "^3.0.0",
|
"shvl": "^3.0.0",
|
||||||
"swr": "^1.3.0"
|
"swr": "^1.3.0",
|
||||||
|
"tough-cookie": "^4.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.9",
|
||||||
"eslint": "8.22.0",
|
"eslint": "^8.23.1",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-next": "12.2.5",
|
"eslint-config-next": "^12.3.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.16",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.1.8",
|
||||||
"typescript": "^4.8.2"
|
"typescript": "^4.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
510
pnpm-lock.yaml
generated
510
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
"free": "Frei",
|
"free": "Frei",
|
||||||
"used": "Gebraucht"
|
"used": "Gebraucht",
|
||||||
|
"load": "Belastung"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Rx",
|
"rx": "Rx",
|
||||||
@@ -23,18 +24,13 @@
|
|||||||
"playing": "Spielen",
|
"playing": "Spielen",
|
||||||
"transcoding": "Transcodierung",
|
"transcoding": "Transcodierung",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Keine aktiven streamen"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Spielen",
|
"playing": "Spielen",
|
||||||
"transcoding": "Transcodierung",
|
"transcoding": "Transcodierung",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Keine aktiven streamen"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "Rate",
|
|
||||||
"remaining": "Verblieben",
|
|
||||||
"downloaded": "Heruntergeladen"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
@@ -51,6 +47,11 @@
|
|||||||
"queued": "In Warteschlange",
|
"queued": "In Warteschlange",
|
||||||
"movies": "Filme"
|
"movies": "Filme"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"approved": "Genehmigt",
|
"approved": "Genehmigt",
|
||||||
@@ -96,5 +97,65 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
"available": "Available"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"free": "Free",
|
"free": "Free",
|
||||||
"used": "Used"
|
"used": "Used",
|
||||||
|
"load": "Load"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -53,11 +54,28 @@
|
|||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"downloaded": "Downloaded"
|
"downloaded": "Downloaded"
|
||||||
},
|
},
|
||||||
|
"sabnzbd": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"queue": "Queue",
|
||||||
|
"timeleft": "Time Left"
|
||||||
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"download": "Download"
|
"download": "Download"
|
||||||
},
|
},
|
||||||
|
"transmission": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
@@ -68,6 +86,20 @@
|
|||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"movies": "Movies"
|
"movies": "Movies"
|
||||||
},
|
},
|
||||||
|
"lidarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"albums": "Albums"
|
||||||
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
|
"bazarr": {
|
||||||
|
"missingEpisodes": "Missing Episodes",
|
||||||
|
"missingMovies": "Missing Movies"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
@@ -82,12 +114,18 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
"available": "Available"
|
"available": "Available"
|
||||||
},
|
},
|
||||||
"pihole": {
|
"pihole": {
|
||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"gravity": "Gravity"
|
"gravity": "Gravity"
|
||||||
},
|
},
|
||||||
|
"adguard": {
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"filtered": "Filtered",
|
||||||
|
"latency": "Latency"
|
||||||
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -107,5 +145,28 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"total": "Total"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Tipo de widget faltante: {{type}}",
|
"missing_type": "Falta el tipo de widget: {{type}}",
|
||||||
"api_error": "Error de API",
|
"api_error": "Error de API",
|
||||||
"status": "Estado"
|
"status": "Estado"
|
||||||
},
|
},
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"free": "Libre",
|
"free": "Libre",
|
||||||
"used": "Usado"
|
"used": "Usado",
|
||||||
|
"load": "Carga"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Recibido",
|
"rx": "Recibido",
|
||||||
@@ -20,37 +21,37 @@
|
|||||||
"offline": "Desconectado"
|
"offline": "Desconectado"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "En ejecución",
|
"playing": "Reproduciendo",
|
||||||
"transcoding": "Transcodificando",
|
"transcoding": "Transcodificando",
|
||||||
"bitrate": "Tasa de Bits",
|
"bitrate": "Tasa de bits",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Sin transmisiones activas"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "En ejecución",
|
"playing": "Reproduciendo",
|
||||||
"transcoding": "Transcodificación",
|
"transcoding": "Transcodificando",
|
||||||
"bitrate": "Tasa de bits",
|
"bitrate": "Tasa de bits",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Sin transmisiones activas"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "Velocidad",
|
|
||||||
"remaining": "Restante",
|
|
||||||
"downloaded": "Descargado"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Activo",
|
"active": "Activo",
|
||||||
"upload": "Subir",
|
"upload": "Subida",
|
||||||
"download": "Descargar"
|
"download": "Descarga"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Más deseado",
|
"wanted": "Más deseado",
|
||||||
"queued": "Puesto en cola",
|
"queued": "En cola",
|
||||||
"series": "Series"
|
"series": "Series"
|
||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Más deseado",
|
"wanted": "Más deseado",
|
||||||
"queued": "Puesto en cola",
|
"queued": "En cola",
|
||||||
"movies": "Películas"
|
"movies": "Películas"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Más deseado",
|
||||||
|
"queued": "En cola",
|
||||||
|
"books": "Libros"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Pendiente",
|
"pending": "Pendiente",
|
||||||
"approved": "Aprobado",
|
"approved": "Aprobado",
|
||||||
@@ -67,8 +68,8 @@
|
|||||||
"gravity": "Gravedad"
|
"gravity": "Gravedad"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Subir",
|
"upload": "Subida",
|
||||||
"download": "Descargar",
|
"download": "Descarga",
|
||||||
"ping": "Ping"
|
"ping": "Ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
@@ -87,14 +88,74 @@
|
|||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"current": "Ubicación Actual",
|
"current": "Ubicación actual",
|
||||||
"allow": "Haga clic para permitir",
|
"allow": "Haga clic para permitir",
|
||||||
"updating": "Actualizando",
|
"updating": "Actualizando",
|
||||||
"wait": "Espere, por favor"
|
"wait": "Espere, por favor"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "Pendiente",
|
||||||
"approved": "Approved",
|
"approved": "Aprobado",
|
||||||
"available": "Available"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,83 +8,84 @@
|
|||||||
"placeholder": "Recherche…"
|
"placeholder": "Recherche…"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"total": "Totale",
|
"total": "Total",
|
||||||
"free": "Libre",
|
"free": "Libre",
|
||||||
"used": "Utilisée"
|
"used": "Utilisé",
|
||||||
|
"load": "Charge"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Rx",
|
"rx": "Rx",
|
||||||
"tx": "TX",
|
"tx": "Tx",
|
||||||
"mem": "Mem",
|
"mem": "Mém",
|
||||||
"cpu": "CPU",
|
"cpu": "Cpu",
|
||||||
"offline": "Hors ligne"
|
"offline": "Hors ligne"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "En jouant",
|
"playing": "En lecture",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcodage",
|
||||||
"bitrate": "Débiter",
|
"bitrate": "Débit",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Aucun flux actif"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "En jouant",
|
"playing": "En lecture",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcodage",
|
||||||
"bitrate": "Débiter",
|
"bitrate": "Débit",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Aucun flux actif"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "Évaluer",
|
|
||||||
"remaining": "Restante",
|
|
||||||
"downloaded": "Téléchargé"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Active",
|
"active": "Actif",
|
||||||
"upload": "Téléverser",
|
"upload": "Envoi",
|
||||||
"download": "Télécharger"
|
"download": "Réception"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Recherchée",
|
"wanted": "Demandé",
|
||||||
"queued": "En queue",
|
"queued": "En queue",
|
||||||
"series": "Série"
|
"series": "Séries"
|
||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Recherchée",
|
"wanted": "Demandé",
|
||||||
"queued": "En queue",
|
"queued": "En queue",
|
||||||
"movies": "Films"
|
"movies": "Films"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Demandé",
|
||||||
|
"queued": "En Queue",
|
||||||
|
"books": "Livres"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"approved": "Approuvée",
|
"approved": "Validé",
|
||||||
"available": "Disponible"
|
"available": "Disponible"
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"approved": "Approuvée",
|
"approved": "Validé",
|
||||||
"available": "Disponible"
|
"available": "Disponible"
|
||||||
},
|
},
|
||||||
"pihole": {
|
"pihole": {
|
||||||
"queries": "Requêtes",
|
"queries": "Requêtes",
|
||||||
"blocked": "Bloquée",
|
"blocked": "Bloqué",
|
||||||
"gravity": "La gravité"
|
"gravity": "Listes dom. bloqués"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Téléversement",
|
"upload": "Envoi",
|
||||||
"download": "Téléchargement",
|
"download": "Récept.",
|
||||||
"ping": "Ping-ping"
|
"ping": "Ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "Fonctionnement",
|
"running": "Démarré",
|
||||||
"stopped": "Arrêté",
|
"stopped": "Arrêté",
|
||||||
"total": "Totale"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"traefik": {
|
"traefik": {
|
||||||
"routers": "Routeurs",
|
"routers": "Routeurs",
|
||||||
"services": "Prestations de service",
|
"services": "Services",
|
||||||
"middleware": "Middleware"
|
"middleware": "Middleware"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"enabled": "Activé",
|
"enabled": "Activé",
|
||||||
"disabled": "Handicapée",
|
"disabled": "Désactivé",
|
||||||
"total": "Totale"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"bbytes": "{{value, bytes(binary: true)}}",
|
"bbytes": "{{value, bytes(binary: true)}}",
|
||||||
@@ -104,8 +105,68 @@
|
|||||||
"wait": "Veuillez patienter"
|
"wait": "Veuillez patienter"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "En attente",
|
||||||
"approved": "Approved",
|
"approved": "Demande",
|
||||||
"available": "Available"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,16 @@
|
|||||||
"rx": "RX"
|
"rx": "RX"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "Playing",
|
"playing": "In riproduzione",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Nessuno Stream Attivo"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Playing",
|
"playing": "In riproduzione",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Nessuno Stream Attivo"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
@@ -24,77 +24,138 @@
|
|||||||
"ping": "Ping"
|
"ping": "Ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "Running",
|
"running": "In esecuzione",
|
||||||
"stopped": "Stopped",
|
"stopped": "Fermati",
|
||||||
"total": "Total"
|
"total": "Totali"
|
||||||
},
|
},
|
||||||
"traefik": {
|
"traefik": {
|
||||||
"routers": "Routers",
|
"routers": "Routers",
|
||||||
"services": "Services",
|
"services": "Servizi",
|
||||||
"middleware": "Middleware"
|
"middleware": "Middleware"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Missing Widget Type: {{type}}",
|
"missing_type": "Missing Widget Type: {{type}}",
|
||||||
"api_error": "API Error",
|
"api_error": "Errore API",
|
||||||
"status": "Status"
|
"status": "Stato"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search…"
|
"placeholder": "Cerca…"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"total": "Total",
|
"total": "Totale",
|
||||||
"free": "Free",
|
"free": "Libero",
|
||||||
"used": "Used"
|
"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": {
|
"nzbget": {
|
||||||
"rate": "Rate",
|
"rate": "Rate",
|
||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"downloaded": "Downloaded"
|
"downloaded": "Downloaded"
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"coinmarketcap": {
|
||||||
"active": "Active",
|
"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",
|
"upload": "Upload",
|
||||||
"download": "Download"
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"jackett": {
|
||||||
"series": "Series",
|
"configured": "Configured",
|
||||||
"wanted": "Wanted",
|
"errored": "Errored"
|
||||||
"queued": "Queued"
|
|
||||||
},
|
},
|
||||||
"radarr": {
|
"bazarr": {
|
||||||
|
"missingEpisodes": "Missing Episodes",
|
||||||
|
"missingMovies": "Missing Movies"
|
||||||
|
},
|
||||||
|
"lidarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"movies": "Movies"
|
"albums": "Albums"
|
||||||
},
|
},
|
||||||
"ombi": {
|
"adguard": {
|
||||||
"pending": "Pending",
|
|
||||||
"approved": "Approved",
|
|
||||||
"available": "Available"
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"pending": "Pending",
|
|
||||||
"approved": "Approved",
|
|
||||||
"available": "Available"
|
|
||||||
},
|
|
||||||
"pihole": {
|
|
||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
"gravity": "Gravity"
|
"filtered": "Filtered",
|
||||||
|
"latency": "Latency"
|
||||||
},
|
},
|
||||||
"npm": {
|
"qbittorrent": {
|
||||||
"enabled": "Enabled",
|
"download": "Download",
|
||||||
"disabled": "Disabled",
|
"leech": "Leech",
|
||||||
"total": "Total"
|
"upload": "Upload",
|
||||||
},
|
"seed": "Seed"
|
||||||
"weather": {
|
|
||||||
"current": "Current Location",
|
|
||||||
"allow": "Click to allow",
|
|
||||||
"updating": "Updating",
|
|
||||||
"wait": "Please wait"
|
|
||||||
},
|
|
||||||
"overseerr": {
|
|
||||||
"pending": "Pending",
|
|
||||||
"approved": "Approved",
|
|
||||||
"available": "Available"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Totalt",
|
"total": "Totalt",
|
||||||
"free": "Ledig",
|
"free": "Ledig",
|
||||||
"used": "Brukt"
|
"used": "Brukt",
|
||||||
|
"load": "Last inn"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Mottatt",
|
"rx": "Mottatt",
|
||||||
@@ -23,18 +24,13 @@
|
|||||||
"playing": "Spiller",
|
"playing": "Spiller",
|
||||||
"transcoding": "Transkoding",
|
"transcoding": "Transkoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Ingen aktive strømmer"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Spiller",
|
"playing": "Spiller",
|
||||||
"transcoding": "Transkoding",
|
"transcoding": "Transkoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Ingen aktive strømmer"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "Takt",
|
|
||||||
"remaining": "Gjenstående",
|
|
||||||
"downloaded": "Nedlastet"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
@@ -51,6 +47,11 @@
|
|||||||
"queued": "I kø",
|
"queued": "I kø",
|
||||||
"movies": "Filmer"
|
"movies": "Filmer"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Venter",
|
"pending": "Venter",
|
||||||
"approved": "Godkjent",
|
"approved": "Godkjent",
|
||||||
@@ -93,8 +94,68 @@
|
|||||||
"current": "Nåværende posisjon"
|
"current": "Nåværende posisjon"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "Venter",
|
||||||
"approved": "Approved",
|
"approved": "Godkjent",
|
||||||
"available": "Available"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Totaal",
|
"total": "Totaal",
|
||||||
"free": "Vrij",
|
"free": "Vrij",
|
||||||
"used": "Gebruikt"
|
"used": "Gebruikt",
|
||||||
|
"load": "Load"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -16,11 +17,6 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"offline": "Offline"
|
"offline": "Offline"
|
||||||
},
|
},
|
||||||
"nzbget": {
|
|
||||||
"rate": "Rate",
|
|
||||||
"remaining": "Overgebleven",
|
|
||||||
"downloaded": "Gedownload"
|
|
||||||
},
|
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -67,6 +63,11 @@
|
|||||||
"wanted": "Gezocht",
|
"wanted": "Gezocht",
|
||||||
"queued": "In de wachtrij"
|
"queued": "In de wachtrij"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "In afwachting",
|
"pending": "In afwachting",
|
||||||
"approved": "Goedgekeurd",
|
"approved": "Goedgekeurd",
|
||||||
@@ -96,5 +97,65 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
"available": "Available"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Tipo de widget ausente: {{type}}",
|
"missing_type": "Widget ausente: {{type}}",
|
||||||
"api_error": "Erro da API",
|
"api_error": "Erro da API",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
@@ -10,34 +10,30 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"free": "Livre",
|
"free": "Livre",
|
||||||
"used": "Usada"
|
"used": "Usado",
|
||||||
|
"load": "Load"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Rx",
|
"rx": "Rx",
|
||||||
"tx": "Tx",
|
"tx": "Tx",
|
||||||
"mem": "Mem",
|
"mem": "Mem",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"offline": "Desligada"
|
"offline": "Desligado"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "A reproduzir",
|
"playing": "A reproduzir",
|
||||||
"transcoding": "Transcodificação",
|
"transcoding": "Transcodificação",
|
||||||
"bitrate": "Taxa de bits",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Sem streams ativas"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Reproduzindo",
|
"playing": "Reproduzindo",
|
||||||
"transcoding": "Transcodificação",
|
"transcoding": "Transcodificação",
|
||||||
"bitrate": "Taxa de bits",
|
"bitrate": "Taxa de bits",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "Sem streams ativas"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "Avaliar",
|
|
||||||
"remaining": "Em falta",
|
|
||||||
"downloaded": "Baixada"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Ativa",
|
"active": "Ativo",
|
||||||
"upload": "Envio",
|
"upload": "Envio",
|
||||||
"download": "ReceçãoDownload"
|
"download": "ReceçãoDownload"
|
||||||
},
|
},
|
||||||
@@ -48,9 +44,14 @@
|
|||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Desejado",
|
"wanted": "Desejado",
|
||||||
"queued": "Enfileiradas",
|
"queued": "Fila",
|
||||||
"movies": "Filmes"
|
"movies": "Filmes"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Em fila",
|
||||||
|
"books": "Livros"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Pendente",
|
"pending": "Pendente",
|
||||||
"approved": "Aprovada",
|
"approved": "Aprovada",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"pihole": {
|
"pihole": {
|
||||||
"queries": "Consultas",
|
"queries": "Consultas",
|
||||||
"blocked": "Bloqueado",
|
"blocked": "Bloqueado",
|
||||||
"gravity": "Gravidade"
|
"gravity": "Gravity"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Envio",
|
"upload": "Envio",
|
||||||
@@ -72,18 +73,18 @@
|
|||||||
"ping": "Ping"
|
"ping": "Ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "Corrida",
|
"running": "A correr",
|
||||||
"stopped": "Parou",
|
"stopped": "Parado",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"traefik": {
|
"traefik": {
|
||||||
"routers": "Roteadores",
|
"routers": "Routers",
|
||||||
"services": "Serviços",
|
"services": "Serviços",
|
||||||
"middleware": "Middleware"
|
"middleware": "Middleware"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"enabled": "Habilitada",
|
"enabled": "Ativo",
|
||||||
"disabled": "Desabilitada",
|
"disabled": "Desabilitado",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -104,8 +105,68 @@
|
|||||||
"wait": "Por favor aguarde"
|
"wait": "Por favor aguarde"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "Pendente",
|
||||||
"approved": "Approved",
|
"approved": "Aprovado",
|
||||||
"available": "Available"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
"placeholder": "Поиск…"
|
"placeholder": "Поиск…"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"total": "Общий",
|
"total": "Всего",
|
||||||
"free": "Свободно",
|
"free": "Свободно",
|
||||||
"used": "Использовано"
|
"used": "Использовано",
|
||||||
|
"load": "Load"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "Rx",
|
"rx": "Rx",
|
||||||
@@ -31,11 +32,6 @@
|
|||||||
"bitrate": "Битрейт",
|
"bitrate": "Битрейт",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "No Active Streams"
|
||||||
},
|
},
|
||||||
"nzbget": {
|
|
||||||
"rate": "Оценка",
|
|
||||||
"remaining": "Осталось",
|
|
||||||
"downloaded": "Загружено"
|
|
||||||
},
|
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Активный",
|
"active": "Активный",
|
||||||
"upload": "Загрузить",
|
"upload": "Загрузить",
|
||||||
@@ -51,6 +47,11 @@
|
|||||||
"queued": "В очереди",
|
"queued": "В очереди",
|
||||||
"movies": "Фильмы"
|
"movies": "Фильмы"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Ожидание",
|
"pending": "Ожидание",
|
||||||
"approved": "Одобрено",
|
"approved": "Одобрено",
|
||||||
@@ -88,13 +89,73 @@
|
|||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"wait": "Пожалуйста подождите",
|
"wait": "Пожалуйста подождите",
|
||||||
"current": "Текущее местоположение",
|
"current": "Текущая локация",
|
||||||
"allow": "Click to allow",
|
"allow": "Нажмите, чтобы разрешить",
|
||||||
"updating": "Обновление"
|
"updating": "Обновление"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
"available": "Available"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"total": "Tổng",
|
"total": "Tổng",
|
||||||
"free": "Dư",
|
"free": "Dư",
|
||||||
"used": "Đã dùng"
|
"used": "Đã dùng",
|
||||||
|
"load": "Load"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -31,11 +32,6 @@
|
|||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "No Active Streams"
|
||||||
},
|
},
|
||||||
"nzbget": {
|
|
||||||
"rate": "Rate",
|
|
||||||
"remaining": "Còn lại",
|
|
||||||
"downloaded": "Đã tải"
|
|
||||||
},
|
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Hoạt động",
|
"active": "Hoạt động",
|
||||||
"upload": "Tải lên",
|
"upload": "Tải lên",
|
||||||
@@ -49,11 +45,16 @@
|
|||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"movies": "Movies"
|
"movies": "Phim"
|
||||||
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Đang tìm",
|
||||||
|
"queued": "Đang chờ",
|
||||||
|
"books": "Sách"
|
||||||
},
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "Pending",
|
"pending": "Đang xử lý",
|
||||||
"approved": "Approved",
|
"approved": "Đã duyệt",
|
||||||
"available": "Available"
|
"available": "Available"
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
@@ -87,14 +88,74 @@
|
|||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"current": "Current Location",
|
"current": "Vị trí hiện tại",
|
||||||
"allow": "Click to allow",
|
"allow": "Bấm để đồng ý",
|
||||||
"updating": "Updating",
|
"updating": "Đang cập nhật",
|
||||||
"wait": "Please wait"
|
"wait": "Vui lòng chờ"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"approved": "Approved",
|
"approved": "Đã duyệt",
|
||||||
"available": "Available"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,38 @@
|
|||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "缺少小部件类型:{{type}}",
|
"missing_type": "缺少小部件类型:{{type}}",
|
||||||
"api_error": "API错误",
|
"api_error": "API错误",
|
||||||
"status": "地位"
|
"status": "状态"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索…"
|
"placeholder": "搜索…"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"total": "全部的",
|
"total": "共",
|
||||||
"free": "自由的",
|
"free": "空闲",
|
||||||
"used": "用过的"
|
"used": "已用",
|
||||||
|
"load": "负载"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "rx",
|
"rx": "接收",
|
||||||
"tx": "TX",
|
"tx": "发送",
|
||||||
"mem": "mem",
|
"mem": "内存",
|
||||||
"cpu": "中央处理器",
|
"cpu": "处理器",
|
||||||
"offline": "离线"
|
"offline": "离线"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "玩",
|
"playing": "正在播放",
|
||||||
"transcoding": "转码",
|
"transcoding": "转码",
|
||||||
"bitrate": "比特率",
|
"bitrate": "比特率",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "暂无播放"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "玩",
|
"playing": "正在播放",
|
||||||
"transcoding": "转码",
|
"transcoding": "转码",
|
||||||
"bitrate": "比特率",
|
"bitrate": "比特率",
|
||||||
"no_active": "No Active Streams"
|
"no_active": "暂无播放"
|
||||||
},
|
|
||||||
"nzbget": {
|
|
||||||
"rate": "速度",
|
|
||||||
"remaining": "其余的",
|
|
||||||
"downloaded": "下载"
|
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "积极的",
|
"active": "活动中",
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"download": "下载"
|
"download": "下载"
|
||||||
},
|
},
|
||||||
@@ -47,13 +43,18 @@
|
|||||||
"series": "系列"
|
"series": "系列"
|
||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "通缉",
|
"wanted": "订阅",
|
||||||
"queued": "排队",
|
"queued": "队列",
|
||||||
"movies": "电影"
|
"movies": "电影"
|
||||||
},
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "订阅",
|
||||||
|
"queued": "队列",
|
||||||
|
"books": "书籍"
|
||||||
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
"pending": "待办的",
|
"pending": "待办的",
|
||||||
"approved": "得到正式认可的",
|
"approved": "已批准",
|
||||||
"available": "可用的"
|
"available": "可用的"
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
@@ -72,9 +73,9 @@
|
|||||||
"ping": "ping"
|
"ping": "ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "跑步",
|
"running": "运行中",
|
||||||
"stopped": "停了下来",
|
"stopped": "已停止",
|
||||||
"total": "全部的"
|
"total": "总计"
|
||||||
},
|
},
|
||||||
"traefik": {
|
"traefik": {
|
||||||
"routers": "路由器",
|
"routers": "路由器",
|
||||||
@@ -87,14 +88,74 @@
|
|||||||
"total": "全部的"
|
"total": "全部的"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"current": "Current Location",
|
"current": "当前定位",
|
||||||
"allow": "Click to allow",
|
"allow": "点击并允许",
|
||||||
"updating": "Updating",
|
"updating": "更新中",
|
||||||
"wait": "Please wait"
|
"wait": "请等待"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Pending",
|
"pending": "待办",
|
||||||
"approved": "Approved",
|
"approved": "已批准",
|
||||||
"available": "Available"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export default function Item({ bookmark }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.open(bookmark.href, "_blank").focus()}
|
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 backdrop-blur-md"
|
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">
|
<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">
|
<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">
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function ColorToggle() {
|
|||||||
{colors.map((color) => (
|
{colors.map((color) => (
|
||||||
<button type="button" onClick={() => setColor(color)} key={color}>
|
<button type="button" onClick={() => setColor(color)} key={color}>
|
||||||
<div
|
<div
|
||||||
|
title={color}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active === color ? "border-2" : "border-0",
|
active === color ? "border-2" : "border-0",
|
||||||
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
||||||
|
|||||||
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";
|
import List from "components/services/list";
|
||||||
|
|
||||||
export default function ServicesGroup({ services }) {
|
export default function ServicesGroup({ services, layout }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={services.name}
|
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">
|
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||||
{services.name}
|
<List services={services.services} layout={layout} />
|
||||||
</h2>
|
|
||||||
<List services={services.services} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,43 +22,48 @@ function resolveIcon(icon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Item({ service }) {
|
export default function Item({ service }) {
|
||||||
|
const hasLink = service.href && service.href !== "#";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={service.name}>
|
<li key={service.name}>
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
service.href && service.href !== "#" ? "cursor-pointer " : "cursor-default "
|
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 backdrop-blur-md`}
|
}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">
|
<div className="flex select-none">
|
||||||
{service.icon && (
|
{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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasLink ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
href={service.href}
|
||||||
if (service.href && service.href !== "#") {
|
className="flex-1 flex items-center justify-between rounded-r-md "
|
||||||
window.open(service.href, "_blank").focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
|
||||||
>
|
>
|
||||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
<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>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (service.href && service.href !== "#") {
|
|
||||||
window.open(service.href, "_blank").focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
</button>
|
|
||||||
{service.container && (
|
{service.container && (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="div"
|
as="div"
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import Item from "components/services/item";
|
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 (
|
return (
|
||||||
<ul 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) => (
|
{services.map((service) => (
|
||||||
<Item key={service.name} service={service} />
|
<Item key={service.name} service={service} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import Sonarr from "./widgets/service/sonarr";
|
import Sonarr from "./widgets/service/sonarr";
|
||||||
import Radarr from "./widgets/service/radarr";
|
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 Ombi from "./widgets/service/ombi";
|
||||||
import Portainer from "./widgets/service/portainer";
|
import Portainer from "./widgets/service/portainer";
|
||||||
import Emby from "./widgets/service/emby";
|
import Emby from "./widgets/service/emby";
|
||||||
import Nzbget from "./widgets/service/nzbget";
|
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 Docker from "./widgets/service/docker";
|
||||||
import Pihole from "./widgets/service/pihole";
|
import Pihole from "./widgets/service/pihole";
|
||||||
import Rutorrent from "./widgets/service/rutorrent";
|
import Rutorrent from "./widgets/service/rutorrent";
|
||||||
@@ -16,24 +22,40 @@ import Jellyseerr from "./widgets/service/jellyseerr";
|
|||||||
import Overseerr from "./widgets/service/overseerr";
|
import Overseerr from "./widgets/service/overseerr";
|
||||||
import Npm from "./widgets/service/npm";
|
import Npm from "./widgets/service/npm";
|
||||||
import Tautulli from "./widgets/service/tautulli";
|
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 = {
|
const widgetMappings = {
|
||||||
docker: Docker,
|
docker: Docker,
|
||||||
sonarr: Sonarr,
|
sonarr: Sonarr,
|
||||||
radarr: Radarr,
|
radarr: Radarr,
|
||||||
|
lidarr: Lidarr,
|
||||||
|
readarr: Readarr,
|
||||||
|
bazarr: Bazarr,
|
||||||
ombi: Ombi,
|
ombi: Ombi,
|
||||||
portainer: Portainer,
|
portainer: Portainer,
|
||||||
emby: Emby,
|
emby: Emby,
|
||||||
jellyfin: Jellyfin,
|
jellyfin: Jellyfin,
|
||||||
nzbget: Nzbget,
|
nzbget: Nzbget,
|
||||||
|
sabnzbd: SABnzbd,
|
||||||
|
transmission: Transmission,
|
||||||
|
qbittorrent: QBittorrent,
|
||||||
pihole: Pihole,
|
pihole: Pihole,
|
||||||
rutorrent: Rutorrent,
|
rutorrent: Rutorrent,
|
||||||
speedtest: Speedtest,
|
speedtest: Speedtest,
|
||||||
traefik: Traefik,
|
traefik: Traefik,
|
||||||
jellyseerr: Jellyseerr,
|
jellyseerr: Jellyseerr,
|
||||||
overseerr: Overseerr,
|
overseerr: Overseerr,
|
||||||
|
coinmarketcap: CoinMarketCap,
|
||||||
npm: Npm,
|
npm: Npm,
|
||||||
tautulli: Tautulli,
|
tautulli: Tautulli,
|
||||||
|
gotify: Gotify,
|
||||||
|
prowlarr: Prowlarr,
|
||||||
|
jackett: Jackett,
|
||||||
|
adguard: AdGuard,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Widget({ service }) {
|
export default function Widget({ service }) {
|
||||||
|
|||||||
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,6 +1,7 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||||
|
import { MdOutlineSmartDisplay } from "react-icons/md";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
|
|
||||||
@@ -31,19 +32,31 @@ function SingleSessionEntry({ playCommand, session }) {
|
|||||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||||
} = session;
|
} = session;
|
||||||
|
|
||||||
|
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||||
|
IsVideoDirect: true,
|
||||||
|
VideoDecoderIsHardware: true,
|
||||||
|
VideoEncoderIsHardware: true,
|
||||||
|
};
|
||||||
|
|
||||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||||
|
|
||||||
return (
|
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-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">
|
<div className="grow text-xs z-10 self-center ml-2 relative w-full h-4 mr-2">
|
||||||
<span>
|
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||||
{Name}
|
{Name}
|
||||||
{SeriesName && ` - ${SeriesName}`}
|
{SeriesName && ` - ${SeriesName}`}
|
||||||
</span>
|
</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="grow" />
|
|
||||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
|
||||||
</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="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">
|
||||||
@@ -72,7 +85,12 @@ function SingleSessionEntry({ playCommand, session }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grow " />
|
<div className="grow " />
|
||||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</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-2 z-10">
|
||||||
|
{ticksToString(PositionTicks)}
|
||||||
|
<span className="mx-0.5 text-[8px]">/</span>
|
||||||
|
{ticksToString(RunTimeTicks)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -83,6 +101,9 @@ function SessionEntry({ playCommand, session }) {
|
|||||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||||
} = session;
|
} = session;
|
||||||
|
|
||||||
|
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {};
|
||||||
|
|
||||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -110,14 +131,20 @@ function SessionEntry({ playCommand, session }) {
|
|||||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>
|
</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}
|
{Name}
|
||||||
{SeriesName && ` - ${SeriesName}`}
|
{SeriesName && ` - ${SeriesName}`}
|
||||||
</span>
|
</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>
|
||||||
<div className="grow " />
|
|
||||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
|
||||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -175,7 +202,7 @@ export default function Emby({ service }) {
|
|||||||
|
|
||||||
if (playing.length === 0) {
|
if (playing.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<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">
|
<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>
|
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +216,7 @@ export default function Emby({ service }) {
|
|||||||
if (playing.length === 1) {
|
if (playing.length === 1) {
|
||||||
const session = playing[0];
|
const session = playing[0];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
<SingleSessionEntry
|
<SingleSessionEntry
|
||||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||||
session={session}
|
session={session}
|
||||||
@@ -199,7 +226,7 @@ export default function Emby({ service }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
{playing.map((session) => (
|
{playing.map((session) => (
|
||||||
<SessionEntry
|
<SessionEntry
|
||||||
key={session.Id}
|
key={session.Id}
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,14 +28,11 @@ export default function Radarr({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wanted = moviesData.filter((movie) => movie.isAvailable === false);
|
|
||||||
const have = moviesData.filter((movie) => movie.isAvailable === true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
<Block label={t("radarr.wanted")} value={moviesData.wanted} />
|
||||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||||
<Block label={t("radarr.movies")} value={have.length} />
|
<Block label={t("radarr.movies")} value={moviesData.have} />
|
||||||
</Widget>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ export default function Sonarr({ service }) {
|
|||||||
<Widget>
|
<Widget>
|
||||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
<Block label={t("sonarr.series")} value={seriesData.total} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||||
|
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
|
|
||||||
@@ -27,16 +28,26 @@ function millisecondsToString(milliseconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SingleSessionEntry({ session }) {
|
function SingleSessionEntry({ session }) {
|
||||||
const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
|
const { full_title, duration, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||||
|
|
||||||
return (
|
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-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">
|
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||||
<span>{full_title}</span>
|
<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="grow" />
|
|
||||||
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</div>
|
|
||||||
</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="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">
|
||||||
@@ -48,15 +59,17 @@ function SingleSessionEntry({ session }) {
|
|||||||
/>
|
/>
|
||||||
<div className="text-xs z-10 self-center ml-1">
|
<div className="text-xs z-10 self-center ml-1">
|
||||||
{state === "paused" && (
|
{state === "paused" && (
|
||||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||||
)}
|
)}
|
||||||
{state !== "paused" && (
|
{state !== "paused" && (
|
||||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grow " />
|
<div className="grow " />
|
||||||
<div className="self-center text-xs flex justify-end mr-2">
|
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||||
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
|
{millisecondsToString(view_offset)}
|
||||||
|
<span className="mx-0.5 text-[8px]">/</span>
|
||||||
|
{millisecondsToString(duration)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -64,7 +77,7 @@ function SingleSessionEntry({ session }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SessionEntry({ session }) {
|
function SessionEntry({ session }) {
|
||||||
const { full_title, view_offset, progress_percent, state } = session;
|
const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||||
|
|
||||||
return (
|
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-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">
|
||||||
@@ -76,15 +89,28 @@ function SessionEntry({ session }) {
|
|||||||
/>
|
/>
|
||||||
<div className="text-xs z-10 self-center ml-1">
|
<div className="text-xs z-10 self-center ml-1">
|
||||||
{state === "paused" && (
|
{state === "paused" && (
|
||||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
|
||||||
)}
|
|
||||||
{state !== "paused" && (
|
|
||||||
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||||
)}
|
)}
|
||||||
<span>{full_title}</span>
|
{state !== "paused" && (
|
||||||
|
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grow " />
|
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||||
<div className="self-center text-xs flex justify-end mr-2">{millisecondsToString(view_offset)}</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,7 +130,7 @@ export default function Tautulli({ service }) {
|
|||||||
|
|
||||||
if (!activityData) {
|
if (!activityData) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<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">
|
<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>
|
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +153,7 @@ export default function Tautulli({ service }) {
|
|||||||
|
|
||||||
if (playing.length === 0) {
|
if (playing.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<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">
|
<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>
|
<span className="absolute left-2 text-xs mt-[2px]">{t("tautulli.no_active")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,14 +167,14 @@ export default function Tautulli({ service }) {
|
|||||||
if (playing.length === 1) {
|
if (playing.length === 1) {
|
||||||
const session = playing[0];
|
const session = playing[0];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
<SingleSessionEntry session={session} />
|
<SingleSessionEntry session={session} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
{playing.map((session) => (
|
{playing.map((session) => (
|
||||||
<SessionEntry key={session.Id} session={session} />
|
<SessionEntry key={session.Id} session={session} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import WeatherApi from "components/widgets/weather/weather";
|
|||||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||||
import Resources from "components/widgets/resources/resources";
|
import Resources from "components/widgets/resources/resources";
|
||||||
import Search from "components/widgets/search/search";
|
import Search from "components/widgets/search/search";
|
||||||
|
import Greeting from "components/widgets/greeting/greeting";
|
||||||
|
import DateTime from "components/widgets/datetime/datetime";
|
||||||
|
|
||||||
const widgetMappings = {
|
const widgetMappings = {
|
||||||
weather: WeatherApi, // This key will be deprecated in the future
|
weather: WeatherApi, // This key will be deprecated in the future
|
||||||
@@ -9,13 +11,15 @@ const widgetMappings = {
|
|||||||
openweathermap: OpenWeatherMap,
|
openweathermap: OpenWeatherMap,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
search: Search,
|
search: Search,
|
||||||
|
greeting: Greeting,
|
||||||
|
datetime: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Widget({ widget }) {
|
export default function Widget({ widget }) {
|
||||||
const ServiceWidget = widgetMappings[widget.type];
|
const InfoWidget = widgetMappings[widget.type];
|
||||||
|
|
||||||
if (ServiceWidget) {
|
if (InfoWidget) {
|
||||||
return <ServiceWidget options={widget.options} />;
|
return <InfoWidget options={widget.options} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ function Widget({ options }) {
|
|||||||
|
|
||||||
if (error || data?.cod === 401 || data?.error) {
|
if (error || data?.cod === 401 || data?.error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
@@ -32,7 +32,7 @@ function Widget({ options }) {
|
|||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
@@ -49,7 +49,7 @@ function Widget({ options }) {
|
|||||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -100,7 +100,7 @@ export default function OpenWeatherMap({ options }) {
|
|||||||
|
|
||||||
if (!location) {
|
if (!location) {
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
{requesting ? (
|
{requesting ? (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import UsageBar from "./usage-bar";
|
import UsageBar from "./usage-bar";
|
||||||
|
|
||||||
export default function Cpu() {
|
export default function Cpu({ expanded }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||||
@@ -39,11 +39,29 @@ export default function Cpu() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
<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" />
|
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono min-w-[80px]">
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
<div className="text-theme-800 dark:text-theme-200 text-xs">
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}{" "}
|
<div className="pl-0.5">
|
||||||
{t("docker.cpu")}
|
{t("common.number", {
|
||||||
|
value: data.cpu.usage,
|
||||||
|
style: "unit",
|
||||||
|
unit: "percent",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="pr-1">{t("docker.cpu")}</div>
|
||||||
</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} />
|
<UsageBar percent={percent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import UsageBar from "./usage-bar";
|
import UsageBar from "./usage-bar";
|
||||||
|
|
||||||
export default function Disk({ options }) {
|
export default function Disk({ options, expanded }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||||
@@ -37,15 +37,19 @@ export default function Disk({ options }) {
|
|||||||
const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100);
|
const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
|
<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" />
|
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
<div className="pl-0.5">{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })}</div>
|
||||||
</span>
|
<div className="pr-1">{t("resources.free")}</div>
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
|
||||||
{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })} {t("resources.total")}
|
|
||||||
</span>
|
</span>
|
||||||
|
{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} />
|
<UsageBar percent={percent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import UsageBar from "./usage-bar";
|
import UsageBar from "./usage-bar";
|
||||||
|
|
||||||
export default function Memory() {
|
export default function Memory({ expanded }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||||
@@ -37,15 +37,27 @@ export default function Memory() {
|
|||||||
const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100);
|
const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
|
<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" />
|
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
<div className="pl-0.5">
|
||||||
</span>
|
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 0, binary: true })}
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
</div>
|
||||||
{t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
|
<div className="pr-1">{t("resources.free")}</div>
|
||||||
</span>
|
</span>
|
||||||
|
{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} />
|
<UsageBar percent={percent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import Cpu from "./cpu";
|
|||||||
import Memory from "./memory";
|
import Memory from "./memory";
|
||||||
|
|
||||||
export default function Resources({ options }) {
|
export default function Resources({ options }) {
|
||||||
|
const { expanded } = options;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col max-w:full sm:basis-auto self-center m-auto flex-wrap">
|
<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">
|
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||||
{options.cpu && <Cpu />}
|
{options.cpu && <Cpu expanded={expanded} />}
|
||||||
{options.memory && <Memory />}
|
{options.memory && <Memory expanded={expanded} />}
|
||||||
{Array.isArray(options.disk)
|
{Array.isArray(options.disk)
|
||||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} />)
|
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} />)
|
||||||
: options.disk && <Disk options={options} />}
|
: options.disk && <Disk options={options} expanded={expanded} />}
|
||||||
</div>
|
</div>
|
||||||
{options.label && (
|
{options.label && (
|
||||||
<div className="ml-6 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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function UsageBar({ percent }) {
|
export default function UsageBar({ percent }) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20 backdrop-blur-md">
|
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20">
|
||||||
<div
|
<div
|
||||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
|
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function Search({ options }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow mr-4" onSubmit={handleSubmit}>
|
<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" />
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -62,14 +62,15 @@ export default function Search({ options }) {
|
|||||||
bg-white/50 dark:bg-white/10
|
bg-white/50 dark:bg-white/10
|
||||||
focus:ring-theme-500 dark:focus:ring-white/50
|
focus:ring-theme-500 dark:focus:ring-white/50
|
||||||
focus:border-theme-500 dark:focus:border-white/50
|
focus:border-theme-500 dark:focus:border-white/50
|
||||||
border border-theme-300 dark:border-theme-200/50
|
border border-theme-300 dark:border-theme-200/50"
|
||||||
backdrop-blur-md"
|
|
||||||
placeholder={t("search.placeholder")}
|
placeholder={t("search.placeholder")}
|
||||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||||
required
|
required
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus={options.focus}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function Widget({ options }) {
|
|||||||
|
|
||||||
if (error || data?.error) {
|
if (error || data?.error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
@@ -32,7 +32,7 @@ function Widget({ options }) {
|
|||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
@@ -49,7 +49,7 @@ function Widget({ options }) {
|
|||||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
||||||
@@ -101,7 +101,7 @@ export default function WeatherApi({ options }) {
|
|||||||
|
|
||||||
if (!location) {
|
if (!location) {
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
|
<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-row items-center justify-end">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
{requesting ? (
|
{requesting ? (
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import "styles/weather-icons.css";
|
|||||||
import "styles/theme.css";
|
import "styles/theme.css";
|
||||||
|
|
||||||
import "utils/i18n";
|
import "utils/i18n";
|
||||||
|
import { ColorProvider } from "utils/color-context";
|
||||||
|
import { ThemeProvider } from "utils/theme-context";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
@@ -14,7 +16,11 @@ function MyApp({ Component, pageProps }) {
|
|||||||
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Component {...pageProps} />
|
<ColorProvider>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</ColorProvider>
|
||||||
</SWRConfig>
|
</SWRConfig>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.error("Failed to discover services, please check docker.yaml for errors");
|
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||||
|
console.error(e);
|
||||||
discoveredServices = [];
|
discoveredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.error("Failed to load services.yaml, please check for errors");
|
console.error("Failed to load services.yaml, please check for errors");
|
||||||
|
console.error(e);
|
||||||
configuredServices = [];
|
configuredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,26 +3,98 @@ import credentialedProxyHandler from "utils/proxies/credentialed";
|
|||||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||||
import npmProxyHandler from "utils/proxies/npm";
|
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 = {
|
const serviceProxyHandlers = {
|
||||||
// uses query param auth
|
// uses query param auth
|
||||||
emby: genericProxyHandler,
|
emby: genericProxyHandler,
|
||||||
jellyfin: genericProxyHandler,
|
jellyfin: genericProxyHandler,
|
||||||
pihole: genericProxyHandler,
|
pihole: genericProxyHandler,
|
||||||
radarr: genericProxyHandler,
|
radarr: {
|
||||||
sonarr: genericProxyHandler,
|
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,
|
speedtest: genericProxyHandler,
|
||||||
tautulli: genericProxyHandler,
|
tautulli: genericProxyHandler,
|
||||||
traefik: genericProxyHandler,
|
traefik: genericProxyHandler,
|
||||||
// uses X-API-Key header auth
|
sabnzbd: genericProxyHandler,
|
||||||
|
jackett: genericProxyHandler,
|
||||||
|
adguard: genericProxyHandler,
|
||||||
|
// uses X-API-Key (or similar) header auth
|
||||||
|
gotify: credentialedProxyHandler,
|
||||||
portainer: credentialedProxyHandler,
|
portainer: credentialedProxyHandler,
|
||||||
jellyseerr: credentialedProxyHandler,
|
jellyseerr: credentialedProxyHandler,
|
||||||
overseerr: credentialedProxyHandler,
|
overseerr: credentialedProxyHandler,
|
||||||
ombi: credentialedProxyHandler,
|
ombi: credentialedProxyHandler,
|
||||||
|
coinmarketcap: credentialedProxyHandler,
|
||||||
|
prowlarr: credentialedProxyHandler,
|
||||||
// super specific handlers
|
// super specific handlers
|
||||||
rutorrent: rutorrentProxyHandler,
|
rutorrent: rutorrentProxyHandler,
|
||||||
nzbget: nzbgetProxyHandler,
|
nzbget: nzbgetProxyHandler,
|
||||||
npm: npmProxyHandler,
|
npm: npmProxyHandler,
|
||||||
|
transmission: transmissionProxyHandler,
|
||||||
|
qbittorrent: qbittorrentProxyHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
@@ -31,7 +103,14 @@ export default async function handler(req, res) {
|
|||||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||||
|
|
||||||
if (serviceProxyHandler) {
|
if (serviceProxyHandler) {
|
||||||
return serviceProxyHandler(req, res);
|
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" });
|
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);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey && provider) {
|
if (!apiKey && provider) {
|
||||||
const settings = await getSettings();
|
const settings = getSettings();
|
||||||
apiKey = settings?.providers?.openweathermap;
|
apiKey = settings?.providers?.openweathermap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey && provider) {
|
if (!apiKey && provider) {
|
||||||
const settings = await getSettings();
|
const settings = getSettings();
|
||||||
apiKey = settings?.providers?.weatherapi;
|
apiKey = settings?.providers?.weatherapi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
import { BiError } from "react-icons/bi";
|
||||||
|
|
||||||
import ServicesGroup from "components/services/group";
|
import ServicesGroup from "components/services/group";
|
||||||
import BookmarksGroup from "components/bookmarks/group";
|
import BookmarksGroup from "components/bookmarks/group";
|
||||||
import Widget from "components/widget";
|
import Widget from "components/widget";
|
||||||
import Revalidate from "components/revalidate";
|
import Revalidate from "components/revalidate";
|
||||||
import { getSettings } from "utils/config";
|
import { getSettings } from "utils/config";
|
||||||
import { ColorProvider } from "utils/color-context";
|
import { ColorContext } from "utils/color-context";
|
||||||
import { ThemeProvider } from "utils/theme-context";
|
import { ThemeContext } from "utils/theme-context";
|
||||||
|
|
||||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -19,18 +22,60 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
|
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"];
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export function getStaticProps() {
|
||||||
const settings = await getSettings();
|
try {
|
||||||
|
const settings = getSettings();
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
settings,
|
settings,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default function Home({ settings }) {
|
|
||||||
|
export default function Index({ settings }) {
|
||||||
|
const { data: errorsData } = useSWR("/api/validate");
|
||||||
|
|
||||||
|
if (errorsData && errorsData.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="w-full container m-auto justify-center p-10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{errorsData.map((error, i) => (
|
||||||
|
<div
|
||||||
|
className="basis-1/2 bg-theme-500 dark:bg-theme-600 text-theme-600 dark:text-theme-300 m-2 rounded-md font-mono shadow-md border-4 border-transparent"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<div className="bg-amber-200 text-amber-800 dark:text-amber-200 dark:bg-amber-800 p-2 rounded-md font-bold">
|
||||||
|
<BiError className="float-right w-6 h-6" />
|
||||||
|
{error.config}
|
||||||
|
</div>
|
||||||
|
<div className="p-2 text-theme-100 dark:text-theme-200">
|
||||||
|
<pre className="opacity-50 font-bold pb-2">{error.reason}</pre>
|
||||||
|
<pre className="text-sm">{error.mark.snippet}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Home settings={settings} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Home({ settings }) {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const { theme, setTheme } = useContext(ThemeContext);
|
||||||
|
const { color, setColor } = useContext(ColorContext);
|
||||||
|
|
||||||
const { data: services } = useSWR("/api/services");
|
const { data: services } = useSWR("/api/services");
|
||||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||||
const { data: widgets } = useSWR("/api/widgets");
|
const { data: widgets } = useSWR("/api/widgets");
|
||||||
@@ -39,59 +84,74 @@ export default function Home({ settings }) {
|
|||||||
if (settings.background) {
|
if (settings.background) {
|
||||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||||
wrappedStyle.backgroundSize = "cover";
|
wrappedStyle.backgroundSize = "cover";
|
||||||
|
wrappedStyle.opacity = settings.backgroundOpacity ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.language) {
|
||||||
|
i18n.changeLanguage(settings.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.theme && theme !== settings.theme) {
|
||||||
|
setTheme(settings.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.color && color !== settings.color) {
|
||||||
|
setColor(settings.color);
|
||||||
|
}
|
||||||
|
}, [i18n, settings, color, setColor, theme, setTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColorProvider>
|
<>
|
||||||
<ThemeProvider>
|
<Head>
|
||||||
<Head>
|
<title>{settings.title || "Homepage"}</title>
|
||||||
<title>{settings.title || "Homepage"}</title>
|
{settings.base && <base href={settings.base} />}
|
||||||
</Head>
|
{settings.favicon && <link rel="icon" href={settings.favicon} />}
|
||||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
</Head>
|
||||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||||
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
|
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||||
{widgets && (
|
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
|
||||||
<>
|
{widgets && (
|
||||||
|
<>
|
||||||
|
{widgets
|
||||||
|
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||||
|
.map((widget, i) => (
|
||||||
|
<Widget key={i} widget={widget} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
||||||
{widgets
|
{widgets
|
||||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||||
.map((widget, i) => (
|
.map((widget, i) => (
|
||||||
<Widget key={i} widget={widget} />
|
<Widget key={i} widget={widget} />
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
</>
|
||||||
{widgets
|
|
||||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
|
||||||
.map((widget, i) => (
|
|
||||||
<Widget key={i} widget={widget} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{services && (
|
|
||||||
<div className="flex flex-wrap p-8 items-start">
|
|
||||||
{services.map((group) => (
|
|
||||||
<ServicesGroup key={group.name} services={group} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bookmarks && (
|
|
||||||
<div className="grow flex flex-wrap pt-0 p-8">
|
|
||||||
{bookmarks.map((group) => (
|
|
||||||
<BookmarksGroup key={group.name} group={group} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rounded-full flex p-8 w-full justify-between">
|
|
||||||
<ColorToggle />
|
|
||||||
<Revalidate />
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
|
||||||
</ColorProvider>
|
{services && (
|
||||||
|
<div className="flex flex-wrap p-8 items-start">
|
||||||
|
{services.map((group) => (
|
||||||
|
<ServicesGroup key={group.name} services={group} layout={settings.layout?.[group.name]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{bookmarks && (
|
||||||
|
<div className="grow flex flex-wrap pt-0 p-8">
|
||||||
|
{bookmarks.map((group) => (
|
||||||
|
<BookmarksGroup key={group.name} group={group} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-full flex p-8 w-full justify-end">
|
||||||
|
{!settings?.color && <ColorToggle />}
|
||||||
|
<Revalidate />
|
||||||
|
{!settings?.theme && <ThemeToggle />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
# For configuration options and examples, please see:
|
# For configuration options and examples, please see:
|
||||||
# https://github.com/benphelps/homepage/wiki/Bookmarks
|
# https://github.com/benphelps/homepage/wiki/Bookmarks
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
---
|
||||||
# For configuration options and examples, please see:
|
# For configuration options and examples, please see:
|
||||||
# https://github.com/benphelps/homepage/wiki/Docker-Integration
|
# https://github.com/benphelps/homepage/wiki/Docker-Integration
|
||||||
|
|
||||||
my-docker:
|
# my-docker:
|
||||||
host: 127.0.0.1
|
# host: 127.0.0.1
|
||||||
port: 2375
|
# port: 2375
|
||||||
|
|
||||||
other-docker:
|
# my-docker:
|
||||||
socket: /var/run/docker.sock
|
# socket: /var/run/docker.sock
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
# For configuration options and examples, please see:
|
# For configuration options and examples, please see:
|
||||||
# https://github.com/benphelps/homepage/wiki/Services
|
# https://github.com/benphelps/homepage/wiki/Services
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
# For configuration options and examples, please see:
|
# For configuration options and examples, please see:
|
||||||
# https://github.com/benphelps/homepage/wiki/Settings
|
# https://github.com/benphelps/homepage/wiki/Settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
# For configuration options and examples, please see:
|
# For configuration options and examples, please see:
|
||||||
# https://github.com/benphelps/homepage/wiki/Information-Widgets
|
# https://github.com/benphelps/homepage/wiki/Information-Widgets
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,21 @@ const formats = {
|
|||||||
traefik: `{url}/api/{endpoint}`,
|
traefik: `{url}/api/{endpoint}`,
|
||||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||||
|
transmission: `{url}/transmission/rpc`,
|
||||||
|
qbittorrent: `{url}/api/v2/{endpoint}`,
|
||||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||||
overseerr: `{url}/api/v1/{endpoint}`,
|
overseerr: `{url}/api/v1/{endpoint}`,
|
||||||
ombi: `{url}/api/v1/{endpoint}`,
|
ombi: `{url}/api/v1/{endpoint}`,
|
||||||
npm: `{url}/api/{endpoint}`,
|
npm: `{url}/api/{endpoint}`,
|
||||||
|
lidarr: `{url}/api/v1/{endpoint}?apikey={key}`,
|
||||||
|
readarr: `{url}/api/v1/{endpoint}?apikey={key}`,
|
||||||
|
bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`,
|
||||||
|
sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`,
|
||||||
|
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
|
||||||
|
gotify: `{url}/{endpoint}`,
|
||||||
|
prowlarr: `{url}/api/v1/{endpoint}`,
|
||||||
|
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
|
||||||
|
adguard: `{url}/control/{endpoint}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatApiCall(api, args) {
|
export function formatApiCall(api, args) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { existsSync, copyFile, promises as fs } from "fs";
|
import { existsSync, copyFile, readFileSync } from "fs";
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
@@ -15,13 +15,22 @@ export default function checkAndCopyConfig(config) {
|
|||||||
}
|
}
|
||||||
console.info("%s was copied to the config folder", config);
|
console.info("%s was copied to the config folder", config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
yaml.load(readFileSync(configYaml, "utf8"));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return { ...e, config };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSettings() {
|
export function getSettings() {
|
||||||
checkAndCopyConfig("settings.yaml");
|
checkAndCopyConfig("settings.yaml");
|
||||||
|
|
||||||
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
|
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
|
||||||
const fileContents = await fs.readFile(settingsYaml, "utf8");
|
const fileContents = readFileSync(settingsYaml, "utf8");
|
||||||
return yaml.load(fileContents);
|
return yaml.load(fileContents);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/utils/cookie-jar.js
Normal file
34
src/utils/cookie-jar.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { Cookie, CookieJar } from 'tough-cookie';
|
||||||
|
|
||||||
|
const cookieJar = new CookieJar();
|
||||||
|
|
||||||
|
export function setCookieHeader(url, params) {
|
||||||
|
// add cookie header, if we have one in the jar
|
||||||
|
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
||||||
|
if (existingCookie) {
|
||||||
|
params.headers = params.headers ?? {};
|
||||||
|
params.headers.Cookie = existingCookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCookieToJar(url, headers) {
|
||||||
|
let cookieHeader = headers['set-cookie'];
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
cookieHeader = headers.get('set-cookie');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieHeader || cookieHeader.length === 0) return;
|
||||||
|
|
||||||
|
let cookies = null;
|
||||||
|
if (cookieHeader instanceof Array) {
|
||||||
|
cookies = cookieHeader.map(Cookie.parse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cookies = [Cookie.parse(cookieHeader)];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < cookies.length; i += 1) {
|
||||||
|
cookieJar.setCookieSync(cookies[i], url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
/* eslint-disable prefer-promise-reject-errors */
|
/* eslint-disable prefer-promise-reject-errors */
|
||||||
import https from "https";
|
/* eslint-disable no-param-reassign */
|
||||||
import http from "http";
|
import { http, https } from "follow-redirects";
|
||||||
|
|
||||||
|
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||||
|
|
||||||
|
function addCookieHandler(url, params) {
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
// handle cookies during redirects
|
||||||
|
params.beforeRedirect = (options, responseInfo) => {
|
||||||
|
addCookieToJar(options.href, responseInfo.headers);
|
||||||
|
setCookieHeader(options.href, options);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function httpsRequest(url, params) {
|
export function httpsRequest(url, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
addCookieHandler(url, params);
|
||||||
const request = https.request(url, params, (response) => {
|
const request = https.request(url, params, (response) => {
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
@@ -12,7 +25,8 @@ export function httpsRequest(url, params) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.on("end", () => {
|
response.on("end", () => {
|
||||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data)]);
|
addCookieToJar(url, response.headers);
|
||||||
|
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,12 +34,17 @@ export function httpsRequest(url, params) {
|
|||||||
reject([500, error]);
|
reject([500, error]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (params.body) {
|
||||||
|
request.write(params.body);
|
||||||
|
}
|
||||||
|
|
||||||
request.end();
|
request.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function httpRequest(url, params) {
|
export function httpRequest(url, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
addCookieHandler(url, params);
|
||||||
const request = http.request(url, params, (response) => {
|
const request = http.request(url, params, (response) => {
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
@@ -34,7 +53,8 @@ export function httpRequest(url, params) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.on("end", () => {
|
response.on("end", () => {
|
||||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data)]);
|
addCookieToJar(url, response.headers);
|
||||||
|
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,6 +62,10 @@ export function httpRequest(url, params) {
|
|||||||
reject([500, error]);
|
reject([500, error]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (params.body) {
|
||||||
|
request.write(params.body);
|
||||||
|
}
|
||||||
|
|
||||||
request.end();
|
request.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,24 @@ export default async function credentialedProxyHandler(req, res) {
|
|||||||
|
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (widget.type === "coinmarketcap") {
|
||||||
|
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
|
||||||
|
} else if (widget.type === "gotify") {
|
||||||
|
headers["X-gotify-Key"] = `${widget.key}`;
|
||||||
|
} else {
|
||||||
|
headers["X-API-Key"] = `${widget.key}`;
|
||||||
|
}
|
||||||
|
|
||||||
const [status, contentType, data] = await httpProxy(url, {
|
const [status, contentType, data] = await httpProxy(url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers,
|
||||||
"X-API-Key": `${widget.key}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 204 || status === 304) {
|
if (status === 204 || status === 304) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import getServiceWidget from "utils/service-helpers";
|
|||||||
import { formatApiCall } from "utils/api-helpers";
|
import { formatApiCall } from "utils/api-helpers";
|
||||||
import { httpProxy } from "utils/http";
|
import { httpProxy } from "utils/http";
|
||||||
|
|
||||||
export default async function genericProxyHandler(req, res) {
|
export default async function genericProxyHandler(req, res, maps) {
|
||||||
const { group, service, endpoint } = req.query;
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
if (group && service) {
|
if (group && service) {
|
||||||
@@ -10,17 +10,31 @@ export default async function genericProxyHandler(req, res) {
|
|||||||
|
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||||
|
|
||||||
|
let headers;
|
||||||
|
if (widget.username && widget.password) {
|
||||||
|
headers = {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const [status, contentType, data] = await httpProxy(url, {
|
const [status, contentType, data] = await httpProxy(url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let resultData = data;
|
||||||
|
if (maps?.[endpoint]) {
|
||||||
|
resultData = maps[endpoint](data);
|
||||||
|
}
|
||||||
|
|
||||||
if (contentType) res.setHeader("Content-Type", contentType);
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
|
||||||
if (status === 204 || status === 304) {
|
if (status === 204 || status === 304) {
|
||||||
return res.status(status).end();
|
return res.status(status).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(status).send(data);
|
return res.status(status).send(resultData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
src/utils/proxies/qbittorrent.js
Normal file
58
src/utils/proxies/qbittorrent.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { formatApiCall } from "utils/api-helpers";
|
||||||
|
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||||
|
import { httpProxy } from "utils/http";
|
||||||
|
import getServiceWidget from "utils/service-helpers";
|
||||||
|
|
||||||
|
async function login(widget, params) {
|
||||||
|
const loginUrl = new URL(`${widget.url}/api/v2/auth/login`);
|
||||||
|
const loginBody = `username=${encodeURI(widget.username)}&password=${encodeURI(widget.password)}`;
|
||||||
|
|
||||||
|
// using fetch intentionally, for login only, as the httpProxy method causes qBittorrent to
|
||||||
|
// complain about header encoding
|
||||||
|
return fetch(loginUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: loginBody
|
||||||
|
})
|
||||||
|
.then(async response => {
|
||||||
|
addCookieToJar(loginUrl, response.headers);
|
||||||
|
setCookieHeader(loginUrl, params);
|
||||||
|
const data = await response.text();
|
||||||
|
return ([response.status, data]);
|
||||||
|
})
|
||||||
|
.catch(err => ([500, err]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function qbittorrentProxyHandler(req, res) {
|
||||||
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||||
|
const params = { method: "GET", headers: {} };
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
if (!params.headers.Cookie) {
|
||||||
|
const [status, data] = await login(widget, params);
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
return res.status(status).end(data);
|
||||||
|
}
|
||||||
|
if (data.toString() !== 'Ok.') {
|
||||||
|
return res.status(401).end(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status, contentType, data] = await httpProxy(url, params);
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
56
src/utils/proxies/transmission.js
Normal file
56
src/utils/proxies/transmission.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { httpProxy } from "utils/http";
|
||||||
|
import { formatApiCall } from "utils/api-helpers";
|
||||||
|
import getServiceWidget from "utils/service-helpers";
|
||||||
|
|
||||||
|
export default async function transmissionProxyHandler(req, res) {
|
||||||
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||||
|
const csrfHeaderName = "x-transmission-session-id";
|
||||||
|
|
||||||
|
const method = "POST";
|
||||||
|
const auth = `${widget.username}:${widget.password}`;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
method: "torrent-get",
|
||||||
|
arguments: {
|
||||||
|
fields: ["percentDone", "status", "rateDownload", "rateUpload"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
let [status, contentType, data, responseHeaders] = await httpProxy(url, {
|
||||||
|
method,
|
||||||
|
auth,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 409) {
|
||||||
|
// Transmission is rejecting the request, but returning a CSRF token
|
||||||
|
headers[csrfHeaderName] = responseHeaders[csrfHeaderName];
|
||||||
|
|
||||||
|
// retry the request, now with the CSRF token
|
||||||
|
[status, contentType, data, responseHeaders] = await httpProxy(url, {
|
||||||
|
method,
|
||||||
|
auth,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
@@ -110,13 +110,27 @@ export function cleanServiceGroups(groups) {
|
|||||||
const cleanedService = { ...service };
|
const cleanedService = { ...service };
|
||||||
|
|
||||||
if (cleanedService.widget) {
|
if (cleanedService.widget) {
|
||||||
const { type } = cleanedService.widget;
|
// whitelisted set of keys to pass to the frontend
|
||||||
|
const {
|
||||||
|
type, // all widgets
|
||||||
|
server, // docker widget
|
||||||
|
container,
|
||||||
|
currency, // coinmarketcap widget
|
||||||
|
symbols,
|
||||||
|
} = cleanedService.widget;
|
||||||
|
|
||||||
cleanedService.widget = {
|
cleanedService.widget = {
|
||||||
type,
|
type,
|
||||||
|
currency,
|
||||||
|
symbols,
|
||||||
service_name: service.name,
|
service_name: service.name,
|
||||||
service_group: serviceGroup.name,
|
service_group: serviceGroup.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === "docker") {
|
||||||
|
cleanedService.widget.server = server;
|
||||||
|
cleanedService.widget.container = container;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
|||||||
Reference in New Issue
Block a user