mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
340 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea6a668a84 | ||
|
|
08615fe9f6 | ||
|
|
f8ef5ddf5a | ||
|
|
595d81dd2f | ||
|
|
00c654953d | ||
|
|
bf2f3a7d17 | ||
|
|
612f0fde2d | ||
|
|
a701e031a7 | ||
|
|
d51854e663 | ||
|
|
cb9a18fd40 | ||
|
|
ed28d69d76 | ||
|
|
5667cedafc | ||
|
|
42fe535df7 | ||
|
|
0e1e2bde22 | ||
|
|
1a5e2f3cda | ||
|
|
32cb113014 | ||
|
|
559af0cd56 | ||
|
|
0e5477eecf | ||
|
|
ab631fa26e | ||
|
|
5776544c20 | ||
|
|
666e2a42cf | ||
|
|
2fc1dda122 | ||
|
|
0115b594d6 | ||
|
|
c5828978b2 | ||
|
|
3ff756e057 | ||
|
|
1c405ff4ec | ||
|
|
5d1041d564 | ||
|
|
9e8942398c | ||
|
|
a5b7c8439d | ||
|
|
9131a8f118 | ||
|
|
8687fe6b26 | ||
|
|
1d38fd8dea | ||
|
|
4186bbb3c3 | ||
|
|
897c71f36e | ||
|
|
f9ce9b7716 | ||
|
|
ec230ba249 | ||
|
|
6bfa49689e | ||
|
|
177acf86d7 | ||
|
|
c89ed904cc | ||
|
|
6add7c3d82 | ||
|
|
c562035776 | ||
|
|
4757e25fdc | ||
|
|
077bc356b8 | ||
|
|
1dff880a93 | ||
|
|
122b987fa3 | ||
|
|
c024c4f01c | ||
|
|
a677fbefbf | ||
|
|
244a76de0b | ||
|
|
20ac15b18c | ||
|
|
75244cc40e | ||
|
|
3e731298a5 | ||
|
|
ed0bf027fc | ||
|
|
7a10131768 | ||
|
|
19522b8712 | ||
|
|
af79061a45 | ||
|
|
d56d9f7a50 | ||
|
|
45a1a9ed5a | ||
|
|
50954cf3d4 | ||
|
|
6beefbf39a | ||
|
|
8f21d1ae31 | ||
|
|
e4825531c4 | ||
|
|
6174f53f37 | ||
|
|
42baa4b188 | ||
|
|
52eaddae37 | ||
|
|
f43ce0db44 | ||
|
|
b46cb0a1f7 | ||
|
|
e4e5ad7eba | ||
|
|
0e6ea57023 | ||
|
|
280bb5fc81 | ||
|
|
077d21eb7e | ||
|
|
f281d86e8a | ||
|
|
f7000a280e | ||
|
|
586ded6b3f | ||
|
|
539e0f005a | ||
|
|
330575bab3 | ||
|
|
30ec4aed28 | ||
|
|
ee456fd8e5 | ||
|
|
ed25c8a84b | ||
|
|
b0a45fe09c | ||
|
|
50f0f46ad9 | ||
|
|
bd61d459ad | ||
|
|
4f73c60d37 | ||
|
|
7c536f0cb0 | ||
|
|
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 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: benphelps
|
||||
ko_fi: benphelps
|
||||
custom: ["https://paypal.me/phelpsben"]
|
||||
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.
|
||||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -94,9 +94,13 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
# https://github.com/docker/setup-qemu-action#about
|
||||
# platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,6 +19,10 @@
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# log files
|
||||
error.log
|
||||
homepage.log
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
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)
|
||||
@@ -22,6 +22,10 @@ RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm i
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
ARG BUILDTIME
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
|
||||
COPY --link --from=deps /app/node_modules ./node_modules/
|
||||
COPY . .
|
||||
|
||||
@@ -29,7 +33,7 @@ RUN <<EOF
|
||||
set -xe
|
||||
yarn next telemetry disable
|
||||
mkdir config && echo '-' > config/settings.yaml
|
||||
npm run build
|
||||
NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build
|
||||
EOF
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
|
||||
126
README.md
126
README.md
@@ -1,34 +1,42 @@
|
||||

|
||||
|
||||
[](https://github.com/benphelps/homepage/actions/workflows/docker-publish.yml)
|
||||
[](https://hosted.weblate.org/engage/homepage/)
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/k4ruYNrudu"><img src="https://img.shields.io/badge/Discord - Chat-blue?logo=discord&logoColor=white" /></a>
|
||||
<a href="https://paypal.me/phelpsben" title="Donate"><img src="https://img.shields.io/badge/PayPal - Donate-blue?logo=paypal&logoColor=white" alt="Linkedin - phelpsben"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/benphelps/homepage/actions/workflows/docker-publish.yml"><img src="https://github.com/benphelps/homepage/actions/workflows/docker-publish.yml/badge.svg" alt="Docker"></a>
|
||||
<a href="https://hosted.weblate.org/engage/homepage/"><img src="https://hosted.weblate.org/widgets/homepage/-/homepage/svg-badge.svg" alt="Weblate"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Fast! The entire site is statically generated at build time, so you can expect instant load times
|
||||
* Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||
* Full i18n support with automatic language detection
|
||||
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Portuguese, Russian and Spanish
|
||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||
* Service & Web Bookmarks
|
||||
* Docker Integration
|
||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||
- Automatic service discovery (via labels)
|
||||
* Service Integration
|
||||
- Sonarr, Radarr, Readarr, Emby, Jellyfin, Tautulli (Plex)
|
||||
- Ombi, Overseerr, Jellyseerr, NZBGet, SABnzbd, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager, Gotify
|
||||
* Information Providers
|
||||
- Coin Market Cap
|
||||
* Information & Utility Widgets
|
||||
- System Stats (Disk, CPU, Memory)
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap
|
||||
- Automatic location detection (with HTTPS), or manual location selection
|
||||
- Search Bar
|
||||
* Customizable
|
||||
- 21 theme colors with light and dark mode support
|
||||
- Background image support
|
||||
- Fast! The entire site is statically generated at build time, so you can expect instant load times
|
||||
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||
- Full i18n support with automatic language detection
|
||||
- Translations for Chinese, Dutch, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
|
||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||
- Service & Web Bookmarks
|
||||
- Docker Integration
|
||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||
- Automatic service discovery (via labels)
|
||||
- Service Integration
|
||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server
|
||||
- Information Providers
|
||||
- Coin Market Cap, Mastodon
|
||||
- Information & Utility Widgets
|
||||
- System Stats (Disk, CPU, Memory)
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap
|
||||
- Automatic location detection (with HTTPS), or manual location selection
|
||||
- Search Bar
|
||||
- Customizable
|
||||
- 21 theme colors with light and dark mode support
|
||||
- Background image support
|
||||
|
||||
## Support & Suggestions
|
||||
|
||||
@@ -45,16 +53,16 @@ For configuration options, examples and more, [please check out the Wiki](https:
|
||||
Using docker compose:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
version: "3.3"
|
||||
services:
|
||||
homepage:
|
||||
image: ghcr.io/benphelps/homepage:latest
|
||||
container_name: homepage
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- /path/to/config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||
homepage:
|
||||
image: ghcr.io/benphelps/homepage:latest
|
||||
container_name: homepage
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||
```
|
||||
|
||||
or docker run:
|
||||
@@ -78,6 +86,8 @@ pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
If this is your first time starting, copy the `src/skeleton` directory to `config/` to populate initial example config files.
|
||||
|
||||
Finally, run the server:
|
||||
|
||||
```bash
|
||||
@@ -88,7 +98,7 @@ pnpm start
|
||||
|
||||
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.
|
||||
|
||||
You may also check [the wiki](https://github.com/benphelps/homepage/wiki) for detailed configuration instructions, examples and more.
|
||||
@@ -113,22 +123,30 @@ This is a [Next.js](https://nextjs.org/) application, see their doucmentation fo
|
||||
|
||||
## Contributors
|
||||
|
||||
Huge thanks to the all the contributors who have helped make this project what it is today! In alphabetical order:
|
||||
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, SABnzbd Integrations
|
||||
* [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
|
||||
* [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
|
||||
* [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
|
||||
* [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify Integration
|
||||
- [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
|
||||
- [andrii-kryvoviaz](https://github.com/benphelps/homepage/commits?author=andrii-kryvoviaz) - Background opacity option
|
||||
- [boerniee](https://github.com/benphelps/homepage/commits?author=boerniee) - German Translation
|
||||
- [comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu) - Norwegian Bokmål Translation
|
||||
- Daniel Varga - German & Hungarian Translation
|
||||
- [deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony) - Dutch Translation
|
||||
- [desolaris](https://github.com/benphelps/homepage/commits?author=desolaris) - Russian Translation
|
||||
- [DevPGSV](https://github.com/benphelps/homepage/commits?author=DevPGSV) - Syncthing Relay Server & Mastodon widgets
|
||||
- [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
|
||||
- [ShlomiPorush](https://github.com/benphelps/homepage/commits?author=ShlomiPorush) - Hebrew Translation
|
||||
- [SuperDOS](https://github.com/benphelps/homepage/commits?author=SuperDOS) - Swedish Translation
|
||||
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration
|
||||
|
||||
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
|
||||
11
package.json
11
package.json
@@ -6,14 +6,16 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"telemetry": "next telemetry disable"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"classnames": "^2.3.1",
|
||||
"currency-symbol-map": "^5.1.0",
|
||||
"compare-versions": "^5.0.1",
|
||||
"dockerode": "^3.3.4",
|
||||
"follow-redirects": "^1.15.2",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
@@ -30,7 +32,10 @@
|
||||
"react-icons": "^4.4.0",
|
||||
"rutorrent-promise": "^2.0.0",
|
||||
"shvl": "^3.0.0",
|
||||
"swr": "^1.3.0"
|
||||
"swr": "^1.3.0",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tough-cookie": "^4.1.2",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.9",
|
||||
|
||||
213
pnpm-lock.yaml
generated
213
pnpm-lock.yaml
generated
@@ -5,7 +5,7 @@ specifiers:
|
||||
'@tailwindcss/forms': ^0.5.3
|
||||
autoprefixer: ^10.4.9
|
||||
classnames: ^2.3.1
|
||||
currency-symbol-map: ^5.1.0
|
||||
compare-versions: ^5.0.1
|
||||
dockerode: ^3.3.4
|
||||
eslint: ^8.23.1
|
||||
eslint-config-airbnb: ^19.0.4
|
||||
@@ -16,6 +16,7 @@ specifiers:
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
eslint-plugin-react: ^7.31.8
|
||||
eslint-plugin-react-hooks: ^4.6.0
|
||||
follow-redirects: ^1.15.2
|
||||
i18next: ^21.9.1
|
||||
i18next-browser-languagedetector: ^6.1.5
|
||||
i18next-http-backend: ^1.4.1
|
||||
@@ -35,15 +36,19 @@ specifiers:
|
||||
rutorrent-promise: ^2.0.0
|
||||
shvl: ^3.0.0
|
||||
swr: ^1.3.0
|
||||
tailwind-scrollbar: ^2.0.1
|
||||
tailwindcss: ^3.1.8
|
||||
tough-cookie: ^4.1.2
|
||||
typescript: ^4.8.3
|
||||
winston: ^3.8.2
|
||||
|
||||
dependencies:
|
||||
'@headlessui/react': 1.7.0_biqbaboplfbrettd7655fr4n2y
|
||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||
classnames: 2.3.1
|
||||
currency-symbol-map: 5.1.0
|
||||
compare-versions: 5.0.1
|
||||
dockerode: 3.3.4
|
||||
follow-redirects: 1.15.2
|
||||
i18next: 21.9.1
|
||||
i18next-browser-languagedetector: 6.1.5
|
||||
i18next-http-backend: 1.4.1
|
||||
@@ -61,6 +66,9 @@ dependencies:
|
||||
rutorrent-promise: 2.0.0
|
||||
shvl: 3.0.0
|
||||
swr: 1.3.0_react@18.2.0
|
||||
tailwind-scrollbar: 2.0.1_tailwindcss@3.1.8
|
||||
tough-cookie: 4.1.2
|
||||
winston: 3.8.2
|
||||
|
||||
devDependencies:
|
||||
autoprefixer: 10.4.9_postcss@8.4.16
|
||||
@@ -98,6 +106,19 @@ packages:
|
||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||
dev: false
|
||||
|
||||
/@colors/colors/1.5.0:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
dev: false
|
||||
|
||||
/@dabh/diagnostics/2.0.3:
|
||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||
dependencies:
|
||||
colorspace: 1.1.4
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
dev: false
|
||||
|
||||
/@eslint/eslintrc/1.3.2:
|
||||
resolution: {integrity: sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -498,6 +519,10 @@ packages:
|
||||
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
||||
dev: true
|
||||
|
||||
/async/3.2.4:
|
||||
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
||||
dev: false
|
||||
|
||||
/asynckit/0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
@@ -644,6 +669,12 @@ packages:
|
||||
resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==}
|
||||
dev: false
|
||||
|
||||
/color-convert/1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
dev: false
|
||||
|
||||
/color-convert/2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -651,9 +682,34 @@ packages:
|
||||
color-name: 1.1.4
|
||||
dev: true
|
||||
|
||||
/color-name/1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
dev: false
|
||||
|
||||
/color-name/1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
/color-string/1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
dev: false
|
||||
|
||||
/color/3.2.1:
|
||||
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
|
||||
dependencies:
|
||||
color-convert: 1.9.3
|
||||
color-string: 1.9.1
|
||||
dev: false
|
||||
|
||||
/colorspace/1.1.4:
|
||||
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
|
||||
dependencies:
|
||||
color: 3.2.1
|
||||
text-hex: 1.0.0
|
||||
dev: false
|
||||
|
||||
/combined-stream/1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -661,6 +717,10 @@ packages:
|
||||
delayed-stream: 1.0.0
|
||||
dev: false
|
||||
|
||||
/compare-versions/5.0.1:
|
||||
resolution: {integrity: sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ==}
|
||||
dev: false
|
||||
|
||||
/concat-map/0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
@@ -706,10 +766,6 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
/currency-symbol-map/5.1.0:
|
||||
resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==}
|
||||
dev: false
|
||||
|
||||
/damerau-levenshtein/1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
dev: true
|
||||
@@ -839,6 +895,10 @@ packages:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
dev: true
|
||||
|
||||
/enabled/2.0.0:
|
||||
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
|
||||
dev: false
|
||||
|
||||
/end-of-stream/1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
@@ -1325,6 +1385,10 @@ packages:
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
/fecha/4.2.3:
|
||||
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||
dev: false
|
||||
|
||||
/file-entry-cache/6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@@ -1358,6 +1422,20 @@ packages:
|
||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||
dev: true
|
||||
|
||||
/fn.name/1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
dev: false
|
||||
|
||||
/follow-redirects/1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/form-data/3.0.1:
|
||||
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1594,6 +1672,10 @@ packages:
|
||||
side-channel: 1.0.4
|
||||
dev: true
|
||||
|
||||
/is-arrayish/0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
dev: false
|
||||
|
||||
/is-bigint/1.0.4:
|
||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||
dependencies:
|
||||
@@ -1671,6 +1753,11 @@ packages:
|
||||
call-bind: 1.0.2
|
||||
dev: true
|
||||
|
||||
/is-stream/2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/is-string/1.0.7:
|
||||
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1735,6 +1822,10 @@ packages:
|
||||
object.assign: 4.1.4
|
||||
dev: true
|
||||
|
||||
/kuler/2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
dev: false
|
||||
|
||||
/language-subtag-registry/0.3.22:
|
||||
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
|
||||
dev: true
|
||||
@@ -1768,6 +1859,16 @@ packages:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
||||
/logform/2.4.2:
|
||||
resolution: {integrity: sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==}
|
||||
dependencies:
|
||||
'@colors/colors': 1.5.0
|
||||
fecha: 4.2.3
|
||||
ms: 2.1.3
|
||||
safe-stable-stringify: 2.4.0
|
||||
triple-beam: 1.3.0
|
||||
dev: false
|
||||
|
||||
/loose-envify/1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@@ -1835,7 +1936,6 @@ packages:
|
||||
|
||||
/ms/2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/nan/2.16.0:
|
||||
resolution: {integrity: sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==}
|
||||
@@ -1992,6 +2092,12 @@ packages:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
/one-time/1.0.0:
|
||||
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||
dependencies:
|
||||
fn.name: 1.1.0
|
||||
dev: false
|
||||
|
||||
/optionator/0.9.1:
|
||||
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2162,6 +2268,10 @@ packages:
|
||||
react-is: 16.13.1
|
||||
dev: true
|
||||
|
||||
/psl/1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
dev: false
|
||||
|
||||
/pump/3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
dependencies:
|
||||
@@ -2172,7 +2282,10 @@ packages:
|
||||
/punycode/2.1.1:
|
||||
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/querystringify/2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/queue-microtask/1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
@@ -2277,6 +2390,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/requires-port/1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/resolve-from/4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2328,6 +2445,11 @@ packages:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/safe-stable-stringify/2.4.0:
|
||||
resolution: {integrity: sha512-eehKHKpab6E741ud7ZIMcXhKcP6TSIezPkNZhy5U8xC6+VvrRdUA2tMgxGxaGl4cz7c2Ew5+mg5+wNB16KQqrA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/safer-buffer/2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
@@ -2379,6 +2501,12 @@ packages:
|
||||
object-inspect: 1.12.2
|
||||
dev: true
|
||||
|
||||
/simple-swizzle/0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
dev: false
|
||||
|
||||
/slash/3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2404,6 +2532,10 @@ packages:
|
||||
nan: 2.16.0
|
||||
dev: false
|
||||
|
||||
/stack-trace/0.0.10:
|
||||
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||
dev: false
|
||||
|
||||
/statuses/2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2496,6 +2628,15 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/tailwind-scrollbar/2.0.1_tailwindcss@3.1.8:
|
||||
resolution: {integrity: sha512-OcR7qHBbux4k+k6bWqnEQFYFooLK/F4dhkBz6nvswIoaA9ancZ5h20e0tyV7ifSWLDCUBtpG+1NHRA8HMRH/wg==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 3.x
|
||||
dependencies:
|
||||
tailwindcss: 3.1.8_postcss@8.4.16
|
||||
dev: false
|
||||
|
||||
/tailwindcss/3.1.8_postcss@8.4.16:
|
||||
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
@@ -2548,6 +2689,10 @@ packages:
|
||||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/text-hex/1.0.0:
|
||||
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||
dev: false
|
||||
|
||||
/text-table/0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
@@ -2563,10 +2708,24 @@ packages:
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/tough-cookie/4.1.2:
|
||||
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
psl: 1.9.0
|
||||
punycode: 2.1.1
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
dev: false
|
||||
|
||||
/tr46/0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: false
|
||||
|
||||
/triple-beam/1.3.0:
|
||||
resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==}
|
||||
dev: false
|
||||
|
||||
/tsconfig-paths/3.14.1:
|
||||
resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
|
||||
dependencies:
|
||||
@@ -2625,6 +2784,11 @@ packages:
|
||||
which-boxed-primitive: 1.0.2
|
||||
dev: true
|
||||
|
||||
/universalify/0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: false
|
||||
|
||||
/unpipe/1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2647,6 +2811,13 @@ packages:
|
||||
punycode: 2.1.1
|
||||
dev: true
|
||||
|
||||
/url-parse/1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store/1.2.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
@@ -2692,6 +2863,32 @@ packages:
|
||||
isexe: 2.0.0
|
||||
dev: true
|
||||
|
||||
/winston-transport/4.5.0:
|
||||
resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==}
|
||||
engines: {node: '>= 6.4.0'}
|
||||
dependencies:
|
||||
logform: 2.4.2
|
||||
readable-stream: 3.6.0
|
||||
triple-beam: 1.3.0
|
||||
dev: false
|
||||
|
||||
/winston/3.8.2:
|
||||
resolution: {integrity: sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
dependencies:
|
||||
'@colors/colors': 1.5.0
|
||||
'@dabh/diagnostics': 2.0.3
|
||||
async: 3.2.4
|
||||
is-stream: 2.0.1
|
||||
logform: 2.4.2
|
||||
one-time: 1.0.0
|
||||
readable-stream: 3.6.0
|
||||
safe-stable-stringify: 2.4.0
|
||||
stack-trace: 0.0.10
|
||||
triple-beam: 1.3.0
|
||||
winston-transport: 4.5.0
|
||||
dev: false
|
||||
|
||||
/word-wrap/1.2.3:
|
||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
172
public/locales/ca/common.json
Normal file
172
public/locales/ca/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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": "Companys",
|
||||
"seed": "Llavors"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Usuaris",
|
||||
"status_count": "Publicacions",
|
||||
"domain_count": "Dominis"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connexions",
|
||||
"dataRelayed": "Transmès",
|
||||
"transferRate": "Velocitat"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
"resources": {
|
||||
"total": "Gesamt",
|
||||
"free": "Frei",
|
||||
"used": "Gebraucht"
|
||||
"used": "Gebraucht",
|
||||
"load": "Belastung"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
@@ -23,13 +24,13 @@
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Keine aktive Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spielen",
|
||||
"transcoding": "Transcodierung",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Keine aktiven streamen"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
@@ -47,9 +48,9 @@
|
||||
"movies": "Filme"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"books": "Bücher"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Ausstehend",
|
||||
@@ -78,8 +79,8 @@
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Router",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
"services": "Dienste",
|
||||
"middleware": "Zwischenanwendung"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Aktiviert",
|
||||
@@ -93,26 +94,79 @@
|
||||
"wait": "Bitte warten"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"available": "Verfügbar"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "Geschwindigkeit",
|
||||
"queue": "Warteschlange",
|
||||
"timeleft": "Verbleibende Zeit"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"rate": "Geschwindigkeit",
|
||||
"remaining": "Verbleibend",
|
||||
"downloaded": "Heruntergeladen"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Konfiguriere eine oder mehrere Kryptowährungen zur Verfolgung",
|
||||
"1hour": "1 Stunde",
|
||||
"1day": "1 Tag",
|
||||
"7days": "7 Tage",
|
||||
"30days": "30 Tage"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"apps": "Programme",
|
||||
"clients": "Benutzer",
|
||||
"messages": "Nachrichten"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexer",
|
||||
"numberOfGrabs": "Abrufungen",
|
||||
"numberOfQueries": "Anfragen",
|
||||
"numberOfFailGrabs": "Fehlgeschlagene Abrufungen",
|
||||
"numberOfFailQueries": "Fehlgeschlagene Anfragen"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Herunterladen",
|
||||
"upload": "Hochladen",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Konfiguriert",
|
||||
"errored": "Fehlerhaft"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Fehlende Episoden",
|
||||
"missingMovies": "Fehlende Filme"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Gesucht",
|
||||
"queued": "In Warteschlange",
|
||||
"albums": "Alben"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Anfragen",
|
||||
"blocked": "Blockiert",
|
||||
"filtered": "Gefiltert",
|
||||
"latency": "Latenz"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Herunterladen",
|
||||
"upload": "Hochladen",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Nutzer",
|
||||
"status_count": "Beiträge",
|
||||
"domain_count": "Domänen"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sitzungen",
|
||||
"numConnections": "Verbindungen",
|
||||
"dataRelayed": "Weitergeleitet",
|
||||
"transferRate": "Bewerten"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used"
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
@@ -63,6 +64,18 @@
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
@@ -73,11 +86,20 @@
|
||||
"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",
|
||||
@@ -92,12 +114,18 @@
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
@@ -119,11 +147,37 @@
|
||||
"total": "Total"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"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"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Tipo de widget faltante: {{type}}",
|
||||
"missing_type": "Falta el tipo de widget: {{type}}",
|
||||
"api_error": "Error de API",
|
||||
"status": "Estado"
|
||||
},
|
||||
@@ -10,7 +10,8 @@
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Libre",
|
||||
"used": "Usado"
|
||||
"used": "Usado",
|
||||
"load": "Carga"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Recibido",
|
||||
@@ -20,36 +21,36 @@
|
||||
"offline": "Desconectado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "En ejecución",
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de Bits",
|
||||
"no_active": "No hay streams activos"
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "En ejecución",
|
||||
"transcoding": "Transcodificación",
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "No hay streams activos"
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Activo",
|
||||
"upload": "Subir",
|
||||
"download": "Descargar"
|
||||
"upload": "Subida",
|
||||
"download": "Bajada"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"queued": "En cola",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"queued": "En cola",
|
||||
"movies": "Películas"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "Más deseado",
|
||||
"queued": "En cola",
|
||||
"books": "Libros"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendiente",
|
||||
@@ -67,8 +68,8 @@
|
||||
"gravity": "Gravedad"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Subir",
|
||||
"download": "Descargar",
|
||||
"upload": "Subida",
|
||||
"download": "Bajada",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
@@ -87,8 +88,8 @@
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Ubicación Actual",
|
||||
"allow": "Haga clic para permitir",
|
||||
"current": "Ubicación actual",
|
||||
"allow": "Clic para permitir",
|
||||
"updating": "Actualizando",
|
||||
"wait": "Espere, por favor"
|
||||
},
|
||||
@@ -98,21 +99,74 @@
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "Tasa",
|
||||
"queue": "En cola",
|
||||
"timeleft": "Tiempo restante"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"rate": "Tasa",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Descargado"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"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": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"apps": "Aplicaciones",
|
||||
"clients": "Clientes",
|
||||
"messages": "Mensajes"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexadores",
|
||||
"numberOfGrabs": "Capturas",
|
||||
"numberOfQueries": "Consultas",
|
||||
"numberOfFailGrabs": "Capturas fallidas",
|
||||
"numberOfFailQueries": "Consultas fallidas"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Bajada",
|
||||
"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": "Bajada",
|
||||
"upload": "Subida",
|
||||
"leech": "Compañeros",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Usuarios",
|
||||
"status_count": "Publicaciones",
|
||||
"domain_count": "Dominios"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sesiones",
|
||||
"numConnections": "Conexiones",
|
||||
"dataRelayed": "Retransmitido",
|
||||
"transferRate": "Velocidad"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Libre",
|
||||
"used": "Utilisée"
|
||||
"used": "Utilisé",
|
||||
"load": "Charge"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
@@ -32,53 +33,53 @@
|
||||
"no_active": "Aucun flux actif"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Téléverser",
|
||||
"download": "Télécharger"
|
||||
"active": "Actif",
|
||||
"upload": "Envoi",
|
||||
"download": "Réception"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Recherchée",
|
||||
"queued": "En queue",
|
||||
"wanted": "Demande",
|
||||
"queued": "En attente",
|
||||
"series": "Séries"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Recherchée",
|
||||
"queued": "En queue",
|
||||
"wanted": "Demande",
|
||||
"queued": "En attente",
|
||||
"movies": "Films"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "Demande",
|
||||
"queued": "Attente",
|
||||
"books": "Livres"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvée",
|
||||
"approved": "Validé",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvée",
|
||||
"approved": "Validé",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Requêtes",
|
||||
"blocked": "Bloquée",
|
||||
"gravity": "La gravité"
|
||||
"blocked": "Bloqué",
|
||||
"gravity": "Listes dom. bloqués"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Téléversement",
|
||||
"download": "Téléchargement",
|
||||
"ping": "Ping-ping"
|
||||
"upload": "Envoi",
|
||||
"download": "Récep.",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Fonctionnement",
|
||||
"running": "Démarré",
|
||||
"stopped": "Arrêté",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routeurs",
|
||||
"services": "Prestations de service",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
@@ -105,25 +106,78 @@
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvée",
|
||||
"approved": "Demande",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"rate": "Débit",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"timeleft": "Temps restant"
|
||||
},
|
||||
"nzbget": {
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded",
|
||||
"rate": "Rate"
|
||||
"remaining": "Restant",
|
||||
"downloaded": "Téléchargé",
|
||||
"rate": "Débit"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Configurer une ou plusieurs crypto-monnaies à suivre",
|
||||
"1hour": "1 Heure",
|
||||
"1day": "1 Jour",
|
||||
"7days": "7 Jours",
|
||||
"30days": "30 Jours"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"apps": "Applis",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"messages": "Msg"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexeur",
|
||||
"numberOfGrabs": "Capture",
|
||||
"numberOfQueries": "Demande",
|
||||
"numberOfFailGrabs": "Capt. échouée",
|
||||
"numberOfFailQueries": "Dem. é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écep.",
|
||||
"upload": "Envoi",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Utilisateurs",
|
||||
"status_count": "Messages",
|
||||
"domain_count": "Domaines"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Cnx",
|
||||
"dataRelayed": "Relayé",
|
||||
"transferRate": "Débit"
|
||||
}
|
||||
}
|
||||
|
||||
172
public/locales/he/common.json
Normal file
172
public/locales/he/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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": "שגיאה"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
172
public/locales/hr/common.json
Normal file
172
public/locales/hr/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
172
public/locales/hu/common.json
Normal file
172
public/locales/hu/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,8 @@
|
||||
"resources": {
|
||||
"total": "Totale",
|
||||
"free": "Libero",
|
||||
"used": "In utilizzo"
|
||||
"used": "In utilizzo",
|
||||
"load": "Load"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Attivo",
|
||||
@@ -98,21 +99,74 @@
|
||||
"available": "Disponibili"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "Rapporto",
|
||||
"queue": "Coda",
|
||||
"timeleft": "Tempo Rimanente"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"rate": "Rapporto",
|
||||
"remaining": "Rimanente",
|
||||
"downloaded": "Scaricato"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"1hour": "1 Hour",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"apps": "Applicazioni",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"messages": "Messaggi"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"leech": "Leech",
|
||||
"upload": "Upload",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"resources": {
|
||||
"total": "Totalt",
|
||||
"free": "Ledig",
|
||||
"used": "Brukt"
|
||||
"used": "Brukt",
|
||||
"load": "Last inn"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Mottatt",
|
||||
@@ -23,13 +24,13 @@
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Ingen aktive strømmer"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spiller",
|
||||
"transcoding": "Transkoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Ingen aktive strømmer"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiv",
|
||||
@@ -93,26 +94,79 @@
|
||||
"current": "Nåværende posisjon"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
"pending": "Venter",
|
||||
"approved": "Godkjent",
|
||||
"available": "Tilgjengelig"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "Takt",
|
||||
"queue": "Kø",
|
||||
"timeleft": "Gjenstående tid"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"downloaded": "Downloaded",
|
||||
"remaining": "Remaining"
|
||||
"rate": "Takt",
|
||||
"downloaded": "Nedlastet",
|
||||
"remaining": "Gjenstående"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Sett opp én eller flere kryptovalutaer å holde øye med",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"resources": {
|
||||
"total": "Totaal",
|
||||
"free": "Vrij",
|
||||
"used": "Gebruikt"
|
||||
"used": "Gebruikt",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
@@ -39,7 +40,7 @@
|
||||
"playing": "Afspelen",
|
||||
"transcoding": "Transcodering",
|
||||
"bitrate": "Bitsnelheid",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Geen Actieve Steams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Afspelen",
|
||||
@@ -108,11 +109,64 @@
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
|
||||
172
public/locales/pl/common.json
Normal file
172
public/locales/pl/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Tipo de widget ausente: {{type}}",
|
||||
"missing_type": "Widget ausente: {{type}}",
|
||||
"api_error": "Erro da API",
|
||||
"status": "Status"
|
||||
},
|
||||
@@ -10,31 +10,32 @@
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Livre",
|
||||
"used": "Usada"
|
||||
"used": "Usado",
|
||||
"load": "Carregar"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
"tx": "Tx",
|
||||
"mem": "Mem",
|
||||
"cpu": "CPU",
|
||||
"offline": "Desligada"
|
||||
"offline": "Desligado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "A reproduzir",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Sem streams ativas"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Reproduzindo",
|
||||
"transcoding": "Transcodificação",
|
||||
"bitrate": "Taxa de bits",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "Sem streams ativas"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Ativa",
|
||||
"upload": "Envio",
|
||||
"download": "ReceçãoDownload"
|
||||
"active": "Ativo",
|
||||
"upload": "Enviando",
|
||||
"download": "Baixando"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Desejada",
|
||||
@@ -43,13 +44,13 @@
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Desejado",
|
||||
"queued": "Enfileiradas",
|
||||
"queued": "Fila",
|
||||
"movies": "Filmes"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "Desejados",
|
||||
"queued": "Em fila",
|
||||
"books": "Livros"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pendente",
|
||||
@@ -72,8 +73,8 @@
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Corrida",
|
||||
"stopped": "Parou",
|
||||
"running": "A correr",
|
||||
"stopped": "Parado",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
@@ -82,8 +83,8 @@
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Habilitada",
|
||||
"disabled": "Desabilitada",
|
||||
"enabled": "Ativo",
|
||||
"disabled": "Desabilitado",
|
||||
"total": "Total"
|
||||
},
|
||||
"common": {
|
||||
@@ -104,26 +105,79 @@
|
||||
"wait": "Por favor aguarde"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovado",
|
||||
"available": "Disponível"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "Taxa",
|
||||
"queue": "Fila",
|
||||
"timeleft": "Tempo restante"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"rate": "Taxa",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Baixado"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Configurar uma ou mais moedas",
|
||||
"1hour": "1 Hora",
|
||||
"1day": "1 Dia",
|
||||
"7days": "7 Dias",
|
||||
"30days": "30 Dias"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplicações",
|
||||
"clients": "Clientes",
|
||||
"messages": "Mensagens"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexadores",
|
||||
"numberOfGrabs": "Agarrados",
|
||||
"numberOfQueries": "Consultas",
|
||||
"numberOfFailGrabs": "Falhados",
|
||||
"numberOfFailQueries": "Pesquisas falhadas"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Baixando",
|
||||
"upload": "Enviando",
|
||||
"leech": "Sanguessugas",
|
||||
"seed": "Semeadores"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configurado",
|
||||
"errored": "Errado"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Episódios Faltantes",
|
||||
"missingMovies": "Filmes Faltantes"
|
||||
},
|
||||
"lidarr": {
|
||||
"queued": "Enfileirado",
|
||||
"wanted": "Desejado",
|
||||
"albums": "Álbuns"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"filtered": "Filtrado",
|
||||
"latency": "Latência"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Baixando",
|
||||
"upload": "Enviando",
|
||||
"leech": "Sanguessugas",
|
||||
"seed": "Semeadores"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Usuários",
|
||||
"status_count": "Postagens",
|
||||
"domain_count": "Domínios"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessões",
|
||||
"numConnections": "Conexões",
|
||||
"dataRelayed": "Retransmitido",
|
||||
"transferRate": "Taxa"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
"placeholder": "Поиск…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Общий",
|
||||
"total": "Всего",
|
||||
"free": "Свободно",
|
||||
"used": "Использовано"
|
||||
"used": "Использовано",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "Rx",
|
||||
@@ -88,8 +89,8 @@
|
||||
},
|
||||
"weather": {
|
||||
"wait": "Пожалуйста подождите",
|
||||
"current": "Текущее местоположение",
|
||||
"allow": "Click to allow",
|
||||
"current": "Текущая локация",
|
||||
"allow": "Нажмите, чтобы разрешить",
|
||||
"updating": "Обновление"
|
||||
},
|
||||
"overseerr": {
|
||||
@@ -108,11 +109,64 @@
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Дней"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate",
|
||||
"numActiveSessions": "Sessions"
|
||||
}
|
||||
}
|
||||
|
||||
172
public/locales/sv/common.json
Normal file
172
public/locales/sv/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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": "Hämtningar",
|
||||
"numberOfFailGrabs": "Misslyckade hämtningar",
|
||||
"numberOfFailQueries": "Misslyckade hämtningar"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Konfigurerade",
|
||||
"errored": "Felaktiga"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Förfrågningar",
|
||||
"blocked": "Blockerad",
|
||||
"filtered": "Filtrerad",
|
||||
"latency": "Svarstid"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Nedladdning",
|
||||
"upload": "Uppladdning",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Användare",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessioner",
|
||||
"numConnections": "Anslutningar",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
"resources": {
|
||||
"total": "Tổng",
|
||||
"free": "Dư",
|
||||
"used": "Đã dùng"
|
||||
"used": "Đã dùng",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
@@ -44,16 +45,16 @@
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
"movies": "Phim"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "Đang tìm",
|
||||
"queued": "Đang chờ",
|
||||
"books": "Sách"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"pending": "Đang xử lý",
|
||||
"approved": "Đã duyệt",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
@@ -87,32 +88,85 @@
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
"current": "Vị trí hiện tại",
|
||||
"allow": "Bấm để đồng ý",
|
||||
"updating": "Đang cập nhật",
|
||||
"wait": "Vui lòng chờ"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"approved": "Đã duyệt",
|
||||
"available": "Available"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"queue": "Hàng chờ",
|
||||
"timeleft": "Thời gian còn lại"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"downloaded": "Đã tải"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,58 +2,59 @@
|
||||
"widget": {
|
||||
"missing_type": "缺少小部件类型:{{type}}",
|
||||
"api_error": "API错误",
|
||||
"status": "地位"
|
||||
"status": "状态"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "全部的",
|
||||
"free": "自由的",
|
||||
"used": "用过的"
|
||||
"total": "共",
|
||||
"free": "空闲",
|
||||
"used": "已用",
|
||||
"load": "负载"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "rx",
|
||||
"tx": "TX",
|
||||
"mem": "mem",
|
||||
"cpu": "中央处理器",
|
||||
"rx": "接收",
|
||||
"tx": "发送",
|
||||
"mem": "内存",
|
||||
"cpu": "处理器",
|
||||
"offline": "离线"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "玩",
|
||||
"playing": "播放中",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "暂无播放"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "玩",
|
||||
"playing": "播放中",
|
||||
"transcoding": "转码",
|
||||
"bitrate": "比特率",
|
||||
"no_active": "No Active Streams"
|
||||
"no_active": "暂无播放"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "积极的",
|
||||
"active": "活动中",
|
||||
"upload": "上传",
|
||||
"download": "下载"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "通缉",
|
||||
"wanted": "想看",
|
||||
"queued": "排队",
|
||||
"series": "系列"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "通缉",
|
||||
"queued": "排队",
|
||||
"wanted": "想看",
|
||||
"queued": "队列",
|
||||
"movies": "电影"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
"wanted": "订阅",
|
||||
"queued": "队列",
|
||||
"books": "书籍"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "待办的",
|
||||
"approved": "得到正式认可的",
|
||||
"approved": "已批准",
|
||||
"available": "可用的"
|
||||
},
|
||||
"jellyseerr": {
|
||||
@@ -72,9 +73,9 @@
|
||||
"ping": "ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "跑步",
|
||||
"stopped": "停了下来",
|
||||
"total": "全部的"
|
||||
"running": "运行中",
|
||||
"stopped": "停止",
|
||||
"total": "总计"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "路由器",
|
||||
@@ -87,32 +88,85 @@
|
||||
"total": "全部的"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
"current": "当前定位",
|
||||
"allow": "点击并允许",
|
||||
"updating": "更新中",
|
||||
"wait": "请稍后"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
"pending": "待办",
|
||||
"approved": "已批准",
|
||||
"available": "可用"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
"rate": "速率",
|
||||
"queue": "队列",
|
||||
"timeleft": "剩余时间"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
"rate": "速率",
|
||||
"remaining": "剩余",
|
||||
"downloaded": "下载"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track"
|
||||
"configure": "配置一个或多个需要追踪的加密",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"7days": "7天",
|
||||
"30days": "30天"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"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": "做种"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "用户",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"dataRelayed": "Relayed",
|
||||
"numConnections": "Connections",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
|
||||
172
public/locales/zh-Hant/common.json
Normal file
172
public/locales/zh-Hant/common.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "Users",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "Sessions",
|
||||
"numConnections": "Connections",
|
||||
"dataRelayed": "Relayed",
|
||||
"transferRate": "Rate"
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,8 @@ import List from "components/bookmarks/list";
|
||||
|
||||
export default function BookmarksGroup({ group }) {
|
||||
return (
|
||||
<div
|
||||
key={group.name}
|
||||
className="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">
|
||||
{group.name}
|
||||
</h2>
|
||||
<div key={group.name} className="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">{group.name}</h2>
|
||||
<List bookmarks={group.bookmarks} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
export default function Item({ bookmark }) {
|
||||
const { hostname } = new URL(bookmark.href);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<li key={bookmark.name}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(bookmark.href, "_blank").focus()}
|
||||
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20 backdrop-blur-md"
|
||||
<a
|
||||
href={bookmark.href}
|
||||
title={bookmark.name}
|
||||
target={settings.target ?? "_blank"}
|
||||
className="block 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-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">
|
||||
@@ -17,7 +23,7 @@ export default function Item({ bookmark }) {
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-400 opacity-50 text-xs">{hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function ColorToggle() {
|
||||
{colors.map((color) => (
|
||||
<button type="button" onClick={() => setColor(color)} key={color}>
|
||||
<div
|
||||
title={color}
|
||||
className={classNames(
|
||||
active === color ? "border-2" : "border-0",
|
||||
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
||||
|
||||
48
src/components/services/dropdown.jsx
Normal file
48
src/components/services/dropdown.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Fragment } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { BiCog } from "react-icons/bi";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default function Dropdown({ options, value, setValue }) {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="text-xs inline-flex w-full items-center rounded bg-theme-200/50 dark:bg-theme-900/20 px-3 py-1.5">
|
||||
{options.find((option) => option.value === value).label}
|
||||
<BiCog className="-mr-1 ml-2 h-4 w-4" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-theme-200/50 dark:bg-theme-900/50 backdrop-blur shadow-md focus:outline-none text-theme-700 dark:text-theme-200">
|
||||
<div className="py-1">
|
||||
{options.map((option) => (
|
||||
<Menu.Item key={option.value} as={Fragment}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setValue(option.value);
|
||||
}}
|
||||
type="button"
|
||||
className={classNames(
|
||||
value === option.value ? "bg-theme-300/40 dark:bg-theme-900/40" : "",
|
||||
"w-full block px-3 py-1.5 text-sm hover:bg-theme-300/70 hover:dark:bg-theme-900/70 text-left"
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import List from "components/services/list";
|
||||
|
||||
export default function ServicesGroup({ services }) {
|
||||
export default function ServicesGroup({ services, layout }) {
|
||||
return (
|
||||
<div
|
||||
key={services.name}
|
||||
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
|
||||
className={classNames(
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
|
||||
"flex-1 p-1"
|
||||
)}
|
||||
>
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
|
||||
{services.name}
|
||||
</h2>
|
||||
<List services={services.services} />
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
<List services={services.services} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Image from "next/future/image";
|
||||
import { useContext } from "react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
|
||||
import Status from "./status";
|
||||
import Widget from "./widget";
|
||||
import Docker from "./widgets/service/docker";
|
||||
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
function resolveIcon(icon) {
|
||||
if (icon.startsWith("http")) {
|
||||
return `/api/proxy?url=${encodeURIComponent(icon)}`;
|
||||
@@ -22,13 +25,8 @@ function resolveIcon(icon) {
|
||||
}
|
||||
|
||||
export default function Item({ service }) {
|
||||
const handleOnClick = () => {
|
||||
if (service.href && service.href !== "#") {
|
||||
window.open(service.href, "_blank").focus();
|
||||
}
|
||||
};
|
||||
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<li key={service.name}>
|
||||
@@ -36,18 +34,19 @@ export default function Item({ service }) {
|
||||
<div
|
||||
className={`${
|
||||
hasLink ? "cursor-pointer " : " "
|
||||
}transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-700/70 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/20 dark:bg-white/10 dark:hover:bg-white/20 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 select-none">
|
||||
{service.icon &&
|
||||
(hasLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOnClick}
|
||||
<a
|
||||
href={service.href}
|
||||
target={settings.target ?? "_blank"}
|
||||
rel="noreferrer"
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
>
|
||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
||||
</button>
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
||||
@@ -55,16 +54,17 @@ export default function Item({ service }) {
|
||||
))}
|
||||
|
||||
{hasLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOnClick}
|
||||
<a
|
||||
href={service.href}
|
||||
target={settings.target ?? "_blank"}
|
||||
rel="noreferrer"
|
||||
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>
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 px-2 py-2 text-sm text-left">
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ services }) {
|
||||
const columnMap = [
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
|
||||
];
|
||||
|
||||
export default function List({ services, layout }) {
|
||||
return (
|
||||
<ul className="mt-3 flex flex-col">
|
||||
<ul
|
||||
className={classNames(
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
|
||||
"mt-3"
|
||||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item key={service.name} service={service} />
|
||||
))}
|
||||
|
||||
@@ -1,36 +1,51 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import Sonarr from "./widgets/service/sonarr";
|
||||
import Radarr from "./widgets/service/radarr";
|
||||
import Readarr from "./widgets/service/readarr";
|
||||
import Ombi from "./widgets/service/ombi";
|
||||
import Portainer from "./widgets/service/portainer";
|
||||
import Emby from "./widgets/service/emby";
|
||||
import Nzbget from "./widgets/service/nzbget";
|
||||
import SABnzbd from "./widgets/service/sabnzbd";
|
||||
import Docker from "./widgets/service/docker";
|
||||
import Pihole from "./widgets/service/pihole";
|
||||
import Rutorrent from "./widgets/service/rutorrent";
|
||||
import Jellyfin from "./widgets/service/jellyfin";
|
||||
import Speedtest from "./widgets/service/speedtest";
|
||||
import Traefik from "./widgets/service/traefik";
|
||||
import Jellyseerr from "./widgets/service/jellyseerr";
|
||||
import Overseerr from "./widgets/service/overseerr";
|
||||
import Npm from "./widgets/service/npm";
|
||||
import Tautulli from "./widgets/service/tautulli";
|
||||
import CoinMarketCap from "./widgets/service/coinmarketcap";
|
||||
import Gotify from "./widgets/service/gotify";
|
||||
const Sonarr = dynamic(() => import("./widgets/service/sonarr"));
|
||||
const Radarr = dynamic(() => import("./widgets/service/radarr"));
|
||||
const Lidarr = dynamic(() => import("./widgets/service/lidarr"));
|
||||
const Readarr = dynamic(() => import("./widgets/service/readarr"));
|
||||
const Bazarr = dynamic(() => import("./widgets/service/bazarr"));
|
||||
const Ombi = dynamic(() => import("./widgets/service/ombi"));
|
||||
const Portainer = dynamic(() => import("./widgets/service/portainer"));
|
||||
const Emby = dynamic(() => import("./widgets/service/emby"));
|
||||
const Nzbget = dynamic(() => import("./widgets/service/nzbget"));
|
||||
const SABnzbd = dynamic(() => import("./widgets/service/sabnzbd"));
|
||||
const Transmission = dynamic(() => import("./widgets/service/transmission"));
|
||||
const QBittorrent = dynamic(() => import("./widgets/service/qbittorrent"));
|
||||
const Docker = dynamic(() => import("./widgets/service/docker"));
|
||||
const Pihole = dynamic(() => import("./widgets/service/pihole"));
|
||||
const Rutorrent = dynamic(() => import("./widgets/service/rutorrent"));
|
||||
const Jellyfin = dynamic(() => import("./widgets/service/jellyfin"));
|
||||
const Speedtest = dynamic(() => import("./widgets/service/speedtest"));
|
||||
const Traefik = dynamic(() => import("./widgets/service/traefik"));
|
||||
const Jellyseerr = dynamic(() => import("./widgets/service/jellyseerr"));
|
||||
const Overseerr = dynamic(() => import("./widgets/service/overseerr"));
|
||||
const Npm = dynamic(() => import("./widgets/service/npm"));
|
||||
const Tautulli = dynamic(() => import("./widgets/service/tautulli"));
|
||||
const CoinMarketCap = dynamic(() => import("./widgets/service/coinmarketcap"));
|
||||
const Gotify = dynamic(() => import("./widgets/service/gotify"));
|
||||
const Prowlarr = dynamic(() => import("./widgets/service/prowlarr"));
|
||||
const Jackett = dynamic(() => import("./widgets/service/jackett"));
|
||||
const AdGuard = dynamic(() => import("./widgets/service/adguard"));
|
||||
const StRelaySrv = dynamic(() => import("./widgets/service/strelaysrv"));
|
||||
const Mastodon = dynamic(() => import("./widgets/service/mastodon"));
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
sonarr: Sonarr,
|
||||
radarr: Radarr,
|
||||
lidarr: Lidarr,
|
||||
readarr: Readarr,
|
||||
bazarr: Bazarr,
|
||||
ombi: Ombi,
|
||||
portainer: Portainer,
|
||||
emby: Emby,
|
||||
jellyfin: Jellyfin,
|
||||
nzbget: Nzbget,
|
||||
sabnzbd: SABnzbd,
|
||||
transmission: Transmission,
|
||||
qbittorrent: QBittorrent,
|
||||
pihole: Pihole,
|
||||
rutorrent: Rutorrent,
|
||||
speedtest: Speedtest,
|
||||
@@ -41,7 +56,11 @@ const widgetMappings = {
|
||||
npm: Npm,
|
||||
tautulli: Tautulli,
|
||||
gotify: Gotify,
|
||||
sabnzbd: SABnzbd
|
||||
prowlarr: Prowlarr,
|
||||
jackett: Jackett,
|
||||
adguard: AdGuard,
|
||||
strelaysrv: StRelaySrv,
|
||||
mastodon: Mastodon,
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import getSymbolFromCurrency from "currency-symbol-map";
|
||||
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;
|
||||
@@ -30,7 +41,7 @@ export default function CoinMarketCap({ service }) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
if (!statsData || !dateRange) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block value={t("coinmarketcap.configure")} />
|
||||
@@ -39,28 +50,36 @@ export default function CoinMarketCap({ service }) {
|
||||
}
|
||||
|
||||
const { data } = statsData;
|
||||
const currencySymbol = getSymbolFromCurrency(currencyCode);
|
||||
|
||||
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((key) => (
|
||||
{symbols.map((symbol) => (
|
||||
<div
|
||||
key={data[key].symbol}
|
||||
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[key].name}</div>
|
||||
<div className="font-thin pl-2">{data[symbol].name}</div>
|
||||
<div className="flex flex-row text-right">
|
||||
<div className="font-bold mr-2">
|
||||
{currencySymbol}
|
||||
{data[key].quote[currencyCode].price.toFixed(2)}
|
||||
{t("common.number", {
|
||||
value: data[symbol].quote[currencyCode].price,
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`font-bold w-10 mr-2 ${
|
||||
data[key].quote[currencyCode].percent_change_1h > 0 ? "text-emerald-300" : "text-rose-300"
|
||||
data[symbol].quote[currencyCode][`percent_change_${dateRange}`] > 0
|
||||
? "text-emerald-300"
|
||||
: "text-rose-300"
|
||||
}`}
|
||||
>
|
||||
{data[key].quote[currencyCode].percent_change_1h.toFixed(2)}%
|
||||
{data[symbol].quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import useSWR from "swr";
|
||||
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";
|
||||
|
||||
@@ -27,24 +28,35 @@ function ticksToString(ticks) {
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ playCommand, session }) {
|
||||
console.log(session);
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||
IsVideoDirect: true,
|
||||
VideoDecoderIsHardware: true,
|
||||
VideoEncoderIsHardware: true,
|
||||
};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2">
|
||||
<span>
|
||||
<div className="grow text-xs z-10 self-center ml-2 relative w-full h-4 mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</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 className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</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">
|
||||
@@ -73,7 +85,12 @@ function SingleSessionEntry({ playCommand, session }) {
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
@@ -84,6 +101,9 @@ function SessionEntry({ playCommand, session }) {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
@@ -111,14 +131,20 @@ function SessionEntry({ playCommand, session }) {
|
||||
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}
|
||||
{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 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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
37
src/components/services/widgets/service/mastodon.jsx
Normal file
37
src/components/services/widgets/service/mastodon.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 Mastodon({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `instance`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("mastodon.user_count")} />
|
||||
<Block label={t("mastodon.status_count")} />
|
||||
<Block label={t("mastodon.domain_count")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("mastodon.user_count")} value={t("common.number", { value: statsData.stats.user_count })} />
|
||||
<Block label={t("mastodon.status_count")} value={t("common.number", { value: statsData.stats.status_count })} />
|
||||
<Block label={t("mastodon.domain_count")} value={t("common.number", { value: statsData.stats.domain_count })} />
|
||||
</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 (
|
||||
<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.movies")} value={have.length} />
|
||||
<Block label={t("radarr.movies")} value={moviesData.have} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,11 @@ export default function Readarr({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("readarr.queued")} value={queueData.totalCount} />
|
||||
<Block label={t("readarr.books")} value={have.length} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ export default function SABnzbd({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}bps`} />
|
||||
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
|
||||
<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>
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.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>
|
||||
);
|
||||
}
|
||||
|
||||
38
src/components/services/widgets/service/strelaysrv.jsx
Normal file
38
src/components/services/widgets/service/strelaysrv.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function StRelaySrv({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `status`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("strelaysrv.numActiveSessions")} />
|
||||
<Block label={t("strelaysrv.numConnections")} />
|
||||
<Block label={t("strelaysrv.bytesProxied")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("strelaysrv.numActiveSessions")} value={t("common.number", { value: statsData.numActiveSessions })} />
|
||||
<Block label={t("strelaysrv.numConnections")} value={t("common.number", { value: statsData.numConnections })} />
|
||||
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block label={t("strelaysrv.transferRate")} value={t("common.bitrate",{ value: statsData.kbps10s1m5m15m30m60m[5] })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable camelcase */
|
||||
import useSWR from "swr";
|
||||
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";
|
||||
|
||||
@@ -27,16 +28,26 @@ function millisecondsToString(milliseconds) {
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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">
|
||||
<span>{full_title}</span>
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</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">
|
||||
@@ -48,15 +59,17 @@ function SingleSessionEntry({ session }) {
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{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" && (
|
||||
<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 className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">
|
||||
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{millisecondsToString(view_offset)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{millisecondsToString(duration)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -64,7 +77,7 @@ function SingleSessionEntry({ 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 (
|
||||
<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">
|
||||
{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" />
|
||||
)}
|
||||
<span>{full_title}</span>
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{millisecondsToString(view_offset)}</div>
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{video_decision === "direct play" && audio_decision === "direct play" && (
|
||||
<MdSmartDisplay className="opacity-50" />
|
||||
)}
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" &&
|
||||
video_decision !== "direct play" &&
|
||||
(audio_decision !== "copy" || audio_decision !== "direct play") && <BsFillCpuFill className="opacity-50" />}
|
||||
{(video_decision === "copy" || video_decision === "direct play") &&
|
||||
audio_decision !== "copy" &&
|
||||
audio_decision !== "direct play" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">{millisecondsToString(view_offset)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
47
src/components/version.jsx
Normal file
47
src/components/version.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
import { compareVersions } from "compare-versions";
|
||||
import { MdNewReleases } from "react-icons/md";
|
||||
|
||||
export default function Version() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const buildTime = process.env.NEXT_PUBLIC_BUILDTIME ?? new Date().toISOString();
|
||||
const revision = process.env.NEXT_PUBLIC_REVISION ?? "dev";
|
||||
const version = process.env.NEXT_PUBLIC_VERSION ?? "dev";
|
||||
|
||||
const { data: releaseData } = useSWR("https://api.github.com/repos/benphelps/homepage/releases");
|
||||
|
||||
// use Intl.DateTimeFormat to format the date
|
||||
const formatDate = (date) => {
|
||||
const options = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
};
|
||||
return new Intl.DateTimeFormat(i18n.language, options).format(new Date(date));
|
||||
};
|
||||
|
||||
const latestRelease = releaseData?.[0];
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="text-xs text-theme-500 opacity-50">
|
||||
{version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
|
||||
</span>
|
||||
{version === "main" || version === "dev"
|
||||
? null
|
||||
: releaseData &&
|
||||
compareVersions(latestRelease.tag_name, version) > 0 && (
|
||||
<a
|
||||
href={latestRelease.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-xs text-theme-500 opacity-50 flex flex-row items-center"
|
||||
>
|
||||
<MdNewReleases className="mr-1" /> {t("Update Available")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import WeatherApi from "components/widgets/weather/weather";
|
||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||
import Resources from "components/widgets/resources/resources";
|
||||
import Search from "components/widgets/search/search";
|
||||
import Greeting from "components/widgets/greeting/greeting";
|
||||
import DateTime from "components/widgets/datetime/datetime";
|
||||
|
||||
const widgetMappings = {
|
||||
weather: WeatherApi, // This key will be deprecated in the future
|
||||
@@ -9,13 +11,15 @@ const widgetMappings = {
|
||||
openweathermap: OpenWeatherMap,
|
||||
resources: Resources,
|
||||
search: Search,
|
||||
greeting: Greeting,
|
||||
datetime: DateTime,
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
const ServiceWidget = widgetMappings[widget.type];
|
||||
const InfoWidget = widgetMappings[widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
return <ServiceWidget options={widget.options} />;
|
||||
if (InfoWidget) {
|
||||
return <InfoWidget options={widget.options} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
36
src/components/widgets/datetime/datetime.jsx
Normal file
36
src/components/widgets/datetime/datetime.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function DateTime({ options }) {
|
||||
const { text_size: textSize, format } = options;
|
||||
const { i18n } = useTranslation();
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [setDate]);
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center grow justify-end">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
|
||||
{dateFormat.format(date)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/widgets/greeting/greeting.jsx
Normal file
22
src/components/widgets/greeting/greeting.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function Greeting({ options }) {
|
||||
if (options.text) {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-start">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[options.text_size || "xl"]}`}>
|
||||
{options.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Cpu() {
|
||||
export default function Cpu({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||
@@ -39,11 +39,29 @@ export default function Cpu() {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono min-w-[80px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}{" "}
|
||||
{t("docker.cpu")}
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.usage,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("docker.cpu")}</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.load,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.load")}</div>
|
||||
</div>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Disk({ options }) {
|
||||
export default function Disk({ options, expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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);
|
||||
|
||||
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" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<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")}
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })}</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })}</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Memory() {
|
||||
export default function Memory({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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);
|
||||
|
||||
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" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[80px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||
{t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 0, binary: true })}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", {
|
||||
value: data.memory.totalMemMb * 1024 * 1024,
|
||||
maximumFractionDigits: 0,
|
||||
binary: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,15 @@ import Cpu from "./cpu";
|
||||
import Memory from "./memory";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
const { expanded } = options;
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center m-auto flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{options.cpu && <Cpu />}
|
||||
{options.memory && <Memory />}
|
||||
{options.cpu && <Cpu expanded={expanded} />}
|
||||
{options.memory && <Memory expanded={expanded} />}
|
||||
{Array.isArray(options.disk)
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} />)
|
||||
: options.disk && <Disk options={options} />}
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} />)
|
||||
: options.disk && <Disk options={options} expanded={expanded} />}
|
||||
</div>
|
||||
{options.label && (
|
||||
<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 }) {
|
||||
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
|
||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
|
||||
style={{
|
||||
|
||||
@@ -62,14 +62,15 @@ export default function Search({ options }) {
|
||||
bg-white/50 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50
|
||||
focus:border-theme-500 dark:focus:border-white/50
|
||||
border border-theme-300 dark:border-theme-200/50
|
||||
backdrop-blur-md"
|
||||
border border-theme-300 dark:border-theme-200/50"
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={options.focus}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -6,6 +6,9 @@ import "styles/weather-icons.css";
|
||||
import "styles/theme.css";
|
||||
|
||||
import "utils/i18n";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
import { SettingsProvider } from "utils/settings-context";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
@@ -14,7 +17,13 @@ function MyApp({ Component, pageProps }) {
|
||||
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<SettingsProvider>
|
||||
<Component {...pageProps} />
|
||||
</SettingsProvider>
|
||||
</ThemeProvider>
|
||||
</ColorProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ export default async function handler(req, res) {
|
||||
|
||||
try {
|
||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||
} catch {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors");
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||
console.error(e);
|
||||
discoveredServices = [];
|
||||
}
|
||||
|
||||
try {
|
||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error("Failed to load services.yaml, please check for errors");
|
||||
console.error(e);
|
||||
configuredServices = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,91 @@
|
||||
import createLogger from "utils/logger";
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import credentialedProxyHandler from "utils/proxies/credentialed";
|
||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||
import npmProxyHandler from "utils/proxies/npm";
|
||||
import transmissionProxyHandler from "utils/proxies/transmission";
|
||||
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
|
||||
|
||||
const logger = createLogger('servicesProxy');
|
||||
|
||||
function asJson(data) {
|
||||
if (data?.length > 0) {
|
||||
const json = JSON.parse(data.toString());
|
||||
return json;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function jsonArrayTransform(data, transform) {
|
||||
const json = asJson(data);
|
||||
if (json instanceof Array) {
|
||||
return transform(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function jsonArrayFilter(data, filter) {
|
||||
return jsonArrayTransform(data, (items) => items.filter(filter));
|
||||
}
|
||||
|
||||
const serviceProxyHandlers = {
|
||||
// uses query param auth
|
||||
emby: genericProxyHandler,
|
||||
jellyfin: genericProxyHandler,
|
||||
pihole: genericProxyHandler,
|
||||
radarr: genericProxyHandler,
|
||||
sonarr: genericProxyHandler,
|
||||
readarr: genericProxyHandler,
|
||||
radarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movie: (data) => ({
|
||||
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
|
||||
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
sonarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
series: (data) => ({
|
||||
total: asJson(data).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
lidarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
album: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
readarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
book: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
bazarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movies: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
episodes: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
},
|
||||
},
|
||||
speedtest: genericProxyHandler,
|
||||
tautulli: genericProxyHandler,
|
||||
traefik: genericProxyHandler,
|
||||
sabnzbd: genericProxyHandler,
|
||||
jackett: genericProxyHandler,
|
||||
adguard: genericProxyHandler,
|
||||
strelaysrv: genericProxyHandler,
|
||||
mastodon: genericProxyHandler,
|
||||
// uses X-API-Key (or similar) header auth
|
||||
gotify: credentialedProxyHandler,
|
||||
portainer: credentialedProxyHandler,
|
||||
@@ -23,20 +93,37 @@ const serviceProxyHandlers = {
|
||||
overseerr: credentialedProxyHandler,
|
||||
ombi: credentialedProxyHandler,
|
||||
coinmarketcap: credentialedProxyHandler,
|
||||
prowlarr: credentialedProxyHandler,
|
||||
// super specific handlers
|
||||
rutorrent: rutorrentProxyHandler,
|
||||
nzbget: nzbgetProxyHandler,
|
||||
npm: npmProxyHandler,
|
||||
transmission: transmissionProxyHandler,
|
||||
qbittorrent: qbittorrentProxyHandler,
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
try {
|
||||
const { type } = req.query;
|
||||
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
return serviceProxyHandler(req, res);
|
||||
if (serviceProxyHandler) {
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
const { proxy, maps } = serviceProxyHandler;
|
||||
if (proxy) {
|
||||
return proxy(req, res, maps);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Unknown proxy service type: %s", type);
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
catch (ex) {
|
||||
logger.error(ex);
|
||||
return res.status(500).send({ error: "Unexpected error" });
|
||||
}
|
||||
|
||||
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) {
|
||||
const settings = await getSettings();
|
||||
const settings = getSettings();
|
||||
apiKey = settings?.providers?.openweathermap;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
if (!apiKey && provider) {
|
||||
const settings = await getSettings();
|
||||
const settings = getSettings();
|
||||
apiKey = settings?.providers?.weatherapi;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
import useSWR from "swr";
|
||||
import Head from "next/head";
|
||||
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 BookmarksGroup from "components/bookmarks/group";
|
||||
import Widget from "components/widget";
|
||||
import Revalidate from "components/revalidate";
|
||||
import createLogger from "utils/logger";
|
||||
import { getSettings } from "utils/config";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
import { ColorContext } from "utils/color-context";
|
||||
import { ThemeContext } from "utils/theme-context";
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||
ssr: false,
|
||||
@@ -19,79 +24,155 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
|
||||
const Version = dynamic(() => import("components/version"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export async function getStaticProps() {
|
||||
const settings = await getSettings();
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"];
|
||||
|
||||
return {
|
||||
props: {
|
||||
settings,
|
||||
},
|
||||
};
|
||||
export function getStaticProps() {
|
||||
let logger;
|
||||
try {
|
||||
logger = createLogger("index");
|
||||
const { providers, ...settings } = getSettings();
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSettings: settings,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
if (logger) {
|
||||
logger.error(e);
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
initialSettings: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
export default function Home({ settings }) {
|
||||
|
||||
export default function Index({ initialSettings }) {
|
||||
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 initialSettings={initialSettings} />;
|
||||
}
|
||||
|
||||
function Home({ initialSettings }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const { color, setColor } = useContext(ColorContext);
|
||||
const { settings, setSettings } = useContext(SettingsContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSettings(initialSettings);
|
||||
}, [initialSettings, setSettings]);
|
||||
|
||||
const { data: services } = useSWR("/api/services");
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
|
||||
const wrappedStyle = {};
|
||||
if (settings.background) {
|
||||
if (settings && settings.background) {
|
||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||
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 (
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
</Head>
|
||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<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 && (
|
||||
<>
|
||||
<>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon && <link rel="icon" href={settings.favicon} />}
|
||||
</Head>
|
||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<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
|
||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||
.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
|
||||
.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>
|
||||
</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>
|
||||
</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="flex p-8 pb-0 w-full justify-end">
|
||||
{!settings?.color && <ColorToggle />}
|
||||
<Revalidate />
|
||||
{!settings?.theme && <ThemeToggle />}
|
||||
</div>
|
||||
|
||||
<div className="flex p-8 pt-4 w-full justify-end">
|
||||
<Version />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Bookmarks
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Docker-Integration
|
||||
|
||||
my-docker:
|
||||
host: 127.0.0.1
|
||||
port: 2375
|
||||
# my-docker:
|
||||
# host: 127.0.0.1
|
||||
# port: 2375
|
||||
|
||||
other-docker:
|
||||
socket: /var/run/docker.sock
|
||||
# my-docker:
|
||||
# socket: /var/run/docker.sock
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Services
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Settings
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://github.com/benphelps/homepage/wiki/Information-Widgets
|
||||
|
||||
|
||||
@@ -9,14 +9,23 @@ const formats = {
|
||||
traefik: `{url}/api/{endpoint}`,
|
||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||
transmission: `{url}/transmission/rpc`,
|
||||
qbittorrent: `{url}/api/v2/{endpoint}`,
|
||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||
overseerr: `{url}/api/v1/{endpoint}`,
|
||||
ombi: `{url}/api/v1/{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}`,
|
||||
strelaysrv: `{url}/{endpoint}`,
|
||||
mastodon: `{url}/api/v1/{endpoint}`,
|
||||
};
|
||||
|
||||
export function formatApiCall(api, args) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import { join } from "path";
|
||||
import { existsSync, copyFile, promises as fs } from "fs";
|
||||
import { existsSync, copyFile, readFileSync } from "fs";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
@@ -15,13 +15,22 @@ export default function checkAndCopyConfig(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");
|
||||
|
||||
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
|
||||
const fileContents = await fs.readFile(settingsYaml, "utf8");
|
||||
const fileContents = readFileSync(settingsYaml, "utf8");
|
||||
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 */
|
||||
import https from "https";
|
||||
import http from "http";
|
||||
/* eslint-disable no-param-reassign */
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
addCookieHandler(url, params);
|
||||
const request = https.request(url, params, (response) => {
|
||||
const data = [];
|
||||
|
||||
@@ -12,7 +25,8 @@ export function httpsRequest(url, params) {
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
if (params.body) {
|
||||
request.write(params.body);
|
||||
}
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function httpRequest(url, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
addCookieHandler(url, params);
|
||||
const request = http.request(url, params, (response) => {
|
||||
const data = [];
|
||||
|
||||
@@ -34,7 +53,8 @@ export function httpRequest(url, params) {
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
if (params.body) {
|
||||
request.write(params.body);
|
||||
}
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
88
src/utils/logger.js
Normal file
88
src/utils/logger.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable no-console */
|
||||
import { join } from "path";
|
||||
import { format as utilFormat } from "node:util"
|
||||
|
||||
import winston from "winston";
|
||||
|
||||
let winstonLogger;
|
||||
|
||||
function init() {
|
||||
const configPath = join(process.cwd(), "config");
|
||||
|
||||
function combineMessageAndSplat() {
|
||||
return {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
transform: (info, opts) => {
|
||||
// combine message and args if any
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
info.message = utilFormat(info.message, ...info[Symbol.for('splat')] || []);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function messageFormatter(logInfo) {
|
||||
if (logInfo.label) {
|
||||
if (logInfo.stack) {
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: <${logInfo.label}> ${logInfo.stack}`;
|
||||
}
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: <${logInfo.label}> ${logInfo.message}`;
|
||||
}
|
||||
|
||||
if (logInfo.stack) {
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.stack}`;
|
||||
}
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.message}`;
|
||||
};
|
||||
|
||||
winstonLogger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.errors({ stack: true}),
|
||||
combineMessageAndSplat(),
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(messageFormatter)
|
||||
),
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
|
||||
new winston.transports.File({
|
||||
format: winston.format.combine(
|
||||
winston.format.errors({ stack: true}),
|
||||
combineMessageAndSplat(),
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(messageFormatter)
|
||||
),
|
||||
filename: `${configPath}/logs/homepage.log`,
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
// patch the console log mechanism to use our logger
|
||||
const consoleMethods = ['log', 'debug', 'info', 'warn', 'error']
|
||||
consoleMethods.forEach(method => {
|
||||
// workaround for https://github.com/winstonjs/winston/issues/1591
|
||||
switch (method) {
|
||||
case 'log':
|
||||
console[method] = winstonLogger.info.bind(winstonLogger);
|
||||
break;
|
||||
default:
|
||||
console[method] = winstonLogger[method].bind(winstonLogger);
|
||||
break;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default function createLogger(label) {
|
||||
if (!winstonLogger) {
|
||||
init();
|
||||
}
|
||||
|
||||
return winstonLogger.child({ label });
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
export default async function genericProxyHandler(req, res) {
|
||||
const logger = createLogger('genericProxyHandler');
|
||||
|
||||
export default async function genericProxyHandler(req, res, maps) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
@@ -10,19 +13,38 @@ export default async function genericProxyHandler(req, res) {
|
||||
|
||||
if (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, {
|
||||
method: req.method,
|
||||
headers,
|
||||
});
|
||||
|
||||
let resultData = data;
|
||||
if ((status === 200) && (maps?.[endpoint])) {
|
||||
resultData = maps[endpoint](data);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
if (status === 204 || status === 304) {
|
||||
return res.status(status).end();
|
||||
}
|
||||
|
||||
return res.status(status).send(data);
|
||||
if (status >= 400) {
|
||||
logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
|
||||
}
|
||||
|
||||
return res.status(status).send(resultData);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
15
src/utils/settings-context.jsx
Normal file
15
src/utils/settings-context.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createContext, useState, useMemo } from "react";
|
||||
|
||||
export const SettingsContext = createContext();
|
||||
|
||||
export function SettingsProvider({ initialSettings, children }) {
|
||||
const [settings, setSettings] = useState({});
|
||||
|
||||
if (initialSettings) {
|
||||
setSettings(initialSettings);
|
||||
}
|
||||
|
||||
const value = useMemo(() => ({ settings, setSettings }), [settings]);
|
||||
|
||||
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const tailwindForms = require("@tailwindcss/forms");
|
||||
const tailwindScrollbars = require("tailwind-scrollbar");
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"],
|
||||
@@ -6,19 +9,19 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
theme: {
|
||||
["50"]: "rgb(var(--color-50) / <alpha-value>)",
|
||||
["100"]: "rgb(var(--color-100) / <alpha-value>)",
|
||||
["200"]: "rgb(var(--color-200) / <alpha-value>)",
|
||||
["300"]: "rgb(var(--color-300) / <alpha-value>)",
|
||||
["400"]: "rgb(var(--color-400) / <alpha-value>)",
|
||||
["500"]: "rgb(var(--color-500) / <alpha-value>)",
|
||||
["600"]: "rgb(var(--color-600) / <alpha-value>)",
|
||||
["700"]: "rgb(var(--color-700) / <alpha-value>)",
|
||||
["800"]: "rgb(var(--color-800) / <alpha-value>)",
|
||||
["900"]: "rgb(var(--color-900) / <alpha-value>)",
|
||||
50: "rgb(var(--color-50) / <alpha-value>)",
|
||||
100: "rgb(var(--color-100) / <alpha-value>)",
|
||||
200: "rgb(var(--color-200) / <alpha-value>)",
|
||||
300: "rgb(var(--color-300) / <alpha-value>)",
|
||||
400: "rgb(var(--color-400) / <alpha-value>)",
|
||||
500: "rgb(var(--color-500) / <alpha-value>)",
|
||||
600: "rgb(var(--color-600) / <alpha-value>)",
|
||||
700: "rgb(var(--color-700) / <alpha-value>)",
|
||||
800: "rgb(var(--color-800) / <alpha-value>)",
|
||||
900: "rgb(var(--color-900) / <alpha-value>)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms")],
|
||||
plugins: [tailwindForms, tailwindScrollbars],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user