mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-06 21:57:48 +01:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
406358aae9 | ||
|
|
a5d59e7e45 | ||
|
|
92a4ad0c5e | ||
|
|
d963bcd0c4 | ||
|
|
2e4125c81c | ||
|
|
5293ff3580 | ||
|
|
7a1349df83 | ||
|
|
de8de8f731 | ||
|
|
6d36382436 | ||
|
|
e31833b649 | ||
|
|
2dce18563d | ||
|
|
aa55c27ab0 | ||
|
|
16e321af54 | ||
|
|
c4edb29ff3 | ||
|
|
1d5cc05941 | ||
|
|
9faae7cb67 | ||
|
|
ea06fbe666 | ||
|
|
cc0b4be50c | ||
|
|
ea55cde043 | ||
|
|
840c88db89 | ||
|
|
8e8c9755a3 | ||
|
|
ba3b48e8ce | ||
|
|
d3806f7d5b | ||
|
|
0c9c1c599f | ||
|
|
af02440c40 | ||
|
|
cd53440eff | ||
|
|
3660140539 | ||
|
|
7bf1bf5369 | ||
|
|
898e30d6de | ||
|
|
a792d213e9 | ||
|
|
ebee953ebc | ||
|
|
200ab220e8 | ||
|
|
2499d25ce6 | ||
|
|
42356166c0 | ||
|
|
80a31c8427 | ||
|
|
867c6f9e97 | ||
|
|
ee9194fce1 | ||
|
|
f6322077a4 | ||
|
|
15a0e6cc54 | ||
|
|
5ee5adbb1e | ||
|
|
1d4b3eee9b | ||
|
|
fe971d23f8 | ||
|
|
34bf49845f | ||
|
|
34468e5bb0 | ||
|
|
d0dd52c5c2 | ||
|
|
98cca4ca8b | ||
|
|
b88463a785 | ||
|
|
6409188de8 | ||
|
|
510973c761 | ||
|
|
4480c26910 | ||
|
|
e778595296 | ||
|
|
f84ff7cedc | ||
|
|
04f98ae7a9 | ||
|
|
8ef3d7c20e | ||
|
|
428fd6cbba | ||
|
|
ee79335eff | ||
|
|
83d7100dd1 | ||
|
|
ccd9049806 | ||
|
|
769f36fa8e | ||
|
|
ffe89b02e9 | ||
|
|
1c158f743c | ||
|
|
4531985032 | ||
|
|
f8aa1ba391 | ||
|
|
9d790894d5 | ||
|
|
eeac1200e7 | ||
|
|
a304d87b8a | ||
|
|
ffbb1f5f0b | ||
|
|
ad53119088 | ||
|
|
fe1c525fb7 | ||
|
|
323375e8e4 | ||
|
|
8af27ea86d | ||
|
|
9335be0049 | ||
|
|
8e0f265080 | ||
|
|
ae357159ef | ||
|
|
387d40910f | ||
|
|
5344485a4f | ||
|
|
21f2f2a215 | ||
|
|
9586fac665 | ||
|
|
4aedda7ba2 | ||
|
|
f79213c9d3 | ||
|
|
eeddcb26a0 | ||
|
|
725922db78 | ||
|
|
1846dcaba9 | ||
|
|
ab96d88ebe | ||
|
|
a3bf28915d | ||
|
|
47920a5f7a | ||
|
|
1c10903823 | ||
|
|
96e4133517 | ||
|
|
8b5167a911 | ||
|
|
45edab5d88 | ||
|
|
b4375fb6fc | ||
|
|
bd2b28a7ac | ||
|
|
53149df5f1 | ||
|
|
bc2025b3ba | ||
|
|
236450f6f1 | ||
|
|
fb9e03b31d | ||
|
|
31ccb9c933 | ||
|
|
6e01a743df | ||
|
|
ed65c89516 | ||
|
|
b8b8dad9fb | ||
|
|
690d17e132 | ||
|
|
a1e9912b36 | ||
|
|
26914bfb09 | ||
|
|
079fdb3011 | ||
|
|
102cdbd53a | ||
|
|
d861264ecf | ||
|
|
9831df1427 | ||
|
|
5e6312fe93 |
48
README.md
48
README.md
@@ -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
|
||||
|
||||
BIN
images/icons/coinmarketcap.png
Normal file
BIN
images/icons/coinmarketcap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -3,9 +3,9 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
swcMinify: false,
|
||||
experimental: { images: { allowFutureImage: true, unoptimized: true } },
|
||||
images: {
|
||||
domains: ["cdn.jsdelivr.net"],
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
21
package.json
21
package.json
@@ -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
467
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
71
src/components/services/widgets/service/coinmarketcap.jsx
Normal file
71
src/components/services/widgets/service/coinmarketcap.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
29
src/components/services/widgets/service/gotify.jsx
Normal file
29
src/components/services/widgets/service/gotify.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Gotify({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: appsData, error: appsError } = useSWR(formatApiUrl(config, `application`));
|
||||
const { data: messagesData, error: messagesError } = useSWR(formatApiUrl(config, `message`));
|
||||
const { data: clientsData, error: clientsError } = useSWR(formatApiUrl(config, `client`));
|
||||
|
||||
if (appsError || messagesError || clientsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("gotify.apps")} value={appsData?.length} />
|
||||
<Block label={t("gotify.clients")} value={clientsData?.length} />
|
||||
<Block label={t("gotify.messages")} value={messagesData?.messages?.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
37
src/components/services/widgets/service/overseerr.jsx
Normal file
37
src/components/services/widgets/service/overseerr.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Overseerr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} />
|
||||
<Block label={t("overseerr.approved")} />
|
||||
<Block label={t("overseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("overseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("overseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("overseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
41
src/components/services/widgets/service/readarr.jsx
Normal file
41
src/components/services/widgets/service/readarr.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
37
src/components/services/widgets/service/sabnzbd.jsx
Normal file
37
src/components/services/widgets/service/sabnzbd.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function SABnzbd({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue"));
|
||||
|
||||
if (queueError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!queueData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} />
|
||||
<Block label={t("sabnzbd.queue")} />
|
||||
<Block label={t("sabnzbd.timeleft")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}bps`} />
|
||||
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
|
||||
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user