mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Merge remote-tracking branch 'origin/benphelpsMain' into wg-easy-service-widget
This commit is contained in:
commit
0c3c170b0c
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<div
|
||||
key={services.name}
|
||||
@ -21,7 +21,7 @@ export default function ServicesGroup({ services, layout, fiveColumns }) {
|
||||
}
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
</div>
|
||||
<List services={services.services} layout={layout} />
|
||||
<List group={group} services={services.services} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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 }) {
|
||||
<div className="absolute top-0 right-0 w-1/2 flex flex-row justify-end gap-2 mr-2">
|
||||
{service.ping && (
|
||||
<div className="flex-shrink-0 flex items-center justify-center cursor-pointer">
|
||||
<Ping service={service} />
|
||||
<Ping group={group} service={service.name} />
|
||||
<span className="sr-only">Ping status</span>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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 (
|
||||
<ul
|
||||
className={classNames(
|
||||
@ -23,7 +23,7 @@ export default function List({ services, layout }) {
|
||||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item key={service.container ?? service.app ?? service.name} service={service} />
|
||||
<Item key={service.container ?? service.app ?? service.name} service={service} group={group} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -15,7 +15,9 @@ export default function Container({ error = false, children, service }) {
|
||||
return <Error service={service} error={error} />
|
||||
}
|
||||
|
||||
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}`;
|
||||
|
@ -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 (
|
||||
<details className="px-1 pb-1">
|
||||
|
@ -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",
|
||||
@ -27,12 +30,14 @@ export default function DateTime({ options }) {
|
||||
}, [date, setDate, dateLocale, format]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center grow justify-end">
|
||||
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>
|
||||
{date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row items-center grow justify-end">
|
||||
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>
|
||||
{date}
|
||||
</span>
|
||||
</div>
|
||||
</Raw>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import useSWR from "swr";
|
||||
import { useContext } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
|
||||
import { FiCpu, FiHardDrive } from "react-icons/fi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "../resources/usage-bar";
|
||||
import Error from "../widget/error";
|
||||
import Resource from "../widget/resource";
|
||||
import Resources from "../widget/resources";
|
||||
import WidgetLabel from "../widget/widget_label";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
|
||||
@ -26,52 +28,19 @@ export default function Widget({ options }) {
|
||||
);
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-row items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 text-xs">
|
||||
{t("glances.wait")}
|
||||
</div>
|
||||
</div>
|
||||
<UsageBar percent="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 text-xs">
|
||||
{t("glances.wait")}
|
||||
</div>
|
||||
</div>
|
||||
<UsageBar percent="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <Resources options={options}>
|
||||
<Resource icon={FiCpu} label={t("glances.wait")} percentage="0" />
|
||||
<Resource icon={FaMemory} label={t("glances.wait")} percentage="0" />
|
||||
{ options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" /> }
|
||||
{ options.disk && !Array.isArray(options.disk) && <Resource key={options.disk} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> }
|
||||
{ options.disk && Array.isArray(options.disk) && options.disk.map((disk) => <Resource key={disk.mnt_point} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> )}
|
||||
{ options.uptime && <Resource icon={FaRegClock} label={t("glances.wait")} percentage="0" /> }
|
||||
{ options.label && <WidgetLabel label={options.label} /> }
|
||||
</Resources>;
|
||||
}
|
||||
|
||||
const unit = options.units === "imperial" ? "fahrenheit" : "celsius";
|
||||
@ -101,131 +70,84 @@ export default function Widget({ options }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={options.url} target={settings.target ?? "_blank"} className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.total,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.cpu")}</div>
|
||||
</div>
|
||||
{options.expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.number", {
|
||||
value: data.load.min15,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.load")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={data.cpu.total} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", {
|
||||
value: data.mem.free,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.free")}</div>
|
||||
</div>
|
||||
{options.expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.bytes", {
|
||||
value: data.mem.total,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={data.mem.percent} />
|
||||
</div>
|
||||
</div>
|
||||
{disks.map((disk) => (
|
||||
<div key={disk.mnt_point} className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: disk.free })}</div>
|
||||
<div className="pr-1">{t("glances.free")}</div>
|
||||
</span>
|
||||
{options.expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">{t("common.bytes", { value: disk.size })}</div>
|
||||
<div className="pr-1">{t("glances.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={disk.percent} />
|
||||
</div>
|
||||
</div>))}
|
||||
{options.cputemp && mainTemp > 0 &&
|
||||
(<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: mainTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.temp")}</div>
|
||||
</span>
|
||||
{options.expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.number", {
|
||||
value: maxTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.warn")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={tempPercent} />
|
||||
</div>
|
||||
</div>)}
|
||||
{options.uptime && data.uptime &&
|
||||
(<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))}
|
||||
</div>
|
||||
<div className="pr-1">{t("glances.uptime")}</div>
|
||||
</span>
|
||||
<UsageBar percent={Math.round((new Date().getSeconds() / 60) * 100)} />
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
)}
|
||||
</a>
|
||||
<Resources options={options} target={settings.target ?? "_blank"}>
|
||||
<Resource
|
||||
icon={FiCpu}
|
||||
value={t("common.number", {
|
||||
value: data.cpu.total,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
label={t("glances.cpu")}
|
||||
expandedValue={t("common.number", {
|
||||
value: data.load.min15,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0
|
||||
})}
|
||||
expandedLabel={t("glances.load")}
|
||||
percentage={data.cpu.total}
|
||||
expanded={options.expanded}
|
||||
/>
|
||||
<Resource
|
||||
icon={FaMemory}
|
||||
value={t("common.bytes", {
|
||||
value: data.mem.free,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})}
|
||||
label={t("glances.free")}
|
||||
expandedValue={t("common.bytes", {
|
||||
value: data.mem.total,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})}
|
||||
expandedLabel={t("glances.total")}
|
||||
percentage={data.mem.percent}
|
||||
expanded={options.expanded}
|
||||
/>
|
||||
{disks.map((disk) => (
|
||||
<Resource key={disk.mnt_point}
|
||||
icon={FiHardDrive}
|
||||
value={t("common.bytes", { value: disk.free })}
|
||||
label={t("glances.free")}
|
||||
expandedValue={t("common.bytes", { value: disk.size })}
|
||||
expandedLabel={t("glances.total")}
|
||||
percentage={disk.percent}
|
||||
expanded={options.expanded}
|
||||
/>
|
||||
))}
|
||||
{options.cputemp && mainTemp > 0 &&
|
||||
<Resource
|
||||
icon={FaThermometerHalf}
|
||||
value={t("common.number", {
|
||||
value: mainTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
label={t("glances.temp")}
|
||||
expandedValue={t("common.number", {
|
||||
value: maxTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
expandedLabel={t("glances.warn")}
|
||||
percentage={tempPercent}
|
||||
expanded={options.expanded}
|
||||
/>
|
||||
}
|
||||
{options.uptime && data.uptime &&
|
||||
<Resource
|
||||
icon={FaRegClock}
|
||||
value={data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))}
|
||||
label={t("glances.uptime")}
|
||||
percentage={Math.round((new Date().getSeconds() / 60) * 100).toString()}
|
||||
/>
|
||||
}
|
||||
{options.label && <WidgetLabel label={options.label} />}
|
||||
</Resources>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import Container from "../widget/container";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
const textSizes = {
|
||||
"4xl": "text-4xl",
|
||||
"3xl": "text-3xl",
|
||||
@ -11,12 +14,12 @@ const textSizes = {
|
||||
|
||||
export default function Greeting({ options }) {
|
||||
if (options.text) {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-start">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
|
||||
{options.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import useSWR from "swr";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
import Node from "./node";
|
||||
|
||||
export default function Widget({ options }) {
|
||||
const { cluster, nodes } = options;
|
||||
const { t, i18n } = useTranslation();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const defaultData = {
|
||||
cpu: {
|
||||
@ -18,7 +21,7 @@ export default function Widget({ options }) {
|
||||
used: 0,
|
||||
total: 0,
|
||||
free: 0,
|
||||
precent: 0
|
||||
percent: 0
|
||||
}
|
||||
};
|
||||
|
||||
@ -29,23 +32,12 @@ export default function Widget({ options }) {
|
||||
);
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-row items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{cluster.show &&
|
||||
<Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />
|
||||
@ -54,12 +46,12 @@ export default function Widget({ options }) {
|
||||
<Node type="node" key="nodes" options={options.nodes} data={defaultData} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{cluster.show &&
|
||||
<Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />
|
||||
@ -69,6 +61,6 @@ export default function Widget({ options }) {
|
||||
<Node key={node.name} type="node" options={options.nodes} data={node} />)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ import { FiAlertTriangle, FiCpu, FiServer } from "react-icons/fi";
|
||||
import { SiKubernetes } from "react-icons/si";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
import UsageBar from "../resources/usage-bar";
|
||||
|
||||
export default function Node({ type, options, data }) {
|
||||
const { t } = useTranslation();
|
||||
@ -29,7 +28,7 @@ export default function Node({ type, options, data }) {
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: data.cpu.percent,
|
||||
value: data?.cpu?.percent ?? 0,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0
|
||||
@ -37,18 +36,18 @@ export default function Node({ type, options, data }) {
|
||||
</div>
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-3 h-3" />
|
||||
</div>
|
||||
<UsageBar percent={data.cpu.percent} />
|
||||
<UsageBar percent={data?.cpu?.percent ?? 0} />
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.bytes", {
|
||||
value: data.memory.free,
|
||||
value: data?.memory?.free ?? 0,
|
||||
maximumFractionDigits: 0,
|
||||
binary: true
|
||||
})}
|
||||
</div>
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-3 h-3" />
|
||||
</div>
|
||||
<UsageBar percent={data.memory.percent} />
|
||||
<UsageBar percent={data?.memory?.percent} />
|
||||
{options.showLabel && (
|
||||
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{type === "cluster" ? options.label : data.name}</div>
|
||||
)}
|
||||
|
@ -1,12 +0,0 @@
|
||||
export default function UsageBar({ percent }) {
|
||||
return (
|
||||
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20">
|
||||
<div
|
||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
|
||||
{options.icon ?
|
||||
<Container options={options}>
|
||||
<Raw>
|
||||
{options.icon ?
|
||||
<ResolvedIcon icon={options.icon} width={48} height={48} /> :
|
||||
// fallback to homepage logo
|
||||
<svg
|
||||
@ -57,6 +61,7 @@ export default function Logo({ options }) {
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
</Raw>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -1,37 +1,31 @@
|
||||
import useSWR from "swr";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
import Node from "./node";
|
||||
|
||||
export default function Longhorn({ options }) {
|
||||
const { expanded, total, labels, include, nodes } = options;
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/widgets/longhorn`, {
|
||||
refreshInterval: 1500
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between" />
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{data.nodes
|
||||
.filter((node) => {
|
||||
@ -52,6 +46,6 @@ export default function Longhorn({ options }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
|
@ -1,32 +1,20 @@
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { FaThermometerHalf } from "react-icons/fa";
|
||||
|
||||
import UsageBar from "../resources/usage-bar";
|
||||
import Resource from "../widget/resource";
|
||||
import WidgetLabel from "../widget/widget_label";
|
||||
|
||||
export default function Node({ data, expanded, labels }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.node.available })}</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{t("common.bytes", { value: data.node.maximum })}</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)} />
|
||||
</div>
|
||||
</div>
|
||||
{labels && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{data.node.id}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <Resource
|
||||
icon={FaThermometerHalf}
|
||||
value={t("common.bytes", { value: data.node.available })}
|
||||
label={t("resources.free")}
|
||||
expandedValue={t("common.bytes", { value: data.node.maximum })}
|
||||
expandedLabel={t("resources.total")}
|
||||
percentage={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)}
|
||||
expanded={expanded}
|
||||
>{ labels && <WidgetLabel label={data.node.id} /> }
|
||||
</Resource>
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import ContainerButton from "../widget/container_button";
|
||||
import WidgetIcon from "../widget/widget_icon";
|
||||
import PrimaryText from "../widget/primary_text";
|
||||
import SecondaryText from "../widget/secondary_text";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
function Widget({ options }) {
|
||||
@ -15,60 +21,35 @@ function Widget({ options }) {
|
||||
);
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{t("weather.updating")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.wait")}</SecondaryText>
|
||||
<WidgetIcon icon={WiCloudDown} size="l" />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night";
|
||||
const weatherInfo = {
|
||||
condition: data.current_weather.weathercode,
|
||||
timeOfDay: data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon condition={data.current_weather.weathercode} timeOfDay={timeOfDay} />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{t("common.number", {
|
||||
value: data.current_weather.temperature,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t(`wmo.${data.current_weather.weathercode}-${timeOfDay}`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>
|
||||
{options.label && `${options.label}, `}
|
||||
{t("common.number", {
|
||||
value: data.current_weather.temperature,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</PrimaryText>
|
||||
<SecondaryText>{t(`wmo.${data.current_weather.weathercode}-${weatherInfo.timeOfDay}`)}</SecondaryText>
|
||||
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
export default function OpenMeteo({ options }) {
|
||||
@ -103,27 +84,11 @@ export default function OpenMeteo({ options }) {
|
||||
// if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => requestLocation()}
|
||||
className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return <ContainerButton options={options} callback={requestLocation} >
|
||||
<PrimaryText>{t("weather.current")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.allow")}</SecondaryText>
|
||||
<WidgetIcon icon={ requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
|
||||
</ContainerButton>;
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
|
@ -1,12 +1,19 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import ContainerButton from "../widget/container_button";
|
||||
import PrimaryText from "../widget/primary_text";
|
||||
import SecondaryText from "../widget/secondary_text";
|
||||
import WidgetIcon from "../widget/widget_icon";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
|
||||
function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
@ -15,58 +22,29 @@ function Widget({ options }) {
|
||||
);
|
||||
|
||||
if (error || data?.cod === 401 || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="hidden sm:flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="hidden sm:flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{t("weather.updating")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.wait")}</SecondaryText>
|
||||
<WidgetIcon icon={WiCloudDown} size="l" />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-auto ml-2 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="hidden sm:flex flex-col items-center">
|
||||
<Icon
|
||||
condition={data.weather[0].id}
|
||||
timeOfDay={data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{t("common.number", { value: data.main.temp, style: "unit", unit })}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const weatherInfo = {
|
||||
condition: data.weather[0].id,
|
||||
timeOfDay: data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"
|
||||
};
|
||||
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })}</PrimaryText>
|
||||
<SecondaryText>{data.weather[0].description}</SecondaryText>
|
||||
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
export default function OpenWeatherMap({ options }) {
|
||||
@ -98,30 +76,12 @@ export default function OpenWeatherMap({ options }) {
|
||||
}
|
||||
};
|
||||
|
||||
// if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => requestLocation()}
|
||||
className="flex flex-col justify-center first:ml-auto ml-4 mr-2"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="hidden sm:flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return <ContainerButton options={options} callback={requestLocation} >
|
||||
<PrimaryText>{t("weather.current")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.allow")}</SecondaryText>
|
||||
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
|
||||
</ContainerButton>;
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
|
18
src/components/widgets/queue/queueEntry.jsx
Normal file
18
src/components/widgets/queue/queueEntry.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
export default function QueueEntry({ title, activity, timeLeft, progress}) {
|
||||
return (
|
||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
|
||||
<div
|
||||
className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0 -ml-1"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
|
||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{timeLeft ? `${activity} - ${timeLeft}` : activity}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import useSWR from "swr";
|
||||
import { FiCpu } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
import Resource from "../widget/resource";
|
||||
import Error from "../widget/error";
|
||||
|
||||
export default function Cpu({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
@ -13,67 +13,29 @@ export default function Cpu({ expanded }) {
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.cpu")}</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.load")}</div>
|
||||
</div>
|
||||
)}
|
||||
<UsageBar percent={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource icon={FiCpu} value="-" label={t("resources.cpu")} expandedValue="-"
|
||||
expandedLabel={t("resources.load")} percentage="0" expanded={expanded} />
|
||||
}
|
||||
|
||||
const percent = data.cpu.usage;
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.number", {
|
||||
value: data.cpu.usage,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.cpu")}</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.number", {
|
||||
value: data.cpu.load,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.load")}</div>
|
||||
</div>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FiCpu}
|
||||
value={t("common.number", {
|
||||
value: data.cpu.usage,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
label={t("resources.cpu")}
|
||||
expandedValue={t("common.number", {
|
||||
value: data.cpu.load,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
expandedLabel={t("resources.load")}
|
||||
percentage={data.cpu.usage}
|
||||
expanded={expanded}
|
||||
/>
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import useSWR from "swr";
|
||||
import { FaThermometerHalf } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
import Resource from "../widget/resource";
|
||||
import Error from "../widget/error";
|
||||
|
||||
function convertToFahrenheit(t) {
|
||||
return t * 9/5 + 32
|
||||
@ -17,34 +17,18 @@ export default function CpuTemp({ expanded, units }) {
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error />
|
||||
}
|
||||
|
||||
if (!data || !data.cputemp) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
|
||||
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">-</div>
|
||||
<div className="pr-1">{t("resources.temp")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">-</div>
|
||||
<div className="pr-1">{t("resources.max")}</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FaThermometerHalf}
|
||||
value="-"
|
||||
label={t("resources.temp")}
|
||||
expandedValue="-"
|
||||
expandedLabel={t("resources.max")}
|
||||
expanded={expanded}
|
||||
/>;
|
||||
}
|
||||
|
||||
let mainTemp = data.cputemp.main;
|
||||
@ -54,38 +38,24 @@ export default function CpuTemp({ expanded, units }) {
|
||||
const unit = units === "imperial" ? "fahrenheit" : "celsius";
|
||||
mainTemp = (unit === "celsius") ? mainTemp : convertToFahrenheit(mainTemp);
|
||||
const maxTemp = (unit === "celsius") ? data.cputemp.max : convertToFahrenheit(data.cputemp.max);
|
||||
const percent = Math.round((mainTemp / maxTemp) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: mainTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.temp")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{t("common.number", {
|
||||
value: maxTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.max")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FaThermometerHalf}
|
||||
value={t("common.number", {
|
||||
value: mainTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
label={t("resources.temp")}
|
||||
expandedValue={t("common.number", {
|
||||
value: maxTemp,
|
||||
maximumFractionDigits: 1,
|
||||
style: "unit",
|
||||
unit
|
||||
})}
|
||||
expandedLabel={t("resources.max")}
|
||||
percentage={Math.round((mainTemp / maxTemp) * 100)}
|
||||
expanded={expanded}
|
||||
/>;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import useSWR from "swr";
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
import Resource from "../widget/resource";
|
||||
import Error from "../widget/error";
|
||||
|
||||
export default function Disk({ options, expanded }) {
|
||||
const { t } = useTranslation();
|
||||
@ -13,56 +13,31 @@ export default function Disk({ options, expanded }) {
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FiHardDrive}
|
||||
value="-"
|
||||
label={t("resources.free")}
|
||||
expandedValue="-"
|
||||
expandedLabel={t("resources.total")}
|
||||
expanded={expanded}
|
||||
percentage="0"
|
||||
/>;
|
||||
}
|
||||
|
||||
// data.drive.used not accurate?
|
||||
const percent = Math.round(((data.drive.size - data.drive.available) / data.drive.size) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">{t("common.bytes", { value: data.drive.available })}</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">{t("common.bytes", { value: data.drive.size })}</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FiHardDrive}
|
||||
value={t("common.bytes", { value: data.drive.available })}
|
||||
label={t("resources.free")}
|
||||
expandedValue={t("common.bytes", { value: data.drive.size })}
|
||||
expandedLabel={t("resources.total")}
|
||||
percentage={percent}
|
||||
expanded={expanded}
|
||||
/>;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import useSWR from "swr";
|
||||
import { FaMemory } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
import Resource from "../widget/resource";
|
||||
import Error from "../widget/error";
|
||||
|
||||
export default function Memory({ expanded }) {
|
||||
const { t } = useTranslation();
|
||||
@ -13,63 +13,30 @@ export default function Memory({ expanded }) {
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">-</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FaMemory}
|
||||
value="-"
|
||||
label={t("resources.free")}
|
||||
expandedValue="-"
|
||||
expandedLabel={t("resources.total")}
|
||||
expanded={expanded}
|
||||
percentage="0"
|
||||
/>;
|
||||
}
|
||||
|
||||
const percent = Math.round((data.memory.active / data.memory.total) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5 pr-1">
|
||||
{t("common.bytes", {
|
||||
value: data.memory.total,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource
|
||||
icon={FaMemory}
|
||||
value={t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })}
|
||||
label={t("resources.free")}
|
||||
expandedValue={t("common.bytes", { value: data.memory.total, maximumFractionDigits: 1, binary: true })}
|
||||
expandedLabel={t("resources.total")}
|
||||
percentage={percent}
|
||||
expanded={expanded}
|
||||
/>;
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import Container from "../widget/container";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
import Disk from "./disk";
|
||||
import Cpu from "./cpu";
|
||||
import Memory from "./memory";
|
||||
@ -6,8 +9,8 @@ import Uptime from "./uptime";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
const { expanded, units } = options;
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{options.cpu && <Cpu expanded={expanded} />}
|
||||
{options.memory && <Memory expanded={expanded} />}
|
||||
@ -20,6 +23,6 @@ export default function Resources({ options }) {
|
||||
{options.label && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</Raw>
|
||||
</Container>;
|
||||
}
|
||||
|
@ -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,28 +13,11 @@ export default function Uptime() {
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5 animate-pulse">
|
||||
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">-</div>
|
||||
<div className="pr-1">{t("resources.temp")}</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource icon={FaRegClock} value="-" label={t("resources.uptime")} percentage="0" />;
|
||||
}
|
||||
|
||||
const mo = Math.floor(data.uptime / (3600 * 24 * 31));
|
||||
@ -47,20 +30,7 @@ export default function Uptime() {
|
||||
else if (d > 0) uptime = `${d}${t("resources.days")} ${h}${t("resources.hours")}`;
|
||||
else uptime = `${h}${t("resources.hours")} ${m}${t("resources.minutes")}`;
|
||||
|
||||
const percent = Math.round((new Date().getSeconds() / 60) * 100);
|
||||
const percent = Math.round((new Date().getSeconds() / 60) * 100).toString();
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FaRegClock className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">
|
||||
{uptime}
|
||||
</div>
|
||||
<div className="pr-1">{t("resources.uptime")}</div>
|
||||
</span>
|
||||
<UsageBar percent={percent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Resource icon={FaRegClock} value={uptime} label={t("resources.uptime")} percentage={percent} />;
|
||||
}
|
||||
|
@ -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",
|
||||
@ -77,13 +80,8 @@ export default function Search({ options }) {
|
||||
}
|
||||
}, [availableProviderIds]);
|
||||
|
||||
if (!availableProviderIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const submitCallback = useCallback(event => {
|
||||
const q = encodeURIComponent(query);
|
||||
|
||||
const { url } = selectedProvider;
|
||||
if (url) {
|
||||
window.open(`${url}${q}`, options.target || "_blank");
|
||||
@ -94,6 +92,10 @@ export default function Search({ options }) {
|
||||
event.preventDefault();
|
||||
event.target.reset();
|
||||
setQuery("");
|
||||
}, [options.target, options.url, query, selectedProvider]);
|
||||
|
||||
if (!availableProviderIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onChangeProvider = (provider) => {
|
||||
@ -101,77 +103,79 @@ export default function Search({ options }) {
|
||||
localStorage.setItem(localStorageKey, provider.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex-col relative h-8 my-4 min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
||||
<input
|
||||
type="text"
|
||||
className="
|
||||
overflow-hidden w-full h-full rounded-md
|
||||
text-xs text-theme-900 dark:text-white
|
||||
placeholder-theme-900 dark:placeholder-white/80
|
||||
bg-white/50 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50
|
||||
focus:border-theme-500 dark:focus:border-white/50
|
||||
border border-theme-300 dark:border-theme-200/50"
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={options.focus}
|
||||
/>
|
||||
<Listbox as="div" value={selectedProvider} onChange={onChangeProvider} className="relative text-left" disabled={availableProviderIds?.length === 1}>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className="
|
||||
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
|
||||
text-white font-medium text-sm
|
||||
bg-theme-600/40 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50"
|
||||
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow" >
|
||||
<Raw>
|
||||
<div className="flex-col relative h-8 my-4 min-w-fit">
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
||||
<input
|
||||
type="text"
|
||||
className="
|
||||
overflow-hidden w-full h-full rounded-md
|
||||
text-xs text-theme-900 dark:text-white
|
||||
placeholder-theme-900 dark:placeholder-white/80
|
||||
bg-white/50 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50
|
||||
focus:border-theme-500 dark:focus:border-white/50
|
||||
border border-theme-300 dark:border-theme-200/50"
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={options.focus}
|
||||
/>
|
||||
<Listbox as="div" value={selectedProvider} onChange={onChangeProvider} className="relative text-left" disabled={availableProviderIds?.length === 1}>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className="
|
||||
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
|
||||
text-white font-medium text-sm
|
||||
bg-theme-600/40 dark:bg-white/10
|
||||
focus:ring-theme-500 dark:focus:ring-white/50"
|
||||
>
|
||||
<selectedProvider.icon className="text-white w-3 h-3" />
|
||||
<span className="sr-only">{t("search.search")}</span>
|
||||
</Listbox.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<selectedProvider.icon className="text-white w-3 h-3" />
|
||||
<span className="sr-only">{t("search.search")}</span>
|
||||
</Listbox.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options
|
||||
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
|
||||
bg-theme-100 dark:bg-theme-600 shadow-lg
|
||||
ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{availableProviderIds.map((providerId) => {
|
||||
const p = searchProviders[providerId];
|
||||
return (
|
||||
<Listbox.Option key={providerId} value={p} as={Fragment}>
|
||||
{({ active }) => (
|
||||
<li
|
||||
className={classNames(
|
||||
"rounded-md cursor-pointer",
|
||||
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<p.icon className="h-4 w-4 mx-4 my-2" />
|
||||
</li>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</form>
|
||||
);
|
||||
<Listbox.Options
|
||||
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
|
||||
bg-theme-100 dark:bg-theme-600 shadow-lg
|
||||
ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{availableProviderIds.map((providerId) => {
|
||||
const p = searchProviders[providerId];
|
||||
return (
|
||||
<Listbox.Option key={providerId} value={p} as={Fragment}>
|
||||
{({ active }) => (
|
||||
<li
|
||||
className={classNames(
|
||||
"rounded-md cursor-pointer",
|
||||
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<p.icon className="h-4 w-4 mx-4 my-2" />
|
||||
</li>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
</Raw>
|
||||
</ContainerForm>;
|
||||
}
|
||||
|
@ -3,6 +3,12 @@ import { MdSettingsEthernet } from "react-icons/md";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { SiUbiquiti } from "react-icons/si";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import Raw from "../widget/raw";
|
||||
import WidgetIcon from "../widget/widget_icon";
|
||||
import PrimaryText from "../widget/primary_text";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Widget({ options }) {
|
||||
@ -13,35 +19,16 @@ export default function Widget({ options }) {
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
|
||||
|
||||
if (statsError) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default");
|
||||
|
||||
if (!defaultSite) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<SiUbiquiti className="w-5 h-5 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{t("unifi.wait")}</PrimaryText>
|
||||
<WidgetIcon icon={SiUbiquiti} />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
const wan = defaultSite.health.find(h => h.subsystem === "wan");
|
||||
@ -56,8 +43,9 @@ export default function Widget({ options }) {
|
||||
|
||||
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
return <Container options={options}>
|
||||
<Raw>
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row ml-3 mb-0.5">
|
||||
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
|
||||
@ -141,6 +129,7 @@ export default function Widget({ options }) {
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</Raw>
|
||||
</Container>
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { WiCloudDown } from "react-icons/wi";
|
||||
import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import PrimaryText from "../widget/primary_text";
|
||||
import SecondaryText from "../widget/secondary_text";
|
||||
import WidgetIcon from "../widget/widget_icon";
|
||||
import ContainerButton from "../widget/container_button";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
function Widget({ options }) {
|
||||
@ -15,59 +21,35 @@ function Widget({ options }) {
|
||||
);
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Error options={options} />
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.updating")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{t("weather.updating")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.wait")}</SecondaryText>
|
||||
<WidgetIcon icon={WiCloudDown} size="l" />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
const weatherInfo = {
|
||||
condition: data.current.condition.code,
|
||||
timeOfDay: data.current.is_day ? "day" : "night",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{t("common.number", {
|
||||
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Container options={options}>
|
||||
<PrimaryText>
|
||||
{options.label && `${options.label}, `}
|
||||
{t("common.number", {
|
||||
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
|
||||
style: "unit",
|
||||
unit,
|
||||
})}
|
||||
</PrimaryText>
|
||||
<SecondaryText>{data.current.condition.text}</SecondaryText>
|
||||
<WidgetIcon icon={Icon} size="xl" weatherInfo={weatherInfo} />
|
||||
</Container>;
|
||||
}
|
||||
|
||||
export default function WeatherApi({ options }) {
|
||||
@ -99,30 +81,12 @@ export default function WeatherApi({ options }) {
|
||||
}
|
||||
};
|
||||
|
||||
// if (!requesting && !location) requestLocation();
|
||||
|
||||
if (!location) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => requestLocation()}
|
||||
className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
{requesting ? (
|
||||
<MdLocationSearching className="w-6 h-6 text-theme-800 dark:text-theme-200 animate-pulse" />
|
||||
) : (
|
||||
<MdLocationDisabled className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("weather.current")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("weather.allow")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return <ContainerButton options={options} callback={requestLocation} >
|
||||
<PrimaryText>{t("weather.current")}</PrimaryText>
|
||||
<SecondaryText>{t("weather.allow")}</SecondaryText>
|
||||
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
|
||||
</ContainerButton>;
|
||||
}
|
||||
|
||||
return <Widget options={{ ...location, ...options }} />;
|
||||
|
@ -17,13 +17,13 @@ const widgetMappings = {
|
||||
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
export default function Widget({ widget, style }) {
|
||||
const InfoWidget = widgetMappings[widget.type];
|
||||
|
||||
if (InfoWidget) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<InfoWidget options={widget.options} />
|
||||
<InfoWidget options={{ ...widget.options, style }} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
54
src/components/widgets/widget/container.jsx
Normal file
54
src/components/widgets/widget/container.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import WidgetIcon from "./widget_icon";
|
||||
import PrimaryText from "./primary_text";
|
||||
import SecondaryText from "./secondary_text";
|
||||
import Raw from "./raw";
|
||||
|
||||
export function getAllClasses(options, additionalClassNames = '') {
|
||||
if (options?.style?.header === "boxedWidgets") {
|
||||
return classNames(
|
||||
"flex flex-col justify-center first:ml-0 ml-2 mr-2",
|
||||
"mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-2 pl-3 pr-3",
|
||||
additionalClassNames
|
||||
);
|
||||
}
|
||||
|
||||
let widgetAlignedClasses = "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap";
|
||||
if (options?.style?.isRightAligned) {
|
||||
widgetAlignedClasses = "flex flex-col justify-center first:ml-auto ml-2 mr-2 ";
|
||||
}
|
||||
|
||||
return classNames(
|
||||
widgetAlignedClasses,
|
||||
additionalClassNames
|
||||
);
|
||||
}
|
||||
|
||||
export function getInnerBlock(children) {
|
||||
// children won't be an array if it's Raw component
|
||||
return Array.isArray(children) && <div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">{children.find(child => child.type === WidgetIcon)}</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
{children.find(child => child.type === PrimaryText)}
|
||||
{children.find(child => child.type === SecondaryText)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function getBottomBlock(children) {
|
||||
if (children.type !== Raw) {
|
||||
return children.find(child => child.type === Raw) || [];
|
||||
}
|
||||
|
||||
return [children];
|
||||
}
|
||||
|
||||
export default function Container({ children = [], options, additionalClassNames = '' }) {
|
||||
return (
|
||||
<div className={getAllClasses(options, additionalClassNames)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</div>
|
||||
);
|
||||
}
|
10
src/components/widgets/widget/container_button.jsx
Normal file
10
src/components/widgets/widget/container_button.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
|
||||
|
||||
export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) {
|
||||
return (
|
||||
<button type="button" onClick={callback} className={getAllClasses(options, additionalClassNames)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</button>
|
||||
);
|
||||
}
|
10
src/components/widgets/widget/container_form.jsx
Normal file
10
src/components/widgets/widget/container_form.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
|
||||
|
||||
export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) {
|
||||
return (
|
||||
<form type="button" onSubmit={callback} className={getAllClasses(options, additionalClassNames)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</form>
|
||||
);
|
||||
}
|
10
src/components/widgets/widget/container_link.jsx
Normal file
10
src/components/widgets/widget/container_link.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
|
||||
|
||||
export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) {
|
||||
return (
|
||||
<a href={options.url} target={target} className={getAllClasses(options, additionalClassNames)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</a>
|
||||
);
|
||||
}
|
15
src/components/widgets/widget/error.jsx
Normal file
15
src/components/widgets/widget/error.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BiError } from "react-icons/bi";
|
||||
|
||||
import Container from "./container";
|
||||
import PrimaryText from "./primary_text";
|
||||
import WidgetIcon from "./widget_icon";
|
||||
|
||||
export default function Error({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Container options={options}>
|
||||
<PrimaryText>{t("widget.api_error")}</PrimaryText>
|
||||
<WidgetIcon icon={BiError} size="l" />
|
||||
</Container>;
|
||||
}
|
5
src/components/widgets/widget/primary_text.jsx
Normal file
5
src/components/widgets/widget/primary_text.jsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function PrimaryText({ children }) {
|
||||
return (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{children}</span>
|
||||
);
|
||||
}
|
7
src/components/widgets/widget/raw.jsx
Normal file
7
src/components/widgets/widget/raw.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function Raw({ children }) {
|
||||
if (children.type === Raw) {
|
||||
return [children];
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
22
src/components/widgets/widget/resource.jsx
Normal file
22
src/components/widgets/widget/resource.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import UsageBar from "../resources/usage-bar";
|
||||
|
||||
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) {
|
||||
const Icon = icon;
|
||||
|
||||
return <div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5"/>
|
||||
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{value}</div>
|
||||
<div className="pr-1">{label}</div>
|
||||
</div>
|
||||
{ expanded && <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{expandedValue}</div>
|
||||
<div className="pr-1">{expandedLabel}</div>
|
||||
</div>
|
||||
}
|
||||
{ percentage && <UsageBar percent={percentage} /> }
|
||||
{ children }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
17
src/components/widgets/widget/resources.jsx
Normal file
17
src/components/widgets/widget/resources.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import ContainerLink from "./container_link";
|
||||
import Resource from "./resource";
|
||||
import Raw from "./raw";
|
||||
import WidgetLabel from "./widget_label";
|
||||
|
||||
export default function Resources({ options, children, target }) {
|
||||
const widgetParts = [].concat(...children);
|
||||
|
||||
return <ContainerLink options={options} target={target}>
|
||||
<Raw>
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{ widgetParts.filter(child => child && child.type === Resource) }
|
||||
</div>
|
||||
{ widgetParts.filter(child => child && child.type === WidgetLabel) }
|
||||
</Raw>
|
||||
</ContainerLink>;
|
||||
}
|
5
src/components/widgets/widget/secondary_text.jsx
Normal file
5
src/components/widgets/widget/secondary_text.jsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function SecondaryText({ children }) {
|
||||
return (
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{children}</span>
|
||||
);
|
||||
}
|
18
src/components/widgets/widget/widget_icon.jsx
Normal file
18
src/components/widgets/widget/widget_icon.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
export default function WidgetIcon({ icon, size = "s", pulse = false, weatherInfo = {} }) {
|
||||
const Icon = icon;
|
||||
const { condition, timeOfDay } = weatherInfo;
|
||||
let additionalClasses = "text-theme-800 dark:text-theme-200 ";
|
||||
|
||||
switch (size) {
|
||||
case "m": additionalClasses += "w-6 h-6 "; break;
|
||||
case "l": additionalClasses += "w-8 h-8 "; break;
|
||||
case "xl": additionalClasses += "w-10 h-10 "; break;
|
||||
default: additionalClasses += "w-5 h-5 ";
|
||||
}
|
||||
|
||||
if (pulse) {
|
||||
additionalClasses += "animate-pulse ";
|
||||
}
|
||||
|
||||
return <Icon className={additionalClasses} condition={condition} timeOfDay={timeOfDay} />;
|
||||
}
|
3
src/components/widgets/widget/widget_label.jsx
Normal file
3
src/components/widgets/widget/widget_label.jsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function WidgetLabel({ label = "" }) {
|
||||
return <div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
|
||||
}
|
@ -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");
|
||||
|
@ -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) {
|
||||
|
@ -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 }) {
|
||||
/>
|
||||
<meta name="theme-color" content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]} />
|
||||
</Head>
|
||||
<div className="relative container m-auto flex flex-col justify-between z-10 h-full">
|
||||
<div className="relative container m-auto flex flex-col justify-start z-10 h-full">
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-wrap justify-between",
|
||||
headerStyles[initialSettings.headerStyle || "underlined"]
|
||||
headerStyles[headerStyle]
|
||||
)}
|
||||
>
|
||||
<QuickLaunch
|
||||
@ -272,14 +274,17 @@ function Home({ initialSettings }) {
|
||||
{widgets
|
||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false}} />
|
||||
))}
|
||||
|
||||
<div className="m-auto sm:ml-2 flex flex-wrap grow sm:basis-auto justify-between md:justify-end">
|
||||
<div className={classNames(
|
||||
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
|
||||
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
|
||||
)}>
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true}} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
@ -289,7 +294,7 @@ function Home({ initialSettings }) {
|
||||
{services?.length > 0 && (
|
||||
<div className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
|
||||
{services.map((group) => (
|
||||
<ServicesGroup key={group.name} services={group} layout={initialSettings.layout?.[group.name]} fiveColumns={settings.fiveColumns} />
|
||||
<ServicesGroup key={group.name} group={group.name} services={group} layout={initialSettings.layout?.[group.name]} fiveColumns={settings.fiveColumns} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -302,14 +307,16 @@ function Home({ initialSettings }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex p-8 pb-0 w-full justify-end">
|
||||
{!initialSettings?.color && <ColorToggle />}
|
||||
<Revalidate />
|
||||
{!initialSettings?.theme && <ThemeToggle />}
|
||||
</div>
|
||||
<div className="flex flex-col mt-auto p-8 w-full">
|
||||
<div className="flex w-full justify-end">
|
||||
{!initialSettings?.color && <ColorToggle />}
|
||||
<Revalidate />
|
||||
{!initialSettings?.theme && <ThemeToggle />}
|
||||
</div>
|
||||
|
||||
<div className="flex p-8 pt-4 w-full justify-end">
|
||||
{!initialSettings?.hideVersion && <Version />}
|
||||
<div className="flex mt-4 w-full justify-end">
|
||||
{!initialSettings?.hideVersion && <Version />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -63,10 +63,10 @@ export async function servicesFromDocker() {
|
||||
const serviceServers = await Promise.all(
|
||||
Object.keys(servers).map(async (serverName) => {
|
||||
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 <Buffer ...> 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) {
|
||||
@ -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;
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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]);
|
||||
});
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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")),
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
39
src/widgets/jdownloader/component.jsx
Normal file
39
src/widgets/jdownloader/component.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
if (jdownloaderAPIError) {
|
||||
return <Container service={service} error={jdownloaderAPIError} />;
|
||||
}
|
||||
|
||||
if (!jdownloaderData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" />
|
||||
<Block label="jdownloader.downloadTotalBytes" />
|
||||
<Block label="jdownloader.downloadBytesRemaining" />
|
||||
<Block label="jdownloader.downloadSpeed" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" value={t("common.number", { value: jdownloaderData.downloadCount })} />
|
||||
<Block label="jdownloader.downloadTotalBytes" value={t("common.bytes", { value: jdownloaderData.totalBytes })} />
|
||||
<Block label="jdownloader.downloadBytesRemaining" value={t("common.bytes", { value: jdownloaderData.bytesRemaining })} />
|
||||
<Block label="jdownloader.downloadSpeed" value={t("common.byterate", { value: jdownloaderData.totalSpeed })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
196
src/widgets/jdownloader/proxy.js
Normal file
196
src/widgets/jdownloader/proxy.js
Normal file
@ -0,0 +1,196 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import crypto from 'crypto';
|
||||
import querystring from 'querystring';
|
||||
|
||||
import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools"
|
||||
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const proxyName = "jdownloaderProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
const widget = await getServiceWidget(group, service);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
async function login(loginSecret, deviceSecret, params) {
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/connect?${querystring.stringify({ ...params, rid })}`;
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha256', loginSecret)
|
||||
.update(path)
|
||||
.digest('hex');
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret))
|
||||
const sessionToken = decryptedData.sessiontoken;
|
||||
validateRid(decryptedData, rid);
|
||||
const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
|
||||
const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
|
||||
return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getDevice(serverEncryptionToken, deviceName, params) {
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/listdevices?${querystring.stringify({ ...params, rid })}`;
|
||||
const signature = crypto
|
||||
.createHmac('sha256', serverEncryptionToken)
|
||||
.update(path)
|
||||
.digest('hex');
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
||||
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken))
|
||||
const filteredDevice = decryptedData.list.filter(device => device.name === deviceName);
|
||||
return [status, filteredDevice[0].id];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function createBody(rid, query, params) {
|
||||
const baseBody = {
|
||||
apiVer: 1,
|
||||
rid,
|
||||
url: query
|
||||
};
|
||||
return params ? { ...baseBody, params: [JSON.stringify(params)] } : baseBody;
|
||||
}
|
||||
|
||||
async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) {
|
||||
const rid = uniqueRid();
|
||||
const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken);
|
||||
const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}`
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken))
|
||||
return decryptedData.data;
|
||||
} catch (e) {
|
||||
logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default async function jdownloaderProxyHandler(req, res) {
|
||||
const widget = await getWidget(req);
|
||||
|
||||
if (!widget) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
logger.debug("Getting data from JDRss API");
|
||||
const { username } = widget
|
||||
const { password } = widget
|
||||
|
||||
const appKey = "homepage"
|
||||
const loginSecret = sha256(`${username}${password}server`)
|
||||
const deviceSecret = sha256(`${username}${password}device`)
|
||||
const email = username;
|
||||
|
||||
const loginData = await login(loginSecret, deviceSecret, {
|
||||
appKey,
|
||||
email
|
||||
})
|
||||
|
||||
const deviceData = await getDevice(loginData[3], widget.client, {
|
||||
sessiontoken: loginData[5]
|
||||
})
|
||||
|
||||
const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
|
||||
"bytesLoaded": false,
|
||||
"bytesTotal": true,
|
||||
"comment": false,
|
||||
"enabled": true,
|
||||
"eta": false,
|
||||
"priority": false,
|
||||
"finished": true,
|
||||
"running": true,
|
||||
"speed": true,
|
||||
"status": true,
|
||||
"childCount": false,
|
||||
"hosts": false,
|
||||
"saveTo": false,
|
||||
"maxResults": -1,
|
||||
"startAt": 0,
|
||||
}
|
||||
)
|
||||
|
||||
let bytesRemaining = 0;
|
||||
let totalBytes = 0;
|
||||
let totalSpeed = 0;
|
||||
packageStatus.forEach(file => {
|
||||
totalBytes += file.bytesTotal;
|
||||
if (file.finished !== true) {
|
||||
bytesRemaining += file.bytesTotal;
|
||||
if (file.speed) {
|
||||
totalSpeed += file.speed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
downloadCount: packageStatus.length,
|
||||
bytesRemaining,
|
||||
totalBytes,
|
||||
totalSpeed
|
||||
};
|
||||
|
||||
return res.send(data);
|
||||
|
||||
}
|
55
src/widgets/jdownloader/tools.js
Normal file
55
src/widgets/jdownloader/tools.js
Normal file
@ -0,0 +1,55 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function sha256(data) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(data)
|
||||
.digest();
|
||||
}
|
||||
|
||||
export function uniqueRid() {
|
||||
return Math.floor(Math.random() * 10e12);
|
||||
}
|
||||
|
||||
export function validateRid(decryptedData, rid) {
|
||||
if (decryptedData.rid !== rid) {
|
||||
throw new Error('RequestID mismatch');
|
||||
}
|
||||
return decryptedData;
|
||||
|
||||
}
|
||||
|
||||
export function decrypt(data, ivKey) {
|
||||
const iv = ivKey.slice(0, ivKey.length / 2);
|
||||
const key = ivKey.slice(ivKey.length / 2, ivKey.length);
|
||||
const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||
return Buffer.concat([
|
||||
cipher.update(data, 'base64'),
|
||||
cipher.final()
|
||||
]).toString();
|
||||
}
|
||||
|
||||
export function createEncryptionToken(oldTokenBuff, updateToken) {
|
||||
const updateTokenBuff = Buffer.from(updateToken, 'hex');
|
||||
const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length);
|
||||
return sha256(mergedBuffer);
|
||||
}
|
||||
|
||||
export function encrypt(data, ivKey) {
|
||||
if (typeof data !== 'string') {
|
||||
throw new Error('data no es un string');
|
||||
}
|
||||
if (!(ivKey instanceof Buffer)) {
|
||||
throw new Error('ivKey no es un buffer');
|
||||
}
|
||||
if (ivKey.length !== 32) {
|
||||
throw new Error('ivKey tiene que tener tamaño 32');
|
||||
}
|
||||
const stringIVKey = ivKey.toString('hex');
|
||||
const stringIV = stringIVKey.substring(0, stringIVKey.length / 2);
|
||||
const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length);
|
||||
const iv = Buffer.from(stringIV, 'hex');
|
||||
const key = Buffer.from(stringKey, 'hex');
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
||||
return cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
|
||||
}
|
15
src/widgets/jdownloader/widget.js
Normal file
15
src/widgets/jdownloader/widget.js
Normal file
@ -0,0 +1,15 @@
|
||||
import jdownloaderProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "https://api.jdownloader.org/{endpoint}/&signature={signature}",
|
||||
proxyHandler: jdownloaderProxyHandler,
|
||||
|
||||
mappings: {
|
||||
unified: {
|
||||
endpoint: "/",
|
||||
signature: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
@ -16,7 +16,7 @@ export default function Component({ service }) {
|
||||
`/api/kubernetes/stats/${widget.namespace}/${widget.app}?${podSelectorString}`);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Container service={service} error={t("widget.api_error")} />;
|
||||
return <Container service={service} error={statsError ?? statusError} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
|
@ -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 <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
if (!albumsData || !wantedData || !queueData) {
|
||||
if (!artistsData || !wantedData || !queueData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="lidarr.wanted" />
|
||||
<Block label="lidarr.queued" />
|
||||
<Block label="lidarr.albums" />
|
||||
<Block label="lidarr.artists" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@ -32,7 +32,7 @@ export default function Component({ service }) {
|
||||
<Container service={service}>
|
||||
<Block label="lidarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
|
||||
<Block label="lidarr.queued" value={t("common.number", { value: queueData.totalCount })} />
|
||||
<Block label="lidarr.albums" value={t("common.number", { value: albumsData.have })} />
|
||||
<Block label="lidarr.artists" value={t("common.number", { value: artistsData.length })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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 <Container service={service} error={t("widget.api_error")} />;
|
||||
if (containersData.error || containersData.message) {
|
||||
// containersData can be itself an error object e.g. if environment fails
|
||||
return <Container service={service} error={ containersData?.error ?? containersData } />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
|
@ -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 <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
if (!moviesData || !queuedData) {
|
||||
if (!moviesData || !queuedData || !queueDetailsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="radarr.wanted" />
|
||||
@ -27,12 +46,27 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
|
||||
<Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
|
||||
<Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
|
||||
<Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
|
||||
</Container>
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
|
||||
<Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
|
||||
<Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
|
||||
<Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
|
||||
</Container>
|
||||
{enableQueue &&
|
||||
queueDetailsData.map((queueEntry) => (
|
||||
<QueueEntry
|
||||
progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
|
||||
timeLeft={queueEntry.timeLeft}
|
||||
title={moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")}
|
||||
activity={formatDownloadState(queueEntry.trackedDownloadState)}
|
||||
key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user