mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-09 14:58:47 +00:00
Merge remote-tracking branch 'origin/main' into pyload_widget
This commit is contained in:
commit
d6f53ab1e9
46 changed files with 1589 additions and 203 deletions
|
@ -1,6 +1,7 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ bookmark }) {
|
||||
const { hostname } = new URL(bookmark.href);
|
||||
|
@ -16,7 +17,12 @@ export default function Item({ bookmark }) {
|
|||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
{bookmark.abbr}
|
||||
{bookmark.icon &&
|
||||
<div className="flex-shrink-0 w-5 h-5">
|
||||
<ResolvedIcon icon={bookmark.icon} />
|
||||
</div>
|
||||
}
|
||||
{!bookmark.icon && bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md ">
|
||||
<div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { useEffect, useState, useRef, useCallback, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { resolveIcon } from "./services/item";
|
||||
import ResolvedIcon from "./resolvedicon";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
|
||||
|
@ -135,7 +135,7 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
|
|||
)} onClick={handleItemClick}>
|
||||
<div className="flex flex-row items-center mr-4 pointer-events-none">
|
||||
<div className="w-5 text-xs mr-4">
|
||||
{r.icon && resolveIcon(r.icon)}
|
||||
{r.icon && <ResolvedIcon icon={r.icon} />}
|
||||
{r.abbr && r.abbr}
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
|
||||
|
|
37
src/components/resolvedicon.jsx
Normal file
37
src/components/resolvedicon.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import Image from "next/future/image";
|
||||
|
||||
export default function ResolvedIcon({ icon }) {
|
||||
// direct or relative URLs
|
||||
if (icon.startsWith("http") || icon.startsWith("/")) {
|
||||
return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
|
||||
}
|
||||
|
||||
// mdi- prefixed, material design icons
|
||||
if (icon.startsWith("mdi-")) {
|
||||
const iconName = icon.replace("mdi-", "").replace(".svg", "");
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
'max-width': '100%',
|
||||
'max-height': '100%',
|
||||
background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
|
||||
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// fallback to dashboard-icons
|
||||
const iconName = icon.replace(".png", "");
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
|
||||
width={32}
|
||||
height={32}
|
||||
alt="logo"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import classNames from "classnames";
|
||||
|
||||
import List from "components/services/list";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function ServicesGroup({ services, layout }) {
|
||||
return (
|
||||
|
@ -11,7 +12,14 @@ export default function ServicesGroup({ services, layout }) {
|
|||
"flex-1 p-1"
|
||||
)}
|
||||
>
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
<div className="flex select-none items-center">
|
||||
{layout?.icon &&
|
||||
<div className="flex-shrink-0 mr-2 w-7 h-7">
|
||||
<ResolvedIcon icon={layout.icon} />
|
||||
</div>
|
||||
}
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2>
|
||||
</div>
|
||||
<List services={services.services} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Image from "next/future/image";
|
||||
import classNames from "classnames";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
|
@ -7,40 +6,7 @@ import Widget from "./widget";
|
|||
|
||||
import Docker from "widgets/docker/component";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
|
||||
export function resolveIcon(icon) {
|
||||
// direct or relative URLs
|
||||
if (icon.startsWith("http") || icon.startsWith("/")) {
|
||||
return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
|
||||
}
|
||||
|
||||
// mdi- prefixed, material design icons
|
||||
if (icon.startsWith("mdi-")) {
|
||||
const iconName = icon.replace("mdi-", "").replace(".svg", "");
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
|
||||
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// fallback to dashboard-icons
|
||||
const iconName = icon.replace(".png", "");
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
|
||||
width={32}
|
||||
height={32}
|
||||
alt="logo"
|
||||
/>
|
||||
);
|
||||
}
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ service }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
|
@ -75,10 +41,12 @@ export default function Item({ service }) {
|
|||
rel="noreferrer"
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
>
|
||||
{resolveIcon(service.icon)}
|
||||
<ResolvedIcon icon={service.icon} />
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-12 ">{resolveIcon(service.icon)}</div>
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<ResolvedIcon icon={service.icon} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasLink ? (
|
||||
|
|
|
@ -33,6 +33,8 @@ export default async function credentialedProxyHandler(req, res) {
|
|||
headers.Authorization = `PVEAPIToken=${widget.username}=${widget.password}`;
|
||||
} else if (widget.type === "autobrr") {
|
||||
headers["X-API-Token"] = `${widget.key}`;
|
||||
} else if (widget.type === "tubearchivist") {
|
||||
headers.Authorization = `Token ${widget.key}`;
|
||||
} else {
|
||||
headers["X-API-Key"] = `${widget.key}`;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ const components = {
|
|||
tautulli: dynamic(() => import("./tautulli/component")),
|
||||
traefik: dynamic(() => import("./traefik/component")),
|
||||
transmission: dynamic(() => import("./transmission/component")),
|
||||
tubearchivist: dynamic(() => import("./tubearchivist/component")),
|
||||
truenas: dynamic(() => import("./truenas/component")),
|
||||
unifi: dynamic(() => import("./unifi/component")),
|
||||
watchtower: dynamic(() => import("./watchtower/component")),
|
||||
};
|
||||
|
|
|
@ -171,7 +171,7 @@ export default function Component({ service }) {
|
|||
});
|
||||
}
|
||||
|
||||
if (sessionsError) {
|
||||
if (sessionsError || sessionsData?.error) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
|
|
65
src/widgets/truenas/component.jsx
Normal file
65
src/widgets/truenas/component.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const processUptime = uptime => {
|
||||
|
||||
const seconds = uptime.toFixed(0);
|
||||
|
||||
const levels = [
|
||||
[Math.floor(seconds / 31536000), 'year'],
|
||||
[Math.floor((seconds % 31536000) / 2592000), 'month'],
|
||||
[Math.floor(((seconds % 31536000) % 2592000) / 86400), 'day'],
|
||||
[Math.floor(((seconds % 31536000) % 86400) / 3600), 'hour'],
|
||||
[Math.floor((((seconds % 31536000) % 86400) % 3600) / 60), 'minute'],
|
||||
[(((seconds % 31536000) % 86400) % 3600) % 60, 'second'],
|
||||
];
|
||||
|
||||
for (let i = 0; i< levels.length; i += 1) {
|
||||
const level = levels[i];
|
||||
if (level[0] > 0){
|
||||
return {
|
||||
value: level[0],
|
||||
unit: level[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: 0,
|
||||
unit: 'second'
|
||||
};
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts");
|
||||
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
|
||||
|
||||
if (alertError || statusError) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!alertData || !statusData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="truenas.load" />
|
||||
<Block label="truenas.uptime" />
|
||||
<Block label="truenas.alerts" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="truenas.load" value={t("common.number", { value: statusData.loadavg[0] })} />
|
||||
<Block label="truenas.uptime" value={t('truenas.time', processUptime(statusData.uptime_seconds))} />
|
||||
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
21
src/widgets/truenas/widget.js
Normal file
21
src/widgets/truenas/widget.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { jsonArrayFilter } from "utils/proxy/api-helpers";
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v2.0/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
alerts: {
|
||||
endpoint: "alert/list",
|
||||
map: (data) => ({
|
||||
pending: jsonArrayFilter(data, (item) => item?.dismissed === false).length,
|
||||
}),
|
||||
},
|
||||
status: {
|
||||
endpoint: "system/info",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
40
src/widgets/tubearchivist/component.jsx
Normal file
40
src/widgets/tubearchivist/component.jsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: downloadsData, error: downloadsError } = useWidgetAPI(widget, "downloads");
|
||||
const { data: videosData, error: videosError } = useWidgetAPI(widget, "videos");
|
||||
const { data: channelsData, error: channelsError } = useWidgetAPI(widget, "channels");
|
||||
const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists");
|
||||
|
||||
if (downloadsError || videosError || channelsError || playlistsError) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!downloadsData || !videosData || !channelsData || !playlistsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="tubearchivist.downloads" />
|
||||
<Block label="tubearchivist.videos" />
|
||||
<Block label="tubearchivist.channels" />
|
||||
<Block label="tubearchivist.playlists" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="tubearchivist.downloads" value={t("common.number", { value: downloadsData?.paginate?.total_hits })} />
|
||||
<Block label="tubearchivist.videos" value={t("common.number", { value: videosData?.paginate?.total_hits })} />
|
||||
<Block label="tubearchivist.channels" value={t("common.number", { value: channelsData?.paginate?.total_hits })} />
|
||||
<Block label="tubearchivist.playlists" value={t("common.number", { value: playlistsData?.paginate?.total_hits })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
23
src/widgets/tubearchivist/widget.js
Normal file
23
src/widgets/tubearchivist/widget.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
downloads: {
|
||||
endpoint: "download",
|
||||
},
|
||||
videos: {
|
||||
endpoint: "video",
|
||||
},
|
||||
channels: {
|
||||
endpoint: "channel",
|
||||
},
|
||||
playlists: {
|
||||
endpoint: "playlist",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -32,6 +32,8 @@ import strelaysrv from "./strelaysrv/widget";
|
|||
import tautulli from "./tautulli/widget";
|
||||
import traefik from "./traefik/widget";
|
||||
import transmission from "./transmission/widget";
|
||||
import tubearchivist from "./tubearchivist/widget";
|
||||
import truenas from "./truenas/widget";
|
||||
import unifi from "./unifi/widget";
|
||||
import watchtower from './watchtower/widget'
|
||||
|
||||
|
@ -71,6 +73,8 @@ const widgets = {
|
|||
tautulli,
|
||||
traefik,
|
||||
transmission,
|
||||
tubearchivist,
|
||||
truenas,
|
||||
unifi,
|
||||
unifi_console: unifi,
|
||||
watchtower,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue