Compare commits

..

232 Commits

Author SHA1 Message Date
shamoon
82c54f197d Merge pull request #1698 from benphelps/fix/issue-1697
Fixes oversized logo in 0.6.22
2023-07-19 22:58:58 -07:00
shamoon
597a8d8b9a Fixes oversized logo 2023-07-19 22:56:11 -07:00
Dan
2aeb3f4c89 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (472 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-07-18 17:02:15 +02:00
Ash Ed
29f3217abb Translated using Weblate (Russian)
Currently translated at 90.2% (426 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2023-07-18 17:02:15 +02:00
gallegonovato
7f83623ded Translated using Weblate (Spanish)
Currently translated at 100.0% (472 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-07-16 12:00:52 +02:00
Nonoss117
2843ae3f79 Translated using Weblate (French)
Currently translated at 100.0% (472 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-07-15 08:48:03 +02:00
Anonymous
ba1b075cb8 Translated using Weblate (Basque)
Currently translated at 6.9% (33 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eu/
2023-07-14 08:21:59 +02:00
Anonymous
f063e20dab Translated using Weblate (Indonesian)
Currently translated at 3.1% (15 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/id/
2023-07-14 08:21:59 +02:00
Anonymous
176b8fe6a9 Translated using Weblate (Slovenian)
Currently translated at 99.7% (471 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sl/
2023-07-14 08:21:59 +02:00
Anonymous
d71893903e Translated using Weblate (Greek)
Currently translated at 30.2% (143 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/el/
2023-07-14 08:21:58 +02:00
Anonymous
84d82309b0 Translated using Weblate (Korean)
Currently translated at 36.6% (173 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ko/
2023-07-14 08:21:58 +02:00
Anonymous
d7f6113fd9 Translated using Weblate (Slovak)
Currently translated at 1.9% (9 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sk/
2023-07-14 08:21:58 +02:00
Anonymous
cacdd2348a Translated using Weblate (Thai)
Currently translated at 9.9% (47 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/th/
2023-07-14 08:21:58 +02:00
Anonymous
f091f8a6ad Translated using Weblate (Latvian)
Currently translated at 25.0% (118 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/lv/
2023-07-14 08:21:57 +02:00
Anonymous
5636537998 Translated using Weblate (Japanese)
Currently translated at 81.5% (385 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-07-14 08:21:57 +02:00
Anonymous
d49c3dfa06 Translated using Weblate (Ukrainian)
Currently translated at 99.7% (471 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-07-14 08:21:57 +02:00
Anonymous
d612c0073b Translated using Weblate (Esperanto)
Currently translated at 30.7% (145 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eo/
2023-07-14 08:21:57 +02:00
Anonymous
36e63e9736 Translated using Weblate (Hindi)
Currently translated at 1.9% (9 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hi/
2023-07-14 08:21:56 +02:00
Anonymous
328fd8b6b6 Translated using Weblate (Malay)
Currently translated at 53.1% (251 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ms/
2023-07-14 08:21:56 +02:00
Anonymous
5ef4638d88 Translated using Weblate (Danish)
Currently translated at 41.7% (197 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/da/
2023-07-14 08:21:56 +02:00
Anonymous
2251b0cb3e Translated using Weblate (Czech)
Currently translated at 94.0% (444 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/cs/
2023-07-14 08:21:55 +02:00
Anonymous
d97e729e41 Translated using Weblate (Arabic)
Currently translated at 55.5% (262 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ar/
2023-07-14 08:21:55 +02:00
Anonymous
b6e8952d82 Translated using Weblate (Serbian)
Currently translated at 1.9% (9 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sr/
2023-07-14 08:21:55 +02:00
Anonymous
3d18618fc1 Translated using Weblate (Turkish)
Currently translated at 83.2% (393 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/tr/
2023-07-14 08:21:55 +02:00
Anonymous
cc4bf9d221 Translated using Weblate (Bulgarian)
Currently translated at 9.7% (46 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/bg/
2023-07-14 08:21:54 +02:00
Anonymous
96e9a5cc72 Translated using Weblate (Telugu)
Currently translated at 45.5% (215 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/te/
2023-07-14 08:21:54 +02:00
Anonymous
a8bb116ea1 Translated using Weblate (Finnish)
Currently translated at 37.5% (177 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fi/
2023-07-14 08:21:54 +02:00
Anonymous
7f8b66f72d Translated using Weblate (Yue (Traditional))
Currently translated at 24.7% (117 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/yue_Hant/
2023-07-14 08:21:54 +02:00
Anonymous
44fbc31dd6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 85.5% (404 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt_BR/
2023-07-14 08:21:53 +02:00
Anonymous
449d170430 Translated using Weblate (Romanian)
Currently translated at 31.7% (150 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ro/
2023-07-14 08:21:53 +02:00
Anonymous
e34fd362d2 Translated using Weblate (Hebrew)
Currently translated at 21.1% (100 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2023-07-14 08:21:53 +02:00
Anonymous
e4392fc821 Translated using Weblate (Hungarian)
Currently translated at 25.4% (120 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2023-07-14 08:21:53 +02:00
Anonymous
4dc4ba9b2c Translated using Weblate (Croatian)
Currently translated at 99.3% (469 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2023-07-14 08:21:52 +02:00
Anonymous
9b33d1acf1 Translated using Weblate (Swedish)
Currently translated at 27.9% (132 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2023-07-14 08:21:52 +02:00
Anonymous
160352d436 Translated using Weblate (Polish)
Currently translated at 79.2% (374 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2023-07-14 08:21:52 +02:00
Anonymous
b3e9bb6cc2 Translated using Weblate (Catalan)
Currently translated at 55.0% (260 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2023-07-14 08:21:52 +02:00
Anonymous
ad1a3f8395 Translated using Weblate (Chinese (Traditional))
Currently translated at 97.4% (460 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2023-07-14 08:21:51 +02:00
Anonymous
b4af77d3c4 Translated using Weblate (Dutch)
Currently translated at 51.0% (241 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2023-07-14 08:21:51 +02:00
Anonymous
95f1c31e9c Translated using Weblate (Vietnamese)
Currently translated at 9.3% (44 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2023-07-14 08:21:51 +02:00
Anonymous
3b2f7561be Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.9% (80 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2023-07-14 08:21:51 +02:00
Anonymous
2deae19940 Translated using Weblate (Italian)
Currently translated at 99.5% (470 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-07-14 08:21:50 +02:00
Anonymous
4e4e366e4e Translated using Weblate (Chinese (Simplified))
Currently translated at 94.0% (444 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2023-07-14 08:21:50 +02:00
Anonymous
de6e777312 Translated using Weblate (Russian)
Currently translated at 87.5% (413 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2023-07-14 08:21:50 +02:00
Anonymous
bd467ab01f Translated using Weblate (Portuguese)
Currently translated at 87.9% (415 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2023-07-14 08:21:50 +02:00
Anonymous
376ab9a6f3 Translated using Weblate (French)
Currently translated at 99.7% (471 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-07-14 08:21:49 +02:00
Anonymous
48f9bf4f7e Translated using Weblate (Spanish)
Currently translated at 99.7% (471 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-07-14 08:21:49 +02:00
Anonymous
23fd4a5a73 Translated using Weblate (German)
Currently translated at 96.8% (457 of 472 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2023-07-14 08:21:49 +02:00
shamoon
5a7e97222c Merge pull request #1685 from jonathann92/fix-tautulli-sort
Fix: Handle tautulli response when unable to connect to Plex
2023-07-13 23:19:11 -07:00
shamoon
7844991617 Show plex connection error 2023-07-13 23:03:10 -07:00
shamoon
ae4a2e3cf6 Merge pull request #1684 from benphelps/feature-coinmarketcap-slugs
Feature: support coinmarketcap slugs
2023-07-13 22:41:51 -07:00
jonathann92
aeac95db48 early exit if tautulli's data is an empty object
Fix for when tautulli cannot reach to plex
2023-07-13 22:08:00 -07:00
shamoon
1e60553904 Allow coinmarketcap slugs 2023-07-13 21:39:45 -07:00
shamoon
276edb2069 Better handle invalid coinmarketcap data 2023-07-13 17:14:47 -07:00
alpine8
d75505acc8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.9% (80 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2023-07-12 02:52:27 +02:00
shamoon
2d8af6eaf6 Merge pull request #1673 from jnsgruk/conf-dir-override
Override config directory with env var.
2023-07-10 20:54:38 -07:00
antaanimosity
9f4ac4e41e Translated using Weblate (Basque)
Currently translated at 7.0% (33 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eu/
2023-07-10 21:48:24 +02:00
Jon Seager
ca396ce96b Override config directory with env var.
Until this change, the config directory was assumed
to be located at '/config'. This patch retains that
default behaviour, but enables users/devs to override
that behaviour by setting the HOMEPAGE_CONFIG_DIR
variable.
2023-07-10 15:48:51 +01:00
Anonymous
9c0bd8b07a Translated using Weblate (Basque)
Currently translated at 100.0% (0 of 0 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eu/
2023-07-09 20:32:29 +02:00
Smexhy
885058dd41 Translated using Weblate (Czech)
Currently translated at 94.2% (444 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/cs/
2023-07-09 20:32:27 +02:00
antaanimosity
c6b6b4d71b Added translation using Weblate (Basque) 2023-07-09 20:31:43 +02:00
Andrej Kralj
8f953c0d16 Translated using Weblate (Slovenian)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sl/
2023-07-06 13:47:58 +02:00
shamoon
d1f83c0359 Update http.js 2023-07-03 22:12:17 -07:00
shamoon
eb9721334d Merge pull request #1656 from nathan-sankbeil/fix/gzip
Handle missing EOF when decompressing responses
2023-07-03 22:10:50 -07:00
Vincenzo
2279dd37f0 Translated using Weblate (Italian)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-07-03 22:30:47 +02:00
Nonoss117
10d9b2d831 Translated using Weblate (French)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-07-03 22:30:45 +02:00
Denis Papec
1cda437120 Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/en/
2023-07-03 22:30:44 +02:00
nsankbeil
5640798fe4 fix: handle missing EOF when decompressing response
Closes: #1609
2023-07-03 11:49:16 -04:00
My Random Thoughts
1439b6cc9c Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/en/
2023-07-01 12:41:42 +02:00
shamoon
2e892ac906 Merge pull request #1640 from benphelps/feature/handle-invalid-service-fields
Handle invalid fields syntax in service labels
2023-06-28 08:50:04 -07:00
shamoon
a2fe1eef7a Handle invalid fields syntax in service labels 2023-06-28 08:49:25 -07:00
Milo Ivir
b04ed36adb Translated using Weblate (Croatian)
Currently translated at 99.5% (469 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2023-06-26 21:50:38 +02:00
Ado Nishimura
d734343b31 Translated using Weblate (Japanese)
Currently translated at 81.7% (385 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-06-23 17:53:02 +02:00
Dan
8ad0c9c171 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-23 17:53:02 +02:00
Nonoss117
b9edea5286 Translated using Weblate (French)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-23 17:53:01 +02:00
Y0plait
27baf17e08 Translated using Weblate (French)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-23 17:53:01 +02:00
gallegonovato
2a4c449b77 Translated using Weblate (Spanish)
Currently translated at 100.0% (471 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-23 17:53:00 +02:00
shamoon
04d9ceed1a Merge pull request #1629 from benphelps/collapsible-bookmarks
Feature: add collapsible feature to bookmarks
2023-06-22 11:52:07 -07:00
shamoon
b32d610532 Adds collapsible feature to bookmarks 2023-06-22 11:51:38 -07:00
shamoon
1b6e5c4a8d Update group.jsx 2023-06-22 11:51:12 -07:00
shamoon
33492bda3a Merge pull request #1626 from ionyx0/main
Feature: collapsible layout sections
2023-06-22 11:45:00 -07:00
shamoon
714e0a4517 Use Disclosure component for collapsible service groups, add transition
hide collapse arrow if disabled

dont break layout for icons in group title

no-shadow
2023-06-22 10:46:39 -07:00
Alex Higgins
041fae1fb3 Adds ability to collapse layout sections
Reverts pnpm lock file changes

Make entire section above list clickable.

Implement in headlessui instead of pulling in new library.

Remove unecessary packages and clean up ESLint errors
2023-06-22 10:00:04 -07:00
shamoon
0936ba2b6b Update package-lock.json 2023-06-22 09:57:21 -07:00
Anonymous
1d78881a5f Translated using Weblate (Indonesian)
Currently translated at 3.1% (15 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/id/
2023-06-20 19:47:24 +02:00
Anonymous
60463721b8 Translated using Weblate (Slovenian)
Currently translated at 95.3% (449 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sl/
2023-06-20 19:47:23 +02:00
Anonymous
68f755710e Translated using Weblate (Greek)
Currently translated at 30.3% (143 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/el/
2023-06-20 19:47:23 +02:00
Anonymous
84d12b0b21 Translated using Weblate (Korean)
Currently translated at 36.7% (173 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ko/
2023-06-20 19:47:23 +02:00
Anonymous
0598b8bcf5 Translated using Weblate (Slovak)
Currently translated at 1.9% (9 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sk/
2023-06-20 19:47:22 +02:00
Anonymous
ebca1050c4 Translated using Weblate (Thai)
Currently translated at 9.9% (47 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/th/
2023-06-20 19:47:22 +02:00
Anonymous
cc92890dca Translated using Weblate (Latvian)
Currently translated at 25.0% (118 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/lv/
2023-06-20 19:47:22 +02:00
Anonymous
768db388cb Translated using Weblate (Japanese)
Currently translated at 81.5% (384 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-06-20 19:47:21 +02:00
Anonymous
03276eced4 Translated using Weblate (Ukrainian)
Currently translated at 99.5% (469 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-20 19:47:21 +02:00
Anonymous
2e34ab9f1e Translated using Weblate (Esperanto)
Currently translated at 30.7% (145 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eo/
2023-06-20 19:47:21 +02:00
Anonymous
c997b2c87c Translated using Weblate (Hindi)
Currently translated at 1.9% (9 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hi/
2023-06-20 19:47:20 +02:00
Anonymous
ef1be0bb3f Translated using Weblate (Malay)
Currently translated at 53.2% (251 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ms/
2023-06-20 19:47:20 +02:00
Anonymous
92f5bdf659 Translated using Weblate (Danish)
Currently translated at 41.8% (197 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/da/
2023-06-20 19:47:20 +02:00
Anonymous
4ba88229ae Translated using Weblate (Czech)
Currently translated at 94.2% (444 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/cs/
2023-06-20 19:47:19 +02:00
Anonymous
5dde0ae1fa Translated using Weblate (Arabic)
Currently translated at 55.6% (262 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ar/
2023-06-20 19:47:19 +02:00
Anonymous
8f4d64913c Translated using Weblate (Serbian)
Currently translated at 1.9% (9 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sr/
2023-06-20 19:47:19 +02:00
Anonymous
ae060903a0 Translated using Weblate (Turkish)
Currently translated at 83.4% (393 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/tr/
2023-06-20 19:47:18 +02:00
Anonymous
81cf03fa87 Translated using Weblate (Bulgarian)
Currently translated at 9.7% (46 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/bg/
2023-06-20 19:47:18 +02:00
Anonymous
d9aa1dfdbf Translated using Weblate (Telugu)
Currently translated at 45.6% (215 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/te/
2023-06-20 19:47:18 +02:00
Anonymous
7e01545e14 Translated using Weblate (Finnish)
Currently translated at 37.5% (177 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fi/
2023-06-20 19:47:17 +02:00
Anonymous
5f21c20a59 Translated using Weblate (Yue (Traditional))
Currently translated at 24.8% (117 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/yue_Hant/
2023-06-20 19:47:17 +02:00
Anonymous
e552524d99 Translated using Weblate (Portuguese (Brazil))
Currently translated at 85.7% (404 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt_BR/
2023-06-20 19:47:16 +02:00
Anonymous
794902cbd5 Translated using Weblate (Romanian)
Currently translated at 31.8% (150 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ro/
2023-06-20 19:47:16 +02:00
Anonymous
58695dbe75 Translated using Weblate (Hebrew)
Currently translated at 21.2% (100 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2023-06-20 19:47:16 +02:00
Anonymous
514d827ff4 Translated using Weblate (Hungarian)
Currently translated at 25.4% (120 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2023-06-20 19:47:15 +02:00
Anonymous
25f7064286 Translated using Weblate (Croatian)
Currently translated at 97.2% (458 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2023-06-20 19:47:15 +02:00
Anonymous
112f376aa8 Translated using Weblate (Swedish)
Currently translated at 27.6% (130 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2023-06-20 19:47:15 +02:00
Anonymous
4fd107ab45 Translated using Weblate (Polish)
Currently translated at 79.4% (374 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2023-06-20 19:47:14 +02:00
Anonymous
197d8acbe8 Translated using Weblate (Catalan)
Currently translated at 55.2% (260 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2023-06-20 19:47:14 +02:00
Anonymous
0d8f6545dd Translated using Weblate (Chinese (Traditional))
Currently translated at 97.6% (460 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2023-06-20 19:47:14 +02:00
Anonymous
f83dd71682 Translated using Weblate (Dutch)
Currently translated at 51.1% (241 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2023-06-20 19:47:13 +02:00
Anonymous
a25f136628 Translated using Weblate (Vietnamese)
Currently translated at 9.3% (44 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2023-06-20 19:47:13 +02:00
Anonymous
958428db29 Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.7% (79 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2023-06-20 19:47:13 +02:00
Anonymous
297c253c33 Translated using Weblate (Italian)
Currently translated at 70.7% (333 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-06-20 19:47:12 +02:00
Anonymous
3021190959 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.2% (444 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2023-06-20 19:47:12 +02:00
Anonymous
3a8e9ece9b Translated using Weblate (Russian)
Currently translated at 87.6% (413 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2023-06-20 19:47:12 +02:00
Anonymous
8678c67d97 Translated using Weblate (Portuguese)
Currently translated at 88.1% (415 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2023-06-20 19:47:11 +02:00
Anonymous
2a7ca65907 Translated using Weblate (French)
Currently translated at 99.5% (469 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-20 19:47:11 +02:00
Anonymous
edcd7508c9 Translated using Weblate (Spanish)
Currently translated at 99.5% (469 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-20 19:47:11 +02:00
Anonymous
3e691ab446 Translated using Weblate (German)
Currently translated at 97.0% (457 of 471 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2023-06-20 19:47:10 +02:00
shamoon
dc85100e32 Merge pull request #1623 from dimitricappelle/kavita
Adding Kavita
2023-06-20 10:46:18 -07:00
Quan Dong
3161c459b8 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.6% (444 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2023-06-20 17:51:17 +02:00
dimitricappelle
940ce359bf Adding Kavita 2023-06-20 17:05:07 +02:00
mikmik
bc2695323c Translated using Weblate (Hungarian)
Currently translated at 25.5% (120 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2023-06-19 06:14:47 +02:00
Dan
751777a4ad Translated using Weblate (Ukrainian)
Currently translated at 100.0% (469 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-18 10:48:01 +02:00
Daniele Luisetto
34b8689da7 Translated using Weblate (Italian)
Currently translated at 71.0% (333 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-06-18 10:48:01 +02:00
Nonoss117
58b7f8f79a Translated using Weblate (French)
Currently translated at 100.0% (469 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-18 10:48:00 +02:00
gallegonovato
93515578c0 Translated using Weblate (Spanish)
Currently translated at 100.0% (469 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-18 10:48:00 +02:00
Anonymous
009aae7a38 Translated using Weblate (Indonesian)
Currently translated at 3.1% (15 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/id/
2023-06-16 08:41:41 +02:00
Anonymous
15c8c5552c Translated using Weblate (Slovenian)
Currently translated at 95.7% (449 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sl/
2023-06-16 08:41:41 +02:00
Anonymous
ec7d2cf309 Translated using Weblate (Greek)
Currently translated at 30.4% (143 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/el/
2023-06-16 08:41:40 +02:00
Anonymous
3ecc6fbae9 Translated using Weblate (Korean)
Currently translated at 36.8% (173 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ko/
2023-06-16 08:41:40 +02:00
Anonymous
3f60fff12d Translated using Weblate (Slovak)
Currently translated at 1.9% (9 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sk/
2023-06-16 08:41:39 +02:00
Anonymous
f8e2e4bf79 Translated using Weblate (Thai)
Currently translated at 10.0% (47 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/th/
2023-06-16 08:41:39 +02:00
Anonymous
62026cfe9c Translated using Weblate (Latvian)
Currently translated at 25.1% (118 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/lv/
2023-06-16 08:41:39 +02:00
Anonymous
3f8553ce7f Translated using Weblate (Japanese)
Currently translated at 81.8% (384 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-06-16 08:41:38 +02:00
Anonymous
39e30e87d7 Translated using Weblate (Ukrainian)
Currently translated at 99.1% (465 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-16 08:41:38 +02:00
Anonymous
9925865385 Translated using Weblate (Esperanto)
Currently translated at 30.9% (145 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eo/
2023-06-16 08:41:38 +02:00
Anonymous
904b0d3d62 Translated using Weblate (Hindi)
Currently translated at 1.9% (9 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hi/
2023-06-16 08:41:37 +02:00
Anonymous
3841642178 Translated using Weblate (Malay)
Currently translated at 53.5% (251 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ms/
2023-06-16 08:41:37 +02:00
Anonymous
d6c68415d1 Translated using Weblate (Danish)
Currently translated at 42.0% (197 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/da/
2023-06-16 08:41:37 +02:00
Anonymous
1184b5a2b9 Translated using Weblate (Czech)
Currently translated at 94.6% (444 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/cs/
2023-06-16 08:41:36 +02:00
Anonymous
b506558754 Translated using Weblate (Arabic)
Currently translated at 55.8% (262 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ar/
2023-06-16 08:41:36 +02:00
Anonymous
4a08aee090 Translated using Weblate (Serbian)
Currently translated at 1.9% (9 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sr/
2023-06-16 08:41:35 +02:00
Anonymous
c4a7ac9dc6 Translated using Weblate (Turkish)
Currently translated at 83.7% (393 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/tr/
2023-06-16 08:41:35 +02:00
Anonymous
283a52f843 Translated using Weblate (Bulgarian)
Currently translated at 9.8% (46 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/bg/
2023-06-16 08:41:35 +02:00
Anonymous
bb2083b2ec Translated using Weblate (Telugu)
Currently translated at 45.8% (215 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/te/
2023-06-16 08:41:34 +02:00
Anonymous
444ef3f48b Translated using Weblate (Finnish)
Currently translated at 37.7% (177 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fi/
2023-06-16 08:41:34 +02:00
Anonymous
853e741bfa Translated using Weblate (Yue (Traditional))
Currently translated at 24.9% (117 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/yue_Hant/
2023-06-16 08:41:33 +02:00
Anonymous
cf55092ba6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 86.1% (404 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt_BR/
2023-06-16 08:41:32 +02:00
Anonymous
6488e786e1 Translated using Weblate (Romanian)
Currently translated at 31.9% (150 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ro/
2023-06-16 08:41:32 +02:00
Anonymous
2260d250d6 Translated using Weblate (Hebrew)
Currently translated at 21.3% (100 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2023-06-16 08:41:32 +02:00
Anonymous
dcdc93cf06 Translated using Weblate (Hungarian)
Currently translated at 22.8% (107 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2023-06-16 08:41:31 +02:00
Anonymous
ce5b8c1c91 Translated using Weblate (Croatian)
Currently translated at 97.6% (458 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2023-06-16 08:41:31 +02:00
Anonymous
80c93a4e83 Translated using Weblate (Swedish)
Currently translated at 27.7% (130 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2023-06-16 08:41:31 +02:00
Anonymous
634077b5b6 Translated using Weblate (Polish)
Currently translated at 79.7% (374 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2023-06-16 08:41:30 +02:00
Anonymous
e4212074fe Translated using Weblate (Catalan)
Currently translated at 55.4% (260 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2023-06-16 08:41:30 +02:00
Anonymous
af9bca282d Translated using Weblate (Chinese (Traditional))
Currently translated at 98.0% (460 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2023-06-16 08:41:30 +02:00
Anonymous
4b4b871181 Translated using Weblate (Dutch)
Currently translated at 51.3% (241 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2023-06-16 08:41:29 +02:00
Anonymous
3065d95765 Translated using Weblate (Vietnamese)
Currently translated at 9.3% (44 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2023-06-16 08:41:29 +02:00
Anonymous
fe3f7ffdf3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.8% (79 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2023-06-16 08:41:28 +02:00
Anonymous
2319a6ffb7 Translated using Weblate (Italian)
Currently translated at 60.9% (286 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-06-16 08:41:28 +02:00
Anonymous
d2c123e031 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.4% (443 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2023-06-16 08:41:28 +02:00
Anonymous
91d359d5e6 Translated using Weblate (Russian)
Currently translated at 88.0% (413 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2023-06-16 08:41:27 +02:00
Anonymous
4abdce068e Translated using Weblate (Portuguese)
Currently translated at 88.4% (415 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2023-06-16 08:41:27 +02:00
Anonymous
98b4682bfe Translated using Weblate (French)
Currently translated at 99.1% (465 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-16 08:41:27 +02:00
Anonymous
af2cf43639 Translated using Weblate (Spanish)
Currently translated at 99.1% (465 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-16 08:41:26 +02:00
Anonymous
c29c92dafb Translated using Weblate (German)
Currently translated at 97.4% (457 of 469 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2023-06-16 08:41:26 +02:00
Hosted Weblate
079f8d13b5 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/
2023-06-16 08:40:19 +02:00
Karl0ss
8df11acbe8 JDownloader Widget - Add Total Queue and Remaining In Queue (#1612)
undefined
2023-06-15 23:40:10 -07:00
Dan
af00e44550 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (468 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-16 06:50:35 +02:00
Ali
19918ef68b Translated using Weblate (Arabic)
Currently translated at 55.9% (262 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ar/
2023-06-16 06:50:34 +02:00
Nonoss117
211c694e4e Translated using Weblate (French)
Currently translated at 100.0% (468 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-16 06:50:34 +02:00
gallegonovato
35499cffbc Translated using Weblate (Spanish)
Currently translated at 100.0% (468 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-16 06:50:33 +02:00
shamoon
9f265c4381 Merge pull request #1603 from denispapec/header-boxed-widget
Added boxed widgets header styling and error component to information widgets
2023-06-13 20:49:19 -07:00
Anonymous
2807575283 Translated using Weblate (Indonesian)
Currently translated at 3.2% (15 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/id/
2023-06-14 05:47:14 +02:00
Anonymous
315bda6ba1 Translated using Weblate (Slovenian)
Currently translated at 95.9% (449 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sl/
2023-06-14 05:47:14 +02:00
Anonymous
c5b044d196 Translated using Weblate (Greek)
Currently translated at 30.5% (143 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/el/
2023-06-14 05:47:13 +02:00
Anonymous
dbfa14cb51 Translated using Weblate (Korean)
Currently translated at 36.9% (173 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ko/
2023-06-14 05:47:13 +02:00
Anonymous
622255d2ec Translated using Weblate (Slovak)
Currently translated at 1.9% (9 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sk/
2023-06-14 05:47:13 +02:00
Anonymous
f00e26f7ab Translated using Weblate (Thai)
Currently translated at 10.0% (47 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/th/
2023-06-14 05:47:12 +02:00
Anonymous
fb7f32589b Translated using Weblate (Latvian)
Currently translated at 25.2% (118 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/lv/
2023-06-14 05:47:12 +02:00
Anonymous
3423bbc0ee Translated using Weblate (Japanese)
Currently translated at 82.0% (384 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-06-14 05:47:11 +02:00
Anonymous
3d1b18f660 Translated using Weblate (Ukrainian)
Currently translated at 99.3% (465 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/uk/
2023-06-14 05:47:11 +02:00
Anonymous
d353166b59 Translated using Weblate (Esperanto)
Currently translated at 30.9% (145 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/eo/
2023-06-14 05:47:11 +02:00
Anonymous
3d19d65cb6 Translated using Weblate (Hindi)
Currently translated at 1.9% (9 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hi/
2023-06-14 05:47:10 +02:00
Anonymous
a8d130d5cb Translated using Weblate (Malay)
Currently translated at 53.6% (251 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ms/
2023-06-14 05:47:10 +02:00
Anonymous
626d636aae Translated using Weblate (Danish)
Currently translated at 42.0% (197 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/da/
2023-06-14 05:47:10 +02:00
Anonymous
0f6a4c624c Translated using Weblate (Czech)
Currently translated at 94.8% (444 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/cs/
2023-06-14 05:47:09 +02:00
Anonymous
543573fbdb Translated using Weblate (Arabic)
Currently translated at 55.5% (260 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ar/
2023-06-14 05:47:09 +02:00
Anonymous
211c723f04 Translated using Weblate (Serbian)
Currently translated at 1.9% (9 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sr/
2023-06-14 05:47:08 +02:00
Anonymous
f3708189ef Translated using Weblate (Turkish)
Currently translated at 83.9% (393 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/tr/
2023-06-14 05:47:08 +02:00
Anonymous
91b828e97c Translated using Weblate (Bulgarian)
Currently translated at 9.8% (46 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/bg/
2023-06-14 05:47:08 +02:00
Anonymous
a6320bc794 Translated using Weblate (Telugu)
Currently translated at 45.9% (215 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/te/
2023-06-14 05:47:07 +02:00
Anonymous
f3a7c1164b Translated using Weblate (Finnish)
Currently translated at 37.8% (177 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fi/
2023-06-14 05:47:07 +02:00
Anonymous
fd93df77aa Translated using Weblate (Yue (Traditional))
Currently translated at 25.0% (117 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/yue_Hant/
2023-06-14 05:47:06 +02:00
Anonymous
452d36c158 Translated using Weblate (Portuguese (Brazil))
Currently translated at 86.3% (404 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt_BR/
2023-06-14 05:47:05 +02:00
Anonymous
26cff34634 Translated using Weblate (Romanian)
Currently translated at 32.0% (150 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ro/
2023-06-14 05:47:05 +02:00
Anonymous
1021e562d8 Translated using Weblate (Hebrew)
Currently translated at 21.3% (100 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/he/
2023-06-14 05:47:04 +02:00
Anonymous
9d2df38391 Translated using Weblate (Hungarian)
Currently translated at 22.8% (107 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hu/
2023-06-14 05:47:04 +02:00
Anonymous
f3176a25ad Translated using Weblate (Croatian)
Currently translated at 97.8% (458 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/hr/
2023-06-14 05:47:04 +02:00
Anonymous
f92c5aa1e3 Translated using Weblate (Swedish)
Currently translated at 27.7% (130 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/sv/
2023-06-14 05:47:03 +02:00
Anonymous
e0a6a88ba1 Translated using Weblate (Polish)
Currently translated at 79.9% (374 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pl/
2023-06-14 05:47:03 +02:00
Anonymous
ae1d7e0866 Translated using Weblate (Catalan)
Currently translated at 55.5% (260 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ca/
2023-06-14 05:47:02 +02:00
Anonymous
8cc044544e Translated using Weblate (Chinese (Traditional))
Currently translated at 98.2% (460 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hant/
2023-06-14 05:47:02 +02:00
Anonymous
368032c931 Translated using Weblate (Dutch)
Currently translated at 51.4% (241 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2023-06-14 05:47:01 +02:00
Anonymous
03ba36c593 Translated using Weblate (Vietnamese)
Currently translated at 9.4% (44 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2023-06-14 05:47:01 +02:00
Anonymous
9a6a580953 Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.8% (79 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2023-06-14 05:47:00 +02:00
Anonymous
1582ba9438 Translated using Weblate (Italian)
Currently translated at 61.1% (286 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2023-06-14 05:47:00 +02:00
Anonymous
a6a9180aa0 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.6% (443 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2023-06-14 05:47:00 +02:00
Anonymous
8d41834c2a Translated using Weblate (Russian)
Currently translated at 88.2% (413 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2023-06-14 05:46:59 +02:00
Anonymous
28335ca3af Translated using Weblate (Portuguese)
Currently translated at 88.6% (415 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2023-06-14 05:46:59 +02:00
Anonymous
1e7a2cbbc4 Translated using Weblate (French)
Currently translated at 99.3% (465 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2023-06-14 05:46:58 +02:00
Anonymous
250b1a3c53 Translated using Weblate (Spanish)
Currently translated at 99.3% (465 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2023-06-14 05:46:58 +02:00
Anonymous
3f25df954f Translated using Weblate (German)
Currently translated at 97.6% (457 of 468 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2023-06-14 05:46:58 +02:00
shamoon
7afe62df2a Merge pull request #1608 from karl0ss/benphelpsJdownloader
Working Jdownloader
2023-06-13 20:45:57 -07:00
shamoon
b437ccde2f Another attempt to catch zlib decompression errors 2023-06-13 19:21:12 -07:00
shamoon
abcea88d0a Try to handle zlib errors 2023-06-13 15:23:19 -07:00
shamoon
3ee6650e6d fix homebridge proxy logging 2023-06-13 15:16:49 -07:00
shamoon
106eec8cdb Merge pull request #1607 from dan5py/fix/1606
Update Traefik API
2023-06-13 14:56:09 -07:00
Dan5py
4a97fce841 Support old traefik CRD group 2023-06-13 22:04:56 +02:00
Karl Hudgell
f1d6a990ac Working Jdownloader 2023-06-13 20:30:09 +01:00
Dan5py
fce680d981 Fixes #1606 2023-06-13 21:02:06 +02:00
Ado Nishimura
dcb3dccdc8 Translated using Weblate (Japanese)
Currently translated at 82.5% (384 of 465 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ja/
2023-06-13 13:52:29 +02:00
Danilo
7c8638467e Translated using Weblate (Portuguese)
Currently translated at 89.2% (415 of 465 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2023-06-13 13:52:28 +02:00
Denis Papec
1622069063 Fixes for existing header styles, fix for glances
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-06-12 01:15:46 +01:00
Denis Papec
6f750dd83c Further improvements to simplify information widgets
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-06-12 01:15:30 +01:00
Denis Papec
cd5162e39c Refactored information widgets, improve widget-boxed style
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-06-12 01:15:19 +01:00
Denis Papec
c5b6dcc1e0 Add optional boxed styling and error component to information widgets
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-06-12 01:15:09 +01:00
104 changed files with 2730 additions and 1237 deletions

1
package-lock.json generated
View File

@@ -21,7 +21,6 @@
"minecraft-ping-js": "^1.0.2",
"next": "^12.3.1",
"next-i18next": "^12.0.1",
"osx-temperature-sensor": "*",
"pretty-bytes": "^6.0.0",
"raw-body": "^2.5.1",
"react": "^18.2.0",

View File

@@ -23,7 +23,7 @@
"free": "متاح",
"used": "مستخدم",
"load": "الضغط",
"mem": "MEM",
"mem": "الذاكرة",
"temp": "TEMP",
"max": "Max",
"uptime": "UP",
@@ -134,14 +134,15 @@
"episodes": "Episodes"
},
"changedetectionio": {
"totalObserved": "Total Observed",
"totalObserved": "مجموع الملاحظات",
"diffsDetected": "Diffs Detected"
},
"tautulli": {
"playing": "يشتغل",
"transcoding": "التحويل",
"bitrate": "معدل البت",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "معدل",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -89,7 +89,8 @@
"playing": "Възпроизвежда",
"transcoding": "Конвертира",
"bitrate": "Честота",
"no_active": "Няма активни потоци"
"no_active": "Няма активни потоци",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rate",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -80,7 +80,8 @@
"playing": "Reproduint",
"transcoding": "Transcodificant",
"bitrate": "Taxa de bits",
"no_active": "Sense transmissions actives"
"no_active": "Sense transmissions actives",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Taxa",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -46,8 +46,8 @@
},
"unifi": {
"users": "Uživatelé",
"uptime": "Doba provozu systému",
"days": "Dnů",
"uptime": "Doba provozu",
"days": "dní",
"wan": "WAN",
"lan": "LAN",
"wlan": "WLAN",
@@ -56,8 +56,8 @@
"wlan_devices": "Zařízení WLAN",
"lan_users": "Uživatelé LAN",
"wlan_users": "Uživatelé WLAN",
"up": "BĚŽÍ",
"down": "NEBĚŽÍ",
"up": "FUNKČNÍ",
"down": "NEFUNKČNÍ",
"wait": "Počkejte prosím",
"empty_data": "Stav podsystému neznámý"
},
@@ -95,7 +95,8 @@
"playing": "Přehrává",
"transcoding": "Překódovávání",
"bitrate": "Přenosová rychlost",
"no_active": "Žádný aktivní stream"
"no_active": "Žádný aktivní stream",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rychlost",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadSpeed": "Download Speed",
"downloadCount": "Queue Count",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -234,7 +234,8 @@
"playing": "Afspiller",
"transcoding": "Transcoder",
"bitrate": "Bitrate",
"no_active": "Ingen Aktive Streams"
"no_active": "Ingen Aktive Streams",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rate",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadSpeed": "Download Speed",
"downloadCount": "Queue Count",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Spielen",
"transcoding": "Transcodierung",
"bitrate": "Bitrate",
"no_active": "Keine aktiven Streams"
"no_active": "Keine aktiven Streams",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "Aktiv",
@@ -644,5 +645,15 @@
"connected": "Verbunden",
"new_devices": "Neue Geräte",
"down_alerts": "Down Alarme"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -162,7 +162,8 @@
"playing": "Αναπαράγει",
"transcoding": "Μετακωδικοποίηση",
"bitrate": "Ρυθμός bit",
"no_active": "Δεν υπάρχουν ενεργές ροές"
"no_active": "Δεν υπάρχουν ενεργές ροές",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Ρυθμός",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

15
public/locales/en/common.json Executable file → Normal file
View File

@@ -92,7 +92,7 @@
"episodes": "Episodes",
"songs": "Songs"
},
"evcc": {
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
@@ -129,7 +129,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "Connected APs",
@@ -653,5 +654,15 @@
"whatsupdocker": {
"monitoring": "Monitoring",
"updates": "Updates"
},
"jdownloader": {
"downloadCount": "Queue",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size",
"downloadSpeed": "Speed"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -87,7 +87,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rate",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Reproduciendo",
"transcoding": "Transcodificando",
"bitrate": "Tasa de bits",
"no_active": "Sin transmisiones activas"
"no_active": "Sin transmisiones activas",
"plex_connection_error": "Comprueba la conexión a Plex"
},
"rutorrent": {
"active": "Activo",
@@ -644,5 +645,15 @@
"connected": "Conectado",
"new_devices": "Nuevos dispositivos",
"down_alerts": "Alertas"
},
"jdownloader": {
"downloadCount": "Cola",
"downloadSpeed": "Velocidad",
"downloadBytesRemaining": "Restante",
"downloadTotalBytes": "Tamaño"
},
"kavita": {
"seriesCount": "Serie",
"totalFiles": "Archivos"
}
}

View File

@@ -0,0 +1,659 @@
{
"wmo": {
"95-night": "Thunderstorm",
"96-day": "Thunderstorm With Hail",
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"0-day": "Sunny",
"0-night": "Clear",
"1-day": "Mainly Sunny",
"1-night": "Mainly Clear",
"2-day": "Partly Cloudy",
"2-night": "Partly Cloudy",
"3-day": "Cloudy",
"3-night": "Cloudy",
"45-day": "Foggy",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"51-day": "Light Drizzle",
"51-night": "Light Drizzle",
"53-day": "Drizzle",
"53-night": "Drizzle",
"55-day": "Heavy Drizzle",
"55-night": "Heavy Drizzle",
"56-day": "Light Freezing Drizzle",
"56-night": "Light Freezing Drizzle",
"57-day": "Freezing Drizzle",
"57-night": "Freezing Drizzle",
"61-day": "Light Rain",
"61-night": "Light Rain",
"63-day": "Rain",
"63-night": "Rain",
"65-day": "Heavy Rain",
"65-night": "Heavy Rain",
"66-day": "Freezing Rain",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"71-day": "Light Snow",
"71-night": "Light Snow",
"73-day": "Snow",
"73-night": "Snow",
"75-day": "Heavy Snow",
"75-night": "Heavy Snow",
"77-day": "Snow Grains",
"77-night": "Snow Grains",
"80-day": "Light Showers",
"80-night": "Light Showers",
"81-day": "Showers",
"81-night": "Showers",
"82-day": "Heavy Showers",
"82-night": "Heavy Showers",
"85-day": "Snow Showers",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"95-day": "Thunderstorm",
"99-night": "Thunderstorm With Hail"
},
"homebridge": {
"updates": "Updates",
"available_update": "System",
"update_available": "Update Available",
"up_to_date": "Up to Date",
"child_bridges": "Child Bridges",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down"
},
"common": {
"bibyterate": "{{value, rate(bits: false; binary: true)}}",
"bibitrate": "{{value, rate(bits: true; binary: true)}}"
},
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
"information": "Informazioa",
"status": "Status",
"url": "URL",
"raw_error": "Raw Error",
"response_data": "Response Data"
},
"weather": {
"current": "Current Location",
"allow": "Click to allow",
"updating": "Eguneratzen",
"wait": "Itxaron mesedez"
},
"search": {
"placeholder": "Bilatu…"
},
"resources": {
"cpu": "CPU",
"mem": "MEM",
"total": "Guztira",
"free": "Free",
"used": "Erabilita",
"load": "Load",
"temp": "TEMP",
"max": "Max",
"uptime": "UP",
"months": "mo",
"days": "d",
"hours": "h",
"minutes": "m"
},
"unifi": {
"users": "Users",
"uptime": "System Uptime",
"days": "Egun",
"wan": "WAN",
"lan": "LAN",
"wlan": "WLAN",
"devices": "Gailuak",
"lan_devices": "LAN Gailuak",
"wlan_devices": "WLAN Gailuak",
"lan_users": "LAN Erabiltzaileak",
"wlan_users": "WLAN Erabiltzaileak",
"up": "UP",
"down": "DOWN",
"wait": "Itxaron mesedez",
"empty_data": "Subsystem status unknown"
},
"docker": {
"rx": "RX",
"tx": "TX",
"mem": "MEM",
"cpu": "CPU",
"running": "Running",
"offline": "Offline",
"error": "Error",
"unknown": "Ezezaguna",
"healthy": "Osasuntsu",
"starting": "Abiarazten",
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial"
},
"ping": {
"error": "Errorea",
"ping": "Ping"
},
"emby": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bit-tasa",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Abestiak"
},
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"home_power": "Consumption",
"charge_power": "Charger",
"watt_hour": "Wh"
},
"flood": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"freshrss": {
"subscriptions": "Subscriptions",
"unread": "Unread"
},
"caddy": {
"upstreams": "Upstreams",
"requests": "Current requests",
"requests_failed": "Failed requests"
},
"changedetectionio": {
"totalObserved": "Total Observed",
"diffsDetected": "Diffs Detected"
},
"channelsdvrserver": {
"shows": "Shows",
"recordings": "Recordings",
"scheduled": "Scheduled",
"passes": "Passes"
},
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "Connected APs",
"activeUser": "Active devices",
"alerts": "Alerts",
"connectedGateway": "Connected gateways",
"connectedSwitches": "Connected switches"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"plex": {
"streams": "Active Streams",
"albums": "Albums",
"movies": "Movies",
"tv": "TV Shows"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"rutorrent": {
"active": "Active",
"upload": "Kargatu",
"download": "Deskargatu"
},
"transmission": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qnap": {
"cpuUsage": "CPU Usage",
"memUsage": "MEM Usage",
"systemTempC": "System Temp",
"poolUsage": "Pool Usage",
"volumeUsage": "Volume Usage",
"invalid": "Invalid"
},
"deluge": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"downloadstation": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series",
"queue": "Queue",
"unknown": "Unknown"
},
"radarr": {
"wanted": "Wanted",
"missing": "Missing",
"queued": "Queued",
"movies": "Movies",
"queue": "Queue",
"unknown": "Unknown"
},
"lidarr": {
"wanted": "Wanted",
"queued": "Queued",
"artists": "Artists"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"bazarr": {
"missingEpisodes": "Missing Episodes",
"missingMovies": "Missing Movies"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"overseerr": {
"pending": "Pending",
"processing": "Processing",
"approved": "Approved",
"available": "Available"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
"blocked_percent": "Blocked %",
"gravity": "Gravity"
},
"adguard": {
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtered",
"latency": "Latency"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Running",
"stopped": "Stopped",
"total": "Total"
},
"tailscale": {
"address": "Address",
"expires": "Expires",
"never": "Never",
"last_seen": "Last Seen",
"now": "Now",
"years": "{{number}}y",
"weeks": "{{number}}w",
"days": "{{number}}d",
"hours": "{{number}}h",
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "{{value}} Ago"
},
"tdarr": {
"queue": "Queue",
"processed": "Processed",
"errored": "Errored",
"saved": "Saved"
},
"traefik": {
"routers": "Routers",
"services": "Services",
"middleware": "Middleware"
},
"navidrome": {
"nothing_streaming": "No Active Streams",
"please_wait": "Please Wait"
},
"npm": {
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track",
"1hour": "1 Hour",
"1day": "1 Day",
"7days": "7 Days",
"30days": "30 Days"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
},
"prowlarr": {
"enableIndexers": "Indexers",
"numberOfGrabs": "Grabs",
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Fail Grabs",
"numberOfFailQueries": "Fail Queries"
},
"jackett": {
"configured": "Configured",
"errored": "Errored"
},
"strelaysrv": {
"numActiveSessions": "Sessions",
"numConnections": "Connections",
"dataRelayed": "Relayed",
"transferRate": "Rate"
},
"mastodon": {
"user_count": "Users",
"status_count": "Posts",
"domain_count": "Domains"
},
"medusa": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
},
"minecraft": {
"players": "Jokalariak",
"version": "Version",
"status": "Status",
"up": "Online",
"down": "Offline"
},
"miniflux": {
"read": "Read",
"unread": "Unread"
},
"authentik": {
"users": "Users",
"loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)"
},
"proxmox": {
"mem": "MEM",
"cpu": "CPU",
"lxc": "LXC",
"vms": "VMs"
},
"glances": {
"cpu": "CPU",
"load": "Load",
"wait": "Please wait",
"temp": "TEMP",
"warn": "Warn",
"uptime": "UP",
"total": "Total",
"free": "Free",
"used": "Used",
"days": "d",
"hours": "h"
},
"quicklaunch": {
"bookmark": "Bookmark",
"service": "Service",
"search": "Search",
"custom": "Custom",
"visit": "Visit",
"url": "URL"
},
"healthchecks": {
"new": "New",
"up": "Online",
"grace": "In Grace Period",
"down": "Offline",
"paused": "Paused",
"status": "Status",
"last_ping": "Last Ping",
"never": "No pings yet"
},
"watchtower": {
"containers_scanned": "Scanned",
"containers_updated": "Updated",
"containers_failed": "Failed"
},
"autobrr": {
"approvedPushes": "Approved",
"rejectedPushes": "Rejected",
"filters": "Filters",
"indexers": "Indexers"
},
"tubearchivist": {
"downloads": "Queue",
"videos": "Videos",
"channels": "Channels",
"playlists": "Playlists"
},
"truenas": {
"load": "System Load",
"uptime": "Uptime",
"alerts": "Alerts",
"time": "{{value, number(style: unit; unitDisplay: long;)}}"
},
"pyload": {
"speed": "Speed",
"active": "Active",
"queue": "Queue",
"total": "Total"
},
"gluetun": {
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
},
"hdhomerun": {
"channels": "Channels",
"hd": "HD"
},
"scrutiny": {
"passed": "Passed",
"failed": "Failed",
"unknown": "Unknown"
},
"paperlessngx": {
"inbox": "Inbox",
"total": "Total"
},
"nextdns": {
"wait": "Please Wait",
"no_devices": "No Device Data Received"
},
"mikrotik": {
"cpuLoad": "CPU Load",
"memoryUsed": "Memory Used",
"uptime": "Uptime",
"numberOfLeases": "Leases"
},
"xteve": {
"streams_all": "All Streams",
"streams_active": "Active Streams",
"streams_xepg": "XEPG Channels"
},
"opnsense": {
"cpu": "CPU Load",
"memory": "Active Memory",
"wanUpload": "WAN Upload",
"wanDownload": "WAN Download"
},
"moonraker": {
"printer_state": "Printer State",
"print_status": "Print Status",
"print_progress": "Progress",
"layers": "Layers"
},
"octoprint": {
"printer_state": "Status",
"temp_tool": "Tool temp",
"temp_bed": "Bed temp",
"job_completion": "Completion"
},
"cloudflared": {
"origin_ip": "Origin IP",
"status": "Status"
},
"pfsense": {
"load": "Load Avg",
"memory": "Mem Usage",
"wanStatus": "WAN Status",
"up": "Up",
"down": "Down",
"temp": "Temp",
"disk": "Disk Usage",
"wanIP": "WAN IP"
},
"proxmoxbackupserver": {
"datastore_usage": "Datastore",
"failed_tasks_24h": "Failed Tasks 24h",
"cpu_usage": "CPU",
"memory_usage": "Memory"
},
"immich": {
"users": "Users",
"photos": "Photos",
"videos": "Videos",
"storage": "Storage"
},
"uptimekuma": {
"up": "Sites Up",
"down": "Sites Down",
"uptime": "Uptime",
"incident": "Incident",
"m": "m"
},
"komga": {
"libraries": "Libraries",
"series": "Series",
"books": "Books"
},
"diskstation": {
"days": "Days",
"uptime": "Uptime",
"volumeAvailable": "Available"
},
"mylar": {
"series": "Series",
"issues": "Issues",
"wanted": "Wanted"
},
"photoprism": {
"albums": "Albums",
"photos": "Photos",
"videos": "Videos",
"people": "People"
},
"fileflows": {
"queue": "Queue",
"processing": "Processing",
"processed": "Processed",
"time": "Time"
},
"grafana": {
"dashboards": "Dashboards",
"datasources": "Data Sources",
"totalalerts": "Total Alerts",
"alertstriggered": "Alerts Triggered"
},
"nextcloud": {
"cpuload": "Cpu Load",
"memoryusage": "Memory Usage",
"freespace": "Free Space",
"activeusers": "Active Users",
"numfiles": "Files",
"numshares": "Shared Items"
},
"kopia": {
"status": "Status",
"size": "Size",
"lastrun": "Last Run",
"nextrun": "Next Run",
"failed": "Failed"
},
"unmanic": {
"active_workers": "Active Workers",
"total_workers": "Total Workers",
"records_total": "Queue Length"
},
"pterodactyl": {
"servers": "Servers",
"nodes": "Nodes"
},
"prometheus": {
"targets_up": "Targets Up",
"targets_down": "Targets Down",
"targets_total": "Total Targets"
},
"ghostfolio": {
"gross_percent_today": "Today",
"gross_percent_1y": "One year",
"gross_percent_max": "All time"
},
"audiobookshelf": {
"podcasts": "Podcasts",
"books": "Books",
"podcastsDuration": "Duration",
"booksDuration": "Duration"
},
"homeassistant": {
"people_home": "People Home",
"lights_on": "Lights On",
"switches_on": "Switches On"
},
"whatsupdocker": {
"monitoring": "Monitoring",
"updates": "Updates"
},
"jdownloader": {
"downloadCount": "Queue",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size",
"downloadSpeed": "Speed"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -62,7 +62,8 @@
"playing": "Toistaa",
"transcoding": "Transkoodaa",
"bitrate": "Bittinopeus",
"no_active": "Ei aktiivisia striimejä"
"no_active": "Ei aktiivisia striimejä",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Nopeus",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -5,8 +5,8 @@
"status": "Statut",
"information": "Information",
"url": "URL",
"raw_error": "Raw Error",
"response_data": "Response Data"
"raw_error": "Erreur brute",
"response_data": "Données de réponse"
},
"search": {
"placeholder": "Recherche…"
@@ -56,7 +56,8 @@
"playing": "En lecture",
"transcoding": "Transcodage",
"bitrate": "Débit",
"no_active": "Aucun flux actif"
"no_active": "Aucun flux actif",
"plex_connection_error": "Vérifier la connexion à Plex"
},
"rutorrent": {
"active": "Actif",
@@ -578,7 +579,7 @@
"homeassistant": {
"people_home": "People Home",
"lights_on": "Lumières allumées",
"switches_on": "Switches On"
"switches_on": "Commutateur On"
},
"freshrss": {
"unread": "Non lu",
@@ -644,5 +645,15 @@
"connected": "Connecté",
"new_devices": "Nouvel Appareil",
"down_alerts": "Alertes"
},
"jdownloader": {
"downloadCount": "Total en attente",
"downloadSpeed": "Vitesse de téléchargement",
"downloadBytesRemaining": "Restant",
"downloadTotalBytes": "Taille"
},
"kavita": {
"seriesCount": "Séries",
"totalFiles": "Fichiers"
}
}

View File

@@ -62,7 +62,8 @@
"playing": "מנגן",
"transcoding": "מקודד",
"bitrate": "סיביות",
"no_active": "אין הזרמות פעילות"
"no_active": "אין הזרמות פעילות",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "יחס",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -117,7 +117,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rate",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -104,7 +104,8 @@
"playing": "Reprodukcija",
"transcoding": "Prekodiranje",
"bitrate": "Stopa bitova",
"no_active": "Nema aktivnih prijenosa"
"no_active": "Nema aktivnih prijenosa",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Stopa",
@@ -126,21 +127,21 @@
"wanted": "Zatraženo",
"queued": "U redu čekanja",
"series": "Serije",
"unknown": "Unknown",
"queue": "Queue"
"unknown": "Nepoznato",
"queue": "Red čekanja"
},
"radarr": {
"wanted": "Zatraženo",
"queued": "U redu čekanja",
"movies": "Filmovi",
"missing": "Nedostaje",
"queue": "Queue",
"unknown": "Unknown"
"queue": "Red čekanja",
"unknown": "Nepoznato"
},
"lidarr": {
"wanted": "Zatraženo",
"queued": "U redu čekanja",
"artists": "Artists"
"artists": "Umjetnici"
},
"readarr": {
"wanted": "Zatraženo",
@@ -644,5 +645,15 @@
"connected": "Povezano",
"new_devices": "Novi uređaji",
"down_alerts": "Obavijest o rušenju"
},
"jdownloader": {
"downloadCount": "Red čekanja",
"downloadSpeed": "Brzina",
"downloadBytesRemaining": "Preostalo",
"downloadTotalBytes": "Veličina"
},
"kavita": {
"seriesCount": "Serije",
"totalFiles": "Datoteke"
}
}

View File

@@ -67,16 +67,17 @@
"transcoding": "Átkódolás",
"bitrate": "Bitráta",
"no_active": "Nincs aktív lejátszás",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
"movies": "Film",
"series": "Sorozat",
"episodes": "Epizód",
"songs": "Zeneszám"
},
"tautulli": {
"playing": "Lejátszás folyamatban",
"transcoding": "Átkódolás",
"bitrate": "Bitráta",
"no_active": "Nincs aktív lejátszás"
"no_active": "Nincs aktív lejátszás",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Ráta",
@@ -254,16 +255,16 @@
"diffsDetected": "Diffs Detected"
},
"wmo": {
"0-day": "Sunny",
"0-night": "Clear",
"0-day": "Napos",
"0-night": "Derült",
"3-day": "Cloudy",
"3-night": "Cloudy",
"45-day": "Foggy",
"53-day": "Drizzle",
"56-night": "Light Freezing Drizzle",
"57-day": "Freezing Drizzle",
"1-day": "Mainly Sunny",
"1-night": "Mainly Clear",
"1-day": "Többnyire napos",
"1-night": "Többnyire derült",
"2-day": "Partly Cloudy",
"2-night": "Partly Cloudy",
"45-night": "Foggy",
@@ -373,7 +374,7 @@
"hd": "HD"
},
"ping": {
"error": "Error",
"error": "Hiba",
"ping": "Ping"
},
"scrutiny": {
@@ -570,10 +571,10 @@
"gross_percent_max": "All time"
},
"audiobookshelf": {
"podcasts": "Podcasts",
"books": "Books",
"podcastsDuration": "Duration",
"booksDuration": "Duration"
"podcasts": "Podcast",
"books": "Könyv",
"podcastsDuration": "Időtartam",
"booksDuration": "Időtartam"
},
"homeassistant": {
"people_home": "People Home",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadSpeed": "Download Speed",
"downloadCount": "Queue Count",
"downloadTotalBytes": "Size",
"downloadBytesRemaining": "Remaining"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -643,6 +643,17 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -7,12 +7,12 @@
"rx": "RX",
"error": "Errore",
"unknown": "Sconosciuto",
"running": "Running",
"starting": "Starting",
"running": "In esecuzione",
"starting": "In avvio",
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"not_found": "Non trovato",
"exited": "Uscito",
"partial": "Parziale",
"healthy": "Healthy"
},
"emby": {
@@ -20,16 +20,17 @@
"transcoding": "Transcodifica",
"bitrate": "Bitrate",
"no_active": "Nessuno Stream Attivo",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
"movies": "Film",
"series": "Serie",
"episodes": "Episodi",
"songs": "Canzoni"
},
"tautulli": {
"playing": "In riproduzione",
"transcoding": "Transcodifica",
"bitrate": "Bitrate",
"no_active": "Nessuno Stream Attivo"
"no_active": "Nessuno Stream Attivo",
"plex_connection_error": "Check Plex Connection"
},
"speedtest": {
"upload": "Upload",
@@ -82,16 +83,16 @@
"series": "Serie",
"wanted": "Richiesti",
"queued": "In coda",
"queue": "Queue",
"unknown": "Unknown"
"queue": "Coda",
"unknown": "Sconosciuto"
},
"radarr": {
"wanted": "Richiesti",
"queued": "In coda",
"movies": "Film",
"missing": "Mancanti",
"queue": "Queue",
"unknown": "Unknown"
"queue": "Coda",
"unknown": "Sconosciuto"
},
"readarr": {
"wanted": "Richiesti",
@@ -112,7 +113,7 @@
"queries": "Richieste",
"blocked": "Bloccati",
"gravity": "Severità",
"blocked_percent": "Blocked %"
"blocked_percent": "Bloccato %"
},
"npm": {
"enabled": "Attivi",
@@ -175,9 +176,9 @@
"missingMovies": "Film Mancanti"
},
"lidarr": {
"wanted": "Mancanti",
"wanted": "Richiesto",
"queued": "In coda",
"artists": "Artists"
"artists": "Artisti"
},
"adguard": {
"queries": "Interrogazioni",
@@ -228,13 +229,13 @@
"devices": "Dispositivi",
"lan_devices": "Dispositivi LAN",
"wlan_devices": "Dispositivi WLAN",
"empty_data": "Subsystem status unknown"
"empty_data": "Stato del sottosistema sconosciuto"
},
"plex": {
"streams": "Trasmissioni attive",
"movies": "Film",
"tv": "Programma televisivo",
"albums": "Albums"
"albums": "Album"
},
"glances": {
"cpu": "CPU",
@@ -243,11 +244,11 @@
"uptime": "UP",
"days": "d",
"hours": "h",
"load": "Load",
"warn": "Warn",
"total": "Total",
"free": "Free",
"used": "Used"
"load": "Carico",
"warn": "Avviso",
"total": "Totale",
"free": "Libero",
"used": "Usato"
},
"changedetectionio": {
"totalObserved": "Totale Osservato",
@@ -314,9 +315,9 @@
"quicklaunch": {
"bookmark": "Segnalibro",
"service": "Servizio",
"search": "Search",
"custom": "Custom",
"visit": "Visit",
"search": "Cerca",
"custom": "Personalizzato",
"visit": "Visita",
"url": "URL"
},
"homebridge": {
@@ -327,7 +328,7 @@
"child_bridges": "Child Bridges",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"pending": "In attesa",
"down": "Down"
},
"autobrr": {
@@ -432,7 +433,7 @@
"cpuLoad": "Carico della CPU",
"memoryUsed": "Memoria Utilizzata",
"uptime": "Tempo di attività",
"numberOfLeases": "Lease"
"numberOfLeases": "Rilasci"
},
"xteve": {
"streams_all": "Tutti gli stream",
@@ -440,209 +441,219 @@
"streams_xepg": "Canali XEPG"
},
"opnsense": {
"cpu": "Carico CPU",
"cpu": "Carico della CPU",
"memory": "Memoria in uso",
"wanUpload": "WAN Upload",
"wanDownload": "WAN Download"
},
"moonraker": {
"printer_state": "Printer State",
"print_status": "Print Status",
"print_progress": "Progress",
"layers": "Layers"
"printer_state": "Stato stampante",
"print_status": "Stato Stampante",
"print_progress": "Avanzamento",
"layers": "Livelli"
},
"medusa": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
"wanted": "Richiesto",
"queued": "In coda",
"series": "Serie"
},
"octoprint": {
"printer_state": "Status",
"printer_state": "Stato",
"temp_tool": "Tool temp",
"temp_bed": "Bed temp",
"job_completion": "Completion"
"job_completion": "Completamento"
},
"cloudflared": {
"origin_ip": "Origin IP",
"status": "Status"
"origin_ip": "IP sorgente",
"status": "Stato"
},
"proxmoxbackupserver": {
"datastore_usage": "Datastore",
"failed_tasks_24h": "Failed Tasks 24h",
"failed_tasks_24h": "Attività Non Riuscite 24h",
"cpu_usage": "CPU",
"memory_usage": "Memory"
"memory_usage": "Memoria"
},
"immich": {
"users": "Users",
"photos": "Photos",
"videos": "Videos",
"storage": "Storage"
"users": "Utenti",
"photos": "Foto",
"videos": "Video",
"storage": "Memoria"
},
"uptimekuma": {
"up": "Sites Up",
"down": "Sites Down",
"up": "Siti On",
"down": "Siti Down",
"uptime": "Uptime",
"incident": "Incident",
"incident": "Incidente",
"m": "m"
},
"komga": {
"libraries": "Libraries",
"series": "Series",
"books": "Books"
"libraries": "Librerie",
"series": "Serie",
"books": "Libri"
},
"mylar": {
"series": "Series",
"issues": "Issues",
"wanted": "Wanted"
"series": "Serie",
"issues": "Problemi",
"wanted": "Richiesto"
},
"photoprism": {
"albums": "Albums",
"photos": "Photos",
"videos": "Videos",
"people": "People"
"albums": "Album",
"photos": "Foto",
"videos": "Video",
"people": "Persone"
},
"diskstation": {
"days": "Days",
"days": "Giorni",
"uptime": "Uptime",
"volumeAvailable": "Available"
"volumeAvailable": "Disponibile"
},
"fileflows": {
"queue": "Queue",
"processing": "Processing",
"processed": "Processed",
"time": "Time"
"queue": "Coda",
"processing": "In Lavorazione",
"processed": "Elaborato",
"time": "Tempo"
},
"grafana": {
"dashboards": "Dashboards",
"datasources": "Data Sources",
"totalalerts": "Total Alerts",
"alertstriggered": "Alerts Triggered"
"datasources": "Origine dei Dati",
"totalalerts": "Avvisi Totali",
"alertstriggered": "Avvisi Attivati"
},
"nextcloud": {
"memoryusage": "Memory Usage",
"cpuload": "Cpu Load",
"freespace": "Free Space",
"activeusers": "Active Users",
"numfiles": "Files",
"numshares": "Shared Items"
"memoryusage": "Uso della Memoria",
"cpuload": "Carico della CPU",
"freespace": "Spazio Libero",
"activeusers": "Utenti Attivi",
"numfiles": "File",
"numshares": "Oggetti Condivisi"
},
"kopia": {
"status": "Status",
"size": "Size",
"lastrun": "Last Run",
"nextrun": "Next Run",
"failed": "Failed"
"status": "Stato",
"size": "Dimensione",
"lastrun": "Ultima esecuzione",
"nextrun": "Prossima esecuzione",
"failed": "Fallito"
},
"unmanic": {
"active_workers": "Active Workers",
"total_workers": "Total Workers",
"records_total": "Queue Length"
"active_workers": "Lavoratori Attivi",
"total_workers": "Lavoratori Totali",
"records_total": "Lunghezza della Coda"
},
"healthchecks": {
"new": "New",
"new": "Nuovo",
"up": "Online",
"grace": "In Grace Period",
"grace": "Periodo di Tolleranza",
"down": "Offline",
"paused": "Paused",
"status": "Status",
"last_ping": "Last Ping",
"never": "No pings yet"
"paused": "In Pausa",
"status": "Stato",
"last_ping": "Ultimo Ping",
"never": "Ancora nessun ping"
},
"pterodactyl": {
"servers": "Servers",
"nodes": "Nodes"
"servers": "Server",
"nodes": "Nodi"
},
"prometheus": {
"targets_up": "Targets Up",
"targets_down": "Targets Down",
"targets_total": "Total Targets"
"targets_total": "Targets Totali"
},
"minecraft": {
"players": "Players",
"version": "Version",
"status": "Status",
"players": "Giocatori",
"version": "Versione",
"status": "Stato",
"up": "Online",
"down": "Offline"
},
"ghostfolio": {
"gross_percent_today": "Today",
"gross_percent_1y": "One year",
"gross_percent_max": "All time"
"gross_percent_today": "Oggi",
"gross_percent_1y": "Un anno",
"gross_percent_max": "Sempre"
},
"audiobookshelf": {
"podcasts": "Podcasts",
"books": "Books",
"podcastsDuration": "Duration",
"booksDuration": "Duration"
"podcasts": "Podcast",
"books": "Libri",
"podcastsDuration": "Durata",
"booksDuration": "Durata"
},
"homeassistant": {
"people_home": "People Home",
"lights_on": "Lights On",
"switches_on": "Switches On"
"people_home": "Persone a Casa",
"lights_on": "Luci Accese",
"switches_on": "Switch Accesi"
},
"freshrss": {
"subscriptions": "Subscriptions",
"unread": "Unread"
"subscriptions": "Iscrizioni",
"unread": "Non letto"
},
"channelsdvrserver": {
"shows": "Shows",
"recordings": "Recordings",
"scheduled": "Scheduled",
"passes": "Passes"
"shows": "Spettacoli",
"recordings": "Registrazioni",
"scheduled": "Programmati",
"passes": "Tessere"
},
"whatsupdocker": {
"monitoring": "Monitoring",
"updates": "Updates"
"monitoring": "Monitoraggio",
"updates": "Aggiornamenti"
},
"tailscale": {
"never": "Never",
"address": "Address",
"expires": "Expires",
"last_seen": "Last Seen",
"now": "Now",
"never": "Mai",
"address": "Indirizzo",
"expires": "Scade",
"last_seen": "Ultima visualizzazione",
"now": "Adesso",
"years": "{{number}}y",
"weeks": "{{number}}w",
"hours": "{{number}}h",
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "{{value}} Ago",
"ago": "{{value}} Fa",
"days": "{{number}}d"
},
"qnap": {
"cpuUsage": "CPU Usage",
"memUsage": "MEM Usage",
"systemTempC": "System Temp",
"poolUsage": "Pool Usage",
"volumeUsage": "Volume Usage",
"invalid": "Invalid"
"cpuUsage": "Utilizzo CPU",
"memUsage": "Utilizzo MEM",
"systemTempC": "Temp sistema",
"poolUsage": "Utilizzo Pool",
"volumeUsage": "Utilizzo Volume",
"invalid": "Invalido"
},
"pfsense": {
"load": "Load Avg",
"memory": "Mem Usage",
"wanStatus": "WAN Status",
"load": "Carico Medio",
"memory": "Uso Memoria",
"wanStatus": "Stato WAN",
"up": "Up",
"down": "Down",
"temp": "Temp",
"disk": "Disk Usage",
"wanIP": "WAN IP"
"temp": "Temperatura",
"disk": "Uso Disco",
"wanIP": "IP WAN"
},
"caddy": {
"upstreams": "Upstreams",
"requests": "Current requests",
"requests_failed": "Failed requests"
"upstreams": "Upstream",
"requests": "Richieste correnti",
"requests_failed": "Richieste fallite"
},
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"home_power": "Consumption",
"charge_power": "Charger",
"pv_power": "Produzione",
"battery_soc": "Batteria",
"grid_power": "Griglia",
"home_power": "Consumo",
"charge_power": "Caricatore",
"watt_hour": "Wh"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
"total": "Totali",
"connected": "Connesso",
"new_devices": "Nuovi Dispositivi",
"down_alerts": "Avvisi di Disservizio"
},
"jdownloader": {
"downloadCount": "Coda",
"downloadSpeed": "Velocità Download",
"downloadBytesRemaining": "Residuo",
"downloadTotalBytes": "Dimensione"
},
"kavita": {
"seriesCount": "Serie",
"totalFiles": "File"
}
}

View File

@@ -63,7 +63,7 @@
"resources": {
"cpu": "CPU",
"total": "合計",
"free": "フリー",
"free": "Free",
"used": "使用",
"load": "ロード",
"mem": "MEM",
@@ -136,7 +136,8 @@
"playing": "再生中",
"transcoding": "変換中",
"bitrate": "ビットレート",
"no_active": "アクティブストリームなし"
"no_active": "アクティブストリームなし",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "接続されたAP",
@@ -239,7 +240,7 @@
"queries": "クエリ",
"blocked": "ブロック中",
"gravity": "グラビティ",
"blocked_percent": "Blocked %"
"blocked_percent": "ブロック %"
},
"adguard": {
"queries": "クエリ",
@@ -609,11 +610,11 @@
"ago": "{{value}} 前"
},
"qnap": {
"cpuUsage": "CPU Usage",
"memUsage": "MEM Usage",
"systemTempC": "System Temp",
"poolUsage": "Pool Usage",
"volumeUsage": "Volume Usage",
"cpuUsage": "CPU使用量",
"memUsage": "MEM使用量",
"systemTempC": "システム温度",
"poolUsage": "プール使用量",
"volumeUsage": "ボリューム使用量",
"invalid": "Invalid"
},
"pfsense": {
@@ -633,16 +634,26 @@
},
"evcc": {
"watt_hour": "Wh",
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"home_power": "Consumption",
"charge_power": "Charger"
"pv_power": "発電量",
"battery_soc": "バッテリー",
"grid_power": "グリッド",
"home_power": "消費",
"charge_power": "チャージャー"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -111,7 +111,8 @@
"playing": "재생 중",
"transcoding": "트랜스코딩",
"bitrate": "비트레이트",
"no_active": "활성 스트림 없음"
"no_active": "활성 스트림 없음",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "연결된 AP",
@@ -644,5 +645,15 @@
"connected": "Connected",
"down_alerts": "Down Alerts",
"new_devices": "New Devices"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -97,7 +97,8 @@
"playing": "Atskaņo",
"transcoding": "Pārkodē",
"bitrate": "Bitrate",
"no_active": "Nav aktīvu straumju"
"no_active": "Nav aktīvu straumju",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "Savienotie piekļuves punkti",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -200,7 +200,8 @@
"playing": "Sedang Dimainkan",
"transcoding": "Transkoding",
"bitrate": "Kadar bit",
"no_active": "Tiada Strim Aktif"
"no_active": "Tiada Strim Aktif",
"plex_connection_error": "Check Plex Connection"
},
"plex": {
"streams": "Strim Aktif",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Spiller",
"transcoding": "Transkoding",
"bitrate": "Bitrate",
"no_active": "Ingen aktive strømmer"
"no_active": "Ingen aktive strømmer",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "Aktiv",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Filer"
}
}

View File

@@ -72,7 +72,8 @@
"playing": "Afspelen",
"transcoding": "Transcodering",
"bitrate": "Bitsnelheid",
"no_active": "Geen Actieve Streams"
"no_active": "Geen Actieve Streams",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "Actief",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -37,7 +37,8 @@
"playing": "Odtwarzanie",
"transcoding": "Transkodowanie",
"bitrate": "Bitrate",
"no_active": "Brak aktywnych strumieni"
"no_active": "Brak aktywnych strumieni",
"plex_connection_error": "Check Plex Connection"
},
"speedtest": {
"download": "Pobieranie",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -90,7 +90,8 @@
"playing": "Reproduzindo",
"transcoding": "Transcodificando",
"bitrate": "Taxa de bits",
"no_active": "Sem transmissões ativas"
"no_active": "Sem transmissões ativas",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Taxa",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Reproduzindo",
"transcoding": "Transcodificação",
"bitrate": "Taxa de bits",
"no_active": "Sem streams ativas"
"no_active": "Sem streams ativas",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "Ativo",
@@ -590,12 +591,12 @@
"switches_on": "Interruptores Ligados"
},
"freshrss": {
"subscriptions": "Subscriptions",
"unread": "Unread"
"subscriptions": "Assinaturas",
"unread": "Não lida"
},
"channelsdvrserver": {
"shows": "Shows",
"recordings": "Recordings",
"recordings": "Gravações",
"scheduled": "Scheduled",
"passes": "Passes"
},
@@ -637,21 +638,31 @@
},
"caddy": {
"upstreams": "Upstreams",
"requests": "Current requests",
"requests_failed": "Failed requests"
"requests": "Solicitações atuais",
"requests_failed": "Solicitações com falha"
},
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"home_power": "Consumption",
"charge_power": "Charger",
"watt_hour": "Wh"
"pv_power": "Produção",
"battery_soc": "Bateria",
"grid_power": "Grade",
"home_power": "Consumo",
"charge_power": "Carregador",
"watt_hour": "Kw"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -92,7 +92,8 @@
"no_active": "Niciun stream activ",
"playing": "Activ",
"transcoding": "Transcodare",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rată",
@@ -644,5 +645,15 @@
"down_alerts": "Down Alerts",
"total": "Total",
"connected": "Connected"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Воспроизведение",
"transcoding": "Транскодирование",
"bitrate": "Битрейт",
"no_active": "Нет активных трансляций"
"no_active": "Нет активных трансляций",
"plex_connection_error": "Проверьте соединение с Plex"
},
"rutorrent": {
"active": "Активный",
@@ -64,22 +65,22 @@
"download": "Загрузка"
},
"sonarr": {
"wanted": "Хотел",
"wanted": "Желаемое",
"queued": "В очереди",
"series": "Серии",
"queue": "Queue",
"queue": "Очередь",
"unknown": "Unknown"
},
"radarr": {
"wanted": "Хотел",
"wanted": "Желаемое",
"queued": "В очереди",
"movies": "Фильмы",
"missing": "Пропущено",
"queue": "Queue",
"queue": "Очередь",
"unknown": "Unknown"
},
"readarr": {
"wanted": "Хотел",
"wanted": "Желаемое",
"queued": "В очереди",
"books": "Книги"
},
@@ -175,9 +176,9 @@
"missingMovies": "Отсутствующие фильмы"
},
"lidarr": {
"wanted": "Хотел",
"wanted": "Желаемое",
"queued": "В очереди",
"artists": "Artists"
"artists": "Артисты"
},
"adguard": {
"queries": "Запросы",
@@ -612,8 +613,8 @@
"cpuUsage": "CPU Usage",
"memUsage": "MEM Usage",
"systemTempC": "System Temp",
"poolUsage": "Pool Usage",
"volumeUsage": "Volume Usage",
"poolUsage": "Использование пула",
"volumeUsage": "Использование тома",
"invalid": "Invalid"
},
"pfsense": {
@@ -627,22 +628,32 @@
"temp": "Temp"
},
"caddy": {
"upstreams": "Upstreams",
"requests": "Current requests",
"requests_failed": "Failed requests"
"upstreams": "Апстримы",
"requests": "Текущие запросы",
"requests_failed": "Неудачные запросы"
},
"evcc": {
"home_power": "Consumption",
"home_power": "Потребление",
"pv_power": "Production",
"battery_soc": "Battery",
"grid_power": "Grid",
"charge_power": "Charger",
"charge_power": "Зарядка",
"watt_hour": "Wh"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
"new_devices": "Новые устройства",
"down_alerts": "Оповещения о сбоях"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -221,7 +221,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "Connected APs",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -168,7 +168,8 @@
"transcoding": "Transkodira",
"bitrate": "Pasovna širina",
"playing": "Predvaja",
"no_active": "Ni aktivne vsebine"
"no_active": "Ni aktivne vsebine",
"plex_connection_error": "Check Plex Connection"
},
"flood": {
"download": "Prenos",
@@ -236,21 +237,21 @@
"wanted": "Iskano",
"queued": "V vrsti",
"series": "Serije",
"queue": "Queue",
"unknown": "Unknown"
"queue": "Vrsta",
"unknown": "Neznano"
},
"radarr": {
"wanted": "Iskano",
"missing": "Manjka",
"queued": "V vrsti",
"movies": "Filmi",
"queue": "Queue",
"unknown": "Unknown"
"queue": "Vrsta",
"unknown": "Neznano"
},
"lidarr": {
"wanted": "Iskano",
"queued": "V vrsti",
"artists": "Artists"
"artists": "Avtorji"
},
"readarr": {
"wanted": "Iskano",
@@ -359,11 +360,11 @@
"uptime": "UP",
"days": "d",
"hours": "u",
"free": "Free",
"load": "Load",
"warn": "Warn",
"total": "Total",
"used": "Used"
"free": "Prosto",
"load": "Obremenitev",
"warn": "Opoz.",
"total": "Skupaj",
"used": "V uporabi"
},
"authentik": {
"users": "Uporabniki",
@@ -613,8 +614,8 @@
"memUsage": "MEM",
"systemTempC": "Temperatura",
"poolUsage": "Prostor",
"volumeUsage": "Volume Usage",
"invalid": "Invalid"
"volumeUsage": "Prostora",
"invalid": "Neveljavno"
},
"pfsense": {
"load": "Povp. obremenitev",
@@ -640,9 +641,19 @@
"watt_hour": "Wh"
},
"pialert": {
"total": "Total",
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
"total": "Skupaj",
"connected": "Povezanih",
"new_devices": "Nove naprave",
"down_alerts": "Izključeno"
},
"jdownloader": {
"downloadCount": "Vrsta",
"downloadSpeed": "Hitrost prenosa",
"downloadBytesRemaining": "Še ostane",
"downloadTotalBytes": "Velikost"
},
"kavita": {
"seriesCount": "Serije",
"totalFiles": "Datoteke"
}
}

View File

@@ -79,7 +79,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Rate",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -62,7 +62,8 @@
"playing": "Spelar",
"transcoding": "Omkodning",
"bitrate": "Bitrate",
"no_active": "Inga aktiva strömmar"
"no_active": "Inga aktiva strömmar",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Hastighet",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"totalFiles": "Files",
"seriesCount": "Series"
}
}

View File

@@ -79,7 +79,8 @@
"playing": "ఆడుతున్నారు",
"transcoding": "ట్రాన్స్‌కోడింగ్",
"bitrate": "బిట్రేట్",
"no_active": "యాక్టివ్ స్ట్రీమ్‌లు లేవు"
"no_active": "యాక్టివ్ స్ట్రీమ్‌లు లేవు",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "రేట్",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"totalFiles": "Files",
"seriesCount": "Series"
}
}

View File

@@ -139,7 +139,8 @@
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"omada": {
"connectedAp": "Connected APs",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -79,7 +79,8 @@
"playing": "Oynatılıyor",
"transcoding": "Dönüştürülüyor",
"bitrate": "Bit Oranı",
"no_active": "Aktif akış yok"
"no_active": "Aktif akış yok",
"plex_connection_error": "Check Plex Connection"
},
"nzbget": {
"rate": "Oran",
@@ -644,5 +645,15 @@
"connected": "Bağlandı",
"new_devices": "Yeni Cihazlar",
"down_alerts": "Düşme Uyarıları"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -198,7 +198,8 @@
"playing": "Відтворення",
"transcoding": "Перекодування",
"bitrate": "Бітрейт",
"no_active": "Немає активних потоків"
"no_active": "Немає активних потоків",
"plex_connection_error": "Перевірте з'єднання Plex"
},
"nzbget": {
"rate": "Швидкість",
@@ -644,5 +645,15 @@
"connected": "Підключено",
"new_devices": "Нові пристрої",
"down_alerts": "Сповіщення про збій"
},
"jdownloader": {
"downloadCount": "Черга",
"downloadSpeed": "Швидкість",
"downloadBytesRemaining": "Залишилося",
"downloadTotalBytes": "Розмір"
},
"kavita": {
"seriesCount": "Серій",
"totalFiles": "Файлів"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate",
"no_active": "No Active Streams"
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "Hoạt động",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -13,7 +13,8 @@
"playing": "播放緊",
"no_active": "無任何活動",
"transcoding": "轉碼緊",
"bitrate": "比特率"
"bitrate": "比特率",
"plex_connection_error": "Check Plex Connection"
},
"transmission": {
"download": "下載速度",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -56,7 +56,8 @@
"playing": "播放中",
"transcoding": "转码",
"bitrate": "比特率",
"no_active": "暂无播放"
"no_active": "暂无播放",
"plex_connection_error": "Check Plex Connection"
},
"rutorrent": {
"active": "活动中",
@@ -632,7 +633,7 @@
"requests_failed": "失败请求"
},
"evcc": {
"pv_power": "Production",
"pv_power": "正式环境",
"battery_soc": "Battery",
"grid_power": "Grid",
"home_power": "Consumption",
@@ -644,5 +645,15 @@
"connected": "Connected",
"new_devices": "New Devices",
"down_alerts": "Down Alerts"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -44,7 +44,8 @@
"playing": "正在播放",
"transcoding": "轉碼",
"bitrate": "位元率",
"no_active": "無播放活動"
"no_active": "無播放活動",
"plex_connection_error": "Check Plex Connection"
},
"jellyseerr": {
"pending": "待下載",
@@ -644,5 +645,15 @@
"connected": "已連線",
"new_devices": "新裝置",
"down_alerts": "離線警告"
},
"jdownloader": {
"downloadCount": "Queue Count",
"downloadSpeed": "Download Speed",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size"
},
"kavita": {
"seriesCount": "Series",
"totalFiles": "Files"
}
}

View File

@@ -1,13 +1,41 @@
import classNames from "classnames";
import { Disclosure, Transition } from '@headlessui/react';
import { MdKeyboardArrowDown } from "react-icons/md";
import ErrorBoundary from "components/errorboundry";
import List from "components/bookmarks/list";
export default function BookmarksGroup({ group }) {
export default function BookmarksGroup({ group, disableCollapse }) {
return (
<div key={group.name} className="flex-1">
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{group.name}</h2>
<ErrorBoundary>
<List bookmarks={group.bookmarks} />
</ErrorBoundary>
<Disclosure defaultOpen>
{({ open }) => (
<>
<Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group">
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{group.name}</h2>
<MdKeyboardArrowDown className={classNames(
disableCollapse ? 'hidden' : '',
'transition-opacity opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl',
open ? 'rotate-180 transform' : ''
)} />
</Disclosure.Button>
<Transition
enter="transition duration-200 ease-out"
enterFrom="transform scale-75 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-75 opacity-0"
>
<Disclosure.Panel>
<ErrorBoundary>
<List bookmarks={group.bookmarks} />
</ErrorBoundary>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
</div>
);
}

View File

@@ -1,9 +1,12 @@
import classNames from "classnames";
import { Disclosure, Transition } from '@headlessui/react';
import { MdKeyboardArrowDown } from "react-icons/md";
import List from "components/services/list";
import ResolvedIcon from "components/resolvedicon";
export default function ServicesGroup({ group, services, layout, fiveColumns }) {
export default function ServicesGroup({ group, services, layout, fiveColumns, disableCollapse }) {
return (
<div
key={services.name}
@@ -13,15 +16,37 @@ export default function ServicesGroup({ group, services, layout, fiveColumns })
"flex-1 p-1"
)}
>
<div className="flex select-none items-center">
{layout?.icon &&
<div className="flex-shrink-0 mr-2 w-7 h-7">
<ResolvedIcon icon={layout.icon} />
</div>
}
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
</div>
<List group={group} services={services.services} layout={layout} />
<Disclosure defaultOpen>
{({ open }) => (
<>
<Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group">
{layout?.icon &&
<div className="flex-shrink-0 mr-2 w-7 h-7">
<ResolvedIcon icon={layout.icon} />
</div>
}
<h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
<MdKeyboardArrowDown className={classNames(
disableCollapse ? 'hidden' : '',
'transition-opacity opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl',
open ? 'rotate-180 transform' : ''
)} />
</Disclosure.Button>
<Transition
enter="transition duration-200 ease-out"
enterFrom="transform scale-75 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-75 opacity-0"
>
<Disclosure.Panel>
<List group={group} services={services.services} layout={layout} />
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
</div>
);
}

View File

@@ -1,6 +1,9 @@
import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
import Container from "../widget/container";
import Raw from "../widget/raw";
const textSizes = {
"4xl": "text-4xl",
"3xl": "text-3xl",
@@ -17,7 +20,7 @@ export default function DateTime({ options }) {
const { i18n } = useTranslation();
const [date, setDate] = useState("");
const dateLocale = locale ?? i18n.language;
useEffect(() => {
const dateFormat = new Intl.DateTimeFormat(dateLocale, { ...format });
const interval = setInterval(() => {
@@ -27,12 +30,14 @@ export default function DateTime({ options }) {
}, [date, setDate, dateLocale, format]);
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>
{date}
</span>
</div>
</div>
<Container options={options}>
<Raw>
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>
{date}
</span>
</div>
</Raw>
</Container>
);
}

View File

@@ -1,11 +1,13 @@
import useSWR from "swr";
import { useContext } from "react";
import { BiError } from "react-icons/bi";
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
import { FiCpu, FiHardDrive } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import UsageBar from "../resources/usage-bar";
import Error from "../widget/error";
import Resource from "../widget/resource";
import Resources from "../widget/resources";
import WidgetLabel from "../widget/widget_label";
import { SettingsContext } from "utils/contexts/settings";
@@ -26,52 +28,19 @@ export default function Widget({ options }) {
);
if (error || data?.error) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-row items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
<div className="flex flex-row self-center flex-wrap justify-between">
<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 min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 text-xs">
{t("glances.wait")}
</div>
</div>
<UsageBar percent="0" />
</div>
</div>
<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-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 text-xs">
{t("glances.wait")}
</div>
</div>
<UsageBar percent="0" />
</div>
</div>
</div>
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</div>
);
return <Resources options={options}>
<Resource icon={FiCpu} label={t("glances.wait")} percentage="0" />
<Resource icon={FaMemory} label={t("glances.wait")} percentage="0" />
{ options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" /> }
{ options.disk && !Array.isArray(options.disk) && <Resource key={options.disk} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> }
{ options.disk && Array.isArray(options.disk) && options.disk.map((disk) => <Resource key={disk.mnt_point} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> )}
{ options.uptime && <Resource icon={FaRegClock} label={t("glances.wait")} percentage="0" /> }
{ options.label && <WidgetLabel label={options.label} /> }
</Resources>;
}
const unit = options.units === "imperial" ? "fahrenheit" : "celsius";
@@ -101,131 +70,84 @@ export default function Widget({ options }) {
}
return (
<a href={options.url} target={settings.target ?? "_blank"} className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
<div className="flex flex-row self-center flex-wrap justify-between">
<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 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.total,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
</div>
<div className="pr-1">{t("glances.cpu")}</div>
</div>
{options.expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">
{t("common.number", {
value: data.load.min15,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
</div>
<div className="pr-1">{t("glances.load")}</div>
</span>
)}
<UsageBar percent={data.cpu.total} />
</div>
</div>
<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-[85px]">
<div 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.mem.free,
maximumFractionDigits: 1,
binary: true,
})}
</div>
<div className="pr-1">{t("glances.free")}</div>
</div>
{options.expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">
{t("common.bytes", {
value: data.mem.total,
maximumFractionDigits: 1,
binary: true,
})}
</div>
<div className="pr-1">{t("glances.total")}</div>
</span>
)}
<UsageBar percent={data.mem.percent} />
</div>
</div>
{disks.map((disk) => (
<div key={disk.mnt_point} 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-[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: disk.free })}</div>
<div className="pr-1">{t("glances.free")}</div>
</span>
{options.expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">{t("common.bytes", { value: disk.size })}</div>
<div className="pr-1">{t("glances.total")}</div>
</span>
)}
<UsageBar percent={disk.percent} />
</div>
</div>))}
{options.cputemp && mainTemp > 0 &&
(<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
</div>
<div className="pr-1">{t("glances.temp")}</div>
</span>
{options.expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">
{t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
</div>
<div className="pr-1">{t("glances.warn")}</div>
</span>
)}
<UsageBar percent={tempPercent} />
</div>
</div>)}
{options.uptime && data.uptime &&
(<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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">
{data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))}
</div>
<div className="pr-1">{t("glances.uptime")}</div>
</span>
<UsageBar percent={Math.round((new Date().getSeconds() / 60) * 100)} />
</div>
</div>)}
</div>
{options.label && (
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</a>
<Resources options={options} target={settings.target ?? "_blank"}>
<Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.total,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("glances.cpu")}
expandedValue={t("common.number", {
value: data.load.min15,
style: "unit",
unit: "percent",
maximumFractionDigits: 0
})}
expandedLabel={t("glances.load")}
percentage={data.cpu.total}
expanded={options.expanded}
/>
<Resource
icon={FaMemory}
value={t("common.bytes", {
value: data.mem.free,
maximumFractionDigits: 1,
binary: true,
})}
label={t("glances.free")}
expandedValue={t("common.bytes", {
value: data.mem.total,
maximumFractionDigits: 1,
binary: true,
})}
expandedLabel={t("glances.total")}
percentage={data.mem.percent}
expanded={options.expanded}
/>
{disks.map((disk) => (
<Resource key={disk.mnt_point}
icon={FiHardDrive}
value={t("common.bytes", { value: disk.free })}
label={t("glances.free")}
expandedValue={t("common.bytes", { value: disk.size })}
expandedLabel={t("glances.total")}
percentage={disk.percent}
expanded={options.expanded}
/>
))}
{options.cputemp && mainTemp > 0 &&
<Resource
icon={FaThermometerHalf}
value={t("common.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
label={t("glances.temp")}
expandedValue={t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
expandedLabel={t("glances.warn")}
percentage={tempPercent}
expanded={options.expanded}
/>
}
{options.uptime && data.uptime &&
<Resource
icon={FaRegClock}
value={data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))}
label={t("glances.uptime")}
percentage={Math.round((new Date().getSeconds() / 60) * 100).toString()}
/>
}
{options.label && <WidgetLabel label={options.label} />}
</Resources>
);
}

View File

@@ -1,3 +1,6 @@
import Container from "../widget/container";
import Raw from "../widget/raw";
const textSizes = {
"4xl": "text-4xl",
"3xl": "text-3xl",
@@ -11,12 +14,12 @@ const textSizes = {
export default function Greeting({ options }) {
if (options.text) {
return (
<div className="flex flex-row items-center justify-start">
return <Container options={options}>
<Raw>
<span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
{options.text}
</span>
</div>
);
</Raw>
</Container>;
}
}

View File

@@ -1,12 +1,15 @@
import useSWR from "swr";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import Error from "../widget/error";
import Container from "../widget/container";
import Raw from "../widget/raw";
import Node from "./node";
export default function Widget({ options }) {
const { cluster, nodes } = options;
const { t, i18n } = useTranslation();
const { i18n } = useTranslation();
const defaultData = {
cpu: {
@@ -18,7 +21,7 @@ export default function Widget({ options }) {
used: 0,
total: 0,
free: 0,
precent: 0
percent: 0
}
};
@@ -29,23 +32,12 @@ export default function Widget({ options }) {
);
if (error || data?.error) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-row items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
<Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />
@@ -54,12 +46,12 @@ export default function Widget({ options }) {
<Node type="node" key="nodes" options={options.nodes} data={defaultData} />
}
</div>
</div>
);
</Raw>
</Container>;
}
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
<Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />
@@ -69,6 +61,6 @@ export default function Widget({ options }) {
<Node key={node.name} type="node" options={options.nodes} data={node} />)
}
</div>
</div>
);
</Raw>
</Container>;
}

View File

@@ -3,8 +3,7 @@ import { FiAlertTriangle, FiCpu, FiServer } from "react-icons/fi";
import { SiKubernetes } from "react-icons/si";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import UsageBar from "../resources/usage-bar";
export default function Node({ type, options, data }) {
const { t } = useTranslation();
@@ -29,7 +28,7 @@ export default function Node({ type, options, data }) {
<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.percent,
value: data?.cpu?.percent ?? 0,
style: "unit",
unit: "percent",
maximumFractionDigits: 0
@@ -37,18 +36,18 @@ export default function Node({ type, options, data }) {
</div>
<FiCpu className="text-theme-800 dark:text-theme-200 w-3 h-3" />
</div>
<UsageBar percent={data.cpu.percent} />
<UsageBar percent={data?.cpu?.percent ?? 0} />
<div 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.free,
value: data?.memory?.free ?? 0,
maximumFractionDigits: 0,
binary: true
})}
</div>
<FaMemory className="text-theme-800 dark:text-theme-200 w-3 h-3" />
</div>
<UsageBar percent={data.memory.percent} />
<UsageBar percent={data?.memory?.percent} />
{options.showLabel && (
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{type === "cluster" ? options.label : data.name}</div>
)}

View File

@@ -1,12 +0,0 @@
export default function UsageBar({ percent }) {
return (
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20">
<div
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
style={{
width: `${percent}%`,
}}
/>
</div>
);
}

View File

@@ -1,62 +1,69 @@
import Container from "../widget/container";
import Raw from "../widget/raw";
import ResolvedIcon from "components/resolvedicon"
export default function Logo({ options }) {
return (
<div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
{options.icon ?
<Container options={options}>
<Raw>
{options.icon ?
<ResolvedIcon icon={options.icon} width={48} height={48} /> :
// fallback to homepage logo
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
fill: "rgba(var(--color-logo-start))",
}}
/>
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
<div className="w-12 h-12">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
stopColor: "rgba(var(--color-logo-start))",
fill: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
style={{
stopColor: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
style={{
stopColor: "rgba(var(--color-logo-stop))",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
stopColor: "rgba(var(--color-logo-stop))",
fill: "url(#homepage_logo_gradient)",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
fill: "url(#homepage_logo_gradient)",
}}
/>
</g>
</svg>
</g>
</svg>
</div>
}
</div>
</Raw>
</Container>
)
}

View File

@@ -1,37 +1,31 @@
import useSWR from "swr";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import Error from "../widget/error";
import Container from "../widget/container";
import Raw from "../widget/raw";
import Node from "./node";
export default function Longhorn({ options }) {
const { expanded, total, labels, include, nodes } = options;
const { t } = useTranslation();
const { data, error } = useSWR(`/api/widgets/longhorn`, {
refreshInterval: 1500
});
if (error || data?.error) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between" />
</div>
);
</Raw>
</Container>;
}
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes
.filter((node) => {
@@ -52,6 +46,6 @@ export default function Longhorn({ options }) {
</div>
)}
</div>
</div>
);
</Raw>
</Container>;
}

View File

@@ -1,32 +1,20 @@
import { FiHardDrive } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import { FaThermometerHalf } from "react-icons/fa";
import UsageBar from "../resources/usage-bar";
import Resource from "../widget/resource";
import WidgetLabel from "../widget/widget_label";
export default function Node({ data, expanded, labels }) {
const { t } = useTranslation();
return (
<>
<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-[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.node.available })}</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.node.maximum })}</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)} />
</div>
</div>
{labels && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{data.node.id}</div>
)}
</>
);
return <Resource
icon={FaThermometerHalf}
value={t("common.bytes", { value: data.node.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.node.maximum })}
expandedLabel={t("resources.total")}
percentage={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)}
expanded={expanded}
>{ labels && <WidgetLabel label={data.node.id} /> }
</Resource>
}

View File

@@ -1,10 +1,16 @@
import useSWR from "swr";
import { useState } from "react";
import { BiError } from "react-icons/bi";
import { WiCloudDown } from "react-icons/wi";
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
import { useTranslation } from "next-i18next";
import Error from "../widget/error";
import Container from "../widget/container";
import ContainerButton from "../widget/container_button";
import WidgetIcon from "../widget/widget_icon";
import PrimaryText from "../widget/primary_text";
import SecondaryText from "../widget/secondary_text";
import Icon from "./icon";
function Widget({ options }) {
@@ -15,60 +21,35 @@ function Widget({ options }) {
);
if (error || data?.error) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night";
const weatherInfo = {
condition: data.current_weather.weathercode,
timeOfDay: data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night"
};
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon condition={data.current_weather.weathercode} timeOfDay={timeOfDay} />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">
{options.label && `${options.label}, `}
{t("common.number", {
value: data.current_weather.temperature,
style: "unit",
unit,
})}
</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t(`wmo.${data.current_weather.weathercode}-${timeOfDay}`)}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: data.current_weather.temperature,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{t(`wmo.${data.current_weather.weathercode}-${weatherInfo.timeOfDay}`)}</SecondaryText>
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
</Container>;
}
export default function OpenMeteo({ options }) {
@@ -103,27 +84,11 @@ export default function OpenMeteo({ options }) {
// if (!requesting && !location) requestLocation();
if (!location) {
return (
<button
type="button"
onClick={() => requestLocation()}
className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
>
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
) : (
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
)}
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
</div>
</div>
</button>
);
return <ContainerButton options={options} callback={requestLocation} >
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={ requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
}
return <Widget options={{ ...location, ...options }} />;

View File

@@ -1,12 +1,19 @@
import useSWR from "swr";
import { useState } from "react";
import { BiError } from "react-icons/bi";
import { WiCloudDown } from "react-icons/wi";
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
import { useTranslation } from "next-i18next";
import Error from "../widget/error";
import Container from "../widget/container";
import ContainerButton from "../widget/container_button";
import PrimaryText from "../widget/primary_text";
import SecondaryText from "../widget/secondary_text";
import WidgetIcon from "../widget/widget_icon";
import Icon from "./icon";
function Widget({ options }) {
const { t, i18n } = useTranslation();
@@ -15,58 +22,29 @@ function Widget({ options }) {
);
if (error || data?.cod === 401 || data?.error) {
return (
<div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="hidden sm:flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="hidden sm:flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
return (
<div className="flex flex-col justify-center first:ml-auto ml-2 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="hidden sm:flex flex-col items-center">
<Icon
condition={data.weather[0].id}
timeOfDay={data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"}
/>
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">
{options.label && `${options.label}, `}
{t("common.number", { value: data.main.temp, style: "unit", unit })}
</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
</div>
</div>
</div>
);
const weatherInfo = {
condition: data.weather[0].id,
timeOfDay: data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"
};
return <Container options={options}>
<PrimaryText>{options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })}</PrimaryText>
<SecondaryText>{data.weather[0].description}</SecondaryText>
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
</Container>;
}
export default function OpenWeatherMap({ options }) {
@@ -98,30 +76,12 @@ export default function OpenWeatherMap({ options }) {
}
};
// if (!requesting && !location) requestLocation();
if (!location) {
return (
<button
type="button"
onClick={() => requestLocation()}
className="flex flex-col justify-center first:ml-auto ml-4 mr-2"
>
<div className="flex flex-row items-center justify-end">
<div className="hidden sm:flex flex-col items-center">
{requesting ? (
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
) : (
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
)}
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
</div>
</div>
</button>
);
return <ContainerButton options={options} callback={requestLocation} >
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
}
return <Widget options={{ ...location, ...options }} />;

View File

@@ -1,9 +1,9 @@
import useSWR from "swr";
import { FiCpu } from "react-icons/fi";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import Resource from "../widget/resource";
import Error from "../widget/error";
export default function Cpu({ expanded }) {
const { t } = useTranslation();
@@ -13,67 +13,29 @@ export default function Cpu({ expanded }) {
});
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error />
}
if (!data) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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 pr-1">-</div>
<div className="pr-1">{t("resources.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 pr-1">-</div>
<div className="pr-1">{t("resources.load")}</div>
</div>
)}
<UsageBar percent={0} />
</div>
</div>
);
return <Resource icon={FiCpu} value="-" label={t("resources.cpu")} expandedValue="-"
expandedLabel={t("resources.load")} percentage="0" expanded={expanded} />
}
const percent = data.cpu.usage;
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 min-w-[85px]">
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">
{t("common.number", {
value: data.cpu.usage,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
</div>
<div className="pr-1">{t("resources.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 pr-1">
{t("common.number", {
value: data.cpu.load,
maximumFractionDigits: 2,
})}
</div>
<div className="pr-1">{t("resources.load")}</div>
</div>
)}
<UsageBar percent={percent} />
</div>
</div>
);
return <Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.usage,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("resources.cpu")}
expandedValue={t("common.number", {
value: data.cpu.load,
maximumFractionDigits: 2,
})}
expandedLabel={t("resources.load")}
percentage={data.cpu.usage}
expanded={expanded}
/>
}

View File

@@ -1,9 +1,9 @@
import useSWR from "swr";
import { FaThermometerHalf } from "react-icons/fa";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import Resource from "../widget/resource";
import Error from "../widget/error";
function convertToFahrenheit(t) {
return t * 9/5 + 32
@@ -17,34 +17,18 @@ export default function CpuTemp({ expanded, units }) {
});
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error />
}
if (!data || !data.cputemp) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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">-</div>
<div className="pr-1">{t("resources.temp")}</div>
</span>
{expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">-</div>
<div className="pr-1">{t("resources.max")}</div>
</span>
)}
</div>
</div>
);
return <Resource
icon={FaThermometerHalf}
value="-"
label={t("resources.temp")}
expandedValue="-"
expandedLabel={t("resources.max")}
expanded={expanded}
/>;
}
let mainTemp = data.cputemp.main;
@@ -54,38 +38,24 @@ export default function CpuTemp({ expanded, units }) {
const unit = units === "imperial" ? "fahrenheit" : "celsius";
mainTemp = (unit === "celsius") ? mainTemp : convertToFahrenheit(mainTemp);
const maxTemp = (unit === "celsius") ? data.cputemp.max : convertToFahrenheit(data.cputemp.max);
const percent = Math.round((mainTemp / maxTemp) * 100);
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
</div>
<div className="pr-1">{t("resources.temp")}</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.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
</div>
<div className="pr-1">{t("resources.max")}</div>
</span>
)}
<UsageBar percent={percent} />
</div>
</div>
);
return <Resource
icon={FaThermometerHalf}
value={t("common.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
label={t("resources.temp")}
expandedValue={t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
expandedLabel={t("resources.max")}
percentage={Math.round((mainTemp / maxTemp) * 100)}
expanded={expanded}
/>;
}

View File

@@ -1,9 +1,9 @@
import useSWR from "swr";
import { FiHardDrive } from "react-icons/fi";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import Resource from "../widget/resource";
import Error from "../widget/error";
export default function Disk({ options, expanded }) {
const { t } = useTranslation();
@@ -13,56 +13,31 @@ export default function Disk({ options, expanded }) {
});
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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 pr-1">-</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 pr-1">-</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={0} />
</div>
</div>
);
return <Resource
icon={FiHardDrive}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>;
}
// data.drive.used not accurate?
const percent = Math.round(((data.drive.size - data.drive.available) / data.drive.size) * 100);
return (
<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-[85px]">
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">{t("common.bytes", { value: data.drive.available })}</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 pr-1">{t("common.bytes", { value: data.drive.size })}</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={percent} />
</div>
</div>
);
return <Resource
icon={FiHardDrive}
value={t("common.bytes", { value: data.drive.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.drive.size })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>;
}

View File

@@ -1,9 +1,9 @@
import useSWR from "swr";
import { FaMemory } from "react-icons/fa";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import Resource from "../widget/resource";
import Error from "../widget/error";
export default function Memory({ expanded }) {
const { t } = useTranslation();
@@ -13,63 +13,30 @@ export default function Memory({ expanded }) {
});
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error />
}
if (!data) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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 pr-1">-</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 pr-1">-</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={0} />
</div>
</div>
);
return <Resource
icon={FaMemory}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>;
}
const percent = Math.round((data.memory.active / data.memory.total) * 100);
return (
<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-[85px]">
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5 pr-1">
{t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, 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 pr-1">
{t("common.bytes", {
value: data.memory.total,
maximumFractionDigits: 1,
binary: true,
})}
</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={percent} />
</div>
</div>
);
return <Resource
icon={FaMemory}
value={t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.memory.total, maximumFractionDigits: 1, binary: true })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>;
}

View File

@@ -1,3 +1,6 @@
import Container from "../widget/container";
import Raw from "../widget/raw";
import Disk from "./disk";
import Cpu from "./cpu";
import Memory from "./memory";
@@ -6,8 +9,8 @@ import Uptime from "./uptime";
export default function Resources({ options }) {
const { expanded, units } = options;
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{options.cpu && <Cpu expanded={expanded} />}
{options.memory && <Memory expanded={expanded} />}
@@ -20,6 +23,6 @@ export default function Resources({ options }) {
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</div>
);
</Raw>
</Container>;
}

View File

@@ -1,9 +1,9 @@
import useSWR from "swr";
import { FaRegClock } from "react-icons/fa";
import { BiError } from "react-icons/bi";
import { useTranslation } from "next-i18next";
import UsageBar from "./usage-bar";
import Resource from "../widget/resource";
import Error from "../widget/error";
export default function Uptime() {
const { t } = useTranslation();
@@ -13,54 +13,24 @@ export default function Uptime() {
});
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
return <Error />
}
if (!data) {
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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">-</div>
<div className="pr-1">{t("resources.temp")}</div>
</span>
</div>
</div>
);
return <Resource icon={FaRegClock} value="-" label={t("resources.uptime")} percentage="0" />;
}
const mo = Math.floor(data.uptime / (3600 * 24 * 31));
const d = Math.floor(data.uptime % (3600 * 24 * 31) / (3600 * 24));
const h = Math.floor(data.uptime % (3600 * 24) / 3600);
const m = Math.floor(data.uptime % 3600 / 60);
let uptime;
if (mo > 0) uptime = `${mo}${t("resources.months")} ${d}${t("resources.days")}`;
else if (d > 0) uptime = `${d}${t("resources.days")} ${h}${t("resources.hours")}`;
else uptime = `${h}${t("resources.hours")} ${m}${t("resources.minutes")}`;
const percent = Math.round((new Date().getSeconds() / 60) * 100);
const percent = Math.round((new Date().getSeconds() / 60) * 100).toString();
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<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">
{uptime}
</div>
<div className="pr-1">{t("resources.uptime")}</div>
</span>
<UsageBar percent={percent} />
</div>
</div>
);
return <Resource icon={FaRegClock} value={uptime} label={t("resources.uptime")} percentage={percent} />;
}

View File

@@ -1,10 +1,13 @@
import { useState, useEffect, Fragment } from "react";
import { useState, useEffect, useCallback, Fragment } from "react";
import { useTranslation } from "next-i18next";
import { FiSearch } from "react-icons/fi";
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle, SiBaidu, SiBrave } from "react-icons/si";
import { Listbox, Transition } from "@headlessui/react";
import classNames from "classnames";
import ContainerForm from "../widget/container_form";
import Raw from "../widget/raw";
export const searchProviders = {
google: {
name: "Google",
@@ -76,14 +79,9 @@ export default function Search({ options }) {
setSelectedProvider(storedProvider);
}
}, [availableProviderIds]);
if (!availableProviderIds) {
return null;
}
function handleSubmit(event) {
const submitCallback = useCallback(event => {
const q = encodeURIComponent(query);
const { url } = selectedProvider;
if (url) {
window.open(`${url}${q}`, options.target || "_blank");
@@ -94,6 +92,10 @@ export default function Search({ options }) {
event.preventDefault();
event.target.reset();
setQuery("");
}, [options.target, options.url, query, selectedProvider]);
if (!availableProviderIds) {
return null;
}
const onChangeProvider = (provider) => {
@@ -101,77 +103,79 @@ export default function Search({ options }) {
localStorage.setItem(localStorageKey, provider.name);
}
return (
<form className="flex-col relative h-8 my-4 min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"
className="
overflow-hidden w-full h-full rounded-md
text-xs text-theme-900 dark:text-white
placeholder-theme-900 dark:placeholder-white/80
bg-white/50 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50
focus:border-theme-500 dark:focus:border-white/50
border border-theme-300 dark:border-theme-200/50"
placeholder={t("search.placeholder")}
onChange={(s) => setQuery(s.currentTarget.value)}
required
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={options.focus}
/>
<Listbox as="div" value={selectedProvider} onChange={onChangeProvider} className="relative text-left" disabled={availableProviderIds?.length === 1}>
<div>
<Listbox.Button
className="
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
text-white font-medium text-sm
bg-theme-600/40 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50"
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow" >
<Raw>
<div className="flex-col relative h-8 my-4 min-w-fit">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"
className="
overflow-hidden w-full h-full rounded-md
text-xs text-theme-900 dark:text-white
placeholder-theme-900 dark:placeholder-white/80
bg-white/50 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50
focus:border-theme-500 dark:focus:border-white/50
border border-theme-300 dark:border-theme-200/50"
placeholder={t("search.placeholder")}
onChange={(s) => setQuery(s.currentTarget.value)}
required
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={options.focus}
/>
<Listbox as="div" value={selectedProvider} onChange={onChangeProvider} className="relative text-left" disabled={availableProviderIds?.length === 1}>
<div>
<Listbox.Button
className="
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
text-white font-medium text-sm
bg-theme-600/40 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50"
>
<selectedProvider.icon className="text-white w-3 h-3" />
<span className="sr-only">{t("search.search")}</span>
</Listbox.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"
>
<selectedProvider.icon className="text-white w-3 h-3" />
<span className="sr-only">{t("search.search")}</span>
</Listbox.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"
>
<Listbox.Options
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
bg-theme-100 dark:bg-theme-600 shadow-lg
ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="flex flex-col">
{availableProviderIds.map((providerId) => {
const p = searchProviders[providerId];
return (
<Listbox.Option key={providerId} value={p} as={Fragment}>
{({ active }) => (
<li
className={classNames(
"rounded-md cursor-pointer",
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100"
)}
>
<p.icon className="h-4 w-4 mx-4 my-2" />
</li>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</form>
);
<Listbox.Options
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
bg-theme-100 dark:bg-theme-600 shadow-lg
ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="flex flex-col">
{availableProviderIds.map((providerId) => {
const p = searchProviders[providerId];
return (
<Listbox.Option key={providerId} value={p} as={Fragment}>
{({ active }) => (
<li
className={classNames(
"rounded-md cursor-pointer",
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100"
)}
>
<p.icon className="h-4 w-4 mx-4 my-2" />
</li>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</div>
</Raw>
</ContainerForm>;
}

View File

@@ -3,6 +3,12 @@ import { MdSettingsEthernet } from "react-icons/md";
import { useTranslation } from "next-i18next";
import { SiUbiquiti } from "react-icons/si";
import Error from "../widget/error";
import Container from "../widget/container";
import Raw from "../widget/raw";
import WidgetIcon from "../widget/widget_icon";
import PrimaryText from "../widget/primary_text";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Widget({ options }) {
@@ -13,35 +19,16 @@ export default function Widget({ options }) {
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
if (statsError) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default");
if (!defaultSite) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<SiUbiquiti className="w-5 h-5 text-theme-800 dark:text-theme-200" />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.wait")}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>{t("unifi.wait")}</PrimaryText>
<WidgetIcon icon={SiUbiquiti} />
</Container>;
}
const wan = defaultSite.health.find(h => h.subsystem === "wan");
@@ -56,8 +43,9 @@ export default function Widget({ options }) {
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
return (
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
return <Container options={options}>
<Raw>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<div className="flex flex-col">
<div className="flex flex-row ml-3 mb-0.5">
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
@@ -141,6 +129,7 @@ export default function Widget({ options }) {
</div>
</div>}
</div>
</div>
);
</div>
</Raw>
</Container>
}

View File

@@ -1,10 +1,16 @@
import useSWR from "swr";
import { useState } from "react";
import { BiError } from "react-icons/bi";
import { WiCloudDown } from "react-icons/wi";
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
import { useTranslation } from "next-i18next";
import Error from "../widget/error";
import Container from "../widget/container";
import PrimaryText from "../widget/primary_text";
import SecondaryText from "../widget/secondary_text";
import WidgetIcon from "../widget/widget_icon";
import ContainerButton from "../widget/container_button";
import Icon from "./icon";
function Widget({ options }) {
@@ -15,59 +21,35 @@ function Widget({ options }) {
);
if (error || data?.error) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
</div>
</div>
</div>
</div>
);
return <Error options={options} />
}
if (!data) {
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
const weatherInfo = {
condition: data.current.condition.code,
timeOfDay: data.current.is_day ? "day" : "night",
};
return (
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">
{options.label && `${options.label}, `}
{t("common.number", {
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
style: "unit",
unit,
})}
</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
</div>
</div>
</div>
);
return <Container options={options}>
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{data.current.condition.text}</SecondaryText>
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
</Container>;
}
export default function WeatherApi({ options }) {
@@ -99,30 +81,12 @@ export default function WeatherApi({ options }) {
}
};
// if (!requesting && !location) requestLocation();
if (!location) {
return (
<button
type="button"
onClick={() => requestLocation()}
className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
>
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
) : (
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
)}
</div>
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
</div>
</div>
</button>
);
return <ContainerButton options={options} callback={requestLocation} >
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
}
return <Widget options={{ ...location, ...options }} />;

View File

@@ -17,13 +17,13 @@ const widgetMappings = {
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
};
export default function Widget({ widget }) {
export default function Widget({ widget, style }) {
const InfoWidget = widgetMappings[widget.type];
if (InfoWidget) {
return (
<ErrorBoundary>
<InfoWidget options={widget.options} />
<InfoWidget options={{ ...widget.options, style }} />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,54 @@
import classNames from "classnames";
import WidgetIcon from "./widget_icon";
import PrimaryText from "./primary_text";
import SecondaryText from "./secondary_text";
import Raw from "./raw";
export function getAllClasses(options, additionalClassNames = '') {
if (options?.style?.header === "boxedWidgets") {
return classNames(
"flex flex-col justify-center first:ml-0 ml-2 mr-2",
"mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-2 pl-3 pr-3",
additionalClassNames
);
}
let widgetAlignedClasses = "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap";
if (options?.style?.isRightAligned) {
widgetAlignedClasses = "flex flex-col justify-center first:ml-auto ml-2 mr-2 ";
}
return classNames(
widgetAlignedClasses,
additionalClassNames
);
}
export function getInnerBlock(children) {
// children won't be an array if it's Raw component
return Array.isArray(children) && <div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">{children.find(child => child.type === WidgetIcon)}</div>
<div className="flex flex-col ml-3 text-left">
{children.find(child => child.type === PrimaryText)}
{children.find(child => child.type === SecondaryText)}
</div>
</div>;
}
export function getBottomBlock(children) {
if (children.type !== Raw) {
return children.find(child => child.type === Raw) || [];
}
return [children];
}
export default function Container({ children = [], options, additionalClassNames = '' }) {
return (
<div className={getAllClasses(options, additionalClassNames)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) {
return (
<button type="button" onClick={callback} className={getAllClasses(options, additionalClassNames)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</button>
);
}

View File

@@ -0,0 +1,10 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) {
return (
<form type="button" onSubmit={callback} className={getAllClasses(options, additionalClassNames)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</form>
);
}

View File

@@ -0,0 +1,10 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) {
return (
<a href={options.url} target={target} className={getAllClasses(options, additionalClassNames)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</a>
);
}

View File

@@ -0,0 +1,15 @@
import { useTranslation } from "react-i18next";
import { BiError } from "react-icons/bi";
import Container from "./container";
import PrimaryText from "./primary_text";
import WidgetIcon from "./widget_icon";
export default function Error({ options }) {
const { t } = useTranslation();
return <Container options={options}>
<PrimaryText>{t("widget.api_error")}</PrimaryText>
<WidgetIcon icon={BiError} size="l" />
</Container>;
}

View File

@@ -0,0 +1,5 @@
export default function PrimaryText({ children }) {
return (
<span className="text-theme-800 dark:text-theme-200 text-sm">{children}</span>
);
}

View File

@@ -0,0 +1,7 @@
export default function Raw({ children }) {
if (children.type === Raw) {
return [children];
}
return children;
}

View File

@@ -0,0 +1,22 @@
import UsageBar from "../resources/usage-bar";
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) {
const Icon = icon;
return <div className="flex-none flex flex-row items-center mr-3 py-1.5">
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5"/>
<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">{value}</div>
<div className="pr-1">{label}</div>
</div>
{ expanded && <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{expandedValue}</div>
<div className="pr-1">{expandedLabel}</div>
</div>
}
{ percentage && <UsageBar percent={percentage} /> }
{ children }
</div>
</div>;
}

View File

@@ -0,0 +1,17 @@
import ContainerLink from "./container_link";
import Resource from "./resource";
import Raw from "./raw";
import WidgetLabel from "./widget_label";
export default function Resources({ options, children, target }) {
const widgetParts = [].concat(...children);
return <ContainerLink options={options} target={target}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{ widgetParts.filter(child => child && child.type === Resource) }
</div>
{ widgetParts.filter(child => child && child.type === WidgetLabel) }
</Raw>
</ContainerLink>;
}

View File

@@ -0,0 +1,5 @@
export default function SecondaryText({ children }) {
return (
<span className="text-theme-800 dark:text-theme-200 text-xs">{children}</span>
);
}

View File

@@ -0,0 +1,18 @@
export default function WidgetIcon({ icon, size = "s", pulse = false, weatherInfo = {} }) {
const Icon = icon;
const { condition, timeOfDay } = weatherInfo;
let additionalClasses = "text-theme-800 dark:text-theme-200 ";
switch (size) {
case "m": additionalClasses += "w-6 h-6 "; break;
case "l": additionalClasses += "w-8 h-8 "; break;
case "xl": additionalClasses += "w-10 h-10 "; break;
default: additionalClasses += "w-5 h-5 ";
}
if (pulse) {
additionalClasses += "animate-pulse ";
}
return <Icon className={additionalClasses} condition={condition} timeOfDay={timeOfDay} />;
}

View File

@@ -0,0 +1,3 @@
export default function WidgetLabel({ label = "" }) {
return <div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
}

View File

@@ -2,7 +2,7 @@ import { join } from "path";
import { createHash } from "crypto";
import { readFileSync } from "fs";
import checkAndCopyConfig from "utils/config/config";
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml"];
@@ -15,7 +15,7 @@ function hash(buffer) {
export default async function handler(req, res) {
const hashes = configs.map((config) => {
checkAndCopyConfig(config);
const configYaml = join(process.cwd(), "config", config);
const configYaml = join(CONF_DIR, config);
return hash(readFileSync(configYaml, "utf8"));
});

View File

@@ -22,6 +22,7 @@ export default async function handler(req, res) {
if (widget?.mappings) {
const mapping = widget?.mappings?.[req.query.endpoint];
const mappingParams = mapping?.params;
const optionalParams = mapping?.optionalParams;
const map = mapping?.map;
const endpoint = mapping?.endpoint;
const endpointProxy = mapping?.proxyHandler || serviceProxyHandler;
@@ -40,9 +41,17 @@ export default async function handler(req, res) {
req.query.endpoint = formatApiCall(endpoint, segments);
}
if (req.query.query && mappingParams) {
if (req.query.query && (mappingParams || optionalParams)) {
const queryParams = JSON.parse(req.query.query);
const query = new URLSearchParams(mappingParams.map((p) => [p, queryParams[p]]));
let filteredOptionalParams = []
if (optionalParams) filteredOptionalParams = optionalParams.filter(p => queryParams[p] !== undefined);
let params = [];
if (mappingParams) params = params.concat(mappingParams);
if (filteredOptionalParams) params = params.concat(filteredOptionalParams);
const query = new URLSearchParams(params.map((p) => [p, queryParams[p]]));
req.query.endpoint = `${req.query.endpoint}?${query}`;
}

View File

@@ -46,7 +46,7 @@ function parseLonghornData(data) {
export default async function handler(req, res) {
const settings = getSettings();
const longhornSettings = settings?.providers?.longhorn;
const longhornSettings = settings?.providers?.longhorn || {};
const {url, username, password} = longhornSettings;
if (!url) {

View File

@@ -160,6 +160,7 @@ const headerStyles = {
"m-4 mb-0 sm:m-8 sm:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
underlined: "m-4 mb-0 sm:m-8 sm:mb-1 border-b-2 pb-4 border-theme-800 dark:border-theme-200/50",
clean: "m-4 mb-0 sm:m-8 sm:mb-0",
boxedWidgets: "m-4 mb-0 sm:m-8 sm:mb-0 sm:mt-1",
};
function Home({ initialSettings }) {
@@ -208,6 +209,7 @@ function Home({ initialSettings }) {
searchProvider = searchProviders[searchWidget.options?.provider];
}
}
const headerStyle = initialSettings?.headerStyle || "underlined";
useEffect(() => {
function handleKeyDown(e) {
@@ -256,7 +258,7 @@ function Home({ initialSettings }) {
<div
className={classNames(
"flex flex-row flex-wrap justify-between",
headerStyles[initialSettings.headerStyle || "underlined"]
headerStyles[headerStyle]
)}
>
<QuickLaunch
@@ -272,14 +274,17 @@ function Home({ initialSettings }) {
{widgets
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false}} />
))}
<div className="m-auto sm:ml-2 flex flex-wrap grow sm:basis-auto justify-between md:justify-end">
<div className={classNames(
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
)}>
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true}} />
))}
</div>
</>
@@ -289,7 +294,13 @@ function Home({ initialSettings }) {
{services?.length > 0 && (
<div className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{services.map((group) => (
<ServicesGroup key={group.name} group={group.name} services={group} layout={initialSettings.layout?.[group.name]} fiveColumns={settings.fiveColumns} />
<ServicesGroup
key={group.name}
group={group.name}
services={group}
layout={initialSettings.layout?.[group.name]}
fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse} />
))}
</div>
)}
@@ -297,7 +308,10 @@ function Home({ initialSettings }) {
{bookmarks?.length > 0 && (
<div className={`grow flex flex-wrap pt-0 p-4 sm:p-8 gap-2 grid-cols-1 lg:grid-cols-2 lg:grid-cols-${Math.min(6, bookmarks.length)}`}>
{bookmarks.map((group) => (
<BookmarksGroup key={group.name} group={group} />
<BookmarksGroup
key={group.name}
group={group}
disableCollapse={settings.disableCollapse} />
))}
</div>
)}
@@ -359,7 +373,7 @@ export default function Wrapper({ initialSettings, fallback }) {
style={wrappedStyle}
>
<div
id="inner_wrapper"
id="inner_wrapper"
className={classNames(
'fixed overflow-auto w-full h-full',
backgroundBlur && `backdrop-blur${initialSettings.background.blur.length ? '-' : ""}${initialSettings.background.blur}`,

View File

@@ -4,7 +4,7 @@ import path from "path";
import yaml from "js-yaml";
import checkAndCopyConfig, { getSettings, substituteEnvironmentVars } from "utils/config/config";
import checkAndCopyConfig, { getSettings, substituteEnvironmentVars, CONF_DIR } from "utils/config/config";
import {
servicesFromConfig,
servicesFromDocker,
@@ -27,7 +27,7 @@ function compareServices(service1, service2) {
export async function bookmarksResponse() {
checkAndCopyConfig("bookmarks.yaml");
const bookmarksYaml = path.join(process.cwd(), "config", "bookmarks.yaml");
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
const rawFileContents = await fs.readFile(bookmarksYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const bookmarks = yaml.load(fileContents);

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { join } from "path";
import { existsSync, readFileSync, copyFileSync } from "fs";
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
import cache from "memory-cache";
import yaml from "js-yaml";
@@ -9,8 +9,14 @@ const cacheKey = "homepageEnvironmentVariables";
const homepageVarPrefix = "HOMEPAGE_VAR_";
const homepageFilePrefix = "HOMEPAGE_FILE_";
export const CONF_DIR = process.env.HOMEPAGE_CONFIG_DIR ? process.env.HOMEPAGE_CONFIG_DIR : join(process.cwd(), "config");
export default function checkAndCopyConfig(config) {
const configYaml = join(process.cwd(), "config", config);
if (!existsSync(CONF_DIR)) {
mkdirSync(CONF_DIR, { recursive: true });
}
const configYaml = join(CONF_DIR, config);
if (!existsSync(configYaml)) {
const configSkeleton = join(process.cwd(), "src", "skeleton", config);
try {
@@ -62,7 +68,7 @@ export function substituteEnvironmentVars(str) {
export function getSettings() {
checkAndCopyConfig("settings.yaml");
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
const settingsYaml = join(CONF_DIR, "settings.yaml");
const rawFileContents = readFileSync(settingsYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const initialSettings = yaml.load(fileContents) ?? {};
@@ -79,6 +85,5 @@ export function getSettings() {
})
}
}
return initialSettings
}

View File

@@ -3,12 +3,12 @@ import { readFileSync } from "fs";
import yaml from "js-yaml";
import checkAndCopyConfig, { substituteEnvironmentVars } from "utils/config/config";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
export default function getDockerArguments(server) {
checkAndCopyConfig("docker.yaml");
const configFile = path.join(process.cwd(), "config", "docker.yaml");
const configFile = path.join(CONF_DIR, "docker.yaml");
const rawConfigData = readFileSync(configFile, "utf8");
const configData = substituteEnvironmentVars(rawConfigData);
const servers = yaml.load(configData);
@@ -37,9 +37,9 @@ export default function getDockerArguments(server) {
}
if (servers[server].tls){
res.conn.ca = readFileSync(path.join(process.cwd(), "config", servers[server].tls.caFile));
res.conn.cert = readFileSync(path.join(process.cwd(), "config", servers[server].tls.certFile));
res.conn.key = readFileSync(path.join(process.cwd(), "config", servers[server].tls.keyFile));
res.conn.ca = readFileSync(path.join(CONF_DIR, servers[server].tls.caFile));
res.conn.cert = readFileSync(path.join(CONF_DIR, servers[server].tls.certFile));
res.conn.key = readFileSync(path.join(CONF_DIR, servers[server].tls.keyFile));
}
return res;

View File

@@ -4,12 +4,12 @@ import { readFileSync } from "fs";
import yaml from "js-yaml";
import { KubeConfig } from "@kubernetes/client-node";
import checkAndCopyConfig, { substituteEnvironmentVars } from "utils/config/config";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
export default function getKubeConfig() {
checkAndCopyConfig("kubernetes.yaml");
const configFile = path.join(process.cwd(), "config", "kubernetes.yaml");
const configFile = path.join(CONF_DIR, "kubernetes.yaml");
const rawConfigData = readFileSync(configFile, "utf8");
const configData = substituteEnvironmentVars(rawConfigData);
const config = yaml.load(configData);

View File

@@ -7,7 +7,7 @@ import * as shvl from "shvl";
import { CustomObjectsApi, NetworkingV1Api } from "@kubernetes/client-node";
import createLogger from "utils/logger";
import checkAndCopyConfig, { substituteEnvironmentVars } from "utils/config/config";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
import getDockerArguments from "utils/config/docker";
import getKubeConfig from "utils/config/kubernetes";
@@ -17,7 +17,7 @@ const logger = createLogger("service-helpers");
export async function servicesFromConfig() {
checkAndCopyConfig("services.yaml");
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
const servicesYaml = path.join(CONF_DIR, "services.yaml");
const rawFileContents = await fs.readFile(servicesYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const services = yaml.load(fileContents);
@@ -51,7 +51,7 @@ export async function servicesFromConfig() {
export async function servicesFromDocker() {
checkAndCopyConfig("docker.yaml");
const dockerYaml = path.join(process.cwd(), "config", "docker.yaml");
const dockerYaml = path.join(CONF_DIR, "docker.yaml");
const rawDockerFileContents = await fs.readFile(dockerYaml, "utf8");
const dockerFileContents = substituteEnvironmentVars(rawDockerFileContents);
const servers = yaml.load(dockerFileContents);
@@ -158,11 +158,20 @@ export async function servicesFromKubernetes() {
return null;
});
const traefikIngressList = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
const traefikIngressList = await crd.listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes")
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting traefik ingresses: %d %s %s", error.statusCode, error.body, error.response);
return null;
.catch(async (error) => {
logger.error("Error getting traefik ingresses from traefik.io: %d %s %s", error.statusCode, error.body, error.response);
// Fallback to the old traefik CRD group
const fallbackIngressList = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
.then((response) => response.body)
.catch((fallbackError) => {
logger.error("Error getting traefik ingresses from traefik.containo.us: %d %s %s", fallbackError.statusCode, fallbackError.body, fallbackError.response);
return null;
});
return fallbackIngressList;
});
if (traefikIngressList && traefikIngressList.items.length > 0) {
@@ -270,6 +279,7 @@ export function cleanServiceGroups(groups) {
container,
currency, // coinmarketcap widget
symbols,
slugs,
defaultinterval,
site, // unifi widget
namespace, // kubernetes widget
@@ -282,8 +292,15 @@ export function cleanServiceGroups(groups) {
enableQueue, // sonarr/radarr
} = cleanedService.widget;
const fieldsList = typeof fields === 'string' ? JSON.parse(fields) : fields;
let fieldsList = fields;
if (typeof fields === 'string') {
try { JSON.parse(fields) }
catch (e) {
logger.error("Invalid fields list detected in config for service '%s'", service.name);
fieldsList = null;
}
}
cleanedService.widget = {
type,
fields: fieldsList || null,
@@ -292,9 +309,12 @@ export function cleanServiceGroups(groups) {
service_group: serviceGroup.name,
};
if (currency) cleanedService.widget.currency = currency;
if (symbols) cleanedService.widget.symbols = symbols;
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
if (type === "coinmarketcap") {
if (currency) cleanedService.widget.currency = currency;
if (symbols) cleanedService.widget.symbols = symbols;
if (slugs) cleanedService.widget.slugs = slugs;
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
}
if (type === "docker") {
if (server) cleanedService.widget.server = server;

View File

@@ -3,12 +3,12 @@ import path from "path";
import yaml from "js-yaml";
import checkAndCopyConfig, { substituteEnvironmentVars } from "utils/config/config";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
export async function widgetsFromConfig() {
checkAndCopyConfig("widgets.yaml");
const widgetsYaml = path.join(process.cwd(), "config", "widgets.yaml");
const widgetsYaml = path.join(CONF_DIR, "widgets.yaml");
const rawFileContents = await fs.readFile(widgetsYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const widgets = yaml.load(fileContents);

View File

@@ -1,18 +1,17 @@
/* eslint-disable no-console */
import { join } from "path";
import { format as utilFormat } from "node:util";
import winston from "winston";
import checkAndCopyConfig, { getSettings } from "utils/config/config";
import checkAndCopyConfig, { getSettings, CONF_DIR } from "utils/config/config";
let winstonLogger;
function init() {
const configPath = join(process.cwd(), "config");
checkAndCopyConfig("settings.yaml");
const settings = getSettings();
const logpath = settings.logpath || configPath;
const logpath = settings.logpath || CONF_DIR;
function combineMessageAndSplat() {
return {

View File

@@ -1,6 +1,6 @@
/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable no-param-reassign */
import { createUnzip } from "node:zlib";
import { createUnzip, constants as zlibConstants } from "node:zlib";
import { http, https } from "follow-redirects";
@@ -34,7 +34,19 @@ function handleRequest(requestor, url, params) {
let responseContent = response;
if (contentEncoding === 'gzip' || contentEncoding === 'deflate') {
responseContent = createUnzip();
// https://github.com/request/request/blob/3c0cddc7c8eb60b470e9519da85896ed7ee0081e/request.js#L1018-L1025
// Be more lenient with decoding compressed responses, in case of invalid gzip responses that are still accepted
// by common browsers.
responseContent = createUnzip({
flush: zlibConstants.Z_SYNC_FLUSH,
finishFlush: zlibConstants.Z_SYNC_FLUSH
});
// zlib errors
responseContent.on("error", (e) => {
logger.error(e);
responseContent = response; // fallback
});
response.pipe(responseContent);
}
@@ -98,6 +110,6 @@ export async function httpProxy(url, params = {}) {
constructedUrl.pathname
);
logger.error(err);
return [500, "application/json", { error: {message: err?.message ?? "Unknown error", url, rawError: err} }, null];
return [500, "application/json", { error: { message: err?.message ?? "Unknown error", url, rawError: err } }, null];
}
}

View File

@@ -19,17 +19,26 @@ export default function Component({ service }) {
const { widget } = service;
const { symbols } = widget;
const { slugs } = widget;
const currencyCode = widget.currency ?? "USD";
const interval = widget.defaultinterval ?? dateRangeOptions[0].value;
const [dateRange, setDateRange] = useState(interval);
const { data: statsData, error: statsError } = useWidgetAPI(widget, "v1/cryptocurrency/quotes/latest", {
symbol: `${symbols.join(",")}`,
const params = {
convert: `${currencyCode}`,
});
}
if (!symbols || symbols.length === 0) {
// slugs >> symbols, not both
if (slugs?.length) {
params.slug = slugs.join(",");
} else if (symbols?.length) {
params.symbol = symbols.join(",");
}
const { data: statsData, error: statsError } = useWidgetAPI(widget, "v1/cryptocurrency/quotes/latest", params);
if ((!symbols && !slugs) || (symbols?.length === 0 && slugs?.length === 0)) {
return (
<Container service={service}>
<Block value={t("coinmarketcap.configure")} />
@@ -50,6 +59,7 @@ export default function Component({ service }) {
}
const { data } = statsData;
const validCryptos = Object.values(data).filter(crypto => crypto.quote[currencyCode][`percent_change_${dateRange}`] !== null)
return (
<Container service={service}>
@@ -58,28 +68,28 @@ export default function Component({ service }) {
</div>
<div className="flex flex-col w-full">
{symbols.map((symbol) => (
{validCryptos.map((crypto) => (
<div
key={data[symbol].symbol}
key={crypto.id}
className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs"
>
<div className="font-thin pl-2">{data[symbol].name}</div>
<div className="font-thin pl-2">{crypto.name}</div>
<div className="flex flex-row text-right">
<div className="font-bold mr-2">
{t("common.number", {
value: data[symbol].quote[currencyCode].price,
value: crypto.quote[currencyCode].price,
style: "currency",
currency: currencyCode,
})}
</div>
<div
className={`font-bold w-10 mr-2 ${
data[symbol].quote[currencyCode][`percent_change_${dateRange}`] > 0
crypto.quote[currencyCode][`percent_change_${dateRange}`] > 0
? "text-emerald-300"
: "text-rose-300"
}`}
>
{data[symbol].quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
{crypto.quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
</div>
</div>
</div>

View File

@@ -7,7 +7,8 @@ const widget = {
mappings: {
"v1/cryptocurrency/quotes/latest": {
endpoint: "v1/cryptocurrency/quotes/latest",
params: ["symbol", "convert"],
params: ["convert"],
optionalParams: ["symbol", "slug"],
},
},
};

View File

@@ -31,8 +31,10 @@ const components = {
healthchecks: dynamic(() => import("./healthchecks/component")),
immich: dynamic(() => import("./immich/component")),
jackett: dynamic(() => import("./jackett/component")),
jdownloader: dynamic(() => import("./jdownloader/component")),
jellyfin: dynamic(() => import("./emby/component")),
jellyseerr: dynamic(() => import("./jellyseerr/component")),
kavita: dynamic(() => import("./kavita/component")),
komga: dynamic(() => import("./komga/component")),
kopia: dynamic(() => import("./kopia/component")),
lidarr: dynamic(() => import("./lidarr/component")),

View File

@@ -63,7 +63,7 @@ async function apiCall(widget, endpoint, service) {
}
if (status !== 200) {
logger.error("Error getting data from Homebridge: %s status %d. Data: %s", url, status, data);
logger.error("Error getting data from Homebridge: %s status %d. Data: %s", url, status, JSON.stringify(data));
return { status, contentType, data: null, responseHeaders };
}

View File

@@ -0,0 +1,39 @@
import { useTranslation } from "next-i18next";
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", {
refreshInterval: 30000,
});
if (jdownloaderAPIError) {
return <Container service={service} error={jdownloaderAPIError} />;
}
if (!jdownloaderData) {
return (
<Container service={service}>
<Block label="jdownloader.downloadCount" />
<Block label="jdownloader.downloadTotalBytes" />
<Block label="jdownloader.downloadBytesRemaining" />
<Block label="jdownloader.downloadSpeed" />
</Container>
);
}
return (
<Container service={service}>
<Block label="jdownloader.downloadCount" value={t("common.number", { value: jdownloaderData.downloadCount })} />
<Block label="jdownloader.downloadTotalBytes" value={t("common.bytes", { value: jdownloaderData.totalBytes })} />
<Block label="jdownloader.downloadBytesRemaining" value={t("common.bytes", { value: jdownloaderData.bytesRemaining })} />
<Block label="jdownloader.downloadSpeed" value={t("common.byterate", { value: jdownloaderData.totalSpeed })} />
</Container>
);
}

View File

@@ -0,0 +1,196 @@
/* eslint-disable no-underscore-dangle */
import crypto from 'crypto';
import querystring from 'querystring';
import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools"
import getServiceWidget from "utils/config/service-helpers";
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
const proxyName = "jdownloaderProxyHandler";
const logger = createLogger(proxyName);
async function getWidget(req) {
const { group, service } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return null;
}
const widget = await getServiceWidget(group, service);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}
return widget;
}
async function login(loginSecret, deviceSecret, params) {
const rid = uniqueRid();
const path = `/my/connect?${querystring.stringify({ ...params, rid })}`;
const signature = crypto
.createHmac('sha256', loginSecret)
.update(path)
.digest('hex');
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
const [status, contentType, data] = await httpProxy(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (status !== 200) {
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
return [status, data];
}
try {
const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret))
const sessionToken = decryptedData.sessiontoken;
validateRid(decryptedData, rid);
const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
} catch (e) {
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
return [status, null];
}
}
async function getDevice(serverEncryptionToken, deviceName, params) {
const rid = uniqueRid();
const path = `/my/listdevices?${querystring.stringify({ ...params, rid })}`;
const signature = crypto
.createHmac('sha256', serverEncryptionToken)
.update(path)
.digest('hex');
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
const [status, , data] = await httpProxy(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (status !== 200) {
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
return [status, data];
}
try {
const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken))
const filteredDevice = decryptedData.list.filter(device => device.name === deviceName);
return [status, filteredDevice[0].id];
} catch (e) {
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
return [status, null];
}
}
function createBody(rid, query, params) {
const baseBody = {
apiVer: 1,
rid,
url: query
};
return params ? { ...baseBody, params: [JSON.stringify(params)] } : baseBody;
}
async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) {
const rid = uniqueRid();
const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken);
const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}`
const [status, , data] = await httpProxy(url, {
method: 'POST',
body,
});
if (status !== 200) {
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
return [status, data];
}
try {
const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken))
return decryptedData.data;
} catch (e) {
logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
return [status, null];
}
}
export default async function jdownloaderProxyHandler(req, res) {
const widget = await getWidget(req);
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
logger.debug("Getting data from JDRss API");
const { username } = widget
const { password } = widget
const appKey = "homepage"
const loginSecret = sha256(`${username}${password}server`)
const deviceSecret = sha256(`${username}${password}device`)
const email = username;
const loginData = await login(loginSecret, deviceSecret, {
appKey,
email
})
const deviceData = await getDevice(loginData[3], widget.client, {
sessiontoken: loginData[5]
})
const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
"bytesLoaded": false,
"bytesTotal": true,
"comment": false,
"enabled": true,
"eta": false,
"priority": false,
"finished": true,
"running": true,
"speed": true,
"status": true,
"childCount": false,
"hosts": false,
"saveTo": false,
"maxResults": -1,
"startAt": 0,
}
)
let bytesRemaining = 0;
let totalBytes = 0;
let totalSpeed = 0;
packageStatus.forEach(file => {
totalBytes += file.bytesTotal;
if (file.finished !== true) {
bytesRemaining += file.bytesTotal;
if (file.speed) {
totalSpeed += file.speed;
}
}
});
const data = {
downloadCount: packageStatus.length,
bytesRemaining,
totalBytes,
totalSpeed
};
return res.send(data);
}

View File

@@ -0,0 +1,55 @@
import crypto from 'crypto';
export function sha256(data) {
return crypto
.createHash('sha256')
.update(data)
.digest();
}
export function uniqueRid() {
return Math.floor(Math.random() * 10e12);
}
export function validateRid(decryptedData, rid) {
if (decryptedData.rid !== rid) {
throw new Error('RequestID mismatch');
}
return decryptedData;
}
export function decrypt(data, ivKey) {
const iv = ivKey.slice(0, ivKey.length / 2);
const key = ivKey.slice(ivKey.length / 2, ivKey.length);
const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
return Buffer.concat([
cipher.update(data, 'base64'),
cipher.final()
]).toString();
}
export function createEncryptionToken(oldTokenBuff, updateToken) {
const updateTokenBuff = Buffer.from(updateToken, 'hex');
const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length);
return sha256(mergedBuffer);
}
export function encrypt(data, ivKey) {
if (typeof data !== 'string') {
throw new Error('data no es un string');
}
if (!(ivKey instanceof Buffer)) {
throw new Error('ivKey no es un buffer');
}
if (ivKey.length !== 32) {
throw new Error('ivKey tiene que tener tamaño 32');
}
const stringIVKey = ivKey.toString('hex');
const stringIV = stringIVKey.substring(0, stringIVKey.length / 2);
const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length);
const iv = Buffer.from(stringIV, 'hex');
const key = Buffer.from(stringKey, 'hex');
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
return cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
}

View File

@@ -0,0 +1,15 @@
import jdownloaderProxyHandler from "./proxy";
const widget = {
api: "https://api.jdownloader.org/{endpoint}/&signature={signature}",
proxyHandler: jdownloaderProxyHandler,
mappings: {
unified: {
endpoint: "/",
signature: "",
},
},
};
export default widget;

View File

@@ -0,0 +1,33 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: kavitaData, error: kavitaError } = useWidgetAPI(widget, "info");
if (kavitaError) {
return <Container service={service} error={kavitaError} />;
}
if (!kavitaData) {
return (
<Container service={service}>
<Block label="kavita.seriesCount" />
<Block label="kavita.totalFiles" />
</Container>
);
}
return (
<Container service={service}>
<Block label="kavita.seriesCount" value={t("common.number", { value: kavitaData.seriesCount })} />
<Block label="kavita.totalFiles" value={t("common.number", { value: kavitaData.totalFiles })} />
</Container>
);
}

View File

@@ -0,0 +1,96 @@
import cache from "memory-cache";
import { httpProxy } from "utils/proxy/http";
import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const proxyName = "kavitaProxyHandler";
const sessionTokenCacheKey = `${proxyName}__sessionToken`;
const logger = createLogger(proxyName);
async function login(widget, service) {
const endpoint = "Account/login";
const api = widgets?.[widget.type]?.api
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
const loginBody = { username: widget.username, password: widget.password };
const headers = { "Content-Type": "application/json", "accept": "text/plain" };
const [, , data,] = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify(loginBody),
headers,
});
try {
const { token: accessToken } = JSON.parse(data.toString());
cache.put(`${sessionTokenCacheKey}.${service}`, accessToken);
return { accessToken };
} catch (e) {
logger.error("Unable to login to Kavita API: %s", e);
}
return { token: false };
}
async function apiCall(widget, endpoint, service) {
const key = `${sessionTokenCacheKey}.${service}`;
const headers = {
"content-type": "application/json",
"Authorization": `Bearer ${cache.get(key)}`,
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const method = "GET";
let [status, contentType, data, responseHeaders] = await httpProxy(url, {
method,
headers,
});
if (status === 401 || status === 403) {
logger.debug("Kavita API rejected the request, attempting to obtain new session token");
const { accessToken } = await login(widget, service);
headers.Authorization = `Bearer ${accessToken}`;
// retry the request, now with the new session token
[status, contentType, data, responseHeaders] = await httpProxy(url, {
method,
headers,
});
}
if (status !== 200) {
logger.error("Error getting data from Kavita: %s status %d. Data: %s", url, status, data);
return { status, contentType, data: null, responseHeaders };
}
return { status, contentType, data: JSON.parse(data.toString()), responseHeaders };
}
export default async function KavitaProxyHandler(req, res) {
const { group, service } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
const widget = await getServiceWidget(group, service);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
if (!cache.get(`${sessionTokenCacheKey}.${service}`)) {
await login(widget, service);
}
const { data: statsData } = await apiCall(widget, "Stats/server/stats", service);
return res.status(200).send({
seriesCount: statsData?.seriesCount,
totalFiles: statsData?.totalFiles
});
}

Some files were not shown because too many files have changed in this diff Show More