mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-15 09:20:32 +00:00
implement i18n
This commit is contained in:
parent
d25148c8ae
commit
c08d4b7b9c
29 changed files with 431 additions and 139 deletions
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Sonarr from "./widgets/service/sonarr";
|
||||
import Radarr from "./widgets/service/radarr";
|
||||
import Ombi from "./widgets/service/ombi";
|
||||
|
@ -33,6 +35,8 @@ const widgetMappings = {
|
|||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const ServiceWidget = widgetMappings[service.widget.type];
|
||||
|
||||
if (ServiceWidget) {
|
||||
|
@ -41,9 +45,7 @@ export default function Widget({ service }) {
|
|||
|
||||
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="font-thin text-sm">
|
||||
Missing Widget Type: <strong>{service.widget.type}</strong>
|
||||
</div>
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
||||
import { calculateCPUPercent } from "utils/stats-helpers";
|
||||
|
||||
export default function Docker({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
|
@ -23,13 +26,13 @@ export default function Docker({ service }) {
|
|||
);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Widget error="Error Fetching Data" />;
|
||||
return <Widget error={t("docker.api_error")} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Status" value="Offline" />
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -37,22 +40,22 @@ export default function Docker({ service }) {
|
|||
if (!statsData || !statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" />
|
||||
<Block label="MEM" />
|
||||
<Block label="RX" />
|
||||
<Block label="TX" />
|
||||
<Block label={t("docker.cpu")} />
|
||||
<Block label={t("docker.mem")} />
|
||||
<Block label={t("docker.rx")} />
|
||||
<Block label={t("docker.tx")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
||||
<Block label={t("docker.cpu")} value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
<Block label={t("docker.mem")} value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
<Block label={t("docker.rx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
||||
<Block label={t("docker.tx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Emby({ service, title = "Emby" }) {
|
||||
export default function Emby({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
|
||||
|
||||
if (sessionsError) {
|
||||
return <Widget error={`${title} API Error`} />;
|
||||
return <Widget error={t("docker.api_error")} />;
|
||||
}
|
||||
|
||||
if (!sessionsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
<Block label={t("emby.playing")} />
|
||||
<Block label={t("emby.transcoding")} />
|
||||
<Block label={t("emby.bitrate")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -28,13 +31,14 @@ export default function Emby({ service, title = "Emby" }) {
|
|||
const transcoding = sessionsData.filter(
|
||||
(session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
|
||||
);
|
||||
|
||||
const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={playing.length} />
|
||||
<Block label="Transcoding" value={transcoding.length} />
|
||||
<Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
|
||||
<Block label={t("emby.playing")} value={playing.length} />
|
||||
<Block label={t("emby.transcoding")} value={transcoding.length} />
|
||||
<Block label={t("emby.bitrate")} value={t("common.bitrate", { value: bitrate })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ import Emby from "./emby";
|
|||
|
||||
// Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin.
|
||||
export default function Jellyfin({ service }) {
|
||||
return <Emby service={service} title="Jellyfin" />;
|
||||
return <Emby service={service} />;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,29 +7,31 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Jellyseerr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Jellyseerr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("jellyseerr.pending")} />
|
||||
<Block label={t("jellyseerr.approved")} />
|
||||
<Block label={t("jellyseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("jellyseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("jellyseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("jellyseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,20 +7,22 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Npm({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
||||
|
||||
if (infoError) {
|
||||
return <Widget error="NGINX Proxy Manager API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" />
|
||||
<Block label="Disabled" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("npm.enabled")} />
|
||||
<Block label={t("npm.disabled")} />
|
||||
<Block label={t("npm.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -30,9 +33,9 @@ export default function Npm({ service }) {
|
|||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" value={enabled} />
|
||||
<Block label="Disabled" value={disabled} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("npm.enabled")} value={enabled} />
|
||||
<Block label={t("npm.disabled")} value={disabled} />
|
||||
<Block label={t("npm.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,35 +1,43 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
export default function Nzbget({ service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" />
|
||||
<Block label="Remaining" />
|
||||
<Block label="Downloaded" />
|
||||
<Block label={t("nzbget.rate")} />
|
||||
<Block label={t("nzbget.remaining")} />
|
||||
<Block label={t("nzbget.downloaded")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
|
||||
<Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label={t("nzbget.rate")} value={t("common.bitrate", { value: statusData.DownloadRate })} />
|
||||
<Block
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,29 +7,31 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Ombi({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Ombi API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("ombi.pending")} />
|
||||
<Block label={t("ombi.approved")} />
|
||||
<Block label={t("ombi.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("ombi.pending")} value={statsData.pending} />
|
||||
<Block label={t("ombi.approved")} value={statsData.approved} />
|
||||
<Block label={t("ombi.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,29 +7,31 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Pihole({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
||||
|
||||
if (piholeError) {
|
||||
return <Widget error="PiHole API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!piholeData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" />
|
||||
<Block label="Blocked" />
|
||||
<Block label="Gravity" />
|
||||
<Block label={t("pihole.queries")} />
|
||||
<Block label={t("pihole.blocked")} />
|
||||
<Block label={t("pihole.gravity")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" value={piholeData.dns_queries_today.toLocaleString()} />
|
||||
<Block label="Blocked" value={piholeData.ads_blocked_today.toLocaleString()} />
|
||||
<Block label="Gravity" value={piholeData.domains_being_blocked.toLocaleString()} />
|
||||
<Block label={t("pihole.queries")} value={t("common.number", { value: piholeData.dns_queries_today })} />
|
||||
<Block label={t("pihole.blocked")} value={t("common.number", { value: piholeData.ads_blocked_today })} />
|
||||
<Block label={t("pihole.gravity")} value={t("common.number", { value: piholeData.domains_being_blocked })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,26 +7,28 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Portainer({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
||||
|
||||
if (containersError) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" />
|
||||
<Block label="Stopped" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("portainer.running")} />
|
||||
<Block label={t("portainer.stopped")} />
|
||||
<Block label={t("portainer.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -34,9 +37,9 @@ export default function Portainer({ service }) {
|
|||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" value={running} />
|
||||
<Block label="Stopped" value={stopped} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("portainer.running")} value={running} />
|
||||
<Block label={t("portainer.stopped")} value={stopped} />
|
||||
<Block label={t("portainer.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,21 +7,23 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Radarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (moviesError || queuedError) {
|
||||
return <Widget error="Radarr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!moviesData || !queuedData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Movies" />
|
||||
<Block label={t("radarr.wanted")} />
|
||||
<Block label={t("radarr.queued")} />
|
||||
<Block label={t("radarr.movies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -30,9 +33,9 @@ export default function Radarr({ service }) {
|
|||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wanted.length} />
|
||||
<Block label="Queued" value={queuedData.totalCount} />
|
||||
<Block label="Movies" value={have.length} />
|
||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||
<Block label={t("radarr.movies")} value={have.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
export default function Rutorrent({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Download" />
|
||||
<Block label={t("rutorrent.active")} />
|
||||
<Block label={t("rutorrent.upload")} />
|
||||
<Block label={t("rutorrent.download")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -33,9 +35,9 @@ export default function Rutorrent({ service }) {
|
|||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" value={active.length} />
|
||||
<Block label="Upload" value={`${formatBytes(upload)}/s`} />
|
||||
<Block label="Download" value={`${formatBytes(download)}/s`} />
|
||||
<Block label={t("rutorrent.active")} value={active.length} />
|
||||
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,6 +7,8 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Sonarr({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
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"));
|
||||
|
||||
if (wantedError || queuedError || seriesError) {
|
||||
return <Widget error="Sonar API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!wantedData || !queuedData || !seriesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Series" />
|
||||
<Block label={t("sonarr.wanted")} />
|
||||
<Block label={t("sonarr.queued")} />
|
||||
<Block label={t("sonarr.series")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wantedData.totalRecords} />
|
||||
<Block label="Queued" value={queuedData.totalRecords} />
|
||||
<Block label="Series" value={seriesData.length} />
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,35 +1,46 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatBits } from "utils/stats-helpers";
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Speedtest({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
||||
|
||||
if (speedtestError) {
|
||||
return <Widget error="Speedtest API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!speedtestData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Ping" />
|
||||
<Block label={t("speedtest.download")} />
|
||||
<Block label={t("speedtest.upload")} />
|
||||
<Block label={t("speedtest.ping")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
||||
<Block
|
||||
label={t("speedtest.download")}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,20 +7,22 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Tautulli({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Tautulli API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
<Block label={t("tautulli.playing")} />
|
||||
<Block label={t("tautulli.transcoding")} />
|
||||
<Block label={t("tautulli.bitrate")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -28,10 +31,9 @@ export default function Tautulli({ service }) {
|
|||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={data.stream_count} />
|
||||
<Block label="Transcoding" value={data.stream_count_transcode} />
|
||||
{/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */}
|
||||
<Block label="Bitrate" value={`${Math.round((data.total_bandwidth / 1000) * 100) / 100} Mbps`} />
|
||||
<Block label={t("tautulli.playing")} value={data.stream_count} />
|
||||
<Block label={t("tautulli.transcoding")} value={data.stream_count_transcode} />
|
||||
<Block label={t("tautulli.bitrate")} value={t("common.bitrate", { value: data.total_bandwidth })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
@ -6,29 +7,31 @@ import Block from "../block";
|
|||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Traefik({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
||||
|
||||
if (traefikError) {
|
||||
return <Widget error="Traefik API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!traefikData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" />
|
||||
<Block label="Services" />
|
||||
<Block label="Middleware" />
|
||||
<Block label={t("traefik.routers")} />
|
||||
<Block label={t("traefik.services")} />
|
||||
<Block label={t("traefik.middleware")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" value={traefikData.http.routers.total} />
|
||||
<Block label="Services" value={traefikData.http.services.total} />
|
||||
<Block label="Middleware" value={traefikData.http.middlewares.total} />
|
||||
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import useSWR from "swr";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
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) {
|
||||
return (
|
||||
|
@ -30,6 +35,8 @@ export default function OpenWeatherMap({ options }) {
|
|||
return <div className="flex flex-row items-center" />;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<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">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{data.main.temp.toFixed(1)}°
|
||||
</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)}
|
||||
{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>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import useSWR from "swr";
|
||||
import { FiCpu } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Cpu() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
@ -12,7 +15,7 @@ export default function Cpu() {
|
|||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<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">API Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -23,7 +26,7 @@ export default function Cpu() {
|
|||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Usage</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -35,7 +38,9 @@ export default function Cpu() {
|
|||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<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="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="bg-theme-600 h-1 rounded-full dark:bg-theme-500"
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import useSWR from "swr";
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Disk({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
@ -14,7 +15,7 @@ export default function Disk({ options }) {
|
|||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<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>
|
||||
);
|
||||
|
@ -38,10 +39,10 @@ export default function Disk({ options }) {
|
|||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<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">
|
||||
{formatBytes(data.drive.freeGb * 1024 * 1024 * 1024, 0)} Free
|
||||
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<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>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||
<div
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import useSWR from "swr";
|
||||
import { FaMemory } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Memory() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
@ -14,7 +15,7 @@ export default function Memory() {
|
|||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<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>
|
||||
);
|
||||
|
@ -38,10 +39,10 @@ export default function Memory() {
|
|||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<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">
|
||||
{formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<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>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||
<div
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||
|
||||
|
@ -26,6 +27,8 @@ const providers = {
|
|||
};
|
||||
|
||||
export default function Search({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const provider = providers[options.provider];
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
|
@ -53,7 +56,7 @@ export default function Search({ options }) {
|
|||
<input
|
||||
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"
|
||||
placeholder="Search..."
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import useSWR from "swr";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Icon from "./icon";
|
||||
|
||||
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) {
|
||||
return (
|
||||
|
@ -30,6 +35,8 @@ export default function WeatherApi({ options }) {
|
|||
return <div className="flex flex-row items-center justify-end" />;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<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">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{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 className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
import "styles/globals.css";
|
||||
import "styles/weather-icons.css";
|
||||
import "styles/theme.css";
|
||||
|
||||
import "utils/i18n";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<SWRConfig
|
||||
|
|
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
|||
import { getSettings } from "utils/config";
|
||||
|
||||
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;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
|
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
|||
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));
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
|||
import { getSettings } from "utils/config";
|
||||
|
||||
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;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
|
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
|||
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));
|
||||
}
|
||||
|
|
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…
Add table
Add a link
Reference in a new issue