Compare commits

..

108 Commits

Author SHA1 Message Date
Ben Phelps
406358aae9 package coinmarketcap logo 2022-09-12 18:19:07 +03:00
Ben Phelps
a5d59e7e45 images/future is no longer experimental 2022-09-12 14:30:57 +03:00
Ben Phelps
92a4ad0c5e update packages 2022-09-12 14:29:55 +03:00
Ben Phelps
d963bcd0c4 Update README.md 2022-09-12 13:45:46 +03:00
Ben Phelps
2e4125c81c Update README.md 2022-09-12 13:34:27 +03:00
Ben Phelps
5293ff3580 reorganize contributions in readme 2022-09-12 13:33:55 +03:00
Anonymous
7a1349df83 Translated using Weblate (Dutch)
Currently translated at 78.4% (62 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-12 11:41:23 +02:00
Anonymous
de8de8f731 Translated using Weblate (Vietnamese)
Currently translated at 41.7% (33 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-12 11:41:23 +02:00
Anonymous
6d36382436 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.0% (64 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-12 11:41:23 +02:00
Anonymous
e31833b649 Translated using Weblate (Italian)
Currently translated at 87.3% (69 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-12 11:41:23 +02:00
Anonymous
2dce18563d Translated using Weblate (Chinese (Simplified))
Currently translated at 75.9% (60 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-12 11:41:22 +02:00
Anonymous
aa55c27ab0 Translated using Weblate (Russian)
Currently translated at 16.4% (13 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-12 11:41:22 +02:00
Anonymous
16e321af54 Translated using Weblate (French)
Currently translated at 50.6% (40 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-12 11:41:22 +02:00
Anonymous
c4edb29ff3 Translated using Weblate (Spanish)
Currently translated at 87.3% (69 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-12 11:41:21 +02:00
Anonymous
1d5cc05941 Translated using Weblate (German)
Currently translated at 81.0% (64 of 79 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-12 11:41:21 +02:00
Ben Phelps
9faae7cb67 allow Weblate to handle missing translations 2022-09-12 12:41:03 +03:00
Ben Phelps
ea06fbe666 js linting 2022-09-12 12:39:04 +03:00
Ben Phelps
cc0b4be50c cleanup coinmarketcap widget 2022-09-12 12:38:50 +03:00
Ben Phelps
ea55cde043 Merge pull request #121 from xicopitz/main
Gotify
2022-09-12 12:36:38 +03:00
Francisco Coelho
840c88db89 Update widget.jsx
duplicated widget name
2022-09-12 10:23:44 +01:00
Francisco Coelho
8e8c9755a3 Merge branch 'benphelps:main' into main 2022-09-12 10:07:06 +01:00
Francisco Coelho
ba3b48e8ce Gotify 2022-09-12 10:06:47 +01:00
Ben Phelps
d3806f7d5b better handle non-clickable service tiles 2022-09-12 11:55:01 +03:00
Francisco Coelho
0c9c1c599f Merge branch 'main' of https://github.com/xicopitz/homepage 2022-09-12 09:37:42 +01:00
Francisco Coelho
af02440c40 Revert "Update credentialed.js"
This reverts commit eeac1200e7.
2022-09-12 09:31:44 +01:00
Ben Phelps
cd53440eff fix search and weather widget spacing 2022-09-12 11:00:15 +03:00
Ben Phelps
3660140539 consolidate api handlers 2022-09-12 10:59:56 +03:00
Anonymous
7bf1bf5369 Translated using Weblate (Dutch)
Currently translated at 81.5% (62 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-12 09:41:25 +02:00
Anonymous
898e30d6de Translated using Weblate (Vietnamese)
Currently translated at 43.4% (33 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-12 09:41:25 +02:00
Anonymous
a792d213e9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 84.2% (64 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-12 09:41:25 +02:00
Anonymous
ebee953ebc Translated using Weblate (Italian)
Currently translated at 90.7% (69 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-12 09:41:25 +02:00
Anonymous
200ab220e8 Translated using Weblate (Chinese (Simplified))
Currently translated at 78.9% (60 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-12 09:41:24 +02:00
Anonymous
2499d25ce6 Translated using Weblate (Russian)
Currently translated at 17.1% (13 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-12 09:41:24 +02:00
Anonymous
42356166c0 Translated using Weblate (Portuguese)
Currently translated at 30.2% (23 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-12 09:41:24 +02:00
Anonymous
80a31c8427 Translated using Weblate (French)
Currently translated at 52.6% (40 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-12 09:41:24 +02:00
Anonymous
867c6f9e97 Translated using Weblate (Spanish)
Currently translated at 90.7% (69 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-12 09:41:24 +02:00
Anonymous
ee9194fce1 Translated using Weblate (German)
Currently translated at 84.2% (64 of 76 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-12 09:41:23 +02:00
Ben Phelps
f6322077a4 Merge pull request #118 from ItsJustMeChris/main
Add coin market cap module
2022-09-12 10:41:17 +03:00
Ben Phelps
15a0e6cc54 Merge branch 'main' into main 2022-09-12 10:40:56 +03:00
Anonymous
5ee5adbb1e Translated using Weblate (Dutch)
Currently translated at 82.6% (62 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-12 09:33:05 +02:00
Anonymous
1d4b3eee9b Translated using Weblate (Vietnamese)
Currently translated at 44.0% (33 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-12 09:33:05 +02:00
Anonymous
fe971d23f8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.3% (64 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-12 09:33:04 +02:00
Anonymous
34bf49845f Translated using Weblate (Italian)
Currently translated at 92.0% (69 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-12 09:33:04 +02:00
Anonymous
34468e5bb0 Translated using Weblate (Chinese (Simplified))
Currently translated at 80.0% (60 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-12 09:33:04 +02:00
Anonymous
d0dd52c5c2 Translated using Weblate (Russian)
Currently translated at 17.3% (13 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-12 09:33:04 +02:00
Anonymous
98cca4ca8b Translated using Weblate (Portuguese)
Currently translated at 30.6% (23 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-12 09:33:03 +02:00
Anonymous
b88463a785 Translated using Weblate (French)
Currently translated at 53.3% (40 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-12 09:33:03 +02:00
Anonymous
6409188de8 Translated using Weblate (Spanish)
Currently translated at 92.0% (69 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-12 09:33:03 +02:00
Anonymous
510973c761 Translated using Weblate (German)
Currently translated at 85.3% (64 of 75 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-12 09:33:02 +02:00
Ben Phelps
4480c26910 allow weblate to apply new strings 2022-09-12 10:32:40 +03:00
Ben Phelps
e778595296 Merge branch 'main' of github.com:benphelps/homepage
# Conflicts:
#	public/locales/it/common.json
2022-09-12 10:32:22 +03:00
Ben Phelps
f84ff7cedc allow weblate to apply new strings 2022-09-12 10:26:03 +03:00
Luca Pellegrino
04f98ae7a9 Translated using Weblate (Italian)
Currently translated at 100.0% (69 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-12 09:23:53 +02:00
Nonoss117
8ef3d7c20e Translated using Weblate (French)
Currently translated at 53.6% (37 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-12 09:23:53 +02:00
Ángel Fernández Sánchez
428fd6cbba Translated using Weblate (Spanish)
Currently translated at 100.0% (69 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-12 09:23:53 +02:00
Ben Phelps
ee79335eff Merge pull request #116 from JazzFisch/additional-widgets
Add Readarr and SABnzbd widgets
2022-09-12 10:23:49 +03:00
Ben Phelps
83d7100dd1 normalize media streaming widget padding 2022-09-12 09:41:44 +03:00
Chris McGravey
ccd9049806 Merge branch 'main' of https://github.com/ItsJustMeChris/homepage 2022-09-12 01:38:43 -05:00
Chris McGravey
769f36fa8e - Change block to return configure translation text 2022-09-12 01:38:29 -05:00
Chris
ffe89b02e9 Merge branch 'benphelps:main' into main 2022-09-12 01:32:09 -05:00
Chris McGravey
1c158f743c - Add CoinMarketCap widget 2022-09-12 01:30:42 -05:00
Ben Phelps
4531985032 fix standalone docker widget 2022-09-12 06:18:51 +03:00
Jason Fischer
f8aa1ba391 Add Readarr and SABnzbd widgets 2022-09-11 19:49:18 -07:00
Francisco Coelho
9d790894d5 Sabnzbd Support 2022-09-12 02:22:39 +01:00
Francisco Coelho
eeac1200e7 Update credentialed.js 2022-09-11 22:11:14 +01:00
Francisco Coelho
a304d87b8a Merge branch 'main' of https://github.com/xicopitz/homepage 2022-09-11 22:08:11 +01:00
Ben Phelps
ffbb1f5f0b tweak widget layouts for mobile 2022-09-11 21:02:33 +03:00
Ben Phelps
ad53119088 fix theme selector on mobile 2022-09-11 19:11:58 +03:00
Anonymous
fe1c525fb7 Translated using Weblate (Dutch)
Currently translated at 88.4% (61 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-11 16:24:48 +02:00
Anonymous
323375e8e4 Translated using Weblate (Vietnamese)
Currently translated at 46.3% (32 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-11 16:24:48 +02:00
Anonymous
8af27ea86d Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.7% (64 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-11 16:24:48 +02:00
Anonymous
9335be0049 Translated using Weblate (Italian)
Currently translated at 13.0% (9 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-11 16:24:47 +02:00
Anonymous
8e0f265080 Translated using Weblate (Chinese (Simplified))
Currently translated at 86.9% (60 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-11 16:24:47 +02:00
Anonymous
ae357159ef Translated using Weblate (Russian)
Currently translated at 14.4% (10 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-11 16:24:47 +02:00
Anonymous
387d40910f Translated using Weblate (Portuguese)
Currently translated at 30.4% (21 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-11 16:24:47 +02:00
Anonymous
5344485a4f Translated using Weblate (French)
Currently translated at 26.0% (18 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-11 16:24:46 +02:00
Anonymous
21f2f2a215 Translated using Weblate (Spanish)
Currently translated at 92.7% (64 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-11 16:24:46 +02:00
Anonymous
9586fac665 Translated using Weblate (German)
Currently translated at 92.7% (64 of 69 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-11 16:24:46 +02:00
Ben Phelps
4aedda7ba2 add Overseerr widget 2022-09-11 17:24:33 +03:00
Ben Phelps
f79213c9d3 update language attributions 2022-09-11 17:24:12 +03:00
Anonymous
eeddcb26a0 Translated using Weblate (Dutch)
Currently translated at 92.4% (61 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-11 16:02:46 +02:00
Anonymous
725922db78 Translated using Weblate (Vietnamese)
Currently translated at 48.4% (32 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/vi/
2022-09-11 16:02:46 +02:00
Anonymous
1846dcaba9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.9% (64 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nb_NO/
2022-09-11 16:02:45 +02:00
Anonymous
ab96d88ebe Translated using Weblate (Italian)
Currently translated at 13.6% (9 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/it/
2022-09-11 16:02:45 +02:00
Anonymous
a3bf28915d Translated using Weblate (Chinese (Simplified))
Currently translated at 90.9% (60 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/zh_Hans/
2022-09-11 16:02:45 +02:00
Anonymous
47920a5f7a Translated using Weblate (Russian)
Currently translated at 15.1% (10 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-11 16:02:45 +02:00
Anonymous
1c10903823 Translated using Weblate (Portuguese)
Currently translated at 31.8% (21 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/pt/
2022-09-11 16:02:45 +02:00
Anonymous
96e4133517 Translated using Weblate (French)
Currently translated at 27.2% (18 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-11 16:02:44 +02:00
Anonymous
8b5167a911 Translated using Weblate (Spanish)
Currently translated at 96.9% (64 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-11 16:02:44 +02:00
Anonymous
45edab5d88 Translated using Weblate (German)
Currently translated at 96.9% (64 of 66 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/de/
2022-09-11 16:02:44 +02:00
desolaris
b4375fb6fc Translated using Weblate (Russian)
Currently translated at 15.6% (10 of 64 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/ru/
2022-09-11 16:02:39 +02:00
Ben Phelps
bd2b28a7ac redesigned media streaming widgets 2022-09-11 17:01:51 +03:00
Ben Phelps
53149df5f1 handle proxy methods other than GET 2022-09-11 14:30:28 +03:00
Ben Phelps
bc2025b3ba handle 204 and 304 proxy responses 2022-09-11 14:30:14 +03:00
Ben Phelps
236450f6f1 add error logging to services fetching 2022-09-11 14:28:29 +03:00
Ben Phelps
fb9e03b31d attempt to fix layout shift on resource widgets 2022-09-11 14:28:12 +03:00
Ben Phelps
31ccb9c933 fix no disk case 2022-09-11 14:21:16 +03:00
Ben Phelps
6e01a743df support array of disks, for disk resource widget 2022-09-11 14:13:58 +03:00
Ben Phelps
ed65c89516 blur backdrops for better background image support 2022-09-11 13:46:01 +03:00
Ben Phelps
b8b8dad9fb Update README.md 2022-09-11 11:35:00 +03:00
Ben Phelps
690d17e132 Merge branch 'main' of github.com:benphelps/homepage 2022-09-11 11:34:00 +03:00
Ben Phelps
a1e9912b36 update readme 2022-09-11 11:33:36 +03:00
deffcolony
26914bfb09 Translated using Weblate (Dutch)
Currently translated at 95.3% (61 of 64 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/nl/
2022-09-11 10:23:22 +02:00
J. Lavoie
079fdb3011 Translated using Weblate (French)
Currently translated at 28.1% (18 of 64 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/fr/
2022-09-11 10:23:22 +02:00
Ángel Fernández Sánchez
102cdbd53a Translated using Weblate (Spanish)
Currently translated at 100.0% (64 of 64 strings)

Translation: Homepage/Homepage
Translate-URL: https://hosted.weblate.org/projects/homepage/homepage/es/
2022-09-11 10:23:22 +02:00
Ben Phelps
d861264ecf fix error case cause failure to load anything 2022-09-11 11:13:54 +03:00
Francisco Coelho
9831df1427 Update proxy.js 2022-09-11 04:39:40 +01:00
Francisco Coelho
5e6312fe93 Add Gotify Service 2022-09-11 04:11:02 +01:00
43 changed files with 1448 additions and 590 deletions

View File

@@ -5,23 +5,27 @@
## Features
* Fast! The entire site is statically generated at build time, so you can expect instant load times.
* Full i18n support with automatic language detection.
- Human translations for English, Norwegian Bokmål ([comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu)) and Spanish ([AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves)).
- Machine translations for Portuguese, French, German, Russian and Chinese (simplified).
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/).
* Complete Docker support, including AMD64, ARM64, ARMv7 and ARMv6 support ([schklom](https://github.com/benphelps/homepage/pull/3) and [modem7](https://github.com/benphelps/homepage/pull/62))
* Fast! The entire site is statically generated at build time, so you can expect instant load times
* Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
* Full i18n support with automatic language detection
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Portuguese, Russian and Spanish
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
* Service & Web Bookmarks
* Docker Integration
- Status light + CPU, Memory & Network Reporting *(click on the status light)*
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
- Automatic service discovery (via labels)
* Service Integration
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Tautulli (Plex), Jellyseerr ([ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
- Sonarr, Radarr, Readarr, Emby, Jellyfin, Tautulli (Plex)
- Ombi, Overseerr, Jellyseerr, NZBGet, SABnzbd, ruTorrent
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager, Gotify
* Information Providers
- Coin Market Cap
* Information & Utility Widgets
- System Stats (Disk, CPU, Memory)
- Weather via WeatherAPI.com or OpenWeatherMap ([AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
- Weather via WeatherAPI.com or OpenWeatherMap
- Automatic location detection (with HTTPS), or manual location selection
- Search Bar ([aidenpwnz](https://github.com/benphelps/homepage/pull/45))
- Search Bar
* Customizable
- 21 theme colors with light and dark mode support
- Background image support
@@ -106,3 +110,25 @@ pnpm dev
Open [http://localhost:3000](http://localhost:3000) to start.
This is a [Next.js](https://nextjs.org/) application, see their doucmentation for more information:
## Contributors
Huge thanks to the all the contributors who have helped make this project what it is today! In alphabetical order:
* [aidenpwnz](https://github.com/benphelps/homepage/commits?author=aidenpwnz) - Nginx Proxy Manager, Search Bar Widget
* [AlexFullmoon](https://github.com/benphelps/homepage/commits?author=AlexFullmoon) - OpenWeatherMap Widget
* [AmadeusGraves](https://github.com/benphelps/homepage/commits?author=AmadeusGraves) - Spanish Translation
* [boerniee](https://github.com/benphelps/homepage/commits?author=boerniee) - German Translation
* [comradekingu](https://github.com/benphelps/homepage/commits?author=comradekingu) - Norwegian Bokmål Translation
* [deffcolony](https://github.com/benphelps/homepage/commits?author=deffcolony) - Dutch Translation
* [desolaris](https://github.com/benphelps/homepage/commits?author=desolaris) - Russian Translation
* [ilusi0n](https://github.com/benphelps/homepage/commits?author=ilusi0n) - Jellyseerr Integration
* [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
* [jackblk](https://github.com/benphelps/homepage/commits?author=jackblk) - Vietnamese Translation
* [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, SABnzbd Integrations
* [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
* [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese Translation
* [Nonoss117](https://github.com/benphelps/homepage/commits?author=Nonoss117) - French Translation
* [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
* [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
* [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify Integration

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -3,9 +3,9 @@ const nextConfig = {
reactStrictMode: true,
output: "standalone",
swcMinify: false,
experimental: { images: { allowFutureImage: true, unoptimized: true } },
images: {
domains: ["cdn.jsdelivr.net"],
unoptimized: true,
},
};

View File

@@ -9,9 +9,10 @@
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^1.6.6",
"@headlessui/react": "^1.7.0",
"@tailwindcss/forms": "^0.5.3",
"classnames": "^2.3.1",
"currency-symbol-map": "^5.1.0",
"dockerode": "^3.3.4",
"i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.5",
@@ -19,32 +20,32 @@
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.4.1",
"memory-cache": "^0.2.0",
"next": "12.2.5",
"next": "^12.3.0",
"node-os-utils": "^1.3.7",
"pretty-bytes": "^6.0.0",
"raw-body": "^2.5.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^11.18.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.6",
"react-icons": "^4.4.0",
"rutorrent-promise": "^2.0.0",
"shvl": "^3.0.0",
"swr": "^1.3.0"
},
"devDependencies": {
"autoprefixer": "^10.4.8",
"eslint": "8.22.0",
"autoprefixer": "^10.4.9",
"eslint": "^8.23.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "12.2.5",
"eslint-config-next": "^12.3.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"tailwindcss": "^3.1.8",
"typescript": "^4.8.2"
"typescript": "^4.8.3"
}
}

467
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,17 +22,14 @@
"emby": {
"playing": "Spielen",
"transcoding": "Transcodierung",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Spielen",
"transcoding": "Transcodierung",
"bitrate": "Bitrate"
},
"nzbget": {
"rate": "Rate",
"remaining": "Verblieben",
"downloaded": "Heruntergeladen"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Aktiv",
@@ -49,6 +46,11 @@
"queued": "In Warteschlange",
"movies": "Filme"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Ausstehend",
"approved": "Genehmigt",
@@ -89,5 +91,28 @@
"allow": "Zum Zulassen anklicken",
"updating": "Aktualisieren",
"wait": "Bitte warten"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -39,18 +39,25 @@
"emby": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"rutorrent": {
"active": "Active",
"upload": "Upload",
@@ -66,6 +73,11 @@
"queued": "Queued",
"movies": "Movies"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
@@ -76,6 +88,11 @@
"approved": "Approved",
"available": "Available"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
@@ -100,5 +117,13 @@
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -13,8 +13,8 @@
"used": "Usado"
},
"docker": {
"rx": "RX",
"tx": "TX",
"rx": "Recibido",
"tx": "Transmitido",
"mem": "Memoria",
"cpu": "Procesador",
"offline": "Desconectado"
@@ -22,17 +22,14 @@
"emby": {
"playing": "En ejecución",
"transcoding": "Transcodificando",
"bitrate": "Tasa de Bits"
"bitrate": "Tasa de Bits",
"no_active": "No hay streams activos"
},
"tautulli": {
"playing": "En ejecución",
"transcoding": "Transcodificación",
"bitrate": "Tasa de bits"
},
"nzbget": {
"rate": "Velocidad",
"remaining": "Restante",
"downloaded": "Descargado"
"bitrate": "Tasa de bits",
"no_active": "No hay streams activos"
},
"rutorrent": {
"active": "Activo",
@@ -49,6 +46,11 @@
"queued": "Puesto en cola",
"movies": "Películas"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pendiente",
"approved": "Aprobado",
@@ -85,9 +87,32 @@
"total": "Total"
},
"weather": {
"current": "Current Location",
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
"current": "Ubicación Actual",
"allow": "Haga clic para permitir",
"updating": "Actualizando",
"wait": "Espere, por favor"
},
"overseerr": {
"pending": "Pendiente",
"approved": "Aprobado",
"available": "Disponible"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -5,50 +5,52 @@
"status": "Statut"
},
"search": {
"placeholder": "Chercher…"
"placeholder": "Recherche…"
},
"resources": {
"total": "Totale",
"total": "Total",
"free": "Libre",
"used": "Utilisée"
},
"docker": {
"rx": "Rx",
"tx": "TX",
"mem": "Mem",
"cpu": "CPU",
"tx": "Tx",
"mem": "Mém",
"cpu": "Cpu",
"offline": "Hors ligne"
},
"emby": {
"playing": "En jouant",
"transcoding": "Transcoding",
"bitrate": "Débiter"
"playing": "En lecture",
"transcoding": "Transcodage",
"bitrate": "Débit",
"no_active": "Aucun flux actif"
},
"tautulli": {
"playing": "En jouant",
"transcoding": "Transcoding",
"bitrate": "Débiter"
},
"nzbget": {
"rate": "Évaluer",
"remaining": "Restante",
"downloaded": "Téléchargé"
"playing": "En lecture",
"transcoding": "Transcodage",
"bitrate": "Débit",
"no_active": "Aucun flux actif"
},
"rutorrent": {
"active": "Active",
"upload": "Télécharger",
"upload": "Téléverser",
"download": "Télécharger"
},
"sonarr": {
"wanted": "Recherchée",
"queued": "En queue",
"series": "Série"
"series": "Séries"
},
"radarr": {
"wanted": "Recherchée",
"queued": "En queue",
"movies": "Films"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "En attente",
"approved": "Approuvée",
@@ -65,14 +67,14 @@
"gravity": "La gravité"
},
"speedtest": {
"upload": "Télécharger",
"download": "Télécharger",
"upload": "Téléversement",
"download": "Téléchargement",
"ping": "Ping-ping"
},
"portainer": {
"running": "Fonctionnement",
"stopped": "Arrêté",
"total": "Totale"
"total": "Total"
},
"traefik": {
"routers": "Routeurs",
@@ -81,8 +83,8 @@
},
"npm": {
"enabled": "Activé",
"disabled": "Handicapée",
"total": "Totale"
"disabled": "Désactivé",
"total": "Total"
},
"common": {
"bbytes": "{{value, bytes(binary: true)}}",
@@ -96,9 +98,32 @@
"ms": "{{value, number}}"
},
"weather": {
"current": "Current Location",
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
"current": "Localisation actuelle",
"allow": "Cliquez pour autoriser",
"updating": "Mise à jour",
"wait": "Veuillez patienter"
},
"overseerr": {
"pending": "En attente",
"approved": "Approuvée",
"available": "Disponible"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"remaining": "Remaining",
"downloaded": "Downloaded",
"rate": "Rate"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -7,14 +7,16 @@
"rx": "RX"
},
"emby": {
"playing": "Playing",
"playing": "In riproduzione",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "Nessuno Stream Attivo"
},
"tautulli": {
"playing": "Playing",
"playing": "In riproduzione",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "Nessuno Stream Attivo"
},
"speedtest": {
"upload": "Upload",
@@ -22,72 +24,95 @@
"ping": "Ping"
},
"portainer": {
"running": "Running",
"stopped": "Stopped",
"total": "Total"
"running": "In esecuzione",
"stopped": "Fermati",
"total": "Totali"
},
"traefik": {
"routers": "Routers",
"services": "Services",
"services": "Servizi",
"middleware": "Middleware"
},
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
"status": "Status"
"api_error": "Errore API",
"status": "Stato"
},
"search": {
"placeholder": "Search…"
"placeholder": "Cerca…"
},
"resources": {
"total": "Total",
"free": "Free",
"used": "Used"
"total": "Totale",
"free": "Libero",
"used": "In utilizzo"
},
"rutorrent": {
"active": "Attivo",
"upload": "Upload",
"download": "Download"
},
"sonarr": {
"series": "Serie",
"wanted": "Rchiesti",
"queued": "In coda"
},
"radarr": {
"wanted": "Richiesti",
"queued": "In coda",
"movies": "Film"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"jellyseerr": {
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"pihole": {
"queries": "Richieste",
"blocked": "Bloccati",
"gravity": "Severità"
},
"npm": {
"enabled": "Attivi",
"disabled": "Disabilitati",
"total": "Totali"
},
"weather": {
"current": "Posizione Attuale",
"allow": "Clicca per consentire",
"updating": "Aggiornamento in corso",
"wait": "Attendi per favore"
},
"overseerr": {
"pending": "In attesa",
"approved": "Approvati",
"available": "Disponibili"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"rutorrent": {
"active": "Active",
"upload": "Upload",
"download": "Download"
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"sonarr": {
"series": "Series",
"wanted": "Wanted",
"queued": "Queued"
},
"radarr": {
"wanted": "Wanted",
"queued": "Queued",
"movies": "Movies"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
"gravity": "Gravity"
},
"npm": {
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"weather": {
"current": "Current Location",
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -22,17 +22,14 @@
"emby": {
"playing": "Spiller",
"transcoding": "Transkoding",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Spiller",
"transcoding": "Transkoding",
"bitrate": "Bitrate"
},
"nzbget": {
"rate": "Takt",
"remaining": "Gjenstående",
"downloaded": "Nedlastet"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Aktiv",
@@ -49,6 +46,11 @@
"queued": "I kø",
"movies": "Filmer"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Venter",
"approved": "Godkjent",
@@ -89,5 +91,28 @@
"updating": "Oppdaterer …",
"wait": "Vent litt …",
"current": "Nåværende posisjon"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"downloaded": "Downloaded",
"remaining": "Remaining"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -5,9 +5,9 @@
"status": "Status"
},
"resources": {
"total": "Total",
"free": "Free",
"used": "Used"
"total": "Totaal",
"free": "Vrij",
"used": "Gebruikt"
},
"docker": {
"rx": "RX",
@@ -16,68 +16,70 @@
"cpu": "CPU",
"offline": "Offline"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"speedtest": {
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Running",
"stopped": "Stopped",
"total": "Total"
"running": "Draaiend",
"stopped": "Gestopt",
"total": "Totaal"
},
"weather": {
"updating": "Updating",
"wait": "Please wait",
"current": "Current Location",
"allow": "Click to allow"
"updating": "Updaten",
"wait": "Even geduld",
"current": "Huidige Locatie",
"allow": "Klik om toe te staan"
},
"search": {
"placeholder": "Search…"
"placeholder": "Zoeken…"
},
"emby": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"playing": "Afspelen",
"transcoding": "Transcodering",
"bitrate": "Bitsnelheid",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate"
"playing": "Afspelen",
"transcoding": "Transcodering",
"bitrate": "Bitsnelheid",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Active",
"active": "Actief",
"upload": "Upload",
"download": "Download"
},
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",
"wanted": "Gezocht",
"queued": "In de wachtrij",
"series": "Series"
},
"radarr": {
"movies": "Movies",
"movies": "Films",
"wanted": "Gezocht",
"queued": "In de wachtrij"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued"
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "In afwachting",
"approved": "Goedgekeurd",
"available": "Beschikbaar"
},
"jellyseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
"pending": "In afwachting",
"approved": "Goedgekeurd",
"available": "Beschikbaar"
},
"pihole": {
"queries": "Queries",
"blocked": "Blocked",
"blocked": "Geblokkeerd",
"gravity": "Gravity"
},
"traefik": {
@@ -86,8 +88,31 @@
"middleware": "Middleware"
},
"npm": {
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"total": "Totaal"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -22,17 +22,14 @@
"emby": {
"playing": "A reproduzir",
"transcoding": "Transcodificação",
"bitrate": "Taxa de bits"
"bitrate": "Taxa de bits",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Reproduzindo",
"transcoding": "Transcodificação",
"bitrate": "Taxa de bits"
},
"nzbget": {
"rate": "Avaliar",
"remaining": "Em falta",
"downloaded": "Baixada"
"bitrate": "Taxa de bits",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Ativa",
@@ -49,6 +46,11 @@
"queued": "Enfileiradas",
"movies": "Filmes"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pendente",
"approved": "Aprovada",
@@ -100,5 +102,28 @@
"allow": "Clicar para permitir",
"updating": "A atualizar",
"wait": "Por favor aguarde"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Aplicações",
"clients": "Clientes",
"messages": "Mensagens"
}
}

View File

@@ -10,29 +10,26 @@
"resources": {
"total": "Общий",
"free": "Свободно",
"used": "Использовал"
"used": "Использовано"
},
"docker": {
"rx": "Rx",
"tx": "Техас",
"mem": "Мем",
"tx": "Тx",
"mem": "Память",
"cpu": "Процессор",
"offline": "Не в сети"
},
"emby": {
"playing": "Игра",
"playing": "Воспроизведение",
"transcoding": "Транскодирование",
"bitrate": "Битрейт"
"bitrate": "Битрейт",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Игра",
"playing": "Воспроизведение",
"transcoding": "Транскодирование",
"bitrate": "Битрейт"
},
"nzbget": {
"rate": "Оценивать",
"remaining": "Оставшийся",
"downloaded": "Загружен"
"bitrate": "Битрейт",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Активный",
@@ -49,45 +46,73 @@
"queued": "В очереди",
"movies": "Фильмы"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "В ожидании",
"approved": "Одобренный",
"available": "Доступный"
"pending": "Ожидание",
"approved": "Одобрено",
"available": "Доступно"
},
"jellyseerr": {
"pending": "В ожидании",
"approved": "Одобренный",
"available": "Доступный"
"pending": "Ожидание",
"approved": "Одобрено",
"available": "Доступно"
},
"pihole": {
"queries": "Запросы",
"blocked": "Заблокированный",
"blocked": "Заблокировано",
"gravity": "Сила тяжести"
},
"speedtest": {
"upload": "Загрузить",
"upload": "Загрузка",
"download": "Скачать",
"ping": "пинг"
},
"portainer": {
"running": "Бег",
"stopped": "Остановился",
"total": "Общий"
"running": "Запущено",
"stopped": "Остановлено",
"total": "Всего"
},
"traefik": {
"routers": "Маршрутизаторы",
"services": "Услуги",
"services": "Сервисы",
"middleware": "Промежуточное программное обеспечение"
},
"npm": {
"enabled": "Включено",
"disabled": "Неполноценный",
"total": "Общий"
"disabled": "Отключено",
"total": "Всего"
},
"weather": {
"wait": "Please wait",
"current": "Current Location",
"wait": "Пожалуйста подождите",
"current": "Текущее местоположение",
"allow": "Click to allow",
"updating": "Updating"
"updating": "Обновление"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -22,17 +22,14 @@
"emby": {
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate"
},
"nzbget": {
"rate": "Rate",
"remaining": "Còn lại",
"downloaded": "Đã tải"
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "Hoạt động",
@@ -49,6 +46,11 @@
"queued": "Queued",
"movies": "Movies"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "Pending",
"approved": "Approved",
@@ -89,5 +91,28 @@
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -22,17 +22,14 @@
"emby": {
"playing": "玩",
"transcoding": "转码",
"bitrate": "比特率"
"bitrate": "比特率",
"no_active": "No Active Streams"
},
"tautulli": {
"playing": "玩",
"transcoding": "转码",
"bitrate": "比特率"
},
"nzbget": {
"rate": "速度",
"remaining": "其余的",
"downloaded": "下载"
"bitrate": "比特率",
"no_active": "No Active Streams"
},
"rutorrent": {
"active": "积极的",
@@ -49,6 +46,11 @@
"queued": "排队",
"movies": "电影"
},
"readarr": {
"wanted": "Wanted",
"queued": "Queued",
"books": "Books"
},
"ombi": {
"pending": "待办的",
"approved": "得到正式认可的",
@@ -89,5 +91,28 @@
"allow": "Click to allow",
"updating": "Updating",
"wait": "Please wait"
},
"overseerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available"
},
"sabnzbd": {
"rate": "Rate",
"queue": "Queue",
"timeleft": "Time Left"
},
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Downloaded"
},
"coinmarketcap": {
"configure": "Configure one or more crypto currencies to track"
},
"gotify": {
"apps": "Applications",
"clients": "Clients",
"messages": "Messages"
}
}

View File

@@ -6,7 +6,7 @@ export default function Item({ bookmark }) {
<button
type="button"
onClick={() => window.open(bookmark.href, "_blank").focus()}
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20"
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20 backdrop-blur-md"
>
<div className="flex">
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">

View File

@@ -56,7 +56,7 @@ export default function ColorToggle() {
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute -top-[75px] left-0">
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5 w-[85vw] sm:w-full">
<div className="relative grid gap-2 p-2 grid-cols-11 bg-white/50 dark:bg-white/10 shadow-black/10 dark:shadow-black/20 rounded-md shadow-md">
{colors.map((color) => (
<button type="button" onClick={() => setColor(color)} key={color}>

View File

@@ -22,43 +22,58 @@ function resolveIcon(icon) {
}
export default function Item({ service }) {
const handleOnClick = () => {
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
};
const hasLink = service.href && service.href !== "#";
return (
<li key={service.name}>
<Disclosure>
<div
className={`${
service.href && service.href !== "#" ? "cursor-pointer " : "cursor-default "
}transition-all h-15 overflow-hidden mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-700/70 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/20 dark:bg-white/10 dark:hover:bg-white/20`}
hasLink ? "cursor-pointer " : " "
}transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-700/70 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/20 dark:bg-white/10 dark:hover:bg-white/20 backdrop-blur-md`}
>
<div className="flex">
{service.icon && (
<div className="flex select-none">
{service.icon &&
(hasLink ? (
<button
type="button"
onClick={handleOnClick}
className="flex-shrink-0 flex items-center justify-center w-12 "
>
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
</button>
) : (
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
</div>
))}
{hasLink ? (
<button
type="button"
onClick={() => {
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
}}
className="flex-shrink-0 flex items-center justify-center w-12 "
onClick={handleOnClick}
className="flex-1 flex items-center justify-between rounded-r-md "
>
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
<div className="flex-1 px-2 py-2 text-sm text-left">
{service.name}
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
</div>
</button>
) : (
<div className="flex-1 flex items-center justify-between rounded-r-md ">
<div className="flex-1 px-2 py-2 text-sm text-left">
{service.name}
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
</div>
</div>
)}
<button
type="button"
onClick={() => {
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
}}
className="flex-1 flex items-center justify-between rounded-r-md "
>
<div className="flex-1 px-2 py-2 text-sm text-left">
{service.name}
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
</div>
</button>
{service.container && (
<Disclosure.Button
as="div"

View File

@@ -2,10 +2,12 @@ import { useTranslation } from "react-i18next";
import Sonarr from "./widgets/service/sonarr";
import Radarr from "./widgets/service/radarr";
import Readarr from "./widgets/service/readarr";
import Ombi from "./widgets/service/ombi";
import Portainer from "./widgets/service/portainer";
import Emby from "./widgets/service/emby";
import Nzbget from "./widgets/service/nzbget";
import SABnzbd from "./widgets/service/sabnzbd";
import Docker from "./widgets/service/docker";
import Pihole from "./widgets/service/pihole";
import Rutorrent from "./widgets/service/rutorrent";
@@ -13,13 +15,17 @@ import Jellyfin from "./widgets/service/jellyfin";
import Speedtest from "./widgets/service/speedtest";
import Traefik from "./widgets/service/traefik";
import Jellyseerr from "./widgets/service/jellyseerr";
import Overseerr from "./widgets/service/overseerr";
import Npm from "./widgets/service/npm";
import Tautulli from "./widgets/service/tautulli";
import CoinMarketCap from "./widgets/service/coinmarketcap";
import Gotify from "./widgets/service/gotify";
const widgetMappings = {
docker: Docker,
sonarr: Sonarr,
radarr: Radarr,
readarr: Readarr,
ombi: Ombi,
portainer: Portainer,
emby: Emby,
@@ -30,8 +36,12 @@ const widgetMappings = {
speedtest: Speedtest,
traefik: Traefik,
jellyseerr: Jellyseerr,
overseerr: Overseerr,
coinmarketcap: CoinMarketCap,
npm: Npm,
tautulli: Tautulli,
gotify: Gotify,
sabnzbd: SABnzbd
};
export default function Widget({ service }) {

View File

@@ -0,0 +1,71 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import getSymbolFromCurrency from "currency-symbol-map";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function CoinMarketCap({ service }) {
const { t } = useTranslation();
const config = service.widget;
const currencyCode = config.currency ?? "USD";
const { symbols } = config;
const { data: statsData, error: statsError } = useSWR(
formatApiUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`)
);
if (!symbols || symbols.length === 0) {
return (
<Widget>
<Block value={t("coinmarketcap.configure")} />
</Widget>
);
}
if (statsError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statsData) {
return (
<Widget>
<Block value={t("coinmarketcap.configure")} />
</Widget>
);
}
const { data } = statsData;
const currencySymbol = getSymbolFromCurrency(currencyCode);
return (
<Widget>
<div className="flex flex-col w-full">
{symbols.map((key) => (
<div
key={data[key].symbol}
className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs"
>
<div className="font-thin pl-2">{data[key].name}</div>
<div className="flex flex-row text-right">
<div className="font-bold mr-2">
{currencySymbol}
{data[key].quote[currencyCode].price.toFixed(2)}
</div>
<div
className={`font-bold w-10 mr-2 ${
data[key].quote[currencyCode].percent_change_1h > 0 ? "text-emerald-300" : "text-rose-300"
}`}
>
{data[key].quote[currencyCode].percent_change_1h.toFixed(2)}%
</div>
</div>
</div>
))}
</div>
</Widget>
);
}

View File

@@ -1,17 +1,149 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
function ticksToTime(ticks) {
const milliseconds = ticks / 10000;
const seconds = Math.floor((milliseconds / 1000) % 60);
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
return { hours, minutes, seconds };
}
function ticksToString(ticks) {
const { hours, minutes, seconds } = ticksToTime(ticks);
const parts = [];
if (hours > 0) {
parts.push(hours);
}
parts.push(minutes);
parts.push(seconds);
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
}
function SingleSessionEntry({ playCommand, session }) {
console.log(session);
const {
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
PlayState: { PositionTicks, IsPaused, IsMuted },
} = session;
const percent = (PositionTicks / RunTimeTicks) * 100;
return (
<>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div className="text-xs z-10 self-center ml-2">
<span>
{Name}
{SeriesName && ` - ${SeriesName}`}
</span>
</div>
<div className="grow" />
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
</div>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
style={{
width: `${percent}%`,
}}
/>
<div className="text-xs z-10 self-center ml-1">
{IsPaused && (
<BsFillPlayFill
onClick={() => {
playCommand(session, "Unpause");
}}
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
/>
)}
{!IsPaused && (
<BsPauseFill
onClick={() => {
playCommand(session, "Pause");
}}
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
/>
)}
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
</div>
</>
);
}
function SessionEntry({ playCommand, session }) {
const {
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
PlayState: { PositionTicks, IsPaused, IsMuted },
} = session;
const percent = (PositionTicks / RunTimeTicks) * 100;
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
style={{
width: `${percent}%`,
}}
/>
<div className="text-xs z-10 self-center ml-1">
{IsPaused && (
<BsFillPlayFill
onClick={() => {
playCommand(session, "Unpause");
}}
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
/>
)}
{!IsPaused && (
<BsPauseFill
onClick={() => {
playCommand(session, "Pause");
}}
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
/>
)}
<span>
{Name}
{SeriesName && ` - ${SeriesName}`}
</span>
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
</div>
);
}
export default function Emby({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
const {
data: sessionsData,
error: sessionsError,
mutate: sessionMutate,
} = useSWR(formatApiUrl(config, "Sessions"), {
refreshInterval: 5000,
});
async function handlePlayCommand(session, command) {
const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`);
await fetch(url, {
method: "POST",
}).then(() => {
sessionMutate();
});
}
if (sessionsError) {
return <Widget error={t("widget.api_error")} />;
@@ -19,26 +151,63 @@ export default function Emby({ service }) {
if (!sessionsData) {
return (
<Widget>
<Block label={t("emby.playing")} />
<Block label={t("emby.transcoding")} />
<Block label={t("emby.bitrate")} />
</Widget>
<div className="flex flex-col pb-1">
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
</div>
);
}
const playing = sessionsData.filter((session) => session?.NowPlayingItem);
const transcoding = sessionsData.filter(
(session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
);
const playing = sessionsData
.filter((session) => session?.NowPlayingItem)
.sort((a, b) => {
if (a.PlayState.PositionTicks > b.PlayState.PositionTicks) {
return 1;
}
if (a.PlayState.PositionTicks < b.PlayState.PositionTicks) {
return -1;
}
return 0;
});
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
if (playing.length === 0) {
return (
<div className="flex flex-col pb-1 mx-1">
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
</div>
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
</div>
);
}
if (playing.length === 1) {
const session = playing[0];
return (
<div className="flex flex-col pb-1 mx-1">
<SingleSessionEntry
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
session={session}
/>
</div>
);
}
return (
<Widget>
<Block label={t("emby.playing")} value={playing.length} />
<Block label={t("emby.transcoding")} value={transcoding.length} />
<Block label={t("emby.bitrate")} value={t("common.bitrate", { value: bitrate })} />
</Widget>
<div className="flex flex-col pb-1 mx-1">
{playing.map((session) => (
<SessionEntry
key={session.Id}
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
session={session}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,29 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Gotify({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: appsData, error: appsError } = useSWR(formatApiUrl(config, `application`));
const { data: messagesData, error: messagesError } = useSWR(formatApiUrl(config, `message`));
const { data: clientsData, error: clientsError } = useSWR(formatApiUrl(config, `client`));
if (appsError || messagesError || clientsError) {
return <Widget error={t("widget.api_error")} />;
}
return (
<Widget>
<Block label={t("gotify.apps")} value={appsData?.length} />
<Block label={t("gotify.clients")} value={clientsData?.length} />
<Block label={t("gotify.messages")} value={messagesData?.messages?.length} />
</Widget>
);
}

View File

@@ -1,48 +1,5 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
import Emby from "./emby";
export default function Jellyfin({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
if (sessionsError) {
return <Widget error={t("widget.api_error")} />;
}
if (!sessionsData) {
return (
<Widget>
<Block label={t("emby.playing")} />
<Block label={t("emby.transcoding")} />
<Block label={t("emby.bitrate")} />
</Widget>
);
}
const playing = sessionsData.filter((session) => session?.NowPlayingItem);
const transcoding = sessionsData.filter(
(session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
);
const bitrate = playing.reduce(
(acc, session) =>
acc + session.NowPlayingQueueFullItems[0].MediaSources.reduce((acb, source) => acb + source.Bitrate, 0),
0
);
return (
<Widget>
<Block label={t("emby.playing")} value={playing.length} />
<Block label={t("emby.transcoding")} value={transcoding.length} />
<Block label={t("emby.bitrate")} value={t("common.bitrate", { value: bitrate })} />
</Widget>
);
return <Emby service={service} />;
}

View File

@@ -0,0 +1,37 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Overseerr({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
if (statsError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statsData) {
return (
<Widget>
<Block label={t("overseerr.pending")} />
<Block label={t("overseerr.approved")} />
<Block label={t("overseerr.available")} />
</Widget>
);
}
return (
<Widget>
<Block label={t("overseerr.pending")} value={statsData.pending} />
<Block label={t("overseerr.approved")} value={statsData.approved} />
<Block label={t("overseerr.available")} value={statsData.available} />
</Widget>
);
}

View File

@@ -0,0 +1,41 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function Readarr({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: booksData, error: booksError } = useSWR(formatApiUrl(config, "book"));
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status"));
if (booksError || wantedError || queueError) {
return <Widget error={t("widget.api_error")} />;
}
if (!booksData || !wantedData || !queueData) {
return (
<Widget>
<Block label={t("readarr.wanted")} />
<Block label={t("readarr.queued")} />
<Block label={t("readarr.books")} />
</Widget>
);
}
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
return (
<Widget>
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("readarr.queued")} value={queueData.totalCount} />
<Block label={t("readarr.books")} value={have.length} />
</Widget>
);
}

View File

@@ -0,0 +1,37 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
export default function SABnzbd({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue"));
if (queueError) {
return <Widget error={t("widget.api_error")} />;
}
if (!queueData) {
return (
<Widget>
<Block label={t("sabnzbd.rate")} />
<Block label={t("sabnzbd.queue")} />
<Block label={t("sabnzbd.timeleft")} />
</Widget>
);
}
return (
<Widget>
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}bps`} />
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
</Widget>
);
}

View File

@@ -1,39 +1,157 @@
/* eslint-disable camelcase */
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import Widget from "../widget";
import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
function millisecondsToTime(milliseconds) {
const seconds = Math.floor((milliseconds / 1000) % 60);
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
return { hours, minutes, seconds };
}
function millisecondsToString(milliseconds) {
const { hours, minutes, seconds } = millisecondsToTime(milliseconds);
const parts = [];
if (hours > 0) {
parts.push(hours);
}
parts.push(minutes);
parts.push(seconds);
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
}
function SingleSessionEntry({ session }) {
const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
return (
<>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div className="text-xs z-10 self-center ml-2">
<span>{full_title}</span>
</div>
<div className="grow" />
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</div>
</div>
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
style={{
width: `${progress_percent}%`,
}}
/>
<div className="text-xs z-10 self-center ml-1">
{state === "paused" && (
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
)}
{state !== "paused" && (
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
)}
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
</div>
</div>
</>
);
}
function SessionEntry({ session }) {
const { full_title, view_offset, progress_percent, state } = session;
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0"
style={{
width: `${progress_percent}%`,
}}
/>
<div className="text-xs z-10 self-center ml-1">
{state === "paused" && (
<BsFillPlayFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
)}
{state !== "paused" && (
<BsPauseFill className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" />
)}
<span>{full_title}</span>
</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2">{millisecondsToString(view_offset)}</div>
</div>
);
}
export default function Tautulli({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), {
refreshInterval: 5000,
});
if (statsError) {
if (activityError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statsData) {
if (!activityData) {
return (
<Widget>
<Block label={t("tautulli.playing")} />
<Block label={t("tautulli.transcoding")} />
<Block label={t("tautulli.bitrate")} />
</Widget>
<div className="flex flex-col pb-1 mx-1">
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
</div>
);
}
const { data } = statsData.response;
const playing = activityData.response.data.sessions.sort((a, b) => {
if (a.view_offset > b.view_offset) {
return 1;
}
if (a.view_offset < b.view_offset) {
return -1;
}
return 0;
});
if (playing.length === 0) {
return (
<div className="flex flex-col pb-1 mx-1">
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">{t("tautulli.no_active")}</span>
</div>
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<span className="absolute left-2 text-xs mt-[2px]">-</span>
</div>
</div>
);
}
if (playing.length === 1) {
const session = playing[0];
return (
<div className="flex flex-col pb-1 mx-1">
<SingleSessionEntry session={session} />
</div>
);
}
return (
<Widget>
<Block label={t("tautulli.playing")} value={data.stream_count} />
<Block label={t("tautulli.transcoding")} value={data.stream_count_transcode} />
<Block label={t("tautulli.bitrate")} value={t("common.bitrate", { value: data.total_bandwidth })} />
</Widget>
<div className="flex flex-col pb-1 mx-1">
{playing.map((session) => (
<SessionEntry key={session.Id} session={session} />
))}
</div>
);
}

View File

@@ -16,7 +16,7 @@ function Widget({ options }) {
if (error || data?.cod === 401 || data?.error) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -32,7 +32,7 @@ function Widget({ options }) {
if (!data) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -49,7 +49,7 @@ function Widget({ options }) {
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon
@@ -100,7 +100,7 @@ export default function OpenWeatherMap({ options }) {
if (!location) {
return (
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (

View File

@@ -14,7 +14,7 @@ export default function Cpu() {
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
@@ -25,7 +25,7 @@ export default function Cpu() {
if (!data) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
@@ -37,11 +37,12 @@ export default function Cpu() {
const percent = data.cpu.usage;
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left font-mono min-w-[50px]">
<div className="flex flex-col ml-3 text-left font-mono min-w-[80px]">
<div className="text-theme-800 dark:text-theme-200 text-xs">
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}{" "}
{t("docker.cpu")}
</div>
<UsageBar percent={percent} />
</div>

View File

@@ -14,7 +14,7 @@ export default function Disk({ options }) {
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
@@ -25,7 +25,7 @@ export default function Disk({ options }) {
if (!data) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
@@ -37,9 +37,9 @@ export default function Disk({ options }) {
const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100);
return (
<div className="flex-none flex flex-row items-center justify-center group">
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left ">
<div className="flex flex-col ml-3 text-left min-w-[80px]">
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
</span>

View File

@@ -14,7 +14,7 @@ export default function Memory() {
if (error || data?.error) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
@@ -25,7 +25,7 @@ export default function Memory() {
if (!data) {
return (
<div className="flex-none flex flex-row items-center justify-center">
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
@@ -37,9 +37,9 @@ export default function Memory() {
const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100);
return (
<div className="flex-none flex flex-row items-center justify-center group">
<div className="flex-none flex flex-row items-center mr-3 py-1.5 group">
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<div className="flex flex-col ml-3 text-left min-w-[80px]">
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
</span>

View File

@@ -4,11 +4,13 @@ import Memory from "./memory";
export default function Resources({ options }) {
return (
<div className="flex flex-col max-w:full basis-1/2 sm:basis-auto self-center">
<div className="flex flex-row space-x-4 self-center">
<div className="flex flex-col max-w:full sm:basis-auto self-center m-auto flex-wrap">
<div className="flex flex-row self-center flex-wrap justify-between">
{options.cpu && <Cpu />}
{options.memory && <Memory />}
{options.disk && <Disk options={options} />}
{Array.isArray(options.disk)
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} />)
: options.disk && <Disk options={options} />}
</div>
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>

View File

@@ -1,6 +1,6 @@
export default function UsageBar({ percent }) {
return (
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20">
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-white/20 backdrop-blur-md">
<div
className="bg-theme-800/70 h-1 rounded-full dark:bg-white/50"
style={{

View File

@@ -51,7 +51,7 @@ export default function Search({ options }) {
}
return (
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow" onSubmit={handleSubmit}>
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"
@@ -62,7 +62,8 @@ export default function Search({ options }) {
bg-white/50 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50
focus:border-theme-500 dark:focus:border-white/50
border border-theme-300 dark:border-theme-200/50"
border border-theme-300 dark:border-theme-200/50
backdrop-blur-md"
placeholder={t("search.placeholder")}
onChange={(s) => setQuery(s.currentTarget.value)}
required

View File

@@ -16,7 +16,7 @@ function Widget({ options }) {
if (error || data?.error) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -32,7 +32,7 @@ function Widget({ options }) {
if (!data) {
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -49,7 +49,7 @@ function Widget({ options }) {
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
@@ -101,7 +101,7 @@ export default function WeatherApi({ options }) {
if (!location) {
return (
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center">
<button type="button" onClick={() => requestLocation()} className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
{requesting ? (

View File

@@ -1,8 +1,23 @@
/* eslint-disable no-console */
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/service-helpers";
export default async function handler(req, res) {
const discoveredServices = cleanServiceGroups(await servicesFromDocker());
const configuredServices = cleanServiceGroups(await servicesFromConfig());
let discoveredServices;
let configuredServices;
try {
discoveredServices = cleanServiceGroups(await servicesFromDocker());
} catch {
console.error("Failed to discover services, please check docker.yaml for errors");
discoveredServices = [];
}
try {
configuredServices = cleanServiceGroups(await servicesFromConfig());
} catch {
console.error("Failed to load services.yaml, please check for errors");
configuredServices = [];
}
const mergedGroupsNames = [
...new Set([discoveredServices.map((group) => group.name), configuredServices.map((group) => group.name)].flat()),

View File

@@ -11,13 +11,18 @@ const serviceProxyHandlers = {
pihole: genericProxyHandler,
radarr: genericProxyHandler,
sonarr: genericProxyHandler,
readarr: genericProxyHandler,
speedtest: genericProxyHandler,
tautulli: genericProxyHandler,
traefik: genericProxyHandler,
// uses X-API-Key header auth
sabnzbd: genericProxyHandler,
// uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler,
jellyseerr: credentialedProxyHandler,
overseerr: credentialedProxyHandler,
ombi: credentialedProxyHandler,
coinmarketcap: credentialedProxyHandler,
// super specific handlers
rutorrent: rutorrentProxyHandler,
nzbget: nzbgetProxyHandler,

View File

@@ -49,7 +49,7 @@ export default function Home({ settings }) {
</Head>
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
<div className="flex flex-row flex-wrap space-x-0 sm:space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between md:justify-start">
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
{widgets && (
<>
{widgets
@@ -58,7 +58,7 @@ export default function Home({ settings }) {
<Widget key={i} widget={widget} />
))}
<div className="flex flex-wrap basis-full space-x-0 sm:space-x-4 grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (

View File

@@ -10,8 +10,13 @@ const formats = {
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
rutorrent: `{url}/plugins/httprpc/action.php`,
jellyseerr: `{url}/api/v1/{endpoint}`,
overseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`,
npm: `{url}/api/{endpoint}`,
readarr: `{url}/api/v1/{endpoint}?apikey={key}`,
sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`,
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
gotify: `{url}/{endpoint}`,
};
export function formatApiCall(api, args) {

View File

@@ -10,15 +10,30 @@ export default async function credentialedProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const headers = {
"Content-Type": "application/json",
};
if (widget.type === "coinmarketcap") {
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
} else if (widget.type === "gotify") {
headers["X-gotify-Key"] = `${widget.key}`;
} else {
headers["X-API-Key"] = `${widget.key}`;
}
const [status, contentType, data] = await httpProxy(url, {
method: req.method,
withCredentials: true,
credentials: "include",
headers: {
"X-API-Key": `${widget.key}`,
"Content-Type": "application/json",
},
headers,
});
if (status === 204 || status === 304) {
return res.status(status).end();
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}

View File

@@ -10,9 +10,16 @@ export default async function genericProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
const [status, contentType, data] = await httpProxy(url);
const [status, contentType, data] = await httpProxy(url, {
method: req.method,
});
if (contentType) res.setHeader("Content-Type", contentType);
if (status === 204 || status === 304) {
return res.status(status).end();
}
return res.status(status).send(data);
}
}

View File

@@ -110,13 +110,27 @@ export function cleanServiceGroups(groups) {
const cleanedService = { ...service };
if (cleanedService.widget) {
const { type } = cleanedService.widget;
// whitelisted set of keys to pass to the frontend
const {
type, // all widgets
server, // docker widget
container,
currency, // coinmarketcap widget
symbols,
} = cleanedService.widget;
cleanedService.widget = {
type,
currency,
symbols,
service_name: service.name,
service_group: serviceGroup.name,
};
if (type === "docker") {
cleanedService.widget.server = server;
cleanedService.widget.container = container;
}
}
return cleanedService;