diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json index 39b77411..a1bf5578 100644 --- a/public/locales/ar/common.json +++ b/public/locales/ar/common.json @@ -23,7 +23,7 @@ "free": "متاح", "used": "مستخدم", "load": "الضغط", - "mem": "MEM", + "mem": "الذاكرة", "temp": "TEMP", "max": "Max", "uptime": "UP", @@ -134,7 +134,7 @@ "episodes": "Episodes" }, "changedetectionio": { - "totalObserved": "Total Observed", + "totalObserved": "مجموع الملاحظات", "diffsDetected": "Diffs Detected" }, "tautulli": { @@ -179,18 +179,22 @@ "sonarr": { "wanted": "مطلوب", "queued": "في الإنتظار", - "series": "سلسلة" + "series": "سلسلة", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "مطلوب", "missing": "مفقود", "queued": "في الإنتظار", - "movies": "أفلام" + "movies": "أفلام", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "مطلوب", "queued": "في الإنتظار", - "albums": "ألبومات" + "artists": "Artists" }, "readarr": { "wanted": "مطلوب", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index 3fec6dbd..35dd13f4 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -117,18 +117,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "queued": "Queued", "movies": "Movies", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json index 33fe5338..3842feec 100644 --- a/public/locales/ca/common.json +++ b/public/locales/ca/common.json @@ -26,7 +26,9 @@ "sonarr": { "wanted": "Volgut", "queued": "En cua", - "series": "Sèries" + "series": "Sèries", + "queue": "Queue", + "unknown": "Unknown" }, "speedtest": { "ping": "Ping", @@ -99,7 +101,9 @@ "wanted": "Volgut", "queued": "En cua", "movies": "Pel·lícules", - "missing": "Faltant" + "missing": "Faltant", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Volgut", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Volgut", "queued": "En cua", - "albums": "Àlbums" + "artists": "Artists" }, "adguard": { "queries": "Consultes", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json index 6088851b..4e16a37a 100644 --- a/public/locales/cs/common.json +++ b/public/locales/cs/common.json @@ -133,18 +133,22 @@ "sonarr": { "wanted": "Hledané", "queued": "Ve frontě", - "series": "Seriály" + "series": "Seriály", + "unknown": "Unknown", + "queue": "Queue" }, "radarr": { "wanted": "Hledané", "missing": "Chybějící", "queued": "Ve frontě", - "movies": "Filmy" + "movies": "Filmy", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Hledané", "queued": "Ve frontě", - "albums": "Alba" + "artists": "Artists" }, "readarr": { "wanted": "Hledané", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadSpeed": "Download Speed", + "downloadCount": "Queue Count", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/da/common.json b/public/locales/da/common.json index 3769da6f..76ff86dd 100644 --- a/public/locales/da/common.json +++ b/public/locales/da/common.json @@ -9,12 +9,14 @@ "queued": "I Kø", "movies": "Film", "wanted": "Ønskede", - "missing": "Mangler" + "missing": "Mangler", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Ønsket", "queued": "I Kø", - "albums": "Albums" + "artists": "Artists" }, "jellyseerr": { "available": "Tilgængelig", @@ -264,7 +266,9 @@ "sonarr": { "wanted": "Ønsket", "queued": "I Kø", - "series": "Serier" + "series": "Serier", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Ønskede", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadSpeed": "Download Speed", + "downloadCount": "Queue Count", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/de/common.json b/public/locales/de/common.json index b62c5e6a..521e2cec 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Gesucht", "queued": "In Warteschlange", - "series": "Serien" + "series": "Serien", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Gesucht", "queued": "In Warteschlange", "movies": "Filme", - "missing": "Fehlt" + "missing": "Fehlt", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Gesucht", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Gesucht", "queued": "In Warteschlange", - "albums": "Alben" + "artists": "Artists" }, "adguard": { "queries": "Anfragen", @@ -640,5 +644,11 @@ "connected": "Verbunden", "new_devices": "Neue Geräte", "down_alerts": "Down Alarme" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/el/common.json b/public/locales/el/common.json index d42fdfe5..b36b43f6 100644 --- a/public/locales/el/common.json +++ b/public/locales/el/common.json @@ -206,7 +206,9 @@ "sonarr": { "series": "Σειρές", "wanted": "Επιθυμούντε", - "queued": "Σε σειρά" + "queued": "Σε σειρά", + "queue": "Queue", + "unknown": "Unknown" }, "downloadstation": { "download": "Μεταφόρτωση", @@ -218,12 +220,14 @@ "wanted": "Επιθυμούντε", "missing": "Απουσιάζει", "queued": "Σε σειρά", - "movies": "Ταινίες" + "movies": "Ταινίες", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Θέλετε", "queued": "Στη σειρά", - "albums": "Δίσκοι" + "artists": "Artists" }, "readarr": { "wanted": "Θέλετε", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 1de843cc..e26f4a0b 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -194,18 +194,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -650,11 +654,13 @@ "monitoring": "Monitoring", "updates": "Updates" }, - "nextpvr": { - "upcoming": "Upcoming", - "ready": "Recent" - }, "wgeasy": { "clients": "Total Clients" + }, + "jdownloader": { + "downloadCount": "Queue", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size", + "downloadSpeed": "Speed" } } \ No newline at end of file diff --git a/public/locales/eo/common.json b/public/locales/eo/common.json index 50d305ac..908169cb 100644 --- a/public/locales/eo/common.json +++ b/public/locales/eo/common.json @@ -131,18 +131,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Serio" + "series": "Serio", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Filmoj" + "movies": "Filmoj", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albumoj" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 1ca5a62f..91c48ce1 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Buscando", "queued": "En cola", - "series": "Series" + "series": "Series", + "queue": "Poner a la cola", + "unknown": "Desconocido" }, "radarr": { "wanted": "Buscando", "queued": "En cola", "movies": "Películas", - "missing": "Faltan" + "missing": "Faltan", + "queue": "Poner a la cola", + "unknown": "Desconocido" }, "readarr": { "wanted": "Buscando", @@ -173,7 +177,7 @@ "lidarr": { "queued": "En cola", "wanted": "Buscando", - "albums": "Álbumes" + "artists": "Artistas" }, "adguard": { "queries": "Consultas", @@ -640,5 +644,11 @@ "connected": "Conectado", "new_devices": "Nuevos dispositivos", "down_alerts": "Alertas" + }, + "jdownloader": { + "downloadCount": "Recuento de las colas", + "downloadSpeed": "Velocidad de Descarga", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json index aad674a1..c3875b21 100644 --- a/public/locales/fi/common.json +++ b/public/locales/fi/common.json @@ -94,18 +94,22 @@ "sonarr": { "wanted": "Haluttu", "queued": "Jonossa", - "series": "Sarja" + "series": "Sarja", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Haluttu", "queued": "Jonossa", "movies": "Elokuvia", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Haluttu", "queued": "Jonossa", - "albums": "Albumeja" + "artists": "Artists" }, "readarr": { "wanted": "Haluttu", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 2e9e0e0d..43b44d45 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Demande", "queued": "Attente", - "series": "Séries" + "series": "Séries", + "queue": "Attente", + "unknown": "Inconnu" }, "radarr": { "wanted": "Demande", "queued": "Attente", "movies": "Films", - "missing": "Manquant" + "missing": "Manquant", + "queue": "Attente", + "unknown": "Inconnu" }, "readarr": { "wanted": "Demande", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Demandé", "queued": "En queue", - "albums": "Albums" + "artists": "Artistes" }, "adguard": { "queries": "Requêtes", @@ -397,7 +401,7 @@ "queue": "À traiter", "processed": "Traité", "errored": "En erreur", - "saved": "Economisé" + "saved": "Libéré" }, "miniflux": { "read": "Lu", @@ -640,5 +644,11 @@ "connected": "Connecté", "new_devices": "Nouvel Appareil", "down_alerts": "Alertes" + }, + "jdownloader": { + "downloadCount": "Total en attente", + "downloadSpeed": "Vitesse de téléchargement", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/he/common.json b/public/locales/he/common.json index 50f61ae5..41e5746b 100644 --- a/public/locales/he/common.json +++ b/public/locales/he/common.json @@ -94,18 +94,22 @@ "sonarr": { "wanted": "מבוקש", "queued": "בתור", - "series": "סדרות" + "series": "סדרות", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "מבוקש", "queued": "בתור", "movies": "סרטים", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "מבוקש", "queued": "בתור", - "albums": "אלבומים" + "artists": "Artists" }, "readarr": { "wanted": "מבוקש", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index 6736caf0..155235a9 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -155,18 +155,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "overseerr": { "pending": "Pending", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json index db20a999..94dfae26 100644 --- a/public/locales/hr/common.json +++ b/public/locales/hr/common.json @@ -125,18 +125,22 @@ "sonarr": { "wanted": "Zatraženo", "queued": "U redu čekanja", - "series": "Serije" + "series": "Serije", + "unknown": "Unknown", + "queue": "Queue" }, "radarr": { "wanted": "Zatraženo", "queued": "U redu čekanja", "movies": "Filmovi", - "missing": "Nedostaje" + "missing": "Nedostaje", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Zatraženo", "queued": "U redu čekanja", - "albums": "Albumi" + "artists": "Artists" }, "readarr": { "wanted": "Zatraženo", @@ -239,11 +243,11 @@ "uptime": "UP", "days": "d", "hours": "h", - "used": "Used", - "load": "Load", - "warn": "Warn", - "total": "Total", - "free": "Free" + "used": "Korišteno", + "load": "Opterećenje", + "warn": "Upozori", + "total": "Ukupno", + "free": "Slobodno" }, "changedetectionio": { "totalObserved": "Ukupno promatrano", @@ -478,7 +482,7 @@ "up": "Aktivne stranice", "down": "Neaktivne stranice", "uptime": "Radno vrijeme", - "incident": "Incident", + "incident": "Slučaj", "m": "min" }, "komga": { @@ -609,36 +613,42 @@ "poolUsage": "Korištenje memorijskog skupa", "cpuUsage": "Korištenje procesora", "memUsage": "Korištenje memorije", - "volumeUsage": "Volume Usage", - "invalid": "Invalid" + "volumeUsage": "Korištenje jedinice memorije", + "invalid": "Neispravno" }, "pfsense": { - "load": "Load Avg", - "memory": "Mem Usage", - "wanStatus": "WAN Status", + "load": "Prosječno opterećenje", + "memory": "Korištenje memorije", + "wanStatus": "Stanje WAN-a", "up": "Up", "down": "Down", - "temp": "Temp", - "disk": "Disk Usage", + "temp": "Temperatura", + "disk": "Korištenje diska", "wanIP": "WAN IP" }, "caddy": { - "upstreams": "Upstreams", - "requests": "Current requests", - "requests_failed": "Failed requests" + "upstreams": "Glavne grane", + "requests": "Aktualni zahtjevi", + "requests_failed": "Neuspjeli zahtjevi" }, "evcc": { - "pv_power": "Production", - "battery_soc": "Battery", - "grid_power": "Grid", - "home_power": "Consumption", - "charge_power": "Charger", + "pv_power": "Proizvodnja", + "battery_soc": "Baterija", + "grid_power": "Raspored", + "home_power": "Potrošnja", + "charge_power": "Punjač", "watt_hour": "Wh" }, "pialert": { - "total": "Total", - "connected": "Connected", - "new_devices": "New Devices", - "down_alerts": "Down Alerts" + "total": "Ukupno", + "connected": "Povezano", + "new_devices": "Novi uređaji", + "down_alerts": "Obavijest o rušenju" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json index b417692d..016a54d8 100644 --- a/public/locales/hu/common.json +++ b/public/locales/hu/common.json @@ -31,9 +31,9 @@ "healthy": "Healthy" }, "lidarr": { - "albums": "Albumok", "wanted": "Keresett", - "queued": "Sorban áll" + "queued": "Sorban áll", + "artists": "Artists" }, "readarr": { "wanted": "Keresett", @@ -108,13 +108,17 @@ "sonarr": { "wanted": "Keresett", "queued": "Sorban áll", - "series": "Sorozat" + "series": "Sorozat", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Keresett", "queued": "Sorban áll", "movies": "Filmek", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "ombi": { "pending": "Függőben", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadSpeed": "Download Speed", + "downloadCount": "Queue Count", + "downloadTotalBytes": "Size", + "downloadBytesRemaining": "Remaining" } } diff --git a/public/locales/id/common.json b/public/locales/id/common.json index e9169cbd..0be279a5 100644 --- a/public/locales/id/common.json +++ b/public/locales/id/common.json @@ -55,18 +55,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "transcoding": "Transcoding", "bitrate": "Bitrate", "no_active": "No Active Streams" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 8dc065d4..68aaa2f5 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -81,13 +81,17 @@ "sonarr": { "series": "Serie", "wanted": "Richiesti", - "queued": "In coda" + "queued": "In coda", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Richiesti", "queued": "In coda", "movies": "Film", - "missing": "Mancanti" + "missing": "Mancanti", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Richiesti", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Mancanti", "queued": "In coda", - "albums": "Album" + "artists": "Artists" }, "adguard": { "queries": "Interrogazioni", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index bcca3d2a..1bc37e96 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -63,7 +63,7 @@ "resources": { "cpu": "CPU", "total": "合計", - "free": "フリー", + "free": "Free", "used": "使用", "load": "ロード", "mem": "MEM", @@ -193,18 +193,22 @@ "sonarr": { "wanted": "募集中", "queued": "待機中", - "series": "シリーズ" + "series": "シリーズ", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "募集中", "missing": "不明", "queued": "キュー", - "movies": "映画" + "movies": "映画", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "募集中", "queued": "キュー", - "albums": "アルバム" + "artists": "Artists" }, "readarr": { "wanted": "募集中", @@ -605,11 +609,11 @@ "ago": "{{value}} 前" }, "qnap": { - "cpuUsage": "CPU Usage", - "memUsage": "MEM Usage", - "systemTempC": "System Temp", - "poolUsage": "Pool Usage", - "volumeUsage": "Volume Usage", + "cpuUsage": "CPU使用量", + "memUsage": "MEM使用量", + "systemTempC": "システム温度", + "poolUsage": "プール使用量", + "volumeUsage": "ボリューム使用量", "invalid": "Invalid" }, "pfsense": { @@ -629,16 +633,22 @@ }, "evcc": { "watt_hour": "Wh", - "pv_power": "Production", - "battery_soc": "Battery", - "grid_power": "Grid", - "home_power": "Consumption", - "charge_power": "Charger" + "pv_power": "発電量", + "battery_soc": "バッテリー", + "grid_power": "グリッド", + "home_power": "消費", + "charge_power": "チャージャー" }, "pialert": { "total": "Total", "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ko/common.json b/public/locales/ko/common.json index 4e5e6298..f9037f52 100644 --- a/public/locales/ko/common.json +++ b/public/locales/ko/common.json @@ -163,18 +163,22 @@ "sonarr": { "wanted": "요청", "queued": "대기 중", - "series": "시리즈" + "series": "시리즈", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "요청", "missing": "빠짐", "queued": "대기 중", - "movies": "영화" + "movies": "영화", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "요청", "queued": "대기 중", - "albums": "앨범" + "artists": "Artists" }, "readarr": { "wanted": "요청", @@ -640,5 +644,11 @@ "connected": "Connected", "down_alerts": "Down Alerts", "new_devices": "New Devices" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/lv/common.json b/public/locales/lv/common.json index fa73f2c7..70361ee2 100644 --- a/public/locales/lv/common.json +++ b/public/locales/lv/common.json @@ -154,18 +154,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Filmas" + "movies": "Filmas", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albumi" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ms/common.json b/public/locales/ms/common.json index da42ff44..70fd66e3 100644 --- a/public/locales/ms/common.json +++ b/public/locales/ms/common.json @@ -33,8 +33,8 @@ }, "lidarr": { "queued": "Dibaris Gilir", - "albums": "Album", - "wanted": "Mahu" + "wanted": "Mahu", + "artists": "Artists" }, "readarr": { "wanted": "Mahu", @@ -233,13 +233,17 @@ "sonarr": { "wanted": "Mahu", "queued": "Dibaris Gilir", - "series": "Bersiri" + "series": "Bersiri", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Mahu", "missing": "Hilang", "queued": "Dibaris Gilir", - "movies": "Filem" + "movies": "Filem", + "queue": "Queue", + "unknown": "Unknown" }, "bazarr": { "missingEpisodes": "Episod Yang Hilang", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/nb-NO/common.json b/public/locales/nb-NO/common.json index e7a2dd5c..79124218 100644 --- a/public/locales/nb-NO/common.json +++ b/public/locales/nb-NO/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Ønsket", "queued": "I kø", - "series": "Serie" + "series": "Serie", + "unknown": "Unknown", + "queue": "Queue" }, "radarr": { "wanted": "Ønsket", "queued": "I kø", "movies": "Filmer", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Wanted", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "adguard": { "queries": "Queries", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 0be39980..139e9051 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -82,13 +82,17 @@ "sonarr": { "wanted": "Gezocht", "queued": "In de wachtrij", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "movies": "Films", "wanted": "Gezocht", "queued": "In de wachtrij", - "missing": "Missend" + "missing": "Missend", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Gezocht", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Gezocht", "queued": "In de wachtrij", - "albums": "Albums" + "artists": "Artists" }, "adguard": { "queries": "Queries", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json index a900de72..6af51828 100644 --- a/public/locales/pl/common.json +++ b/public/locales/pl/common.json @@ -110,18 +110,22 @@ "sonarr": { "wanted": "Poszukiwane", "queued": "W kolejce", - "series": "Seriale" + "series": "Seriale", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Poszukiwane", "queued": "W kolejce", "movies": "Filmy", - "missing": "Brakujące" + "missing": "Brakujące", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Poszukiwane", "queued": "W kolejce", - "albums": "Albumy" + "artists": "Artists" }, "readarr": { "wanted": "Poszukiwane", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json index bc405db0..d2146d2c 100644 --- a/public/locales/pt-BR/common.json +++ b/public/locales/pt-BR/common.json @@ -112,18 +112,22 @@ "sonarr": { "wanted": "Desejado", "queued": "Na fila", - "series": "Séries" + "series": "Séries", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Desejado", "queued": "Na fila", "movies": "Filmes", - "missing": "Faltando" + "missing": "Faltando", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Desejado", "queued": "Na fila", - "albums": "Álbuns" + "artists": "Artists" }, "readarr": { "wanted": "Desejado", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index b0df5367..5c84e6c5 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Desejada", "queued": "Em fila", - "series": "Séries" + "series": "Séries", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Desejado", "queued": "Fila", "movies": "Filmes", - "missing": "Faltando" + "missing": "Faltando", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Desejados", @@ -186,7 +190,7 @@ "lidarr": { "queued": "Enfileirado", "wanted": "Desejado", - "albums": "Álbuns" + "artists": "Artists" }, "adguard": { "queries": "Consultas", @@ -586,12 +590,12 @@ "switches_on": "Interruptores Ligados" }, "freshrss": { - "subscriptions": "Subscriptions", - "unread": "Unread" + "subscriptions": "Assinaturas", + "unread": "Não lida" }, "channelsdvrserver": { "shows": "Shows", - "recordings": "Recordings", + "recordings": "Gravações", "scheduled": "Scheduled", "passes": "Passes" }, @@ -633,21 +637,27 @@ }, "caddy": { "upstreams": "Upstreams", - "requests": "Current requests", - "requests_failed": "Failed requests" + "requests": "Solicitações atuais", + "requests_failed": "Solicitações com falha" }, "evcc": { - "pv_power": "Production", - "battery_soc": "Battery", - "grid_power": "Grid", - "home_power": "Consumption", - "charge_power": "Charger", - "watt_hour": "Wh" + "pv_power": "Produção", + "battery_soc": "Bateria", + "grid_power": "Grade", + "home_power": "Consumo", + "charge_power": "Carregador", + "watt_hour": "Kw" }, "pialert": { "total": "Total", "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json index a1f62db2..e8bf9ac7 100644 --- a/public/locales/ro/common.json +++ b/public/locales/ro/common.json @@ -134,18 +134,22 @@ "sonarr": { "wanted": "Dorite", "queued": "În coadă", - "series": "Seriale" + "series": "Seriale", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "queued": "În coadă", "wanted": "Dorite", "movies": "Filme", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Dorite", "queued": "În coadă", - "albums": "Albume" + "artists": "Artists" }, "readarr": { "wanted": "Dorite", @@ -640,5 +644,11 @@ "down_alerts": "Down Alerts", "total": "Total", "connected": "Connected" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 454bfdfe..d3cd6d48 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Хотел", "queued": "В очереди", - "series": "Серии" + "series": "Серии", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Хотел", "queued": "В очереди", "movies": "Фильмы", - "missing": "Пропущено" + "missing": "Пропущено", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Хотел", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Хотел", "queued": "В очереди", - "albums": "Альбомы" + "artists": "Artists" }, "adguard": { "queries": "Запросы", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/sk/common.json b/public/locales/sk/common.json index a66ef5e6..5f95889c 100644 --- a/public/locales/sk/common.json +++ b/public/locales/sk/common.json @@ -273,18 +273,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/sl/common.json b/public/locales/sl/common.json index afca8429..24d4b8f7 100644 --- a/public/locales/sl/common.json +++ b/public/locales/sl/common.json @@ -235,18 +235,22 @@ "sonarr": { "wanted": "Iskano", "queued": "V vrsti", - "series": "Serije" + "series": "Serije", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Iskano", "missing": "Manjka", "queued": "V vrsti", - "movies": "Filmi" + "movies": "Filmi", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Iskano", "queued": "V vrsti", - "albums": "Albumi" + "artists": "Artists" }, "readarr": { "wanted": "Iskano", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/sr/common.json b/public/locales/sr/common.json index b30db3fe..9ef56193 100644 --- a/public/locales/sr/common.json +++ b/public/locales/sr/common.json @@ -117,18 +117,22 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "queued": "Queued", "movies": "Movies", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "readarr": { "wanted": "Wanted", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index 37b38a71..c3325844 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -88,18 +88,22 @@ "sonarr": { "wanted": "Eftersöker", "queued": "I kö", - "series": "Serier" + "series": "Serier", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Eftersöker", "queued": "I kö", "movies": "Filmer", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Eftersöker", "queued": "I kö", - "albums": "Album" + "artists": "Artists" }, "readarr": { "wanted": "Eftersökt", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/te/common.json b/public/locales/te/common.json index e357b266..b7d11223 100644 --- a/public/locales/te/common.json +++ b/public/locales/te/common.json @@ -111,18 +111,22 @@ "sonarr": { "wanted": "కావలెను", "queued": "క్యూయూఎడ్", - "series": "సిరీస్" + "series": "సిరీస్", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "కావలెను", "queued": "క్యూయూఎడ్", "movies": "సినిమాలు", - "missing": "మిస్సింగ్" + "missing": "మిస్సింగ్", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "కావలెను", "queued": "క్యూయూఎడ్", - "albums": "ఆల్బములు" + "artists": "Artists" }, "bazarr": { "missingEpisodes": "ఎపిసోడ్‌లు లేవు", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/th/common.json b/public/locales/th/common.json index 65c1b90a..2afb51f2 100644 --- a/public/locales/th/common.json +++ b/public/locales/th/common.json @@ -190,7 +190,9 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "queued": "Queued", @@ -216,12 +218,14 @@ "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "ombi": { "pending": "Pending", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json index d5be780e..b152e0f7 100644 --- a/public/locales/tr/common.json +++ b/public/locales/tr/common.json @@ -24,13 +24,13 @@ "used": "Kullanımda", "load": "Yük", "mem": "MEM", - "temp": "TEMP", - "max": "Max", - "uptime": "UP", - "months": "mo", - "days": "d", - "hours": "h", - "minutes": "m" + "temp": "Geçici", + "max": "En Yüksek", + "uptime": "Çalışma Süresi", + "months": "Ay", + "days": "Gün", + "hours": "Saat", + "minutes": "Dakika" }, "unifi": { "users": "Kullanıcılar", @@ -57,23 +57,23 @@ "offline": "Çevrimdışı", "error": "Hata", "unknown": "Bilinmiyor", - "running": "Running", - "starting": "Starting", - "unhealthy": "Unhealthy", - "not_found": "Not Found", - "exited": "Exited", - "partial": "Partial", - "healthy": "Healthy" + "running": "Çalışan", + "starting": "Başlatılıyor", + "unhealthy": "Sağlıksız", + "not_found": "Bulunamadı", + "exited": "Durduruldu", + "partial": "Parçalı", + "healthy": "Sağlık" }, "emby": { "playing": "Oynatılıyor", "transcoding": "Dönüştürülüyor", "bitrate": "Bit Oranı", "no_active": "Aktif akış yok", - "movies": "Movies", - "series": "Series", - "episodes": "Episodes", - "songs": "Songs" + "movies": "Filmler", + "series": "Diziler", + "episodes": "Bölümler", + "songs": "Şarkılar" }, "tautulli": { "playing": "Oynatılıyor", @@ -90,7 +90,7 @@ "streams": "Aktif Akış", "movies": "Filmler", "tv": "TV Showları", - "albums": "Albums" + "albums": "Albümler" }, "sabnzbd": { "rate": "Oran", @@ -117,18 +117,22 @@ "sonarr": { "wanted": "Aranan", "queued": "Kuyrukta", - "series": "Seriler" + "series": "Seriler", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Aranan", "queued": "Kuyrukta", "movies": "Filmler", - "missing": "Kayıp" + "missing": "Kayıp", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Aranan", "queued": "Kuyrukta", - "albums": "Albümler" + "artists": "Artists" }, "readarr": { "wanted": "Aranan", @@ -159,7 +163,7 @@ "queries": "Sorgular", "blocked": "Engellenen", "gravity": "Yer Çekimi", - "blocked_percent": "Blocked %" + "blocked_percent": "Engellenen %" }, "adguard": { "queries": "Sorgular", @@ -235,15 +239,15 @@ "glances": { "cpu": "İşlemci", "wait": "Lütfen bekleyiniz", - "temp": "TEMP", - "uptime": "UP", - "days": "d", - "hours": "h", - "load": "Load", - "warn": "Warn", - "total": "Total", - "free": "Free", - "used": "Used" + "temp": "Sıcaklık", + "uptime": "Çalışma Süresi", + "days": "Gün", + "hours": "Saat", + "load": "Yük", + "warn": "Uyarı", + "total": "Toplam", + "free": "Boş", + "used": "Kullanım" }, "changedetectionio": { "totalObserved": "Toplam Gözlenen", @@ -311,9 +315,9 @@ "bookmark": "Yer İmi", "service": "Hizmet", "search": "Ara", - "custom": "Custom", - "visit": "Visit", - "url": "URL" + "custom": "Özel", + "visit": "Ziyaret", + "url": "Link" }, "homebridge": { "available_update": "Sistem", @@ -384,14 +388,14 @@ "deluge": { "download": "İndir", "upload": "Yükle", - "leech": "Leech", + "leech": "Tüketici", "seed": "Tohum" }, "flood": { "download": "İndir", "upload": "Yükle", - "leech": "Leech", - "seed": "Tohum" + "leech": "Tüketici", + "seed": "Sağlayıcı" }, "tdarr": { "queue": "Sıra", @@ -421,7 +425,7 @@ "downloadstation": { "download": "İndir", "upload": "Yükle", - "leech": "Leech", + "leech": "Tüketici", "seed": "Tohum" }, "mikrotik": { @@ -448,7 +452,7 @@ "layers": "Katmanlar" }, "medusa": { - "wanted": "Wanted", + "wanted": "Aranan", "queued": "Kuyrukta", "series": "Seri" }, @@ -554,11 +558,11 @@ "targets_total": "Total Targets" }, "minecraft": { - "players": "Players", - "version": "Version", - "status": "Status", - "up": "Online", - "down": "Offline" + "players": "Oyuncular", + "version": "Versiyon", + "status": "Durum", + "up": "Çevrimiçi", + "down": "Çevrimdışı" }, "ghostfolio": { "gross_percent_today": "Today", @@ -577,40 +581,40 @@ "switches_on": "Switches On" }, "freshrss": { - "subscriptions": "Subscriptions", - "unread": "Unread" + "subscriptions": "Abonelikler", + "unread": "Okunmamış" }, "channelsdvrserver": { - "shows": "Shows", - "recordings": "Recordings", - "scheduled": "Scheduled", - "passes": "Passes" + "shows": "Diziler", + "recordings": "Kayıtlar", + "scheduled": "Planlanmış", + "passes": "Geçilenler" }, "whatsupdocker": { "monitoring": "Monitoring", "updates": "Updates" }, "tailscale": { - "never": "Never", - "last_seen": "Last Seen", - "now": "Now", - "years": "{{number}}y", - "weeks": "{{number}}w", - "days": "{{number}}d", - "hours": "{{number}}h", - "minutes": "{{number}}m", - "seconds": "{{number}}s", - "ago": "{{value}} Ago", - "address": "Address", - "expires": "Expires" + "never": "Asla", + "last_seen": "Son Görülme", + "now": "Şimdi", + "years": "{{number}} Yıl", + "weeks": "{{number}} Hafta", + "days": "{{number}} Gün", + "hours": "{{number}} Saat", + "minutes": "{{number}} Dakika", + "seconds": "{{number}} Saniye", + "ago": "{{value}} Önce", + "address": "Adres", + "expires": "Geciken" }, "qnap": { - "cpuUsage": "CPU Usage", - "memUsage": "MEM Usage", - "systemTempC": "System Temp", - "poolUsage": "Pool Usage", - "volumeUsage": "Volume Usage", - "invalid": "Invalid" + "cpuUsage": "İşlemci Kullanımı", + "memUsage": "Bellek Kullanımı", + "systemTempC": "Sistem Sıcaklığı", + "poolUsage": "Havuz Kullanımı", + "volumeUsage": "Alan Kullanımı", + "invalid": "Geçersiz" }, "pfsense": { "load": "Load Avg", @@ -623,22 +627,28 @@ "wanIP": "WAN IP" }, "caddy": { - "upstreams": "Upstreams", - "requests": "Current requests", - "requests_failed": "Failed requests" + "upstreams": "Akış", + "requests": "Anlık İstekler", + "requests_failed": "Başarısız İstekler" }, "evcc": { - "pv_power": "Production", - "battery_soc": "Battery", - "grid_power": "Grid", - "home_power": "Consumption", - "charge_power": "Charger", - "watt_hour": "Wh" + "pv_power": "Üretim", + "battery_soc": "Batarya", + "grid_power": "Güç", + "home_power": "Tüketim", + "charge_power": "Şarj", + "watt_hour": "Watt/Saat" }, "pialert": { - "total": "Total", - "connected": "Connected", - "new_devices": "New Devices", - "down_alerts": "Down Alerts" + "total": "Toplam", + "connected": "Bağlandı", + "new_devices": "Yeni Cihazlar", + "down_alerts": "Düşme Uyarıları" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json index 37320dfd..1abd398d 100644 --- a/public/locales/uk/common.json +++ b/public/locales/uk/common.json @@ -232,18 +232,22 @@ "sonarr": { "wanted": "Розшукується", "queued": "У черзі", - "series": "Серії" + "series": "Серії", + "queue": "Черга", + "unknown": "Невідомо" }, "radarr": { "wanted": "Розшукується", "missing": "Відсутній", "queued": "У черзі", - "movies": "Фільми" + "movies": "Фільми", + "queue": "Черга", + "unknown": "Невідомо" }, "lidarr": { "wanted": "Розшукується", "queued": "У черзі", - "albums": "Альбоми" + "artists": "Виконавці" }, "traefik": { "middleware": "Проміжне програмне забезпечення", @@ -640,5 +644,11 @@ "connected": "Підключено", "new_devices": "Нові пристрої", "down_alerts": "Сповіщення про збій" + }, + "jdownloader": { + "downloadCount": "Всього в черзі", + "downloadSpeed": "Швидкість завантаження", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json index 54431b3c..069eaff2 100644 --- a/public/locales/vi/common.json +++ b/public/locales/vi/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "queued": "Queued", "movies": "Phim", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "Đang tìm", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "Wanted", "queued": "Queued", - "albums": "Albums" + "artists": "Artists" }, "adguard": { "queries": "Queries", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json index 5ab69f88..4e360b26 100644 --- a/public/locales/yue/common.json +++ b/public/locales/yue/common.json @@ -94,18 +94,22 @@ "sonarr": { "wanted": "想睇", "queued": "排緊隊", - "series": "電視劇" + "series": "電視劇", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "想睇", "queued": "排緊隊", "movies": "電影", - "missing": "Missing" + "missing": "Missing", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "想睇", "queued": "排緊隊", - "albums": "專輯" + "artists": "Artists" }, "readarr": { "wanted": "想睇", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index 0d6f427e..a1f0dfda 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -66,13 +66,17 @@ "sonarr": { "wanted": "想看", "queued": "排队", - "series": "系列" + "series": "系列", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "想看", "queued": "队列", "movies": "电影", - "missing": "丢失" + "missing": "丢失", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "订阅", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "订阅", "queued": "队列", - "albums": "相册" + "artists": "Artists" }, "adguard": { "queries": "查询", @@ -640,5 +644,11 @@ "connected": "Connected", "new_devices": "New Devices", "down_alerts": "Down Alerts" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index 75674709..7c0cb9fe 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -88,12 +88,16 @@ "movies": "電影", "wanted": "關注中", "queued": "已加入佇列", - "missing": "缺少" + "missing": "缺少", + "queue": "Queue", + "unknown": "Unknown" }, "sonarr": { "wanted": "關注中", "queued": "已加入佇列", - "series": "影集" + "series": "影集", + "queue": "Queue", + "unknown": "Unknown" }, "readarr": { "wanted": "關注中", @@ -173,7 +177,7 @@ "lidarr": { "wanted": "關注中", "queued": "已加入佇列", - "albums": "專輯" + "artists": "Artists" }, "adguard": { "queries": "查詢", @@ -640,5 +644,11 @@ "connected": "已連線", "new_devices": "新裝置", "down_alerts": "離線警告" + }, + "jdownloader": { + "downloadCount": "Queue Count", + "downloadSpeed": "Download Speed", + "downloadBytesRemaining": "Remaining", + "downloadTotalBytes": "Size" } } diff --git a/src/components/services/group.jsx b/src/components/services/group.jsx index 5f1c5446..94557064 100644 --- a/src/components/services/group.jsx +++ b/src/components/services/group.jsx @@ -3,7 +3,7 @@ import classNames from "classnames"; import List from "components/services/list"; import ResolvedIcon from "components/resolvedicon"; -export default function ServicesGroup({ services, layout, fiveColumns }) { +export default function ServicesGroup({ group, services, layout, fiveColumns }) { return (
{services.name}
- + ); } diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index 08b8d1f4..36e454ce 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -11,7 +11,7 @@ import Kubernetes from "widgets/kubernetes/component"; import { SettingsContext } from "utils/contexts/settings"; import ResolvedIcon from "components/resolvedicon"; -export default function Item({ service }) { +export default function Item({ service, group }) { const hasLink = service.href && service.href !== "#"; const { settings } = useContext(SettingsContext); const showStats = (service.showStats === false) ? false : settings.showStats; @@ -77,7 +77,7 @@ export default function Item({ service }) {
{service.ping && (
- + Ping status
)} diff --git a/src/components/services/list.jsx b/src/components/services/list.jsx index c8028df5..85083af3 100644 --- a/src/components/services/list.jsx +++ b/src/components/services/list.jsx @@ -14,7 +14,7 @@ const columnMap = [ "grid-cols-1 md:grid-cols-2 lg:grid-cols-8", ]; -export default function List({ services, layout }) { +export default function List({ group, services, layout }) { return (
    {services.map((service) => ( - + ))}
); diff --git a/src/components/services/ping.jsx b/src/components/services/ping.jsx index a54b1b55..291bc9e0 100644 --- a/src/components/services/ping.jsx +++ b/src/components/services/ping.jsx @@ -1,9 +1,9 @@ import { useTranslation } from "react-i18next"; import useSWR from "swr"; -export default function Ping({ service }) { +export default function Ping({ group, service }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ping: service.ping}).toString()}`, { + const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ group, service }).toString()}`, { refreshInterval: 30000 }); @@ -23,7 +23,7 @@ export default function Ping({ service }) { ); } - const statusText = `${service.ping}: HTTP status ${data.status}`; + const statusText = `${service}: HTTP status ${data.status}`; if (data.status > 403) { return ( diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index f4d8c13e..4b8a06ca 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -15,7 +15,9 @@ export default function Container({ error = false, children, service }) { return } - let visibleChildren = children; + const childrenArray = Array.isArray(children) ? children : [children]; + + let visibleChildren = childrenArray; const fields = service?.widget?.fields; const type = service?.widget?.type; if (fields && type) { @@ -24,7 +26,7 @@ export default function Container({ error = false, children, service }) { // fields: [ "resources.cpu", "resources.mem", "field"] // or even // fields: [ "resources.cpu", "widget_type.field" ] - visibleChildren = children?.filter(child => fields.some(field => { + visibleChildren = childrenArray?.filter(child => fields.some(field => { let fullField = field; if (!field.includes(".")) { fullField = `${type}.${field}`; diff --git a/src/components/services/widget/error.jsx b/src/components/services/widget/error.jsx index 587c572f..cf5e1366 100644 --- a/src/components/services/widget/error.jsx +++ b/src/components/services/widget/error.jsx @@ -9,10 +9,12 @@ function displayData(data) { return (data.type === 'Buffer') ? Buffer.from(data).toString() : JSON.stringify(data, 4); } -export default function Error({ error: err }) { +export default function Error({ error }) { const { t } = useTranslation(); - const { error } = err?.data ?? { error: err }; + if (error?.data?.error) { + error = error.data.error; // eslint-disable-line no-param-reassign + } return (
diff --git a/src/components/widgets/datetime/datetime.jsx b/src/components/widgets/datetime/datetime.jsx index 86983473..454d004d 100644 --- a/src/components/widgets/datetime/datetime.jsx +++ b/src/components/widgets/datetime/datetime.jsx @@ -1,6 +1,9 @@ import { useState, useEffect } from "react"; import { useTranslation } from "next-i18next"; +import Container from "../widget/container"; +import Raw from "../widget/raw"; + const textSizes = { "4xl": "text-4xl", "3xl": "text-3xl", @@ -17,7 +20,7 @@ export default function DateTime({ options }) { const { i18n } = useTranslation(); const [date, setDate] = useState(""); const dateLocale = locale ?? i18n.language; - + useEffect(() => { const dateFormat = new Intl.DateTimeFormat(dateLocale, { ...format }); const interval = setInterval(() => { @@ -27,12 +30,14 @@ export default function DateTime({ options }) { }, [date, setDate, dateLocale, format]); return ( -
-
- - {date} - -
-
+ + +
+ + {date} + +
+
+
); } diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx index 85dd44c0..e5cf3fbd 100644 --- a/src/components/widgets/glances/glances.jsx +++ b/src/components/widgets/glances/glances.jsx @@ -1,11 +1,13 @@ import useSWR from "swr"; import { useContext } from "react"; -import { BiError } from "react-icons/bi"; import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa"; import { FiCpu, FiHardDrive } from "react-icons/fi"; import { useTranslation } from "next-i18next"; -import UsageBar from "../resources/usage-bar"; +import Error from "../widget/error"; +import Resource from "../widget/resource"; +import Resources from "../widget/resources"; +import WidgetLabel from "../widget/widget_label"; import { SettingsContext } from "utils/contexts/settings"; @@ -26,52 +28,19 @@ export default function Widget({ options }) { ); if (error || data?.error) { - return ( -
-
-
- -
- {t("widget.api_error")} -
-
-
-
- ); + return } if (!data) { - return ( -
-
-
- -
-
-
- {t("glances.wait")} -
-
- -
-
-
- -
-
-
- {t("glances.wait")} -
-
- -
-
-
- {options.label && ( -
{options.label}
- )} -
- ); + return + + + { options.cputemp && } + { options.disk && !Array.isArray(options.disk) && } + { options.disk && Array.isArray(options.disk) && options.disk.map((disk) => )} + { options.uptime && } + { options.label && } + ; } const unit = options.units === "imperial" ? "fahrenheit" : "celsius"; @@ -101,131 +70,84 @@ export default function Widget({ options }) { } return ( - -
-
- -
-
-
- {t("common.number", { - value: data.cpu.total, - style: "unit", - unit: "percent", - maximumFractionDigits: 0, - })} -
-
{t("glances.cpu")}
-
- {options.expanded && ( - -
- {t("common.number", { - value: data.load.min15, - style: "unit", - unit: "percent", - maximumFractionDigits: 0, - })} -
-
{t("glances.load")}
-
- )} - -
-
-
- -
-
-
- {t("common.bytes", { - value: data.mem.free, - maximumFractionDigits: 1, - binary: true, - })} -
-
{t("glances.free")}
-
- {options.expanded && ( - -
- {t("common.bytes", { - value: data.mem.total, - maximumFractionDigits: 1, - binary: true, - })} -
-
{t("glances.total")}
-
- )} - -
-
- {disks.map((disk) => ( -
- -
- -
{t("common.bytes", { value: disk.free })}
-
{t("glances.free")}
-
- {options.expanded && ( - -
{t("common.bytes", { value: disk.size })}
-
{t("glances.total")}
-
- )} - -
-
))} - {options.cputemp && mainTemp > 0 && - (
- -
- -
- {t("common.number", { - value: mainTemp, - maximumFractionDigits: 1, - style: "unit", - unit - })} -
-
{t("glances.temp")}
-
- {options.expanded && ( - -
- {t("common.number", { - value: maxTemp, - maximumFractionDigits: 1, - style: "unit", - unit - })} -
-
{t("glances.warn")}
-
- )} - -
-
)} - {options.uptime && data.uptime && - (
- -
- -
- {data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))} -
-
{t("glances.uptime")}
-
- -
-
)} -
- {options.label && ( -
{options.label}
- )} -
+ + + + {disks.map((disk) => ( + + ))} + {options.cputemp && mainTemp > 0 && + + } + {options.uptime && data.uptime && + + } + {options.label && } + ); } diff --git a/src/components/widgets/greeting/greeting.jsx b/src/components/widgets/greeting/greeting.jsx index da0f063d..11de571c 100644 --- a/src/components/widgets/greeting/greeting.jsx +++ b/src/components/widgets/greeting/greeting.jsx @@ -1,3 +1,6 @@ +import Container from "../widget/container"; +import Raw from "../widget/raw"; + const textSizes = { "4xl": "text-4xl", "3xl": "text-3xl", @@ -11,12 +14,12 @@ const textSizes = { export default function Greeting({ options }) { if (options.text) { - return ( -
+ return + {options.text} -
- ); + + ; } } diff --git a/src/components/widgets/kubernetes/kubernetes.jsx b/src/components/widgets/kubernetes/kubernetes.jsx index 78c4caaf..2d1f55e4 100644 --- a/src/components/widgets/kubernetes/kubernetes.jsx +++ b/src/components/widgets/kubernetes/kubernetes.jsx @@ -1,12 +1,15 @@ import useSWR from "swr"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; +import Error from "../widget/error"; +import Container from "../widget/container"; +import Raw from "../widget/raw"; + import Node from "./node"; export default function Widget({ options }) { const { cluster, nodes } = options; - const { t, i18n } = useTranslation(); + const { i18n } = useTranslation(); const defaultData = { cpu: { @@ -18,7 +21,7 @@ export default function Widget({ options }) { used: 0, total: 0, free: 0, - precent: 0 + percent: 0 } }; @@ -29,23 +32,12 @@ export default function Widget({ options }) { ); if (error || data?.error) { - return ( -
-
-
- -
- {t("widget.api_error")} -
-
-
-
- ); + return } if (!data) { - return ( -
+ return +
{cluster.show && @@ -54,12 +46,12 @@ export default function Widget({ options }) { }
-
- ); + + ; } - return ( -
+ return +
{cluster.show && @@ -69,6 +61,6 @@ export default function Widget({ options }) { ) }
-
- ); + + ; } diff --git a/src/components/widgets/kubernetes/node.jsx b/src/components/widgets/kubernetes/node.jsx index 7a7c322d..cc864be6 100644 --- a/src/components/widgets/kubernetes/node.jsx +++ b/src/components/widgets/kubernetes/node.jsx @@ -3,8 +3,7 @@ import { FiAlertTriangle, FiCpu, FiServer } from "react-icons/fi"; import { SiKubernetes } from "react-icons/si"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; - +import UsageBar from "../resources/usage-bar"; export default function Node({ type, options, data }) { const { t } = useTranslation(); @@ -29,7 +28,7 @@ export default function Node({ type, options, data }) {
{t("common.number", { - value: data.cpu.percent, + value: data?.cpu?.percent ?? 0, style: "unit", unit: "percent", maximumFractionDigits: 0 @@ -37,18 +36,18 @@ export default function Node({ type, options, data }) {
- +
{t("common.bytes", { - value: data.memory.free, + value: data?.memory?.free ?? 0, maximumFractionDigits: 0, binary: true })}
- + {options.showLabel && (
{type === "cluster" ? options.label : data.name}
)} diff --git a/src/components/widgets/kubernetes/usage-bar.jsx b/src/components/widgets/kubernetes/usage-bar.jsx deleted file mode 100644 index c817db4c..00000000 --- a/src/components/widgets/kubernetes/usage-bar.jsx +++ /dev/null @@ -1,12 +0,0 @@ -export default function UsageBar({ percent }) { - return ( -
-
-
- ); -} diff --git a/src/components/widgets/logo/logo.jsx b/src/components/widgets/logo/logo.jsx index 96e8569f..3a4a2565 100644 --- a/src/components/widgets/logo/logo.jsx +++ b/src/components/widgets/logo/logo.jsx @@ -1,9 +1,13 @@ +import Container from "../widget/container"; +import Raw from "../widget/raw"; + import ResolvedIcon from "components/resolvedicon" export default function Logo({ options }) { return ( -
- {options.icon ? + + + {options.icon ? : // fallback to homepage logo } -
+ + ) } diff --git a/src/components/widgets/longhorn/longhorn.jsx b/src/components/widgets/longhorn/longhorn.jsx index 9fcb21b4..c0169ceb 100644 --- a/src/components/widgets/longhorn/longhorn.jsx +++ b/src/components/widgets/longhorn/longhorn.jsx @@ -1,37 +1,31 @@ import useSWR from "swr"; -import { BiError } from "react-icons/bi"; -import { useTranslation } from "next-i18next"; + +import Error from "../widget/error"; +import Container from "../widget/container"; +import Raw from "../widget/raw"; import Node from "./node"; export default function Longhorn({ options }) { const { expanded, total, labels, include, nodes } = options; - const { t } = useTranslation(); const { data, error } = useSWR(`/api/widgets/longhorn`, { refreshInterval: 1500 }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data) { - return ( -
+ return +
-
- ); +
+
; } - return ( -
+ return +
{data.nodes .filter((node) => { @@ -52,6 +46,6 @@ export default function Longhorn({ options }) {
)}
-
- ); + + ; } diff --git a/src/components/widgets/longhorn/node.jsx b/src/components/widgets/longhorn/node.jsx index e0ee69af..5235698a 100644 --- a/src/components/widgets/longhorn/node.jsx +++ b/src/components/widgets/longhorn/node.jsx @@ -1,32 +1,20 @@ -import { FiHardDrive } from "react-icons/fi"; import { useTranslation } from "next-i18next"; +import { FaThermometerHalf } from "react-icons/fa"; -import UsageBar from "../resources/usage-bar"; +import Resource from "../widget/resource"; +import WidgetLabel from "../widget/widget_label"; export default function Node({ data, expanded, labels }) { const { t } = useTranslation(); - return ( - <> -
- -
- -
{t("common.bytes", { value: data.node.available })}
-
{t("resources.free")}
-
- {expanded && ( - -
{t("common.bytes", { value: data.node.maximum })}
-
{t("resources.total")}
-
- )} - -
-
- {labels && ( -
{data.node.id}
- )} - - ); + return { labels && } + } diff --git a/src/components/widgets/openmeteo/openmeteo.jsx b/src/components/widgets/openmeteo/openmeteo.jsx index 0d29aef5..040a3b6b 100644 --- a/src/components/widgets/openmeteo/openmeteo.jsx +++ b/src/components/widgets/openmeteo/openmeteo.jsx @@ -1,10 +1,16 @@ import useSWR from "swr"; import { useState } from "react"; -import { BiError } from "react-icons/bi"; import { WiCloudDown } from "react-icons/wi"; import { MdLocationDisabled, MdLocationSearching } from "react-icons/md"; import { useTranslation } from "next-i18next"; +import Error from "../widget/error"; +import Container from "../widget/container"; +import ContainerButton from "../widget/container_button"; +import WidgetIcon from "../widget/widget_icon"; +import PrimaryText from "../widget/primary_text"; +import SecondaryText from "../widget/secondary_text"; + import Icon from "./icon"; function Widget({ options }) { @@ -15,60 +21,35 @@ function Widget({ options }) { ); if (error || data?.error) { - return ( -
-
-
- -
- {t("widget.api_error")} - - -
-
-
-
- ); + return } if (!data) { - return ( -
-
-
- -
-
- {t("weather.updating")} - {t("weather.wait")} -
-
-
- ); + return + {t("weather.updating")} + {t("weather.wait")} + + ; } const unit = options.units === "metric" ? "celsius" : "fahrenheit"; - const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night"; + const weatherInfo = { + condition: data.current_weather.weathercode, + timeOfDay: data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night" + }; - return ( -
-
-
- -
-
- - {options.label && `${options.label}, `} - {t("common.number", { - value: data.current_weather.temperature, - style: "unit", - unit, - })} - - {t(`wmo.${data.current_weather.weathercode}-${timeOfDay}`)} -
-
-
- ); + return + + {options.label && `${options.label}, `} + {t("common.number", { + value: data.current_weather.temperature, + style: "unit", + unit, + })} + + {t(`wmo.${data.current_weather.weathercode}-${weatherInfo.timeOfDay}`)} + + ; } export default function OpenMeteo({ options }) { @@ -103,27 +84,11 @@ export default function OpenMeteo({ options }) { // if (!requesting && !location) requestLocation(); if (!location) { - return ( - - ); + return + {t("weather.current")} + {t("weather.allow")} + + ; } return ; diff --git a/src/components/widgets/openweathermap/weather.jsx b/src/components/widgets/openweathermap/weather.jsx index 49f428a0..a857f13a 100644 --- a/src/components/widgets/openweathermap/weather.jsx +++ b/src/components/widgets/openweathermap/weather.jsx @@ -1,12 +1,19 @@ import useSWR from "swr"; import { useState } from "react"; -import { BiError } from "react-icons/bi"; import { WiCloudDown } from "react-icons/wi"; import { MdLocationDisabled, MdLocationSearching } from "react-icons/md"; import { useTranslation } from "next-i18next"; +import Error from "../widget/error"; +import Container from "../widget/container"; +import ContainerButton from "../widget/container_button"; +import PrimaryText from "../widget/primary_text"; +import SecondaryText from "../widget/secondary_text"; +import WidgetIcon from "../widget/widget_icon"; + import Icon from "./icon"; + function Widget({ options }) { const { t, i18n } = useTranslation(); @@ -15,58 +22,29 @@ function Widget({ options }) { ); if (error || data?.cod === 401 || data?.error) { - return ( -
-
-
- -
- {t("widget.api_error")} - - -
-
-
-
- ); + return } if (!data) { - return ( -
-
-
- -
-
- {t("weather.updating")} - {t("weather.wait")} -
-
-
- ); + return + {t("weather.updating")} + {t("weather.wait")} + + ; } const unit = options.units === "metric" ? "celsius" : "fahrenheit"; - return ( -
-
-
- data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"} - /> -
-
- - {options.label && `${options.label}, `} - {t("common.number", { value: data.main.temp, style: "unit", unit })} - - {data.weather[0].description} -
-
-
- ); + const weatherInfo = { + condition: data.weather[0].id, + timeOfDay: data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night" + }; + + return + {options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })} + {data.weather[0].description} + + ; } export default function OpenWeatherMap({ options }) { @@ -98,30 +76,12 @@ export default function OpenWeatherMap({ options }) { } }; - // if (!requesting && !location) requestLocation(); - if (!location) { - return ( - - ); + return + {t("weather.current")} + {t("weather.allow")} + + ; } return ; diff --git a/src/components/widgets/queue/queueEntry.jsx b/src/components/widgets/queue/queueEntry.jsx new file mode 100644 index 00000000..adea45ad --- /dev/null +++ b/src/components/widgets/queue/queueEntry.jsx @@ -0,0 +1,18 @@ +export default function QueueEntry({ title, activity, timeLeft, progress}) { + return ( +
+
+
+
{title}
+
+
+ {timeLeft ? `${activity} - ${timeLeft}` : activity} +
+
+ ); +} diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 7069e3c4..12972fe8 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -1,9 +1,9 @@ import useSWR from "swr"; import { FiCpu } from "react-icons/fi"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; +import Resource from "../widget/resource"; +import Error from "../widget/error"; export default function Cpu({ expanded }) { const { t } = useTranslation(); @@ -13,67 +13,29 @@ export default function Cpu({ expanded }) { }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data) { - return ( -
- -
-
-
-
-
{t("resources.cpu")}
-
- {expanded && ( -
-
-
-
{t("resources.load")}
-
- )} - -
-
- ); + return } - const percent = data.cpu.usage; - - return ( -
- -
-
-
- {t("common.number", { - value: data.cpu.usage, - style: "unit", - unit: "percent", - maximumFractionDigits: 0, - })} -
-
{t("resources.cpu")}
-
- {expanded && ( -
-
- {t("common.number", { - value: data.cpu.load, - maximumFractionDigits: 2, - })} -
-
{t("resources.load")}
-
- )} - -
-
- ); + return } diff --git a/src/components/widgets/resources/cputemp.jsx b/src/components/widgets/resources/cputemp.jsx index 571e6c8a..ba6d9b73 100644 --- a/src/components/widgets/resources/cputemp.jsx +++ b/src/components/widgets/resources/cputemp.jsx @@ -1,9 +1,9 @@ import useSWR from "swr"; import { FaThermometerHalf } from "react-icons/fa"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; +import Resource from "../widget/resource"; +import Error from "../widget/error"; function convertToFahrenheit(t) { return t * 9/5 + 32 @@ -17,34 +17,18 @@ export default function CpuTemp({ expanded, units }) { }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data || !data.cputemp) { - return ( -
- -
- -
-
-
{t("resources.temp")}
-
- {expanded && ( - -
-
-
{t("resources.max")}
-
- )} -
-
- ); + return ; } let mainTemp = data.cputemp.main; @@ -54,38 +38,24 @@ export default function CpuTemp({ expanded, units }) { const unit = units === "imperial" ? "fahrenheit" : "celsius"; mainTemp = (unit === "celsius") ? mainTemp : convertToFahrenheit(mainTemp); const maxTemp = (unit === "celsius") ? data.cputemp.max : convertToFahrenheit(data.cputemp.max); - const percent = Math.round((mainTemp / maxTemp) * 100); - return ( -
- -
- -
- {t("common.number", { - value: mainTemp, - maximumFractionDigits: 1, - style: "unit", - unit - })} -
-
{t("resources.temp")}
-
- {expanded && ( - -
- {t("common.number", { - value: maxTemp, - maximumFractionDigits: 1, - style: "unit", - unit - })} -
-
{t("resources.max")}
-
- )} - -
-
- ); + return ; } diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index ca09c095..ab56624d 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -1,9 +1,9 @@ import useSWR from "swr"; import { FiHardDrive } from "react-icons/fi"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; +import Resource from "../widget/resource"; +import Error from "../widget/error"; export default function Disk({ options, expanded }) { const { t } = useTranslation(); @@ -13,56 +13,31 @@ export default function Disk({ options, expanded }) { }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data) { - return ( -
- -
- -
-
-
{t("resources.free")}
-
- {expanded && ( - -
-
-
{t("resources.total")}
-
- )} - -
-
- ); + return ; } // data.drive.used not accurate? const percent = Math.round(((data.drive.size - data.drive.available) / data.drive.size) * 100); - return ( -
- -
- -
{t("common.bytes", { value: data.drive.available })}
-
{t("resources.free")}
-
- {expanded && ( - -
{t("common.bytes", { value: data.drive.size })}
-
{t("resources.total")}
-
- )} - -
-
- ); + return ; } diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index 30b7c8eb..19ae8687 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -1,9 +1,9 @@ import useSWR from "swr"; import { FaMemory } from "react-icons/fa"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; +import Resource from "../widget/resource"; +import Error from "../widget/error"; export default function Memory({ expanded }) { const { t } = useTranslation(); @@ -13,63 +13,30 @@ export default function Memory({ expanded }) { }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data) { - return ( -
- -
- -
-
-
{t("resources.free")}
-
- {expanded && ( - -
-
-
{t("resources.total")}
-
- )} - -
-
- ); + return ; } const percent = Math.round((data.memory.active / data.memory.total) * 100); - return ( -
- -
- -
- {t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })} -
-
{t("resources.free")}
-
- {expanded && ( - -
- {t("common.bytes", { - value: data.memory.total, - maximumFractionDigits: 1, - binary: true, - })} -
-
{t("resources.total")}
-
- )} - -
-
- ); + return ; } diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx index 4ff0c81c..0cc2c301 100644 --- a/src/components/widgets/resources/resources.jsx +++ b/src/components/widgets/resources/resources.jsx @@ -1,3 +1,6 @@ +import Container from "../widget/container"; +import Raw from "../widget/raw"; + import Disk from "./disk"; import Cpu from "./cpu"; import Memory from "./memory"; @@ -6,8 +9,8 @@ import Uptime from "./uptime"; export default function Resources({ options }) { const { expanded, units } = options; - return ( -
+ return +
{options.cpu && } {options.memory && } @@ -20,6 +23,6 @@ export default function Resources({ options }) { {options.label && (
{options.label}
)} -
- ); +
+
; } diff --git a/src/components/widgets/resources/uptime.jsx b/src/components/widgets/resources/uptime.jsx index 3bf785b1..3984975f 100644 --- a/src/components/widgets/resources/uptime.jsx +++ b/src/components/widgets/resources/uptime.jsx @@ -1,9 +1,9 @@ import useSWR from "swr"; import { FaRegClock } from "react-icons/fa"; -import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; -import UsageBar from "./usage-bar"; +import Resource from "../widget/resource"; +import Error from "../widget/error"; export default function Uptime() { const { t } = useTranslation(); @@ -13,54 +13,24 @@ export default function Uptime() { }); if (error || data?.error) { - return ( -
- -
- {t("widget.api_error")} -
-
- ); + return } if (!data) { - return ( -
- -
- -
-
-
{t("resources.temp")}
-
-
-
- ); + return ; } const mo = Math.floor(data.uptime / (3600 * 24 * 31)); const d = Math.floor(data.uptime % (3600 * 24 * 31) / (3600 * 24)); const h = Math.floor(data.uptime % (3600 * 24) / 3600); const m = Math.floor(data.uptime % 3600 / 60); - + let uptime; if (mo > 0) uptime = `${mo}${t("resources.months")} ${d}${t("resources.days")}`; else if (d > 0) uptime = `${d}${t("resources.days")} ${h}${t("resources.hours")}`; else uptime = `${h}${t("resources.hours")} ${m}${t("resources.minutes")}`; - const percent = Math.round((new Date().getSeconds() / 60) * 100); + const percent = Math.round((new Date().getSeconds() / 60) * 100).toString(); - return ( -
- -
- -
- {uptime} -
-
{t("resources.uptime")}
-
- -
-
- ); + return ; } diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx index 4689567f..1bac4a61 100644 --- a/src/components/widgets/search/search.jsx +++ b/src/components/widgets/search/search.jsx @@ -1,10 +1,13 @@ -import { useState, useEffect, Fragment } from "react"; +import { useState, useEffect, useCallback, Fragment } from "react"; import { useTranslation } from "next-i18next"; import { FiSearch } from "react-icons/fi"; import { SiDuckduckgo, SiMicrosoftbing, SiGoogle, SiBaidu, SiBrave } from "react-icons/si"; import { Listbox, Transition } from "@headlessui/react"; import classNames from "classnames"; +import ContainerForm from "../widget/container_form"; +import Raw from "../widget/raw"; + export const searchProviders = { google: { name: "Google", @@ -76,14 +79,9 @@ export default function Search({ options }) { setSelectedProvider(storedProvider); } }, [availableProviderIds]); - - if (!availableProviderIds) { - return null; - } - function handleSubmit(event) { + const submitCallback = useCallback(event => { const q = encodeURIComponent(query); - const { url } = selectedProvider; if (url) { window.open(`${url}${q}`, options.target || "_blank"); @@ -94,6 +92,10 @@ export default function Search({ options }) { event.preventDefault(); event.target.reset(); setQuery(""); + }, [options.target, options.url, query, selectedProvider]); + + if (!availableProviderIds) { + return null; } const onChangeProvider = (provider) => { @@ -101,77 +103,79 @@ export default function Search({ options }) { localStorage.setItem(localStorageKey, provider.name); } - return ( -
-
- setQuery(s.currentTarget.value)} - required - autoCapitalize="off" - autoCorrect="off" - autoComplete="off" - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={options.focus} - /> - -
- + +
+
+ setQuery(s.currentTarget.value)} + required + autoCapitalize="off" + autoCorrect="off" + autoComplete="off" + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={options.focus} + /> + +
+ + + {t("search.search")} + +
+ - - {t("search.search")} - -
- - -
- {availableProviderIds.map((providerId) => { - const p = searchProviders[providerId]; - return ( - - {({ active }) => ( -
  • - -
  • - )} -
    - ); - })} -
    -
    -
    - - - ); + +
    + {availableProviderIds.map((providerId) => { + const p = searchProviders[providerId]; + return ( + + {({ active }) => ( +
  • + +
  • + )} +
    + ); + })} +
    +
    + + +
    +
    + ; } diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx index 13c90bd4..dad92cc7 100644 --- a/src/components/widgets/unifi_console/unifi_console.jsx +++ b/src/components/widgets/unifi_console/unifi_console.jsx @@ -3,6 +3,12 @@ import { MdSettingsEthernet } from "react-icons/md"; import { useTranslation } from "next-i18next"; import { SiUbiquiti } from "react-icons/si"; +import Error from "../widget/error"; +import Container from "../widget/container"; +import Raw from "../widget/raw"; +import WidgetIcon from "../widget/widget_icon"; +import PrimaryText from "../widget/primary_text"; + import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Widget({ options }) { @@ -13,35 +19,16 @@ export default function Widget({ options }) { const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index }); if (statsError) { - return ( -
    -
    -
    - -
    - {t("widget.api_error")} -
    -
    -
    -
    - ); + return } const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default"); if (!defaultSite) { - return ( -
    -
    -
    - -
    -
    - {t("unifi.wait")} -
    -
    -
    - ); + return + {t("unifi.wait")} + + ; } const wan = defaultSite.health.find(h => h.subsystem === "wan"); @@ -56,8 +43,9 @@ export default function Widget({ options }) { const dataEmpty = !(wan.show || lan.show || wlan.show || uptime); - return ( -
    + return + +
    @@ -141,6 +129,7 @@ export default function Widget({ options }) {
    }
    -
    - ); +
    + + } diff --git a/src/components/widgets/weather/weather.jsx b/src/components/widgets/weather/weather.jsx index 20bf3dec..702ea669 100644 --- a/src/components/widgets/weather/weather.jsx +++ b/src/components/widgets/weather/weather.jsx @@ -1,10 +1,16 @@ import useSWR from "swr"; import { useState } from "react"; -import { BiError } from "react-icons/bi"; import { WiCloudDown } from "react-icons/wi"; import { MdLocationDisabled, MdLocationSearching } from "react-icons/md"; import { useTranslation } from "next-i18next"; +import Error from "../widget/error"; +import Container from "../widget/container"; +import PrimaryText from "../widget/primary_text"; +import SecondaryText from "../widget/secondary_text"; +import WidgetIcon from "../widget/widget_icon"; +import ContainerButton from "../widget/container_button"; + import Icon from "./icon"; function Widget({ options }) { @@ -15,59 +21,35 @@ function Widget({ options }) { ); if (error || data?.error) { - return ( -
    -
    -
    - -
    - {t("widget.api_error")} - - -
    -
    -
    -
    - ); + return } if (!data) { - return ( -
    -
    -
    - -
    -
    - {t("weather.updating")} - {t("weather.wait")} -
    -
    -
    - ); + return + {t("weather.updating")} + {t("weather.wait")} + + ; } const unit = options.units === "metric" ? "celsius" : "fahrenheit"; + const weatherInfo = { + condition: data.current.condition.code, + timeOfDay: data.current.is_day ? "day" : "night", + }; - return ( -
    -
    -
    - -
    -
    - - {options.label && `${options.label}, `} - {t("common.number", { - value: options.units === "metric" ? data.current.temp_c : data.current.temp_f, - style: "unit", - unit, - })} - - {data.current.condition.text} -
    -
    -
    - ); + return + + {options.label && `${options.label}, `} + {t("common.number", { + value: options.units === "metric" ? data.current.temp_c : data.current.temp_f, + style: "unit", + unit, + })} + + {data.current.condition.text} + + ; } export default function WeatherApi({ options }) { @@ -99,30 +81,12 @@ export default function WeatherApi({ options }) { } }; - // if (!requesting && !location) requestLocation(); - if (!location) { - return ( - - ); + return + {t("weather.current")} + {t("weather.allow")} + + ; } return ; diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index 47141887..b4fdb143 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -17,13 +17,13 @@ const widgetMappings = { kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")), }; -export default function Widget({ widget }) { +export default function Widget({ widget, style }) { const InfoWidget = widgetMappings[widget.type]; if (InfoWidget) { return ( - + ); } diff --git a/src/components/widgets/widget/container.jsx b/src/components/widgets/widget/container.jsx new file mode 100644 index 00000000..59ea5684 --- /dev/null +++ b/src/components/widgets/widget/container.jsx @@ -0,0 +1,54 @@ +import classNames from "classnames"; + +import WidgetIcon from "./widget_icon"; +import PrimaryText from "./primary_text"; +import SecondaryText from "./secondary_text"; +import Raw from "./raw"; + +export function getAllClasses(options, additionalClassNames = '') { + if (options?.style?.header === "boxedWidgets") { + return classNames( + "flex flex-col justify-center first:ml-0 ml-2 mr-2", + "mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-2 pl-3 pr-3", + additionalClassNames + ); + } + + let widgetAlignedClasses = "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap"; + if (options?.style?.isRightAligned) { + widgetAlignedClasses = "flex flex-col justify-center first:ml-auto ml-2 mr-2 "; + } + + return classNames( + widgetAlignedClasses, + additionalClassNames + ); +} + +export function getInnerBlock(children) { + // children won't be an array if it's Raw component + return Array.isArray(children) &&
    +
    {children.find(child => child.type === WidgetIcon)}
    +
    + {children.find(child => child.type === PrimaryText)} + {children.find(child => child.type === SecondaryText)} +
    +
    ; +} + +export function getBottomBlock(children) { + if (children.type !== Raw) { + return children.find(child => child.type === Raw) || []; + } + + return [children]; +} + +export default function Container({ children = [], options, additionalClassNames = '' }) { + return ( +
    + {getInnerBlock(children)} + {getBottomBlock(children)} +
    + ); +} diff --git a/src/components/widgets/widget/container_button.jsx b/src/components/widgets/widget/container_button.jsx new file mode 100644 index 00000000..92d8a416 --- /dev/null +++ b/src/components/widgets/widget/container_button.jsx @@ -0,0 +1,10 @@ +import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; + +export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) { + return ( + + ); +} diff --git a/src/components/widgets/widget/container_form.jsx b/src/components/widgets/widget/container_form.jsx new file mode 100644 index 00000000..7d28a1bb --- /dev/null +++ b/src/components/widgets/widget/container_form.jsx @@ -0,0 +1,10 @@ +import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; + +export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) { + return ( +
    + {getInnerBlock(children)} + {getBottomBlock(children)} +
    + ); +} diff --git a/src/components/widgets/widget/container_link.jsx b/src/components/widgets/widget/container_link.jsx new file mode 100644 index 00000000..8ef0e80a --- /dev/null +++ b/src/components/widgets/widget/container_link.jsx @@ -0,0 +1,10 @@ +import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; + +export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) { + return ( + + {getInnerBlock(children)} + {getBottomBlock(children)} + + ); +} diff --git a/src/components/widgets/widget/error.jsx b/src/components/widgets/widget/error.jsx new file mode 100644 index 00000000..a3dbab85 --- /dev/null +++ b/src/components/widgets/widget/error.jsx @@ -0,0 +1,15 @@ +import { useTranslation } from "react-i18next"; +import { BiError } from "react-icons/bi"; + +import Container from "./container"; +import PrimaryText from "./primary_text"; +import WidgetIcon from "./widget_icon"; + +export default function Error({ options }) { + const { t } = useTranslation(); + + return + {t("widget.api_error")} + + ; +} diff --git a/src/components/widgets/widget/primary_text.jsx b/src/components/widgets/widget/primary_text.jsx new file mode 100644 index 00000000..3418b92c --- /dev/null +++ b/src/components/widgets/widget/primary_text.jsx @@ -0,0 +1,5 @@ +export default function PrimaryText({ children }) { + return ( + {children} + ); +} diff --git a/src/components/widgets/widget/raw.jsx b/src/components/widgets/widget/raw.jsx new file mode 100644 index 00000000..44e3dddc --- /dev/null +++ b/src/components/widgets/widget/raw.jsx @@ -0,0 +1,7 @@ +export default function Raw({ children }) { + if (children.type === Raw) { + return [children]; + } + + return children; +} diff --git a/src/components/widgets/widget/resource.jsx b/src/components/widgets/widget/resource.jsx new file mode 100644 index 00000000..18e7c800 --- /dev/null +++ b/src/components/widgets/widget/resource.jsx @@ -0,0 +1,22 @@ +import UsageBar from "../resources/usage-bar"; + +export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) { + const Icon = icon; + + return
    + +
    +
    +
    {value}
    +
    {label}
    +
    + { expanded &&
    +
    {expandedValue}
    +
    {expandedLabel}
    +
    + } + { percentage && } + { children } +
    +
    ; +} diff --git a/src/components/widgets/widget/resources.jsx b/src/components/widgets/widget/resources.jsx new file mode 100644 index 00000000..394a3058 --- /dev/null +++ b/src/components/widgets/widget/resources.jsx @@ -0,0 +1,17 @@ +import ContainerLink from "./container_link"; +import Resource from "./resource"; +import Raw from "./raw"; +import WidgetLabel from "./widget_label"; + +export default function Resources({ options, children, target }) { + const widgetParts = [].concat(...children); + + return + +
    + { widgetParts.filter(child => child && child.type === Resource) } +
    + { widgetParts.filter(child => child && child.type === WidgetLabel) } +
    +
    ; +} diff --git a/src/components/widgets/widget/secondary_text.jsx b/src/components/widgets/widget/secondary_text.jsx new file mode 100644 index 00000000..363d1bd0 --- /dev/null +++ b/src/components/widgets/widget/secondary_text.jsx @@ -0,0 +1,5 @@ +export default function SecondaryText({ children }) { + return ( + {children} + ); +} diff --git a/src/components/widgets/widget/widget_icon.jsx b/src/components/widgets/widget/widget_icon.jsx new file mode 100644 index 00000000..9766a879 --- /dev/null +++ b/src/components/widgets/widget/widget_icon.jsx @@ -0,0 +1,18 @@ +export default function WidgetIcon({ icon, size = "s", pulse = false, weatherInfo = {} }) { + const Icon = icon; + const { condition, timeOfDay } = weatherInfo; + let additionalClasses = "text-theme-800 dark:text-theme-200 "; + + switch (size) { + case "m": additionalClasses += "w-6 h-6 "; break; + case "l": additionalClasses += "w-8 h-8 "; break; + case "xl": additionalClasses += "w-10 h-10 "; break; + default: additionalClasses += "w-5 h-5 "; + } + + if (pulse) { + additionalClasses += "animate-pulse "; + } + + return ; +} diff --git a/src/components/widgets/widget/widget_label.jsx b/src/components/widgets/widget/widget_label.jsx new file mode 100644 index 00000000..5fc6ced0 --- /dev/null +++ b/src/components/widgets/widget/widget_label.jsx @@ -0,0 +1,3 @@ +export default function WidgetLabel({ label = "" }) { + return
    {label}
    +} diff --git a/src/pages/api/ping.js b/src/pages/api/ping.js index 96c1b12c..cfc2aafa 100644 --- a/src/pages/api/ping.js +++ b/src/pages/api/ping.js @@ -1,12 +1,22 @@ import { performance } from "perf_hooks"; +import { getServiceItem } from "utils/config/service-helpers"; import createLogger from "utils/logger"; import { httpProxy } from "utils/proxy/http"; const logger = createLogger("ping"); export default async function handler(req, res) { - const { ping: pingURL } = req.query; + const { group, service } = req.query; + const serviceItem = await getServiceItem(group, service); + if (!serviceItem) { + logger.debug(`No service item found for group ${group} named ${service}`); + return res.status(400).send({ + error: "Unable to find service, see log for details.", + }); + } + + const { ping: pingURL } = serviceItem; if (!pingURL) { logger.debug("No ping URL specified"); diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js index a6b6781c..d23a7f61 100644 --- a/src/pages/api/widgets/longhorn.js +++ b/src/pages/api/widgets/longhorn.js @@ -46,7 +46,7 @@ function parseLonghornData(data) { export default async function handler(req, res) { const settings = getSettings(); - const longhornSettings = settings?.providers?.longhorn; + const longhornSettings = settings?.providers?.longhorn || {}; const {url, username, password} = longhornSettings; if (!url) { diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 06e55010..d91a8339 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -160,6 +160,7 @@ const headerStyles = { "m-4 mb-0 sm:m-8 sm:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3", underlined: "m-4 mb-0 sm:m-8 sm:mb-1 border-b-2 pb-4 border-theme-800 dark:border-theme-200/50", clean: "m-4 mb-0 sm:m-8 sm:mb-0", + boxedWidgets: "m-4 mb-0 sm:m-8 sm:mb-0 sm:mt-1", }; function Home({ initialSettings }) { @@ -208,6 +209,7 @@ function Home({ initialSettings }) { searchProvider = searchProviders[searchWidget.options?.provider]; } } + const headerStyle = initialSettings?.headerStyle || "underlined"; useEffect(() => { function handleKeyDown(e) { @@ -252,11 +254,11 @@ function Home({ initialSettings }) { /> -
    +
    !rightAlignedWidgets.includes(widget.type)) .map((widget, i) => ( - + ))} -
    +
    {widgets .filter((widget) => rightAlignedWidgets.includes(widget.type)) .map((widget, i) => ( - + ))}
    @@ -289,7 +294,7 @@ function Home({ initialSettings }) { {services?.length > 0 && (
    {services.map((group) => ( - + ))}
    )} @@ -302,14 +307,16 @@ function Home({ initialSettings }) {
    )} -
    - {!initialSettings?.color && } - - {!initialSettings?.theme && } -
    +
    +
    + {!initialSettings?.color && } + + {!initialSettings?.theme && } +
    -
    - {!initialSettings?.hideVersion && } +
    + {!initialSettings?.hideVersion && } +
    @@ -357,7 +364,7 @@ export default function Wrapper({ initialSettings, fallback }) { style={wrappedStyle} >
    { try { + const isSwarm = !!servers[serverName].swarm; const docker = new Docker(getDockerArguments(serverName).conn); - const containers = await docker.listContainers({ - all: true, - }); + const listProperties = { all: true }; + const containers = await ((isSwarm) ? docker.listServices(listProperties) : docker.listContainers(listProperties)); // bad docker connections can result in a object? // in any case, this ensures the result is the expected array @@ -76,17 +76,19 @@ export async function servicesFromDocker() { const discovered = containers.map((container) => { let constructedService = null; + const containerLabels = isSwarm ? shvl.get(container, 'Spec.Labels') : container.Labels; + const containerName = isSwarm ? shvl.get(container, 'Spec.Name') : container.Names[0]; - Object.keys(container.Labels).forEach((label) => { + Object.keys(containerLabels).forEach((label) => { if (label.startsWith("homepage.")) { if (!constructedService) { constructedService = { - container: container.Names[0].replace(/^\//, ""), + container: containerName.replace(/^\//, ""), server: serverName, type: 'service' }; } - shvl.set(constructedService, label.replace("homepage.", ""), substituteEnvironmentVars(container.Labels[label])); + shvl.set(constructedService, label.replace("homepage.", ""), substituteEnvironmentVars(containerLabels[label])); } }); @@ -156,11 +158,20 @@ export async function servicesFromKubernetes() { return null; }); - const traefikIngressList = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes") + const traefikIngressList = await crd.listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes") .then((response) => response.body) - .catch((error) => { - logger.error("Error getting traefik ingresses: %d %s %s", error.statusCode, error.body, error.response); - return null; + .catch(async (error) => { + logger.error("Error getting traefik ingresses from traefik.io: %d %s %s", error.statusCode, error.body, error.response); + + // Fallback to the old traefik CRD group + const fallbackIngressList = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes") + .then((response) => response.body) + .catch((fallbackError) => { + logger.error("Error getting traefik ingresses from traefik.containo.us: %d %s %s", fallbackError.statusCode, fallbackError.body, fallbackError.response); + return null; + }); + + return fallbackIngressList; }); if (traefikIngressList && traefikIngressList.items.length > 0) { @@ -168,7 +179,7 @@ export async function servicesFromKubernetes() { .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]) ingressList.items.push(...traefikServices); } - + if (!ingressList) { return []; } @@ -276,7 +287,8 @@ export function cleanServiceGroups(groups) { wan, // opnsense widget, pfsense widget enableBlocks, // emby/jellyfin enableNowPlaying, - volume, // diskstation widget + volume, // diskstation widget, + enableQueue, // sonarr/radarr } = cleanedService.widget; const fieldsList = typeof fields === 'string' ? JSON.parse(fields) : fields; @@ -312,6 +324,9 @@ export function cleanServiceGroups(groups) { if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks); if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying); } + if (["sonarr", "radarr"].includes(type)) { + if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue); + } if (["diskstation", "qnap"].includes(type)) { if (volume) cleanedService.widget.volume = volume; } @@ -322,16 +337,13 @@ export function cleanServiceGroups(groups) { })); } -export default async function getServiceWidget(group, service) { +export async function getServiceItem(group, service) { const configuredServices = await servicesFromConfig(); const serviceGroup = configuredServices.find((g) => g.name === group); if (serviceGroup) { const serviceEntry = serviceGroup.services.find((s) => s.name === service); - if (serviceEntry) { - const { widget } = serviceEntry; - return widget; - } + if (serviceEntry) return serviceEntry; } const discoveredServices = await servicesFromDocker(); @@ -339,20 +351,24 @@ export default async function getServiceWidget(group, service) { const dockerServiceGroup = discoveredServices.find((g) => g.name === group); if (dockerServiceGroup) { const dockerServiceEntry = dockerServiceGroup.services.find((s) => s.name === service); - if (dockerServiceEntry) { - const { widget } = dockerServiceEntry; - return widget; - } + if (dockerServiceEntry) return dockerServiceEntry; } const kubernetesServices = await servicesFromKubernetes(); const kubernetesServiceGroup = kubernetesServices.find((g) => g.name === group); if (kubernetesServiceGroup) { const kubernetesServiceEntry = kubernetesServiceGroup.services.find((s) => s.name === service); - if (kubernetesServiceEntry) { - const { widget } = kubernetesServiceEntry; - return widget; - } + if (kubernetesServiceEntry) return kubernetesServiceEntry; + } + + return false; +} + +export default async function getServiceWidget(group, service) { + const serviceItem = await getServiceItem(group, service); + if (serviceItem) { + const { widget } = serviceItem; + return widget; } return false; diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 5d4b7e3b..8fa975d0 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -55,6 +55,12 @@ export default async function credentialedProxyHandler(req, res, map) { } else { headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; } + } else if (widget.type === "paperlessngx") { + if (widget.key) { + headers.Authorization = `Token ${widget.key}`; + } else { + headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; + } } else { headers["X-API-Key"] = `${widget.key}`; } diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js index e022fb46..72f65be3 100644 --- a/src/utils/proxy/http.js +++ b/src/utils/proxy/http.js @@ -1,5 +1,7 @@ /* eslint-disable prefer-promise-reject-errors */ /* eslint-disable no-param-reassign */ +import { createUnzip } from "node:zlib"; + import { http, https } from "follow-redirects"; import { addCookieToJar, setCookieHeader } from "./cookie-jar"; @@ -28,12 +30,24 @@ function handleRequest(requestor, url, params) { const request = requestor.request(url, params, (response) => { const data = []; + const contentEncoding = response.headers['content-encoding']?.trim().toLowerCase(); - response.on("data", (chunk) => { + let responseContent = response; + if (contentEncoding === 'gzip' || contentEncoding === 'deflate') { + responseContent = createUnzip(); + // zlib errors + responseContent.on("error", (e) => { + logger.error(e); + responseContent = response; // fallback + }); + response.pipe(responseContent); + } + + responseContent.on("data", (chunk) => { data.push(chunk); }); - response.on("end", () => { + responseContent.on("end", () => { addCookieToJar(url, response.headers); resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]); }); diff --git a/src/utils/proxy/use-widget-api.js b/src/utils/proxy/use-widget-api.js index 52e986e1..e4744038 100644 --- a/src/utils/proxy/use-widget-api.js +++ b/src/utils/proxy/use-widget-api.js @@ -7,7 +7,11 @@ export default function useWidgetAPI(widget, ...options) { if (options && options[1]?.refreshInterval) { config.refreshInterval = options[1].refreshInterval; } - const { data, error, mutate } = useSWR(formatProxyUrl(widget, ...options), config); + let url = formatProxyUrl(widget, ...options) + if (options[0] === "") { + url = null + } + const { data, error, mutate } = useSWR(url, config); // make the data error the top-level error return { data, error: data?.error ?? error, mutate } } diff --git a/src/widgets/components.js b/src/widgets/components.js index 3a918e62..5d2dc7e6 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -31,6 +31,7 @@ const components = { healthchecks: dynamic(() => import("./healthchecks/component")), immich: dynamic(() => import("./immich/component")), jackett: dynamic(() => import("./jackett/component")), + jdownloader: dynamic(() => import("./jdownloader/component")), jellyfin: dynamic(() => import("./emby/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), komga: dynamic(() => import("./komga/component")), @@ -93,4 +94,4 @@ const components = { xteve: dynamic(() => import("./xteve/component")), }; -export default components; +export default components; \ No newline at end of file diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index b0a75a03..a9e1ac97 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -63,7 +63,7 @@ async function apiCall(widget, endpoint, service) { } if (status !== 200) { - logger.error("Error getting data from Homebridge: %s status %d. Data: %s", url, status, data); + logger.error("Error getting data from Homebridge: %s status %d. Data: %s", url, status, JSON.stringify(data)); return { status, contentType, data: null, responseHeaders }; } diff --git a/src/widgets/jdownloader/component.jsx b/src/widgets/jdownloader/component.jsx new file mode 100644 index 00000000..8f271935 --- /dev/null +++ b/src/widgets/jdownloader/component.jsx @@ -0,0 +1,39 @@ +import { useTranslation } from "next-i18next"; + +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", { + refreshInterval: 30000, + }); + + if (jdownloaderAPIError) { + return ; + } + + if (!jdownloaderData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/jdownloader/proxy.js b/src/widgets/jdownloader/proxy.js new file mode 100644 index 00000000..be858d51 --- /dev/null +++ b/src/widgets/jdownloader/proxy.js @@ -0,0 +1,196 @@ +/* eslint-disable no-underscore-dangle */ +import crypto from 'crypto'; +import querystring from 'querystring'; + +import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools" + +import getServiceWidget from "utils/config/service-helpers"; +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; + +const proxyName = "jdownloaderProxyHandler"; +const logger = createLogger(proxyName); + +async function getWidget(req) { + const { group, service } = req.query; + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return null; + } + const widget = await getServiceWidget(group, service); + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return null; + } + + return widget; +} + +async function login(loginSecret, deviceSecret, params) { + const rid = uniqueRid(); + const path = `/my/connect?${querystring.stringify({ ...params, rid })}`; + + const signature = crypto + .createHmac('sha256', loginSecret) + .update(path) + .digest('hex'); + const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}` + + const [status, contentType, data] = await httpProxy(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (status !== 200) { + logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString()); + return [status, data]; + } + + try { + const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret)) + const sessionToken = decryptedData.sessiontoken; + validateRid(decryptedData, rid); + const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken); + const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken); + return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken]; + } catch (e) { + logger.error("Error decoding jdownloader API data. Data: %s", data.toString()); + return [status, null]; + } +} + + +async function getDevice(serverEncryptionToken, deviceName, params) { + const rid = uniqueRid(); + const path = `/my/listdevices?${querystring.stringify({ ...params, rid })}`; + const signature = crypto + .createHmac('sha256', serverEncryptionToken) + .update(path) + .digest('hex'); + const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}` + + const [status, , data] = await httpProxy(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (status !== 200) { + logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString()); + return [status, data]; + } + + try { + const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken)) + const filteredDevice = decryptedData.list.filter(device => device.name === deviceName); + return [status, filteredDevice[0].id]; + } catch (e) { + logger.error("Error decoding jdownloader API data. Data: %s", data.toString()); + return [status, null]; + } + +} + +function createBody(rid, query, params) { + const baseBody = { + apiVer: 1, + rid, + url: query + }; + return params ? { ...baseBody, params: [JSON.stringify(params)] } : baseBody; +} + +async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) { + const rid = uniqueRid(); + const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken); + const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}` + const [status, , data] = await httpProxy(url, { + method: 'POST', + body, + }); + + if (status !== 200) { + logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString()); + return [status, data]; + } + + try { + const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken)) + return decryptedData.data; + } catch (e) { + logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString()); + return [status, null]; + } + +} + + +export default async function jdownloaderProxyHandler(req, res) { + const widget = await getWidget(req); + + if (!widget) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + logger.debug("Getting data from JDRss API"); + const { username } = widget + const { password } = widget + + const appKey = "homepage" + const loginSecret = sha256(`${username}${password}server`) + const deviceSecret = sha256(`${username}${password}device`) + const email = username; + + const loginData = await login(loginSecret, deviceSecret, { + appKey, + email + }) + + const deviceData = await getDevice(loginData[3], widget.client, { + sessiontoken: loginData[5] + }) + + const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], { + "bytesLoaded": false, + "bytesTotal": true, + "comment": false, + "enabled": true, + "eta": false, + "priority": false, + "finished": true, + "running": true, + "speed": true, + "status": true, + "childCount": false, + "hosts": false, + "saveTo": false, + "maxResults": -1, + "startAt": 0, + } + ) + + let bytesRemaining = 0; + let totalBytes = 0; + let totalSpeed = 0; + packageStatus.forEach(file => { + totalBytes += file.bytesTotal; + if (file.finished !== true) { + bytesRemaining += file.bytesTotal; + if (file.speed) { + totalSpeed += file.speed; + } + } + }); + + const data = { + downloadCount: packageStatus.length, + bytesRemaining, + totalBytes, + totalSpeed + }; + + return res.send(data); + +} \ No newline at end of file diff --git a/src/widgets/jdownloader/tools.js b/src/widgets/jdownloader/tools.js new file mode 100644 index 00000000..d678b072 --- /dev/null +++ b/src/widgets/jdownloader/tools.js @@ -0,0 +1,55 @@ +import crypto from 'crypto'; + +export function sha256(data) { + return crypto + .createHash('sha256') + .update(data) + .digest(); +} + +export function uniqueRid() { + return Math.floor(Math.random() * 10e12); +} + +export function validateRid(decryptedData, rid) { + if (decryptedData.rid !== rid) { + throw new Error('RequestID mismatch'); + } + return decryptedData; + +} + +export function decrypt(data, ivKey) { + const iv = ivKey.slice(0, ivKey.length / 2); + const key = ivKey.slice(ivKey.length / 2, ivKey.length); + const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + return Buffer.concat([ + cipher.update(data, 'base64'), + cipher.final() + ]).toString(); +} + +export function createEncryptionToken(oldTokenBuff, updateToken) { + const updateTokenBuff = Buffer.from(updateToken, 'hex'); + const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length); + return sha256(mergedBuffer); +} + +export function encrypt(data, ivKey) { + if (typeof data !== 'string') { + throw new Error('data no es un string'); + } + if (!(ivKey instanceof Buffer)) { + throw new Error('ivKey no es un buffer'); + } + if (ivKey.length !== 32) { + throw new Error('ivKey tiene que tener tamaño 32'); + } + const stringIVKey = ivKey.toString('hex'); + const stringIV = stringIVKey.substring(0, stringIVKey.length / 2); + const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length); + const iv = Buffer.from(stringIV, 'hex'); + const key = Buffer.from(stringKey, 'hex'); + const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + return cipher.update(data, 'utf8', 'base64') + cipher.final('base64'); +} \ No newline at end of file diff --git a/src/widgets/jdownloader/widget.js b/src/widgets/jdownloader/widget.js new file mode 100644 index 00000000..acd25d74 --- /dev/null +++ b/src/widgets/jdownloader/widget.js @@ -0,0 +1,15 @@ +import jdownloaderProxyHandler from "./proxy"; + +const widget = { + api: "https://api.jdownloader.org/{endpoint}/&signature={signature}", + proxyHandler: jdownloaderProxyHandler, + + mappings: { + unified: { + endpoint: "/", + signature: "", + }, + }, +}; + +export default widget; \ No newline at end of file diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx index c4d67553..e756e4be 100644 --- a/src/widgets/kubernetes/component.jsx +++ b/src/widgets/kubernetes/component.jsx @@ -16,7 +16,7 @@ export default function Component({ service }) { `/api/kubernetes/stats/${widget.namespace}/${widget.app}?${podSelectorString}`); if (statsError || statusError) { - return ; + return ; } if (statusData && statusData.status !== "running") { diff --git a/src/widgets/lidarr/component.jsx b/src/widgets/lidarr/component.jsx index a6aa82c7..68360d82 100644 --- a/src/widgets/lidarr/component.jsx +++ b/src/widgets/lidarr/component.jsx @@ -9,21 +9,21 @@ export default function Component({ service }) { const { widget } = service; - const { data: albumsData, error: albumsError } = useWidgetAPI(widget, "album"); + const { data: artistsData, error: artistsError } = useWidgetAPI(widget, "artist"); const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status"); - if (albumsError || wantedError || queueError) { - const finalError = albumsError ?? wantedError ?? queueError; + if (artistsError || wantedError || queueError) { + const finalError = artistsError ?? wantedError ?? queueError; return ; } - if (!albumsData || !wantedData || !queueData) { + if (!artistsData || !wantedData || !queueData) { return ( - + ); } @@ -32,7 +32,7 @@ export default function Component({ service }) { - + ); } diff --git a/src/widgets/lidarr/widget.js b/src/widgets/lidarr/widget.js index 6ff93254..2f036726 100644 --- a/src/widgets/lidarr/widget.js +++ b/src/widgets/lidarr/widget.js @@ -1,16 +1,12 @@ import genericProxyHandler from "utils/proxy/handlers/generic"; -import { jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v1/{endpoint}?apikey={key}", proxyHandler: genericProxyHandler, mappings: { - album: { - endpoint: "album", - map: (data) => ({ - have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length, - }), + artist: { + endpoint: "artist", }, "wanted/missing": { endpoint: "wanted/missing", diff --git a/src/widgets/paperlessngx/widget.js b/src/widgets/paperlessngx/widget.js index 8af8079d..de37f595 100644 --- a/src/widgets/paperlessngx/widget.js +++ b/src/widgets/paperlessngx/widget.js @@ -1,8 +1,8 @@ -import genericProxyHandler from "utils/proxy/handlers/generic"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; const widget = { api: "{url}/api/{endpoint}", - proxyHandler: genericProxyHandler, + proxyHandler: credentialedProxyHandler, mappings: { "statistics": { diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx index aab9eba1..53a6fd26 100644 --- a/src/widgets/portainer/component.jsx +++ b/src/widgets/portainer/component.jsx @@ -1,12 +1,8 @@ -import { useTranslation } from "next-i18next"; - import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { - const { t } = useTranslation(); - const { widget } = service; const { data: containersData, error: containersError } = useWidgetAPI(widget, "docker/containers/json", { @@ -27,8 +23,9 @@ export default function Component({ service }) { ); } - if (containersData.error) { - return ; + if (containersData.error || containersData.message) { + // containersData can be itself an error object e.g. if environment fails + return ; } const running = containersData.filter((c) => c.State === "running").length; diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx index f8a932ea..6ce2f599 100644 --- a/src/widgets/radarr/component.jsx +++ b/src/widgets/radarr/component.jsx @@ -1,22 +1,41 @@ import { useTranslation } from "next-i18next"; +import { useCallback } from 'react'; + +import QueueEntry from "../../components/widgets/queue/queueEntry"; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +function getProgress(sizeLeft, size) { + return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100 +} + export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (moviesError || queuedError) { - const finalError = moviesError ?? queuedError; + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (moviesError || queuedError || queueDetailsError) { + const finalError = moviesError ?? queuedError ?? queueDetailsError; return ; } - if (!moviesData || !queuedData) { + if (!moviesData || !queuedData || !queueDetailsData) { return ( @@ -27,12 +46,27 @@ export default function Component({ service }) { ); } + const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0; + return ( - - - - - - + <> + + + + + + + {enableQueue && + queueDetailsData.map((queueEntry) => ( + entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")} + activity={formatDownloadState(queueEntry.trackedDownloadState)} + key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`} + /> + )) + } + ); } diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js index 78054219..3373975e 100644 --- a/src/widgets/radarr/widget.js +++ b/src/widgets/radarr/widget.js @@ -1,5 +1,5 @@ import genericProxyHandler from "utils/proxy/handlers/generic"; -import { jsonArrayFilter } from "utils/proxy/api-helpers"; +import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v3/{endpoint}?apikey={key}", @@ -12,6 +12,7 @@ const widget = { wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length, have: jsonArrayFilter(data, (item) => item.hasFile).length, missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length, + all: asJson(data), }), }, "queue/status": { @@ -20,6 +21,37 @@ const widget = { "totalCount" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + movieId: entry.movieId ?? entry.id, + status: entry.status + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + }, }, }; diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx index adbb8c30..27b1ab03 100644 --- a/src/widgets/sonarr/component.jsx +++ b/src/widgets/sonarr/component.jsx @@ -1,9 +1,26 @@ import { useTranslation } from "next-i18next"; +import { useCallback } from 'react'; + +import QueueEntry from "../../components/widgets/queue/queueEntry"; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +function getProgress(sizeLeft, size) { + return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100 +} + +function getTitle(queueEntry, seriesData) { + let title = '' + const seriesTitle = seriesData.find((entry) => entry.id === queueEntry.seriesId)?.title; + if (seriesTitle) title += `${seriesTitle}: `; + const { episodeTitle } = queueEntry; + if (episodeTitle) title += episodeTitle; + if (title === '') return null; + return title; +} + export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; @@ -11,13 +28,25 @@ export default function Component({ service }) { const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue"); const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (wantedError || queuedError || seriesError) { - const finalError = wantedError ?? queuedError ?? seriesError; + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (wantedError || queuedError || seriesError || queueDetailsError) { + const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError; return ; } - if (!wantedData || !queuedData || !seriesData) { + if (!wantedData || !queuedData || !seriesData || !queueDetailsData) { return ( @@ -27,11 +56,26 @@ export default function Component({ service }) { ); } + const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0; + return ( - - - - - + <> + + + + + + {enableQueue && + queueDetailsData.map((queueEntry) => ( + + )) + } + ); } diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js index c1413975..7f658eb1 100644 --- a/src/widgets/sonarr/widget.js +++ b/src/widgets/sonarr/widget.js @@ -8,9 +8,10 @@ const widget = { mappings: { series: { endpoint: "series", - map: (data) => ({ - total: asJson(data).length, - }) + map: (data) => asJson(data).map((entry) => ({ + title: entry.title, + id: entry.id + })) }, queue: { endpoint: "queue", @@ -24,6 +25,39 @@ const widget = { "totalRecords" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + seriesId: entry.seriesId, + episodeTitle: entry.episode?.title ?? entry.title, + episodeId: entry.episodeId ?? entry.id, + status: entry.status, + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + } }, }; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index dc675bb8..ab8beb56 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -27,6 +27,7 @@ import healthchecks from "./healthchecks/widget"; import immich from "./immich/widget"; import jackett from "./jackett/widget"; import jellyseerr from "./jellyseerr/widget"; +import jdownloader from "./jdownloader/widget"; import komga from "./komga/widget"; import kopia from "./kopia/widget"; import lidarr from "./lidarr/widget"; @@ -115,6 +116,7 @@ const widgets = { healthchecks, immich, jackett, + jdownloader, jellyfin: emby, jellyseerr, komga,