mirror of
https://github.com/karl0ss/homepage.git
synced 2025-05-01 21:13:39 +01:00
implement i18n
This commit is contained in:
parent
d25148c8ae
commit
c08d4b7b9c
@ -13,14 +13,19 @@
|
|||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dockerode": "^3.3.4",
|
"dockerode": "^3.3.4",
|
||||||
|
"i18next": "^21.9.1",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.5",
|
||||||
|
"i18next-http-backend": "^1.4.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-rpc-2.0": "^1.4.1",
|
"json-rpc-2.0": "^1.4.1",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "12.2.5",
|
"next": "12.2.5",
|
||||||
"node-os-utils": "^1.3.7",
|
"node-os-utils": "^1.3.7",
|
||||||
|
"pretty-bytes": "^6.0.0",
|
||||||
"raw-body": "^2.5.1",
|
"raw-body": "^2.5.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-i18next": "^11.18.5",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"rutorrent-promise": "^2.0.0",
|
"rutorrent-promise": "^2.0.0",
|
||||||
"swr": "^1.3.0"
|
"swr": "^1.3.0"
|
||||||
|
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@ -15,6 +15,9 @@ specifiers:
|
|||||||
eslint-plugin-prettier: ^4.2.1
|
eslint-plugin-prettier: ^4.2.1
|
||||||
eslint-plugin-react: ^7.30.1
|
eslint-plugin-react: ^7.30.1
|
||||||
eslint-plugin-react-hooks: ^4.6.0
|
eslint-plugin-react-hooks: ^4.6.0
|
||||||
|
i18next: ^21.9.1
|
||||||
|
i18next-browser-languagedetector: ^6.1.5
|
||||||
|
i18next-http-backend: ^1.4.1
|
||||||
js-yaml: ^4.1.0
|
js-yaml: ^4.1.0
|
||||||
json-rpc-2.0: ^1.4.1
|
json-rpc-2.0: ^1.4.1
|
||||||
memory-cache: ^0.2.0
|
memory-cache: ^0.2.0
|
||||||
@ -22,9 +25,11 @@ specifiers:
|
|||||||
node-os-utils: ^1.3.7
|
node-os-utils: ^1.3.7
|
||||||
postcss: ^8.4.16
|
postcss: ^8.4.16
|
||||||
prettier: ^2.7.1
|
prettier: ^2.7.1
|
||||||
|
pretty-bytes: ^6.0.0
|
||||||
raw-body: ^2.5.1
|
raw-body: ^2.5.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
|
react-i18next: ^11.18.5
|
||||||
react-icons: ^4.4.0
|
react-icons: ^4.4.0
|
||||||
rutorrent-promise: ^2.0.0
|
rutorrent-promise: ^2.0.0
|
||||||
swr: ^1.3.0
|
swr: ^1.3.0
|
||||||
@ -36,14 +41,19 @@ dependencies:
|
|||||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
dockerode: 3.3.4
|
dockerode: 3.3.4
|
||||||
|
i18next: 21.9.1
|
||||||
|
i18next-browser-languagedetector: 6.1.5
|
||||||
|
i18next-http-backend: 1.4.1
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
json-rpc-2.0: 1.4.1
|
json-rpc-2.0: 1.4.1
|
||||||
memory-cache: 0.2.0
|
memory-cache: 0.2.0
|
||||||
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
|
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
|
||||||
node-os-utils: 1.3.7
|
node-os-utils: 1.3.7
|
||||||
|
pretty-bytes: 6.0.0
|
||||||
raw-body: 2.5.1
|
raw-body: 2.5.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
react-i18next: 11.18.5_4sidbwfhen5r7txudrvpua6nty
|
||||||
react-icons: 4.4.0_react@18.2.0
|
react-icons: 4.4.0_react@18.2.0
|
||||||
rutorrent-promise: 2.0.0
|
rutorrent-promise: 2.0.0
|
||||||
swr: 1.3.0_react@18.2.0
|
swr: 1.3.0_react@18.2.0
|
||||||
@ -79,7 +89,6 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.13.9
|
regenerator-runtime: 0.13.9
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@balena/dockerignore/1.0.2:
|
/@balena/dockerignore/1.0.2:
|
||||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||||
@ -666,6 +675,14 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/cross-fetch/3.1.5:
|
||||||
|
resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==}
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.6.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-spawn/7.0.3:
|
/cross-spawn/7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1483,6 +1500,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.1
|
||||||
|
|
||||||
|
/html-parse-stringify/3.0.1:
|
||||||
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
dependencies:
|
||||||
|
void-elements: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-errors/2.0.0:
|
/http-errors/2.0.0:
|
||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -1494,6 +1517,26 @@ packages:
|
|||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/i18next-browser-languagedetector/6.1.5:
|
||||||
|
resolution: {integrity: sha512-11t7b39oKeZe4uyMxLSPnfw28BCPNLZgUk7zyufex0zKXZ+Bv+JnmJgoB+IfQLZwDt1d71PM8vwBX1NCgliY3g==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.18.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/i18next-http-backend/1.4.1:
|
||||||
|
resolution: {integrity: sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==}
|
||||||
|
dependencies:
|
||||||
|
cross-fetch: 3.1.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/i18next/21.9.1:
|
||||||
|
resolution: {integrity: sha512-ITbDrAjbRR73spZAiu6+ex5WNlHRr1mY+acDi2ioTHuUiviJqSz269Le1xHAf0QaQ6GgIHResUhQNcxGwa/PhA==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.18.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iconv-lite/0.4.24:
|
/iconv-lite/0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2093,6 +2136,11 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pretty-bytes/6.0.0:
|
||||||
|
resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==}
|
||||||
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/prop-types/15.8.1:
|
/prop-types/15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2140,6 +2188,26 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-i18next/11.18.5_4sidbwfhen5r7txudrvpua6nty:
|
||||||
|
resolution: {integrity: sha512-cKcyuuzIv0YUZ4l9WORflVNuhISPAqQShOAsxwFyYuJoCA7HlLmHm7XnvO6hfAGmGpDNRhJHoBX8hG49Cb2xZQ==}
|
||||||
|
peerDependencies:
|
||||||
|
i18next: '>= 19.0.0'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
react-dom: '*'
|
||||||
|
react-native: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.18.9
|
||||||
|
html-parse-stringify: 3.0.1
|
||||||
|
i18next: 21.9.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-icons/4.4.0_react@18.2.0:
|
/react-icons/4.4.0_react@18.2.0:
|
||||||
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
|
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2181,7 +2249,6 @@ packages:
|
|||||||
|
|
||||||
/regenerator-runtime/0.13.9:
|
/regenerator-runtime/0.13.9:
|
||||||
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
|
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/regexp.prototype.flags/1.4.3:
|
/regexp.prototype.flags/1.4.3:
|
||||||
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
|
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
|
||||||
@ -2578,6 +2645,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
|
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/void-elements/3.1.0:
|
||||||
|
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webidl-conversions/3.0.1:
|
/webidl-conversions/3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
98
public/locales/en/common.json
Normal file
98
public/locales/en/common.json
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"bytes": "{{value, bytes}}",
|
||||||
|
"bits": "{{value, bytes(bits: true)}}",
|
||||||
|
"bbytes": "{{value, bytes(binary: true)}}",
|
||||||
|
"bbits": "{{value, bytes(bits: true, binary: true)}}",
|
||||||
|
"byterate": "{{value, bytes}}",
|
||||||
|
"bitrate": "{{value, bytes(bits: true)}}",
|
||||||
|
"percent": "{{value, percent}}",
|
||||||
|
"number": "{{value, number}}",
|
||||||
|
"ms": "{{value, number}}"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"missing_type": "Missing Widget Type: {{type}}",
|
||||||
|
"api_error": "API Error",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search..."
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"total": "Total",
|
||||||
|
"free": "Free",
|
||||||
|
"used": "Used"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"rx": "RX",
|
||||||
|
"tx": "TX",
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"offline": "Offline"
|
||||||
|
},
|
||||||
|
"emby": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate"
|
||||||
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"playing": "Playing",
|
||||||
|
"transcoding": "Transcoding",
|
||||||
|
"bitrate": "Bitrate"
|
||||||
|
},
|
||||||
|
"nzbget": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"remaining": "Remaining",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"rutorrent": {
|
||||||
|
"active": "Active",
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download"
|
||||||
|
},
|
||||||
|
"sonarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"series": "Series"
|
||||||
|
},
|
||||||
|
"radarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"movies": "Movies"
|
||||||
|
},
|
||||||
|
"ombi": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved",
|
||||||
|
"available": "Available"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved",
|
||||||
|
"available": "Available"
|
||||||
|
},
|
||||||
|
"pihole": {
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"gravity": "Gravity"
|
||||||
|
},
|
||||||
|
"speedtest": {
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"ping": "Ping"
|
||||||
|
},
|
||||||
|
"portainer": {
|
||||||
|
"running": "Running",
|
||||||
|
"stopped": "Stopped",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"traefik": {
|
||||||
|
"routers": "Routers",
|
||||||
|
"services": "Services",
|
||||||
|
"middleware": "Middleware"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"total": "Total"
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Sonarr from "./widgets/service/sonarr";
|
import Sonarr from "./widgets/service/sonarr";
|
||||||
import Radarr from "./widgets/service/radarr";
|
import Radarr from "./widgets/service/radarr";
|
||||||
import Ombi from "./widgets/service/ombi";
|
import Ombi from "./widgets/service/ombi";
|
||||||
@ -33,6 +35,8 @@ const widgetMappings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Widget({ service }) {
|
export default function Widget({ service }) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
const ServiceWidget = widgetMappings[service.widget.type];
|
const ServiceWidget = widgetMappings[service.widget.type];
|
||||||
|
|
||||||
if (ServiceWidget) {
|
if (ServiceWidget) {
|
||||||
@ -41,9 +45,7 @@ export default function Widget({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
|
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
|
||||||
<div className="font-thin text-sm">
|
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||||
Missing Widget Type: <strong>{service.widget.type}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
|
|
||||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
import { calculateCPUPercent } from "utils/stats-helpers";
|
||||||
|
|
||||||
export default function Docker({ service }) {
|
export default function Docker({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statusData, error: statusError } = useSWR(
|
const { data: statusData, error: statusError } = useSWR(
|
||||||
@ -23,13 +26,13 @@ export default function Docker({ service }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (statsError || statusError) {
|
if (statsError || statusError) {
|
||||||
return <Widget error="Error Fetching Data" />;
|
return <Widget error={t("docker.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusData && statusData.status !== "running") {
|
if (statusData && statusData.status !== "running") {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Status" value="Offline" />
|
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -37,22 +40,22 @@ export default function Docker({ service }) {
|
|||||||
if (!statsData || !statusData) {
|
if (!statsData || !statusData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="CPU" />
|
<Block label={t("docker.cpu")} />
|
||||||
<Block label="MEM" />
|
<Block label={t("docker.mem")} />
|
||||||
<Block label="RX" />
|
<Block label={t("docker.rx")} />
|
||||||
<Block label="TX" />
|
<Block label={t("docker.tx")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
<Block label={t("docker.cpu")} value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
<Block label={t("docker.mem")} value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||||
{statsData.stats.networks && (
|
{statsData.stats.networks && (
|
||||||
<>
|
<>
|
||||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
<Block label={t("docker.rx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
||||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
<Block label={t("docker.tx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Widget>
|
</Widget>
|
||||||
|
@ -1,25 +1,28 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
|
|
||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Emby({ service, title = "Emby" }) {
|
export default function Emby({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
|
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
|
||||||
|
|
||||||
if (sessionsError) {
|
if (sessionsError) {
|
||||||
return <Widget error={`${title} API Error`} />;
|
return <Widget error={t("docker.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionsData) {
|
if (!sessionsData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Playing" />
|
<Block label={t("emby.playing")} />
|
||||||
<Block label="Transcoding" />
|
<Block label={t("emby.transcoding")} />
|
||||||
<Block label="Bitrate" />
|
<Block label={t("emby.bitrate")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -28,13 +31,14 @@ export default function Emby({ service, title = "Emby" }) {
|
|||||||
const transcoding = sessionsData.filter(
|
const transcoding = sessionsData.filter(
|
||||||
(session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
|
(session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
|
||||||
);
|
);
|
||||||
|
|
||||||
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
|
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Playing" value={playing.length} />
|
<Block label={t("emby.playing")} value={playing.length} />
|
||||||
<Block label="Transcoding" value={transcoding.length} />
|
<Block label={t("emby.transcoding")} value={transcoding.length} />
|
||||||
<Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
|
<Block label={t("emby.bitrate")} value={t("common.bitrate", { value: bitrate })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,5 @@ import Emby from "./emby";
|
|||||||
|
|
||||||
// Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin.
|
// Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin.
|
||||||
export default function Jellyfin({ service }) {
|
export default function Jellyfin({ service }) {
|
||||||
return <Emby service={service} title="Jellyfin" />;
|
return <Emby service={service} />;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,29 +7,31 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Jellyseerr({ service }) {
|
export default function Jellyseerr({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||||
|
|
||||||
if (statsError) {
|
if (statsError) {
|
||||||
return <Widget error="Jellyseerr API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statsData) {
|
if (!statsData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Pending" />
|
<Block label={t("jellyseerr.pending")} />
|
||||||
<Block label="Approved" />
|
<Block label={t("jellyseerr.approved")} />
|
||||||
<Block label="Available" />
|
<Block label={t("jellyseerr.available")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Pending" value={statsData.pending} />
|
<Block label={t("jellyseerr.pending")} value={statsData.pending} />
|
||||||
<Block label="Approved" value={statsData.approved} />
|
<Block label={t("jellyseerr.approved")} value={statsData.approved} />
|
||||||
<Block label="Available" value={statsData.available} />
|
<Block label={t("jellyseerr.available")} value={statsData.available} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,20 +7,22 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Npm({ service }) {
|
export default function Npm({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
||||||
|
|
||||||
if (infoError) {
|
if (infoError) {
|
||||||
return <Widget error="NGINX Proxy Manager API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!infoData) {
|
if (!infoData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Enabled" />
|
<Block label={t("npm.enabled")} />
|
||||||
<Block label="Disabled" />
|
<Block label={t("npm.disabled")} />
|
||||||
<Block label="Total" />
|
<Block label={t("npm.total")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -30,9 +33,9 @@ export default function Npm({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Enabled" value={enabled} />
|
<Block label={t("npm.enabled")} value={enabled} />
|
||||||
<Block label="Disabled" value={disabled} />
|
<Block label={t("npm.disabled")} value={disabled} />
|
||||||
<Block label="Total" value={total} />
|
<Block label={t("npm.total")} value={total} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,43 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
|
|
||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
import { formatBytes } from "utils/stats-helpers";
|
|
||||||
|
|
||||||
export default function Nzbget({ service }) {
|
export default function Nzbget({ service }) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
||||||
|
|
||||||
if (statusError) {
|
if (statusError) {
|
||||||
return <Widget error="Nzbget API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statusData) {
|
if (!statusData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Rate" />
|
<Block label={t("nzbget.rate")} />
|
||||||
<Block label="Remaining" />
|
<Block label={t("nzbget.remaining")} />
|
||||||
<Block label="Downloaded" />
|
<Block label={t("nzbget.downloaded")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
|
<Block label={t("nzbget.rate")} value={t("common.bitrate", { value: statusData.DownloadRate })} />
|
||||||
<Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
|
<Block
|
||||||
<Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
|
label={t("nzbget.remaining")}
|
||||||
|
value={t("common.bytes", { value: statusData.RemainingSizeMB * 1024 * 1024 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label={t("nzbget.downloaded")}
|
||||||
|
value={t("common.bytes", { value: statusData.DownloadedSizeMB * 1024 * 1024 })}
|
||||||
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,29 +7,31 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Ombi({ service }) {
|
export default function Ombi({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
||||||
|
|
||||||
if (statsError) {
|
if (statsError) {
|
||||||
return <Widget error="Ombi API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statsData) {
|
if (!statsData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Pending" />
|
<Block label={t("ombi.pending")} />
|
||||||
<Block label="Approved" />
|
<Block label={t("ombi.approved")} />
|
||||||
<Block label="Available" />
|
<Block label={t("ombi.available")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Pending" value={statsData.pending} />
|
<Block label={t("ombi.pending")} value={statsData.pending} />
|
||||||
<Block label="Approved" value={statsData.approved} />
|
<Block label={t("ombi.approved")} value={statsData.approved} />
|
||||||
<Block label="Available" value={statsData.available} />
|
<Block label={t("ombi.available")} value={statsData.available} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,29 +7,31 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Pihole({ service }) {
|
export default function Pihole({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
||||||
|
|
||||||
if (piholeError) {
|
if (piholeError) {
|
||||||
return <Widget error="PiHole API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!piholeData) {
|
if (!piholeData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Queries" />
|
<Block label={t("pihole.queries")} />
|
||||||
<Block label="Blocked" />
|
<Block label={t("pihole.blocked")} />
|
||||||
<Block label="Gravity" />
|
<Block label={t("pihole.gravity")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Queries" value={piholeData.dns_queries_today.toLocaleString()} />
|
<Block label={t("pihole.queries")} value={t("common.number", { value: piholeData.dns_queries_today })} />
|
||||||
<Block label="Blocked" value={piholeData.ads_blocked_today.toLocaleString()} />
|
<Block label={t("pihole.blocked")} value={t("common.number", { value: piholeData.ads_blocked_today })} />
|
||||||
<Block label="Gravity" value={piholeData.domains_being_blocked.toLocaleString()} />
|
<Block label={t("pihole.gravity")} value={t("common.number", { value: piholeData.domains_being_blocked })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,26 +7,28 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Portainer({ service }) {
|
export default function Portainer({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
||||||
|
|
||||||
if (containersError) {
|
if (containersError) {
|
||||||
return <Widget error="Portainer API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!containersData) {
|
if (!containersData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Running" />
|
<Block label={t("portainer.running")} />
|
||||||
<Block label="Stopped" />
|
<Block label={t("portainer.stopped")} />
|
||||||
<Block label="Total" />
|
<Block label={t("portainer.total")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containersData.error) {
|
if (containersData.error) {
|
||||||
return <Widget error="Portainer API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const running = containersData.filter((c) => c.State === "running").length;
|
const running = containersData.filter((c) => c.State === "running").length;
|
||||||
@ -34,9 +37,9 @@ export default function Portainer({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Running" value={running} />
|
<Block label={t("portainer.running")} value={running} />
|
||||||
<Block label="Stopped" value={stopped} />
|
<Block label={t("portainer.stopped")} value={stopped} />
|
||||||
<Block label="Total" value={total} />
|
<Block label={t("portainer.total")} value={total} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,21 +7,23 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Radarr({ service }) {
|
export default function Radarr({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
||||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||||
|
|
||||||
if (moviesError || queuedError) {
|
if (moviesError || queuedError) {
|
||||||
return <Widget error="Radarr API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!moviesData || !queuedData) {
|
if (!moviesData || !queuedData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Wanted" />
|
<Block label={t("radarr.wanted")} />
|
||||||
<Block label="Queued" />
|
<Block label={t("radarr.queued")} />
|
||||||
<Block label="Movies" />
|
<Block label={t("radarr.movies")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -30,9 +33,9 @@ export default function Radarr({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Wanted" value={wanted.length} />
|
<Block label={t("radarr.wanted")} value={wanted.length} />
|
||||||
<Block label="Queued" value={queuedData.totalCount} />
|
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||||
<Block label="Movies" value={have.length} />
|
<Block label={t("radarr.movies")} value={have.length} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
|
|
||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
import { formatBytes } from "utils/stats-helpers";
|
|
||||||
|
|
||||||
export default function Rutorrent({ service }) {
|
export default function Rutorrent({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
||||||
|
|
||||||
if (statusError) {
|
if (statusError) {
|
||||||
return <Widget error="Nzbget API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statusData) {
|
if (!statusData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Active" />
|
<Block label={t("rutorrent.active")} />
|
||||||
<Block label="Upload" />
|
<Block label={t("rutorrent.upload")} />
|
||||||
<Block label="Download" />
|
<Block label={t("rutorrent.download")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -33,9 +35,9 @@ export default function Rutorrent({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Active" value={active.length} />
|
<Block label={t("rutorrent.active")} value={active.length} />
|
||||||
<Block label="Upload" value={`${formatBytes(upload)}/s`} />
|
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||||
<Block label="Download" value={`${formatBytes(download)}/s`} />
|
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,6 +7,8 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Sonarr({ service }) {
|
export default function Sonarr({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||||
@ -13,24 +16,24 @@ export default function Sonarr({ service }) {
|
|||||||
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
|
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
|
||||||
|
|
||||||
if (wantedError || queuedError || seriesError) {
|
if (wantedError || queuedError || seriesError) {
|
||||||
return <Widget error="Sonar API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wantedData || !queuedData || !seriesData) {
|
if (!wantedData || !queuedData || !seriesData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Wanted" />
|
<Block label={t("sonarr.wanted")} />
|
||||||
<Block label="Queued" />
|
<Block label={t("sonarr.queued")} />
|
||||||
<Block label="Series" />
|
<Block label={t("sonarr.series")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Wanted" value={wantedData.totalRecords} />
|
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||||
<Block label="Queued" value={queuedData.totalRecords} />
|
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||||
<Block label="Series" value={seriesData.length} />
|
<Block label={t("sonarr.series")} value={seriesData.length} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,46 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
|
|
||||||
import { formatBits } from "utils/stats-helpers";
|
|
||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Speedtest({ service }) {
|
export default function Speedtest({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
||||||
|
|
||||||
if (speedtestError) {
|
if (speedtestError) {
|
||||||
return <Widget error="Speedtest API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!speedtestData) {
|
if (!speedtestData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Download" />
|
<Block label={t("speedtest.download")} />
|
||||||
<Block label="Upload" />
|
<Block label={t("speedtest.upload")} />
|
||||||
<Block label="Ping" />
|
<Block label={t("speedtest.ping")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024, 0)}ps`} />
|
<Block
|
||||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024, 0)}ps`} />
|
label={t("speedtest.download")}
|
||||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label={t("speedtest.upload")}
|
||||||
|
value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label={t("speedtest.ping")}
|
||||||
|
value={t("common.ms", { value: speedtestData.data.ping, style: "unit", unit: "millisecond" })}
|
||||||
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,20 +7,22 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Tautulli({ service }) {
|
export default function Tautulli({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
|
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
|
||||||
|
|
||||||
if (statsError) {
|
if (statsError) {
|
||||||
return <Widget error="Tautulli API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statsData) {
|
if (!statsData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Playing" />
|
<Block label={t("tautulli.playing")} />
|
||||||
<Block label="Transcoding" />
|
<Block label={t("tautulli.transcoding")} />
|
||||||
<Block label="Bitrate" />
|
<Block label={t("tautulli.bitrate")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -28,10 +31,9 @@ export default function Tautulli({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Playing" value={data.stream_count} />
|
<Block label={t("tautulli.playing")} value={data.stream_count} />
|
||||||
<Block label="Transcoding" value={data.stream_count_transcode} />
|
<Block label={t("tautulli.transcoding")} value={data.stream_count_transcode} />
|
||||||
{/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */}
|
<Block label={t("tautulli.bitrate")} value={t("common.bitrate", { value: data.total_bandwidth })} />
|
||||||
<Block label="Bitrate" value={`${Math.round((data.total_bandwidth / 1000) * 100) / 100} Mbps`} />
|
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Widget from "../widget";
|
import Widget from "../widget";
|
||||||
import Block from "../block";
|
import Block from "../block";
|
||||||
@ -6,29 +7,31 @@ import Block from "../block";
|
|||||||
import { formatApiUrl } from "utils/api-helpers";
|
import { formatApiUrl } from "utils/api-helpers";
|
||||||
|
|
||||||
export default function Traefik({ service }) {
|
export default function Traefik({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = service.widget;
|
const config = service.widget;
|
||||||
|
|
||||||
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
||||||
|
|
||||||
if (traefikError) {
|
if (traefikError) {
|
||||||
return <Widget error="Traefik API Error" />;
|
return <Widget error={t("widget.api_error")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!traefikData) {
|
if (!traefikData) {
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Routers" />
|
<Block label={t("traefik.routers")} />
|
||||||
<Block label="Services" />
|
<Block label={t("traefik.services")} />
|
||||||
<Block label="Middleware" />
|
<Block label={t("traefik.middleware")} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Block label="Routers" value={traefikData.http.routers.total} />
|
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||||
<Block label="Services" value={traefikData.http.services.total} />
|
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||||
<Block label="Middleware" value={traefikData.http.middlewares.total} />
|
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
|
|
||||||
export default function OpenWeatherMap({ options }) {
|
export default function OpenWeatherMap({ options }) {
|
||||||
const { data, error } = useSWR(`/api/widgets/openweathermap?${new URLSearchParams(options).toString()}`);
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const { data, error } = useSWR(
|
||||||
|
`/api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
if (error || data?.cod === 401) {
|
if (error || data?.cod === 401) {
|
||||||
return (
|
return (
|
||||||
@ -30,6 +35,8 @@ export default function OpenWeatherMap({ options }) {
|
|||||||
return <div className="flex flex-row items-center" />;
|
return <div className="flex flex-row items-center" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<div className="flex flex-row items-center justify-end">
|
<div className="flex flex-row items-center justify-end">
|
||||||
@ -42,11 +49,9 @@ export default function OpenWeatherMap({ options }) {
|
|||||||
<div className="flex flex-col ml-3 text-left">
|
<div className="flex flex-col ml-3 text-left">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||||
{options.label && `${options.label}, `}
|
{options.label && `${options.label}, `}
|
||||||
{data.main.temp.toFixed(1)}°
|
{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.charAt(0).toUpperCase() + data.weather[0].description.slice(1)}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FiCpu } from "react-icons/fi";
|
import { FiCpu } from "react-icons/fi";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Cpu() {
|
export default function Cpu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||||
refreshInterval: 1500,
|
refreshInterval: 1500,
|
||||||
});
|
});
|
||||||
@ -12,7 +15,7 @@ export default function Cpu() {
|
|||||||
<div className="flex-none flex flex-row items-center justify-center">
|
<div className="flex-none flex flex-row items-center justify-center">
|
||||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left">
|
<div className="flex flex-col ml-3 text-left">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -23,7 +26,7 @@ export default function Cpu() {
|
|||||||
<div className="flex-none flex flex-row items-center justify-center">
|
<div className="flex-none flex flex-row items-center justify-center">
|
||||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left">
|
<div className="flex flex-col ml-3 text-left">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Usage</span>
|
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -35,7 +38,9 @@ export default function Cpu() {
|
|||||||
<div className="flex-none flex flex-row items-center justify-center">
|
<div className="flex-none flex flex-row items-center justify-center">
|
||||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono min-w-[50px]">
|
<div className="flex flex-col ml-3 text-left font-mono min-w-[50px]">
|
||||||
<div className="text-theme-800 dark:text-theme-200 text-xs">{`${Math.round(data.cpu.usage)}%`}</div>
|
<div className="text-theme-800 dark:text-theme-200 text-xs">
|
||||||
|
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}
|
||||||
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
className="bg-theme-600 h-1 rounded-full dark:bg-theme-500"
|
className="bg-theme-600 h-1 rounded-full dark:bg-theme-500"
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FiHardDrive } from "react-icons/fi";
|
import { FiHardDrive } from "react-icons/fi";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatBytes } from "utils/stats-helpers";
|
|
||||||
|
|
||||||
export default function Disk({ options }) {
|
export default function Disk({ options }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||||
refreshInterval: 1500,
|
refreshInterval: 1500,
|
||||||
});
|
});
|
||||||
@ -14,7 +15,7 @@ export default function Disk({ options }) {
|
|||||||
<div className="flex-none flex flex-row items-center justify-center">
|
<div className="flex-none flex flex-row items-center justify-center">
|
||||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono">
|
<div className="flex flex-col ml-3 text-left font-mono">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -38,10 +39,10 @@ export default function Disk({ options }) {
|
|||||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono ">
|
<div className="flex flex-col ml-3 text-left font-mono ">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||||
{formatBytes(data.drive.freeGb * 1024 * 1024 * 1024, 0)} Free
|
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||||
{formatBytes(data.drive.totalGb * 1024 * 1024 * 1024, 0)} Total
|
{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })} {t("resources.total")}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FaMemory } from "react-icons/fa";
|
import { FaMemory } from "react-icons/fa";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatBytes } from "utils/stats-helpers";
|
|
||||||
|
|
||||||
export default function Memory() {
|
export default function Memory() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||||
refreshInterval: 1500,
|
refreshInterval: 1500,
|
||||||
});
|
});
|
||||||
@ -14,7 +15,7 @@ export default function Memory() {
|
|||||||
<div className="flex-none flex flex-row items-center justify-center">
|
<div className="flex-none flex flex-row items-center justify-center">
|
||||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono">
|
<div className="flex flex-col ml-3 text-left font-mono">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -38,10 +39,10 @@ export default function Memory() {
|
|||||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
<div className="flex flex-col ml-3 text-left font-mono">
|
<div className="flex flex-col ml-3 text-left font-mono">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||||
{formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free
|
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||||
{formatBytes(data.memory.usedMemMb * 1024 * 1024)} Used
|
{t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { FiSearch } from "react-icons/fi";
|
import { FiSearch } from "react-icons/fi";
|
||||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||||
|
|
||||||
@ -26,6 +27,8 @@ const providers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Search({ options }) {
|
export default function Search({ options }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const provider = providers[options.provider];
|
const provider = providers[options.provider];
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
@ -53,7 +56,7 @@ export default function Search({ options }) {
|
|||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="overflow-hidden w-full placeholder-theme-900 text-xs text-theme-900 bg-theme-50 rounded-md border border-theme-300 focus:ring-theme-500 focus:border-theme-500 dark:bg-theme-800 dark:border-theme-600 dark:placeholder-theme-400 dark:text-white dark:focus:ring-theme-500 dark:focus:border-theme-500 h-full"
|
className="overflow-hidden w-full placeholder-theme-900 text-xs text-theme-900 bg-theme-50 rounded-md border border-theme-300 focus:ring-theme-500 focus:border-theme-500 dark:bg-theme-800 dark:border-theme-600 dark:placeholder-theme-400 dark:text-white dark:focus:ring-theme-500 dark:focus:border-theme-500 h-full"
|
||||||
placeholder="Search..."
|
placeholder={t("search.placeholder")}
|
||||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
|
|
||||||
export default function WeatherApi({ options }) {
|
export default function WeatherApi({ options }) {
|
||||||
const { data, error } = useSWR(`/api/widgets/weather?${new URLSearchParams(options).toString()}`);
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const { data, error } = useSWR(
|
||||||
|
`/api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -30,6 +35,8 @@ export default function WeatherApi({ options }) {
|
|||||||
return <div className="flex flex-row items-center justify-end" />;
|
return <div className="flex flex-row items-center justify-end" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<div className="flex flex-row items-center justify-end">
|
<div className="flex flex-row items-center justify-end">
|
||||||
@ -39,7 +46,11 @@ export default function WeatherApi({ options }) {
|
|||||||
<div className="flex flex-col ml-3 text-left">
|
<div className="flex flex-col ml-3 text-left">
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||||
{options.label && `${options.label}, `}
|
{options.label && `${options.label}, `}
|
||||||
{options.units === "metric" ? data.current.temp_c : data.current.temp_f}°
|
{t("common.number", {
|
||||||
|
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
|
||||||
|
style: "unit",
|
||||||
|
unit,
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
|
|
||||||
import "styles/globals.css";
|
import "styles/globals.css";
|
||||||
import "styles/weather-icons.css";
|
import "styles/weather-icons.css";
|
||||||
import "styles/theme.css";
|
import "styles/theme.css";
|
||||||
|
|
||||||
|
import "utils/i18n";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
|||||||
import { getSettings } from "utils/config";
|
import { getSettings } from "utils/config";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, units, provider, cache } = req.query;
|
const { latitude, longitude, units, provider, cache, lang } = req.query;
|
||||||
let { apiKey } = req.query;
|
let { apiKey } = req.query;
|
||||||
|
|
||||||
if (!apiKey && !provider) {
|
if (!apiKey && !provider) {
|
||||||
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
|||||||
return res.status(400).json({ error: "Missing API key" });
|
return res.status(400).json({ error: "Missing API key" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}`;
|
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
||||||
|
|
||||||
return res.send(await cachedFetch(apiUrl, cache));
|
return res.send(await cachedFetch(apiUrl, cache));
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
|||||||
import { getSettings } from "utils/config";
|
import { getSettings } from "utils/config";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, provider, cache } = req.query;
|
const { latitude, longitude, provider, cache, lang } = req.query;
|
||||||
let { apiKey } = req.query;
|
let { apiKey } = req.query;
|
||||||
|
|
||||||
if (!apiKey && !provider) {
|
if (!apiKey && !provider) {
|
||||||
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
|||||||
return res.status(400).json({ error: "Missing API key" });
|
return res.status(400).json({ error: "Missing API key" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}`;
|
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`;
|
||||||
|
|
||||||
return res.send(await cachedFetch(apiUrl, cache));
|
return res.send(await cachedFetch(apiUrl, cache));
|
||||||
}
|
}
|
||||||
|
32
src/utils/i18n.js
Normal file
32
src/utils/i18n.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import Backend from "i18next-http-backend";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: "en",
|
||||||
|
ns: ["common"],
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
|
defaultNS: "common",
|
||||||
|
nonExplicitSupportedLngs: true,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n.services.formatter.add("bytes", (value, lng, options) =>
|
||||||
|
prettyBytes(parseFloat(value), { locale: lng, ...options })
|
||||||
|
);
|
||||||
|
i18n.services.formatter.add("percent", (value, lng, options) =>
|
||||||
|
new Intl.NumberFormat(lng, { style: "percent", ...options }).format(parseFloat(value) / 100.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default i18n;
|
Loading…
x
Reference in New Issue
Block a user