mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a226ca473 | ||
|
|
33e6d54fd2 | ||
|
|
d36f37a4ed | ||
|
|
f3ebbb6547 | ||
|
|
28b2f79e5b | ||
|
|
9a77115a30 | ||
|
|
2d899e364d | ||
|
|
32b881891c | ||
|
|
9eefc07c7c | ||
|
|
792accffb6 | ||
|
|
03af88aba5 | ||
|
|
f56b6b4ad0 | ||
|
|
4ce1681e79 | ||
|
|
7570fa71f0 | ||
|
|
8a61c76cd9 | ||
|
|
fbf5381699 | ||
|
|
ff77f0db4f | ||
|
|
2e30abedc9 | ||
|
|
c4cb4f7475 | ||
|
|
7432bb813e | ||
|
|
572a104779 | ||
|
|
f77dc23d92 | ||
|
|
e92fc74dd3 | ||
|
|
9479c3d5c3 | ||
|
|
cfc37a64e1 | ||
|
|
2d5294804c | ||
|
|
6c01a85077 | ||
|
|
cf41e988eb | ||
|
|
d7a161c088 | ||
|
|
379c4040fe | ||
|
|
3f17618ad5 | ||
|
|
d7be64c3d9 | ||
|
|
ef7737e9be | ||
|
|
51ad3184b6 | ||
|
|
efc8fd878a | ||
|
|
6da1e98c83 | ||
|
|
513a06740c | ||
|
|
743a070724 | ||
|
|
5fb0e76669 | ||
|
|
bedeab686e | ||
|
|
9d9fa352ce | ||
|
|
1bfa6ce862 | ||
|
|
755b29c859 | ||
|
|
aab5b0247a | ||
|
|
d7e4b0bd17 | ||
|
|
3bacdadb80 | ||
|
|
1d75ee44ed | ||
|
|
230cc343af | ||
|
|
b318ee165c | ||
|
|
f677646365 | ||
|
|
8db7d820d7 | ||
|
|
adb0632566 | ||
|
|
4d5c8db333 | ||
|
|
01d6a3d5f8 | ||
|
|
fbeadbc32f | ||
|
|
1289be888f | ||
|
|
aa7e3a955c | ||
|
|
2823f3b921 | ||
|
|
ddb2a74540 | ||
|
|
37d8d7a2f8 | ||
|
|
578b715a1f | ||
|
|
f14a811ce9 | ||
|
|
06dd6d2213 | ||
|
|
72471c47f4 | ||
|
|
aec5f7173c | ||
|
|
f7b68789ac | ||
|
|
0672da621e | ||
|
|
a7f9b78533 | ||
|
|
0075429e08 | ||
|
|
43f7ccd166 | ||
|
|
8c64e0f288 | ||
|
|
c91a387833 | ||
|
|
93d5dd88ba | ||
|
|
05427253b9 | ||
|
|
e2bc541089 | ||
|
|
9a959bab16 | ||
|
|
45ca4a15f7 | ||
|
|
ddd2ff53ff | ||
|
|
5c3266b48f | ||
|
|
0da6db9d9f |
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
15
README.md
15
README.md
@@ -9,16 +9,16 @@
|
||||
- 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
|
||||
- Translations for Chinese, Dutch, French, German, Norwegian Bokmål, Polish, Portuguese, Russian, Spanish and Swedish
|
||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||
- Service & Web Bookmarks
|
||||
- Docker Integration
|
||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||
- Automatic service discovery (via labels)
|
||||
- Service Integration
|
||||
- Sonarr, Radarr, Readarr, Prowlarr, Emby, Jellyfin, Tautulli (Plex)
|
||||
- Ombi, Overseerr, Jellyseerr, NZBGet, SABnzbd, ruTorrent, Transmission
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager, Gotify
|
||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify
|
||||
- Information Providers
|
||||
- Coin Market Cap
|
||||
- Information & Utility Widgets
|
||||
@@ -50,11 +50,10 @@ services:
|
||||
homepage:
|
||||
image: ghcr.io/benphelps/homepage:latest
|
||||
container_name: homepage
|
||||
user: 1000:1000 # Optional, change to your user and group IDs for permissions
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- /path/to/config:/app/config # Make sure your local config directory is exists
|
||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
|
||||
```
|
||||
|
||||
@@ -129,9 +128,13 @@ Huge thanks to the all the contributors who have helped make this project what i
|
||||
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
||||
- [jackblk](https://github.com/benphelps/homepage/commits?author=jackblk) - Vietnamese Translation
|
||||
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd & Transmission Integrations
|
||||
- [juanmanuelbc](https://github.com/benphelps/homepage/commits?author=juanmanuelbc) - Spanish and Catalan Translations
|
||||
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
||||
- [nicedc](https://github.com/benphelps/homepage/commits?author=nicedc) - Chinese Translation
|
||||
- [Nonoss117](https://github.com/benphelps/homepage/commits?author=Nonoss117) - French Translation
|
||||
- [pacoculebras](https://github.com/benphelps/homepage/commits?author=pacoculebras) - Catalan Translation
|
||||
- [psychodracon](https://github.com/benphelps/homepage/commits?author=psychodracon) - Polish Translation
|
||||
- [quod](https://github.com/benphelps/homepage/commits?author=quod) - Fixed Typos
|
||||
- [schklom](https://github.com/benphelps/homepage/commits?author=schklom) - ARM64, ARMv7 and ARMv6
|
||||
- [SuperDOS](https://github.com/benphelps/homepage/commits?author=SuperDOS) - Swedish Translation
|
||||
- [xicopitz](https://github.com/benphelps/homepage/commits?author=xicopitz) - Gotify & Prowlarr Integration
|
||||
|
||||
@@ -134,16 +134,28 @@
|
||||
"numberOfFailQueries": "Consultes fallides"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
"configured": "Configurat",
|
||||
"errored": "Amb errors"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
"missingEpisodes": "Episodis que falten",
|
||||
"missingMovies": "Pel·lícules que falten"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
"wanted": "Volgut",
|
||||
"queued": "En cua",
|
||||
"albums": "Àlbums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
@@ -108,12 +114,18 @@
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
|
||||
@@ -21,16 +21,16 @@
|
||||
"offline": "Desconectado"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "En ejecución",
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de Bits",
|
||||
"no_active": "No hay streams activos"
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "En ejecución",
|
||||
"transcoding": "Transcodificación",
|
||||
"playing": "Reproduciendo",
|
||||
"transcoding": "Transcodificando",
|
||||
"bitrate": "Tasa de bits",
|
||||
"no_active": "No hay streams activos"
|
||||
"no_active": "Sin transmisiones activas"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Activo",
|
||||
@@ -39,17 +39,17 @@
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"queued": "En cola",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"queued": "En cola",
|
||||
"movies": "Películas"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Más deseado",
|
||||
"queued": "Puesto en cola",
|
||||
"queued": "En cola",
|
||||
"books": "Libros"
|
||||
},
|
||||
"ombi": {
|
||||
@@ -68,7 +68,7 @@
|
||||
"gravity": "Gravedad"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Subir",
|
||||
"upload": "Subida",
|
||||
"download": "Descarga",
|
||||
"ping": "Ping"
|
||||
},
|
||||
@@ -88,7 +88,7 @@
|
||||
"total": "Total"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Ubicación Actual",
|
||||
"current": "Ubicación actual",
|
||||
"allow": "Haga clic para permitir",
|
||||
"updating": "Actualizando",
|
||||
"wait": "Espere, por favor"
|
||||
@@ -99,21 +99,21 @@
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Tasa de descarga",
|
||||
"queue": "Puesto en cola",
|
||||
"timeleft": "Tiempo Restante"
|
||||
"rate": "Tasa",
|
||||
"queue": "En cola",
|
||||
"timeleft": "Tiempo restante"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Tasa de descarga",
|
||||
"rate": "Tasa",
|
||||
"remaining": "Restante",
|
||||
"downloaded": "Descargado"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configurar una o varias criptomonedas para su seguimiento",
|
||||
"configure": "Configurar una o más criptomonedas para rastrear",
|
||||
"1hour": "1 Hora",
|
||||
"1day": "1 Día",
|
||||
"7days": "7 Dias",
|
||||
"30days": "30 Dias"
|
||||
"7days": "7 Días",
|
||||
"30days": "30 Días"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplicaciones",
|
||||
@@ -124,26 +124,38 @@
|
||||
"enableIndexers": "Indexadores",
|
||||
"numberOfGrabs": "Capturas",
|
||||
"numberOfQueries": "Consultas",
|
||||
"numberOfFailGrabs": "Capturas Fallidas",
|
||||
"numberOfFailQueries": "Consultas Fallidas"
|
||||
"numberOfFailGrabs": "Capturas fallidas",
|
||||
"numberOfFailQueries": "Consultas fallidas"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Egoístas (Leech)",
|
||||
"leech": "Compañeros",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
"configured": "Configurado",
|
||||
"errored": "Con errores"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
"missingEpisodes": "Episodios perdidos",
|
||||
"missingMovies": "Películas perdidas"
|
||||
},
|
||||
"lidarr": {
|
||||
"queued": "Queued",
|
||||
"wanted": "Wanted",
|
||||
"albums": "Albums"
|
||||
"queued": "En cola",
|
||||
"wanted": "Más deseado",
|
||||
"albums": "Álbumes"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Consultas",
|
||||
"blocked": "Bloqueado",
|
||||
"filtered": "Filtrado",
|
||||
"latency": "Latencia"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "En attente",
|
||||
"approved": "Validé",
|
||||
"approved": "Demande",
|
||||
"available": "Disponible"
|
||||
},
|
||||
"sabnzbd": {
|
||||
@@ -127,9 +127,9 @@
|
||||
"30days": "30 Jours"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"apps": "Applis",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
"messages": "Msg"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexeurs",
|
||||
@@ -149,12 +149,24 @@
|
||||
"errored": "En erreur"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
"missingEpisodes": "Épisodes manquants",
|
||||
"missingMovies": "Films manquants"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"wanted": "Demandé",
|
||||
"queued": "En queue",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Requêtes",
|
||||
"blocked": "Bloquées",
|
||||
"filtered": "Filtrées",
|
||||
"latency": "Latence"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
161
public/locales/hr/common.json
Normal file
161
public/locales/hr/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"overseerr": {
|
||||
"available": "Available",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"latency": "Latency",
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered"
|
||||
},
|
||||
"npm": {
|
||||
"total": "Total",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"rutorrent": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"active": "Active"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"gotify": {
|
||||
"clients": "Clients",
|
||||
"messages": "Messages",
|
||||
"apps": "Applications"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
161
public/locales/hu/common.json
Normal file
161
public/locales/hu/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"resources": {
|
||||
"total": "Total",
|
||||
"free": "Free",
|
||||
"used": "Used",
|
||||
"load": "Load"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"lidarr": {
|
||||
"albums": "Albums",
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"books": "Books"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Missing Episodes",
|
||||
"missingMovies": "Missing Movies"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Missing Widget Type: {{type}}",
|
||||
"api_error": "API Error",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Current Location",
|
||||
"allow": "Click to allow",
|
||||
"updating": "Updating",
|
||||
"wait": "Please wait"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Playing",
|
||||
"transcoding": "Transcoding",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "No Active Streams"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Rate",
|
||||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
"timeleft": "Time Left"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Active",
|
||||
"upload": "Upload",
|
||||
"download": "Download"
|
||||
},
|
||||
"transmission": {
|
||||
"leech": "Leech",
|
||||
"seed": "Seed",
|
||||
"download": "Download",
|
||||
"upload": "Upload"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"series": "Series"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"movies": "Movies"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"ping": "Ping"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"total": "Total"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"total": "Total"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Configure one or more crypto currencies to track",
|
||||
"1hour": "1 Hour",
|
||||
"1day": "1 Day",
|
||||
"7days": "7 Days",
|
||||
"30days": "30 Days"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Applications",
|
||||
"clients": "Clients",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexers",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"errored": "Errored"
|
||||
}
|
||||
}
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"leech": "Leech",
|
||||
"upload": "Upload",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
161
public/locales/pl/common.json
Normal file
161
public/locales/pl/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"weather": {
|
||||
"allow": "Kliknij, aby zezwolić",
|
||||
"updating": "Aktualizacja",
|
||||
"wait": "Proszę czekać",
|
||||
"current": "Aktualna lokalizacja"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Szukaj…"
|
||||
},
|
||||
"resources": {
|
||||
"used": "Użyte",
|
||||
"load": "Obciążenie",
|
||||
"total": "Całkowite",
|
||||
"free": "Wolne"
|
||||
},
|
||||
"emby": {
|
||||
"no_active": "Brak aktywnych strumieni",
|
||||
"playing": "Odtwarzanie",
|
||||
"transcoding": "Transkodowanie",
|
||||
"bitrate": "Bitrate"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Odtwarzanie",
|
||||
"transcoding": "Transkodowanie",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Brak aktywnych strumieni"
|
||||
},
|
||||
"speedtest": {
|
||||
"download": "Pobieranie",
|
||||
"ping": "Ping",
|
||||
"upload": "Wysyłanie"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Działające",
|
||||
"stopped": "Zatrzymane",
|
||||
"total": "Ogólnie"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"1day": "1 dzień",
|
||||
"7days": "7 dni",
|
||||
"30days": "30 dni",
|
||||
"1hour": "1 godzina",
|
||||
"configure": "Wybierz jedną lub więcej kryptowalut do śledzenia"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Aplikacje",
|
||||
"clients": "Klienci",
|
||||
"messages": "Wiadomości"
|
||||
},
|
||||
"widget": {
|
||||
"missing_type": "Brakujący typ widżetu: {{type}}",
|
||||
"api_error": "Błąd API",
|
||||
"status": "Stan"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Szybkość",
|
||||
"remaining": "Pozostało",
|
||||
"downloaded": "Pobrano"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Szybkość",
|
||||
"queue": "Kolejka",
|
||||
"timeleft": "Pozostało"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktywny",
|
||||
"upload": "Wysyłanie",
|
||||
"download": "Pobieranie"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Pobieranie",
|
||||
"upload": "Wysyłanie",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"series": "Seriale"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"movies": "Filmy"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"albums": "Albumy"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Poszukiwane",
|
||||
"queued": "W kolejce",
|
||||
"books": "Książki"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Brakujące odcinki",
|
||||
"missingMovies": "Brakujące filmy"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Oczekiwane",
|
||||
"approved": "Zaakceptowane",
|
||||
"available": "Dostępne"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Zapytania",
|
||||
"blocked": "Zablokowane",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routery",
|
||||
"services": "Serwisy",
|
||||
"middleware": "Pośrednicy"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
"total": "Ogólnie"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indeksery",
|
||||
"numberOfGrabs": "Pochwycenia",
|
||||
"numberOfQueries": "Zapytania",
|
||||
"numberOfFailGrabs": "Nieudane pochwycenia",
|
||||
"numberOfFailQueries": "Nieudane zapytania"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Skonfigurowane",
|
||||
"errored": "Błędne"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
@@ -156,5 +156,17 @@
|
||||
"queued": "Queued",
|
||||
"wanted": "Wanted",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
161
public/locales/sv/common.json
Normal file
161
public/locales/sv/common.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"widget": {
|
||||
"missing_type": "Saknar Widget-typ: {{type}}",
|
||||
"api_error": "API-fel",
|
||||
"status": "Status"
|
||||
},
|
||||
"weather": {
|
||||
"current": "Nuvarande plats",
|
||||
"allow": "Klicka för att tillåta",
|
||||
"updating": "Uppdaterar",
|
||||
"wait": "Vänligen vänta"
|
||||
},
|
||||
"resources": {
|
||||
"load": "Laddar",
|
||||
"total": "Total",
|
||||
"free": "Ledigt",
|
||||
"used": "Använt"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "MEM",
|
||||
"cpu": "CPU",
|
||||
"offline": "Offline"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Sök…"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Spelar",
|
||||
"transcoding": "Omkodning",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Inga aktiva strömmar"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Spelar",
|
||||
"transcoding": "Omkodning",
|
||||
"bitrate": "Bitrate",
|
||||
"no_active": "Inga aktiva strömmar"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "Hastighet",
|
||||
"remaining": "Återstående",
|
||||
"downloaded": "Nedladdat"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Hastighet",
|
||||
"queue": "Kö",
|
||||
"timeleft": "Tid kvar"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Aktiva",
|
||||
"upload": "Uppladdning",
|
||||
"download": "Nedladdning"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Nedladdning",
|
||||
"upload": "Uppladdning",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"series": "Serier"
|
||||
},
|
||||
"radarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"movies": "Filmer"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "Eftersöker",
|
||||
"queued": "I kö",
|
||||
"albums": "Album"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "Eftersökt",
|
||||
"queued": "I kö",
|
||||
"books": "Böcker"
|
||||
},
|
||||
"bazarr": {
|
||||
"missingEpisodes": "Saknade program",
|
||||
"missingMovies": "Saknade filmer"
|
||||
},
|
||||
"ombi": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Avvaktar",
|
||||
"approved": "Godkända",
|
||||
"available": "Tillgänglig"
|
||||
},
|
||||
"pihole": {
|
||||
"blocked": "Blockerad",
|
||||
"queries": "Förfrågningar",
|
||||
"gravity": "Gravity"
|
||||
},
|
||||
"speedtest": {
|
||||
"upload": "Uppladdning",
|
||||
"download": "Nedladdning",
|
||||
"ping": "Svarstid"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Körs",
|
||||
"stopped": "Stoppade",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"traefik": {
|
||||
"routers": "Routers",
|
||||
"services": "Tjänster",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": "Aktiverad",
|
||||
"disabled": "Inaktiverad",
|
||||
"total": "Totalt"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"configure": "Konfigurera en eller flera kryptovalutor att följa",
|
||||
"1hour": "1 timme",
|
||||
"1day": "1 dag",
|
||||
"7days": "7 dagar",
|
||||
"30days": "30 dagar"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "Program",
|
||||
"clients": "Klienter",
|
||||
"messages": "Meddelande"
|
||||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "Indexerare",
|
||||
"numberOfGrabs": "Hämtningar",
|
||||
"numberOfQueries": "Queries",
|
||||
"numberOfFailGrabs": "Misslyckade hämtningar",
|
||||
"numberOfFailQueries": "Fail Queries"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Konfigurerade",
|
||||
"errored": "Felaktiga"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"widget": {
|
||||
"missing_type": "缺少小部件类型:{{type}}",
|
||||
"api_error": "API错误",
|
||||
"status": "地位"
|
||||
"status": "状态"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索…"
|
||||
@@ -88,7 +88,7 @@
|
||||
"total": "全部的"
|
||||
},
|
||||
"weather": {
|
||||
"current": "当前位置",
|
||||
"current": "当前定位",
|
||||
"allow": "点击并允许",
|
||||
"updating": "更新中",
|
||||
"wait": "请等待"
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,5 +145,17 @@
|
||||
"wanted": "Wanted",
|
||||
"queued": "Queued",
|
||||
"albums": "Albums"
|
||||
},
|
||||
"adguard": {
|
||||
"queries": "Queries",
|
||||
"blocked": "Blocked",
|
||||
"filtered": "Filtered",
|
||||
"latency": "Latency"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function ColorToggle() {
|
||||
{colors.map((color) => (
|
||||
<button type="button" onClick={() => setColor(color)} key={color}>
|
||||
<div
|
||||
title={color}
|
||||
className={classNames(
|
||||
active === color ? "border-2" : "border-0",
|
||||
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import List from "components/services/list";
|
||||
|
||||
export default function ServicesGroup({ services }) {
|
||||
export default function ServicesGroup({ services, layout }) {
|
||||
return (
|
||||
<div
|
||||
key={services.name}
|
||||
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
|
||||
className={classNames(
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
|
||||
"flex-1 p-1"
|
||||
)}
|
||||
>
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
|
||||
{services.name}
|
||||
</h2>
|
||||
<List services={services.services} />
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
<List services={services.services} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ services }) {
|
||||
const columnMap = [
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
|
||||
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
|
||||
];
|
||||
|
||||
export default function List({ services, layout }) {
|
||||
return (
|
||||
<ul className="mt-3 flex flex-col">
|
||||
<ul
|
||||
className={classNames(
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
|
||||
"mt-3"
|
||||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item key={service.name} service={service} />
|
||||
))}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Emby from "./widgets/service/emby";
|
||||
import Nzbget from "./widgets/service/nzbget";
|
||||
import SABnzbd from "./widgets/service/sabnzbd";
|
||||
import Transmission from "./widgets/service/transmission";
|
||||
import QBittorrent from "./widgets/service/qbittorrent";
|
||||
import Docker from "./widgets/service/docker";
|
||||
import Pihole from "./widgets/service/pihole";
|
||||
import Rutorrent from "./widgets/service/rutorrent";
|
||||
@@ -25,6 +26,7 @@ import CoinMarketCap from "./widgets/service/coinmarketcap";
|
||||
import Gotify from "./widgets/service/gotify";
|
||||
import Prowlarr from "./widgets/service/prowlarr";
|
||||
import Jackett from "./widgets/service/jackett";
|
||||
import AdGuard from "./widgets/service/adguard";
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
@@ -40,6 +42,7 @@ const widgetMappings = {
|
||||
nzbget: Nzbget,
|
||||
sabnzbd: SABnzbd,
|
||||
transmission: Transmission,
|
||||
qbittorrent: QBittorrent,
|
||||
pihole: Pihole,
|
||||
rutorrent: Rutorrent,
|
||||
speedtest: Speedtest,
|
||||
@@ -52,6 +55,7 @@ const widgetMappings = {
|
||||
gotify: Gotify,
|
||||
prowlarr: Prowlarr,
|
||||
jackett: Jackett,
|
||||
adguard: AdGuard,
|
||||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
|
||||
45
src/components/services/widgets/service/adguard.jsx
Normal file
45
src/components/services/widgets/service/adguard.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function AdGuard({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: adguardData, error: adguardError } = useSWR(formatApiUrl(config, "stats"));
|
||||
|
||||
if (adguardError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!adguardData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} />
|
||||
<Block label={t("adguard.blocked")} />
|
||||
<Block label={t("adguard.filtered")} />
|
||||
<Block label={t("adguard.latency")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const filtered =
|
||||
adguardData.num_replaced_safebrowsing + adguardData.num_replaced_safesearch + adguardData.num_replaced_parental;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} value={t("common.number", { value: adguardData.num_dns_queries })} />
|
||||
<Block label={t("adguard.blocked")} value={t("common.number", { value: adguardData.num_blocked_filtering })} />
|
||||
<Block label={t("adguard.filtered")} value={t("common.number", { value: filtered })} />
|
||||
<Block
|
||||
label={t("adguard.latency")}
|
||||
value={t("common.ms", { value: adguardData.avg_processing_time * 1000, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -29,8 +29,8 @@ export default function Bazarr({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("bazarr.missingEpisodes")} value={episodesData.total} />
|
||||
<Block label={t("bazarr.missingMovies")} value={moviesData.total} />
|
||||
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
|
||||
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
||||
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||
import { MdOutlineSmartDisplay } from "react-icons/md";
|
||||
|
||||
import Widget from "../widget";
|
||||
|
||||
@@ -27,24 +28,35 @@ function ticksToString(ticks) {
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ playCommand, session }) {
|
||||
console.log(session);
|
||||
const {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||
IsVideoDirect: true,
|
||||
VideoDecoderIsHardware: true,
|
||||
VideoEncoderIsHardware: true,
|
||||
};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2">
|
||||
<span>
|
||||
<div className="grow text-xs z-10 self-center ml-2 relative w-full h-4 mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
|
||||
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && (
|
||||
<BsFillCpuFill className="opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
@@ -73,7 +85,12 @@ function SingleSessionEntry({ playCommand, session }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{ticksToString(PositionTicks)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{ticksToString(RunTimeTicks)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -84,6 +101,9 @@ function SessionEntry({ playCommand, session }) {
|
||||
NowPlayingItem: { Name, SeriesName, RunTimeTicks },
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {};
|
||||
|
||||
const percent = (PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
return (
|
||||
@@ -111,14 +131,20 @@ function SessionEntry({ playCommand, session }) {
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
</div>
|
||||
<div className="grow text-xs z-10 self-center relative w-full h-4">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{Name}
|
||||
{SeriesName && ` - ${SeriesName}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1 z-10">{ticksToString(PositionTicks)}</div>
|
||||
<div className="self-center items-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{IsVideoDirect && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && <BsCpu className="opacity-50" />}
|
||||
{!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && <BsFillCpuFill className="opacity-50" />}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-1">{IsMuted && <BsVolumeMuteFill />}</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2">{ticksToString(PositionTicks)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export default function Jackett({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("jackett.configured")} value={indexersData.length} />
|
||||
<Block label={t("jackett.errored")} value={errored.length} />
|
||||
<Block label={t("jackett.configured")} value={t("common.number", { value: indexersData.length })} />
|
||||
<Block label={t("jackett.errored")} value={t("common.number", { value: errored.length })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,11 @@ export default function Lidarr({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const have = albumsData.filter((album) => album.statistics.percentOfTracks === 100);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("lidarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("lidarr.queued")} value={queueData.totalCount} />
|
||||
<Block label={t("lidarr.albums")} value={have.length} />
|
||||
<Block label={t("lidarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label={t("lidarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label={t("lidarr.albums")} value={t("common.number", { value: albumsData.have })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
69
src/components/services/widgets/service/qbittorrent.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function QBittorrent ({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info"));
|
||||
|
||||
if (torrentError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} />
|
||||
<Block label={t("qbittorrent.download")} />
|
||||
<Block label={t("qbittorrent.seed")} />
|
||||
<Block label={t("qbittorrent.upload")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (let i = 0; i < torrentData.length; i += 1) {
|
||||
const torrent = torrentData[i];
|
||||
rateDl += torrent.dlspeed;
|
||||
rateUl += torrent.upspeed;
|
||||
if (torrent.progress === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrentData.length - completed;
|
||||
|
||||
let unitsDl = "KB/s";
|
||||
let unitsUl = "KB/s";
|
||||
rateDl /= 1024;
|
||||
rateUl /= 1024;
|
||||
|
||||
if (rateDl > 1024) {
|
||||
rateDl /= 1024;
|
||||
unitsDl = "MB/s";
|
||||
}
|
||||
|
||||
if (rateUl > 1024) {
|
||||
rateUl /= 1024;
|
||||
unitsUl = "MB/s";
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("qbittorrent.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("qbittorrent.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("qbittorrent.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("qbittorrent.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -28,14 +28,11 @@ export default function Radarr({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const wanted = moviesData.filter((movie) => movie.isAvailable === false);
|
||||
const have = moviesData.filter((movie) => movie.isAvailable === true);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
||||
<Block label={t("radarr.wanted")} value={moviesData.wanted} />
|
||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||
<Block label={t("radarr.movies")} value={have.length} />
|
||||
<Block label={t("radarr.movies")} value={moviesData.have} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,11 @@ export default function Readarr({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const have = booksData.filter((book) => book.statistics.bookFileCount > 0);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("readarr.queued")} value={queueData.totalCount} />
|
||||
<Block label={t("readarr.books")} value={have.length} />
|
||||
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label={t("readarr.books")} value={t("common.number", { value: booksData.have })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function SABnzbd({ service }) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
|
||||
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
|
||||
<Block label={t("sabnzbd.queue")} value={t("common.number", { value: queueData.queue.noofslots })} />
|
||||
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
|
||||
</Widget>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Sonarr({ service }) {
|
||||
<Widget>
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable camelcase */
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
|
||||
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
|
||||
import { MdOutlineSmartDisplay } from "react-icons/md";
|
||||
|
||||
import Widget from "../widget";
|
||||
|
||||
@@ -27,16 +28,19 @@ function millisecondsToString(milliseconds) {
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ session }) {
|
||||
const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
|
||||
const { full_title, duration, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
<div className="text-xs z-10 self-center ml-2">
|
||||
<span>{full_title}</span>
|
||||
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1">
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" && audio_decision !== "copy" && <BsFillCpuFill className="opacity-50" />}
|
||||
{video_decision === "copy" && audio_decision !== "copy" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="self-center text-xs flex justify-end mr-2">{year || grandparent_year}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
@@ -55,8 +59,10 @@ function SingleSessionEntry({ session }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="grow " />
|
||||
<div className="self-center text-xs flex justify-end mr-2">
|
||||
{millisecondsToString(view_offset)} / {millisecondsToString(duration)}
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">
|
||||
{millisecondsToString(view_offset)}
|
||||
<span className="mx-0.5 text-[8px]">/</span>
|
||||
{millisecondsToString(duration)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -64,7 +70,7 @@ function SingleSessionEntry({ session }) {
|
||||
}
|
||||
|
||||
function SessionEntry({ session }) {
|
||||
const { full_title, view_offset, progress_percent, state } = session;
|
||||
const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
|
||||
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
|
||||
@@ -81,10 +87,16 @@ function SessionEntry({ session }) {
|
||||
{state !== "paused" && (
|
||||
<BsFillPlayFill 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 className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{full_title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10">
|
||||
{video_decision === "copy" && audio_decision === "copy" && <MdOutlineSmartDisplay className="opacity-50" />}
|
||||
{video_decision !== "copy" && audio_decision !== "copy" && <BsFillCpuFill className="opacity-50" />}
|
||||
{video_decision === "copy" && audio_decision !== "copy" && <BsCpu className="opacity-50" />}
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-2 z-10">{millisecondsToString(view_offset)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ export default function Transmission({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("transmission.leech")} value={leech} />
|
||||
<Block label={t("transmission.leech")} value={t("common.number", { value: leech })} />
|
||||
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
|
||||
<Block label={t("transmission.seed")} value={completed} />
|
||||
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
|
||||
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
|
||||
</Widget>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,8 @@ import WeatherApi from "components/widgets/weather/weather";
|
||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||
import Resources from "components/widgets/resources/resources";
|
||||
import Search from "components/widgets/search/search";
|
||||
import Greeting from "components/widgets/greeting/greeting";
|
||||
import DateTime from "components/widgets/datetime/datetime";
|
||||
|
||||
const widgetMappings = {
|
||||
weather: WeatherApi, // This key will be deprecated in the future
|
||||
@@ -9,13 +11,15 @@ const widgetMappings = {
|
||||
openweathermap: OpenWeatherMap,
|
||||
resources: Resources,
|
||||
search: Search,
|
||||
greeting: Greeting,
|
||||
datetime: DateTime,
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
const ServiceWidget = widgetMappings[widget.type];
|
||||
const InfoWidget = widgetMappings[widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
return <ServiceWidget options={widget.options} />;
|
||||
if (InfoWidget) {
|
||||
return <InfoWidget options={widget.options} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
36
src/components/widgets/datetime/datetime.jsx
Normal file
36
src/components/widgets/datetime/datetime.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function DateTime({ options }) {
|
||||
const { text_size: textSize, format } = options;
|
||||
const { i18n } = useTranslation();
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [setDate]);
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center grow justify-end">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
|
||||
{dateFormat.format(date)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/widgets/greeting/greeting.jsx
Normal file
22
src/components/widgets/greeting/greeting.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
"2xl": "text-2xl",
|
||||
xl: "text-xl",
|
||||
lg: "text-lg",
|
||||
md: "text-md",
|
||||
sm: "text-sm",
|
||||
xs: "text-xs",
|
||||
};
|
||||
|
||||
export default function Greeting({ options }) {
|
||||
if (options.text) {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-start">
|
||||
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[options.text_size || "xl"]}`}>
|
||||
{options.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import "styles/weather-icons.css";
|
||||
import "styles/theme.css";
|
||||
|
||||
import "utils/i18n";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
@@ -14,7 +16,11 @@ function MyApp({ Component, pageProps }) {
|
||||
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ColorProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ export default async function handler(req, res) {
|
||||
|
||||
try {
|
||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||
} catch {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors");
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||
console.error(e);
|
||||
discoveredServices = [];
|
||||
}
|
||||
|
||||
try {
|
||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error("Failed to load services.yaml, please check for errors");
|
||||
console.error(e);
|
||||
configuredServices = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,22 +4,83 @@ import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||
import npmProxyHandler from "utils/proxies/npm";
|
||||
import transmissionProxyHandler from "utils/proxies/transmission";
|
||||
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
|
||||
|
||||
function asJson(data) {
|
||||
if (data?.length > 0) {
|
||||
const json = JSON.parse(data.toString());
|
||||
return json;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function jsonArrayTransform(data, transform) {
|
||||
const json = asJson(data);
|
||||
if (json instanceof Array) {
|
||||
return transform(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function jsonArrayFilter(data, filter) {
|
||||
return jsonArrayTransform(data, items => items.filter(filter));
|
||||
}
|
||||
|
||||
const serviceProxyHandlers = {
|
||||
// uses query param auth
|
||||
emby: genericProxyHandler,
|
||||
jellyfin: genericProxyHandler,
|
||||
pihole: genericProxyHandler,
|
||||
radarr: genericProxyHandler,
|
||||
sonarr: genericProxyHandler,
|
||||
lidarr: genericProxyHandler,
|
||||
readarr: genericProxyHandler,
|
||||
bazarr: genericProxyHandler,
|
||||
radarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movie: (data) => ({
|
||||
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
|
||||
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length
|
||||
})
|
||||
},
|
||||
},
|
||||
sonarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
series: (data) => ({
|
||||
total: asJson(data).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
lidarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
album: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item.statistics.percentOfTracks === 100).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
readarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
book: (data) => ({
|
||||
have: jsonArrayFilter(data, (item) => item.statistics.bookFileCount > 0).length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
bazarr: {
|
||||
proxy: genericProxyHandler,
|
||||
maps: {
|
||||
movies: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
episodes: (data) => ({
|
||||
total: asJson(data).total,
|
||||
}),
|
||||
},
|
||||
},
|
||||
speedtest: genericProxyHandler,
|
||||
tautulli: genericProxyHandler,
|
||||
traefik: genericProxyHandler,
|
||||
sabnzbd: genericProxyHandler,
|
||||
jackett: genericProxyHandler,
|
||||
adguard: genericProxyHandler,
|
||||
// uses X-API-Key (or similar) header auth
|
||||
gotify: credentialedProxyHandler,
|
||||
portainer: credentialedProxyHandler,
|
||||
@@ -33,6 +94,7 @@ const serviceProxyHandlers = {
|
||||
nzbget: nzbgetProxyHandler,
|
||||
npm: npmProxyHandler,
|
||||
transmission: transmissionProxyHandler,
|
||||
qbittorrent: qbittorrentProxyHandler,
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -41,7 +103,14 @@ export default async function handler(req, res) {
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
return serviceProxyHandler(req, res);
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
const { proxy, maps } = serviceProxyHandler;
|
||||
if (proxy) {
|
||||
return proxy(req, res, maps);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
|
||||
@@ -3,15 +3,15 @@ import useSWR from "swr";
|
||||
import Head from "next/head";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useContext } from "react";
|
||||
|
||||
import ServicesGroup from "components/services/group";
|
||||
import BookmarksGroup from "components/bookmarks/group";
|
||||
import Widget from "components/widget";
|
||||
import Revalidate from "components/revalidate";
|
||||
import { getSettings } from "utils/config";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
import { ColorContext } from "utils/color-context";
|
||||
import { ThemeContext } from "utils/theme-context";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||
ssr: false,
|
||||
@@ -21,7 +21,7 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"];
|
||||
|
||||
export async function getStaticProps() {
|
||||
const settings = await getSettings();
|
||||
@@ -35,6 +35,8 @@ export async function getStaticProps() {
|
||||
|
||||
export default function Home({ settings }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const { color, setColor } = useContext(ColorContext);
|
||||
|
||||
const { data: services } = useSWR("/api/services");
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
@@ -44,67 +46,74 @@ export default function Home({ settings }) {
|
||||
if (settings.background) {
|
||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||
wrappedStyle.backgroundSize = "cover";
|
||||
wrappedStyle.opacity = settings.backgroundOpacity ?? 1;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.language) {
|
||||
i18n.changeLanguage(settings.language);
|
||||
}
|
||||
}, [i18n, settings.language]);
|
||||
|
||||
if (settings.theme && theme !== settings.theme) {
|
||||
setTheme(settings.theme);
|
||||
}
|
||||
|
||||
if (settings.color && color !== settings.color) {
|
||||
setColor(settings.color);
|
||||
}
|
||||
}, [i18n, settings, color, setColor, theme, setTheme]);
|
||||
|
||||
return (
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon && <link rel="icon" href={settings.favicon} />}
|
||||
</Head>
|
||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
|
||||
{widgets && (
|
||||
<>
|
||||
<>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon && <link rel="icon" href={settings.favicon} />}
|
||||
</Head>
|
||||
<div className="fixed w-full h-full m-0 p-0" style={wrappedStyle} />
|
||||
<div className="relative w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<div className="flex flex-row flex-wrap m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between">
|
||||
{widgets && (
|
||||
<>
|
||||
{widgets
|
||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
|
||||
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
||||
{widgets
|
||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
|
||||
<div className="ml-4 flex flex-wrap basis-full grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{services && (
|
||||
<div className="flex flex-wrap p-8 items-start">
|
||||
{services.map((group) => (
|
||||
<ServicesGroup key={group.name} services={group} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{bookmarks && (
|
||||
<div className="grow flex flex-wrap pt-0 p-8">
|
||||
{bookmarks.map((group) => (
|
||||
<BookmarksGroup key={group.name} group={group} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-full flex p-8 w-full justify-between">
|
||||
<ColorToggle />
|
||||
<Revalidate />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</ColorProvider>
|
||||
|
||||
{services && (
|
||||
<div className="flex flex-wrap p-8 items-start">
|
||||
{services.map((group) => (
|
||||
<ServicesGroup key={group.name} services={group} layout={settings.layout?.[group.name]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bookmarks && (
|
||||
<div className="grow flex flex-wrap pt-0 p-8">
|
||||
{bookmarks.map((group) => (
|
||||
<BookmarksGroup key={group.name} group={group} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-full flex p-8 w-full justify-end">
|
||||
{!settings?.color && <ColorToggle />}
|
||||
<Revalidate />
|
||||
{!settings?.theme && <ThemeToggle />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const formats = {
|
||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||
transmission: `{url}/transmission/rpc`,
|
||||
qbittorrent: `{url}/api/v2/{endpoint}`,
|
||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||
overseerr: `{url}/api/v1/{endpoint}`,
|
||||
ombi: `{url}/api/v1/{endpoint}`,
|
||||
@@ -21,7 +22,8 @@ const formats = {
|
||||
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
|
||||
gotify: `{url}/{endpoint}`,
|
||||
prowlarr: `{url}/api/v1/{endpoint}`,
|
||||
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`
|
||||
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
|
||||
adguard: `{url}/control/{endpoint}`,
|
||||
};
|
||||
|
||||
export function formatApiCall(api, args) {
|
||||
|
||||
34
src/utils/cookie-jar.js
Normal file
34
src/utils/cookie-jar.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
export function setCookieHeader(url, params) {
|
||||
// add cookie header, if we have one in the jar
|
||||
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
||||
if (existingCookie) {
|
||||
params.headers = params.headers ?? {};
|
||||
params.headers.Cookie = existingCookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function addCookieToJar(url, headers) {
|
||||
let cookieHeader = headers['set-cookie'];
|
||||
if (headers instanceof Headers) {
|
||||
cookieHeader = headers.get('set-cookie');
|
||||
}
|
||||
|
||||
if (!cookieHeader || cookieHeader.length === 0) return;
|
||||
|
||||
let cookies = null;
|
||||
if (cookieHeader instanceof Array) {
|
||||
cookies = cookieHeader.map(Cookie.parse);
|
||||
}
|
||||
else {
|
||||
cookies = [Cookie.parse(cookieHeader)];
|
||||
}
|
||||
|
||||
for (let i = 0; i < cookies.length; i += 1) {
|
||||
cookieJar.setCookieSync(cookies[i], url.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,15 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { http, https } from "follow-redirects";
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
function setCookieHeader(url, params) {
|
||||
// add cookie header, if we have one in the jar
|
||||
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
||||
if (existingCookie) {
|
||||
params.headers = params.headers ?? {};
|
||||
params.headers.Cookie = existingCookie;
|
||||
}
|
||||
}
|
||||
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||
|
||||
function addCookieHandler(url, params) {
|
||||
setCookieHeader(url, params);
|
||||
|
||||
// handle cookies during redirects
|
||||
params.beforeRedirect = (options, responseInfo) => {
|
||||
const cookieHeader = responseInfo.headers['set-cookie'];
|
||||
if (!cookieHeader || cookieHeader.length === 0) return;
|
||||
|
||||
let cookies = null;
|
||||
if (cookieHeader instanceof Array) {
|
||||
cookies = cookieHeader.map(Cookie.parse);
|
||||
}
|
||||
else {
|
||||
cookies = [Cookie.parse(cookieHeader)];
|
||||
}
|
||||
|
||||
for (let i = 0; i < cookies.length; i += 1) {
|
||||
cookieJar.setCookieSync(cookies[i], options.href);
|
||||
}
|
||||
|
||||
addCookieToJar(options.href, responseInfo.headers);
|
||||
setCookieHeader(options.href, options);
|
||||
};
|
||||
}
|
||||
@@ -49,6 +25,7 @@ export function httpsRequest(url, params) {
|
||||
});
|
||||
|
||||
response.on("end", () => {
|
||||
addCookieToJar(url, response.headers);
|
||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||
});
|
||||
});
|
||||
@@ -76,6 +53,7 @@ export function httpRequest(url, params) {
|
||||
});
|
||||
|
||||
response.on("end", () => {
|
||||
addCookieToJar(url, response.headers);
|
||||
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
|
||||
export default async function genericProxyHandler(req, res) {
|
||||
export default async function genericProxyHandler(req, res, maps) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
@@ -10,17 +10,31 @@ export default async function genericProxyHandler(req, res) {
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
|
||||
let headers;
|
||||
if (widget.username && widget.password) {
|
||||
headers = {
|
||||
Authorization: `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: req.method,
|
||||
headers,
|
||||
});
|
||||
|
||||
let resultData = data;
|
||||
if (maps?.[endpoint]) {
|
||||
resultData = maps[endpoint](data);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
if (status === 204 || status === 304) {
|
||||
return res.status(status).end();
|
||||
}
|
||||
|
||||
return res.status(status).send(data);
|
||||
return res.status(status).send(resultData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
src/utils/proxies/qbittorrent.js
Normal file
58
src/utils/proxies/qbittorrent.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
|
||||
import { httpProxy } from "utils/http";
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
|
||||
async function login(widget, params) {
|
||||
const loginUrl = new URL(`${widget.url}/api/v2/auth/login`);
|
||||
const loginBody = `username=${encodeURI(widget.username)}&password=${encodeURI(widget.password)}`;
|
||||
|
||||
// using fetch intentionally, for login only, as the httpProxy method causes qBittorrent to
|
||||
// complain about header encoding
|
||||
return fetch(loginUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: loginBody
|
||||
})
|
||||
.then(async response => {
|
||||
addCookieToJar(loginUrl, response.headers);
|
||||
setCookieHeader(loginUrl, params);
|
||||
const data = await response.text();
|
||||
return ([response.status, data]);
|
||||
})
|
||||
.catch(err => ([500, err]));
|
||||
}
|
||||
|
||||
export default async function qbittorrentProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (!widget) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
const params = { method: "GET", headers: {} };
|
||||
setCookieHeader(url, params);
|
||||
|
||||
if (!params.headers.Cookie) {
|
||||
const [status, data] = await login(widget, params);
|
||||
|
||||
if (status !== 200) {
|
||||
return res.status(status).end(data);
|
||||
}
|
||||
if (data.toString() !== 'Ok.') {
|
||||
return res.status(401).end(data);
|
||||
}
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, params);
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
Reference in New Issue
Block a user