mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-06 21:57:48 +01:00
Compare commits
6 Commits
v1.5.0
...
06cf76d724
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cf76d724 | ||
|
|
7aeda56af4 | ||
|
|
2058b7fcae | ||
|
|
1e06e93e47 | ||
|
|
8f756d4084 | ||
|
|
02089a35ee |
@@ -178,3 +178,32 @@ See [ClusterRole and ClusterRoleBinding](../installation/k8s.md#clusterrole-and-
|
|||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
||||||
|
|
||||||
|
## Adding extra configuration files
|
||||||
|
|
||||||
|
Some Homepage features (for example, [Proxmox](../configs/proxmox.md)) require additional configuration files such as `proxmox.yaml`.
|
||||||
|
When running Homepage on Kubernetes, these files must be provided via a `ConfigMap` and mounted into the container at `/app/config`.
|
||||||
|
|
||||||
|
### ConfigMap example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homepage
|
||||||
|
data:
|
||||||
|
proxmox.yaml: |
|
||||||
|
pve:
|
||||||
|
url: https://proxmox.host.or.ip:8006
|
||||||
|
token: username@pam!Token ID
|
||||||
|
secret: secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount the file into `/app/config` by updating the `Deployment`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/config/proxmox.yaml
|
||||||
|
name: homepage-config
|
||||||
|
subPath: proxmox.yaml
|
||||||
|
```
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ description: Proxmox Configuration
|
|||||||
---
|
---
|
||||||
|
|
||||||
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
||||||
You can configure multiple nodes - be sure to use the exact `proxmoxNode` identifier!
|
To configure multiple nodes, ensure the key name in the `proxmox.yaml` matches the `proxmoxNode` field used in your service configuration.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
pve:
|
pve: # must match your actual Proxmox node name
|
||||||
url: https://proxmox.host.or.ip:8006
|
url: https://proxmox.host.or.ip:8006
|
||||||
token: username@pam!Token ID
|
token: username@pam!Token ID
|
||||||
secret: secret
|
secret: secret
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ widget:
|
|||||||
username: username
|
username: username
|
||||||
password: password
|
password: password
|
||||||
enableLeechProgress: true # optional, defaults to false
|
enableLeechProgress: true # optional, defaults to false
|
||||||
|
enableLeechSize: true # optional, defaults to false
|
||||||
```
|
```
|
||||||
|
|||||||
28
docs/widgets/services/yourspotify.md
Normal file
28
docs/widgets/services/yourspotify.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Your Spotify
|
||||||
|
description: Your Spotify Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Your Spotify](https://github.com/Yooooomi/your_spotify).
|
||||||
|
|
||||||
|
Find your API key under `Settings > Account > Public token`, click `Generate` if not yet generated, copy key after
|
||||||
|
`?token=`.
|
||||||
|
|
||||||
|
Allowed fields: `["songs", "time", "artists"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: yourspotify
|
||||||
|
url: http://your-spotify-server.host.or.ip # if using lsio image, add /api/
|
||||||
|
key: apikeyapikeyapikeyapikeyapikey
|
||||||
|
interval: month # optional, defaults to week
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Interval
|
||||||
|
|
||||||
|
Allowed values for `interval`: `day`, `week`, `month`, `year`, `all`.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
`interval` is different from predefined intervals you see in `Your Spotify`'s UI.
|
||||||
|
For example, `This week` in UI means _from the start of this week_, here `week` means _past 7 days_.
|
||||||
@@ -176,6 +176,7 @@ nav:
|
|||||||
- widgets/services/wgeasy.md
|
- widgets/services/wgeasy.md
|
||||||
- widgets/services/whatsupdocker.md
|
- widgets/services/whatsupdocker.md
|
||||||
- widgets/services/xteve.md
|
- widgets/services/xteve.md
|
||||||
|
- widgets/services/yourspotify.md
|
||||||
- widgets/services/zabbix.md
|
- widgets/services/zabbix.md
|
||||||
- "Information Widgets":
|
- "Information Widgets":
|
||||||
- widgets/info/index.md
|
- widgets/info/index.md
|
||||||
|
|||||||
@@ -1114,5 +1114,10 @@
|
|||||||
"num_success_latest": "Succeeding",
|
"num_success_latest": "Succeeding",
|
||||||
"num_failure_latest": "Failing",
|
"num_failure_latest": "Failing",
|
||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Bytes Added"
|
||||||
|
},
|
||||||
|
"yourspotify": {
|
||||||
|
"songs": "Songs",
|
||||||
|
"time": "Time",
|
||||||
|
"artists": "Artists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function QueueEntry({ title, activity, timeLeft, progress }) {
|
export default function QueueEntry({ title, activity, timeLeft, progress, size }) {
|
||||||
return (
|
return (
|
||||||
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
|
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
|
||||||
<div
|
<div
|
||||||
@@ -11,6 +11,7 @@ export default function QueueEntry({ title, activity, timeLeft, progress }) {
|
|||||||
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
|
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
{size && `${size} - `}
|
||||||
{timeLeft ? `${activity} - ${timeLeft}` : activity}
|
{timeLeft ? `${activity} - ${timeLeft}` : activity}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ function Home({ initialSettings }) {
|
|||||||
)}
|
)}
|
||||||
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||||
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||||
|
<meta name="color-scheme" content="dark light"></meta>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Script src="/api/config/custom.js" />
|
<Script src="/api/config/custom.js" />
|
||||||
@@ -539,38 +540,48 @@ export default function Wrapper({ initialSettings, fallback }) {
|
|||||||
html.classList.add(desiredThemeClass);
|
html.classList.add(desiredThemeClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any previously applied inline styles
|
if (backgroundImage) {
|
||||||
body.style.backgroundImage = "";
|
const safeBackgroundImage = backgroundImage.replace(/'/g, "\\'");
|
||||||
body.style.backgroundColor = "";
|
body.style.backgroundImage = `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${safeBackgroundImage}')`;
|
||||||
body.style.backgroundAttachment = "";
|
body.style.backgroundSize = "cover";
|
||||||
|
body.style.backgroundPosition = "center";
|
||||||
|
body.style.backgroundAttachment = "fixed";
|
||||||
|
body.style.backgroundRepeat = "no-repeat";
|
||||||
|
body.style.backgroundColor = "";
|
||||||
|
} else {
|
||||||
|
body.style.backgroundImage = "none";
|
||||||
|
body.style.backgroundColor = "rgb(var(--bg-color))";
|
||||||
|
body.style.backgroundSize = "";
|
||||||
|
body.style.backgroundPosition = "";
|
||||||
|
body.style.backgroundAttachment = "";
|
||||||
|
body.style.backgroundRepeat = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
body.style.backgroundImage = "";
|
||||||
|
body.style.backgroundColor = "";
|
||||||
|
body.style.backgroundSize = "";
|
||||||
|
body.style.backgroundPosition = "";
|
||||||
|
body.style.backgroundAttachment = "";
|
||||||
|
body.style.backgroundRepeat = "";
|
||||||
|
};
|
||||||
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
|
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div id="page_wrapper" className="relative min-h-screen">
|
||||||
{backgroundImage && (
|
<div
|
||||||
<div
|
id="inner_wrapper"
|
||||||
id="background"
|
tabIndex="-1"
|
||||||
aria-hidden="true"
|
className={classNames(
|
||||||
style={{
|
"w-full min-h-screen overflow-auto",
|
||||||
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
|
backgroundBlur &&
|
||||||
}}
|
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
|
||||||
/>
|
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
||||||
)}
|
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
||||||
<div id="page_wrapper" className="relative h-full">
|
)}
|
||||||
<div
|
>
|
||||||
id="inner_wrapper"
|
<Index initialSettings={initialSettings} fallback={fallback} />
|
||||||
tabIndex="-1"
|
|
||||||
className={classNames(
|
|
||||||
"w-full h-full overflow-auto",
|
|
||||||
backgroundBlur &&
|
|
||||||
`backdrop-blur${initialSettings.background.blur?.length ? `-${initialSettings.background.blur}` : ""}`,
|
|
||||||
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
|
||||||
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Index initialSettings={initialSettings} fallback={fallback} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,18 +30,6 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: rgb(var(--bg-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
#background {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-attachment: scroll;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
|
|
||||||
// deluge, qbittorrent
|
// deluge, qbittorrent
|
||||||
enableLeechProgress,
|
enableLeechProgress,
|
||||||
|
enableLeechSize,
|
||||||
|
|
||||||
// diskstation
|
// diskstation
|
||||||
volume,
|
volume,
|
||||||
@@ -408,6 +409,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
// wgeasy
|
// wgeasy
|
||||||
threshold,
|
threshold,
|
||||||
|
|
||||||
|
// yourspotify
|
||||||
|
interval,
|
||||||
|
|
||||||
// technitium
|
// technitium
|
||||||
range,
|
range,
|
||||||
|
|
||||||
@@ -490,6 +494,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
}
|
}
|
||||||
if (["deluge", "qbittorrent"].includes(type)) {
|
if (["deluge", "qbittorrent"].includes(type)) {
|
||||||
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
|
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
|
||||||
|
if (enableLeechSize !== undefined) widget.enableLeechSize = JSON.parse(enableLeechSize);
|
||||||
}
|
}
|
||||||
if (["opnsense", "pfsense"].includes(type)) {
|
if (["opnsense", "pfsense"].includes(type)) {
|
||||||
if (wan) widget.wan = wan;
|
if (wan) widget.wan = wan;
|
||||||
@@ -623,6 +628,11 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (pool3) widget.pool3 = pool3;
|
if (pool3) widget.pool3 = pool3;
|
||||||
if (pool4) widget.pool4 = pool4;
|
if (pool4) widget.pool4 = pool4;
|
||||||
}
|
}
|
||||||
|
if (type === "yourspotify") {
|
||||||
|
if (interval !== undefined) {
|
||||||
|
widget.interval = interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
return widget;
|
return widget;
|
||||||
});
|
});
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ const components = {
|
|||||||
wgeasy: dynamic(() => import("./wgeasy/component")),
|
wgeasy: dynamic(() => import("./wgeasy/component")),
|
||||||
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
|
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
|
||||||
xteve: dynamic(() => import("./xteve/component")),
|
xteve: dynamic(() => import("./xteve/component")),
|
||||||
|
yourspotify: dynamic(() => import("./yourspotify/component")),
|
||||||
zabbix: dynamic(() => import("./zabbix/component")),
|
zabbix: dynamic(() => import("./zabbix/component")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ export default function Component({ service }) {
|
|||||||
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
||||||
title={queueEntry.name}
|
title={queueEntry.name}
|
||||||
activity={queueEntry.state}
|
activity={queueEntry.state}
|
||||||
|
size={
|
||||||
|
widget?.enableLeechSize
|
||||||
|
? t("common.bbytes", { value: queueEntry.size, maximumFractionDigits: 1 })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
key={`${queueEntry.name}-${queueEntry.amount_left}`}
|
key={`${queueEntry.name}-${queueEntry.amount_left}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ import watchtower from "./watchtower/widget";
|
|||||||
import wgeasy from "./wgeasy/widget";
|
import wgeasy from "./wgeasy/widget";
|
||||||
import whatsupdocker from "./whatsupdocker/widget";
|
import whatsupdocker from "./whatsupdocker/widget";
|
||||||
import xteve from "./xteve/widget";
|
import xteve from "./xteve/widget";
|
||||||
|
import yourspotify from "./yourspotify/widget";
|
||||||
import zabbix from "./zabbix/widget";
|
import zabbix from "./zabbix/widget";
|
||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
@@ -291,6 +292,7 @@ const widgets = {
|
|||||||
wgeasy,
|
wgeasy,
|
||||||
whatsupdocker,
|
whatsupdocker,
|
||||||
xteve,
|
xteve,
|
||||||
|
yourspotify,
|
||||||
zabbix,
|
zabbix,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
76
src/widgets/yourspotify/component.jsx
Normal file
76
src/widgets/yourspotify/component.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
function getStartDate(interval) {
|
||||||
|
const d = new Date();
|
||||||
|
switch (interval) {
|
||||||
|
case "day":
|
||||||
|
d.setDate(d.getDate() - 1);
|
||||||
|
break;
|
||||||
|
case "week":
|
||||||
|
d.setDate(d.getDate() - 7);
|
||||||
|
break;
|
||||||
|
case "month":
|
||||||
|
d.setMonth(d.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
case "year":
|
||||||
|
d.setFullYear(d.getFullYear() - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const interval = widget?.interval || "week";
|
||||||
|
|
||||||
|
const date = useMemo(() => {
|
||||||
|
return interval === "all" ? "2006-04-23T00:00:00.000Z" : getStartDate(interval);
|
||||||
|
}, [interval]);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
timeSplit: "all",
|
||||||
|
start: date,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: songsListened, error: songsError } = useWidgetAPI(widget, "songs", params);
|
||||||
|
const { data: timeListened, error: timeError } = useWidgetAPI(widget, "time", params);
|
||||||
|
const { data: artistsListened, error: artistsError } = useWidgetAPI(widget, "artists", params);
|
||||||
|
|
||||||
|
if (songsError || timeError || artistsError) {
|
||||||
|
return <Container service={service} error={songsError ?? timeError ?? artistsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(songsListened) || isNaN(timeListened) || isNaN(artistsListened)) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="yourspotify.songs" />
|
||||||
|
<Block label="yourspotify.time" />
|
||||||
|
<Block label="yourspotify.artists" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="yourspotify.songs" value={t("common.number", { value: songsListened })} />
|
||||||
|
|
||||||
|
<Block
|
||||||
|
label="yourspotify.time"
|
||||||
|
value={t(
|
||||||
|
timeListened > 0 ? "common.duration" : "common.number", // Display 0 if duration is 0
|
||||||
|
{
|
||||||
|
value: timeListened / 1000,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Block label="yourspotify.artists" value={t("common.number", { value: artistsListened })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/widgets/yourspotify/widget.js
Normal file
27
src/widgets/yourspotify/widget.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { asJson } from "utils/proxy/api-helpers";
|
||||||
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/spotify/{endpoint}?token={key}",
|
||||||
|
proxyHandler: genericProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
songs: {
|
||||||
|
endpoint: "songs_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.count || 0,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
endpoint: "time_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.count || 0,
|
||||||
|
},
|
||||||
|
artists: {
|
||||||
|
endpoint: "different_artists_per",
|
||||||
|
params: ["start", "timeSplit"],
|
||||||
|
map: (data) => asJson(data)[0]?.artists?.length || 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
Reference in New Issue
Block a user