diff --git a/src/components/services/widget/block-list.jsx b/src/components/services/widget/block-list.jsx new file mode 100644 index 000000000..138576bc0 --- /dev/null +++ b/src/components/services/widget/block-list.jsx @@ -0,0 +1,31 @@ +import { useTranslation } from "next-i18next"; +import { useCallback, useState } from 'react'; +import classNames from "classnames"; + +import ResolvedIcon from '../../resolvedicon'; + + +export default function BlockList({ label, children, childHeight }) { + const { t } = useTranslation(); + const [isOpen, setOpen] = useState(false); + + const changeState = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]); + + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index f4d8c13ee..4b8a06ca5 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -15,7 +15,9 @@ export default function Container({ error = false, children, service }) { return } - let visibleChildren = children; + const childrenArray = Array.isArray(children) ? children : [children]; + + let visibleChildren = childrenArray; const fields = service?.widget?.fields; const type = service?.widget?.type; if (fields && type) { @@ -24,7 +26,7 @@ export default function Container({ error = false, children, service }) { // fields: [ "resources.cpu", "resources.mem", "field"] // or even // fields: [ "resources.cpu", "widget_type.field" ] - visibleChildren = children?.filter(child => fields.some(field => { + visibleChildren = childrenArray?.filter(child => fields.some(field => { let fullField = field; if (!field.includes(".")) { fullField = `${type}.${field}`; diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx index f8a932eaf..4e53ef9af 100644 --- a/src/widgets/radarr/component.jsx +++ b/src/widgets/radarr/component.jsx @@ -1,7 +1,10 @@ import { useTranslation } from "next-i18next"; +import { useCallback } from 'react'; +import classNames from 'classnames'; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; +import BlockList from "components/services/widget/block-list"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { @@ -10,29 +13,75 @@ export default function Component({ service }) { const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (moviesError || queuedError) { - const finalError = moviesError ?? queuedError; + // information taken from the Radarr docs: https://radarr.video/docs/api/ + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (moviesError || queuedError || queueDetailsError) { + const finalError = moviesError ?? queuedError ?? queueDetailsError; return ; } - if (!moviesData || !queuedData) { + if (!moviesData || !queuedData || !queueDetailsData) { return ( - - - - - - + <> + + + + + + + + + + ); } return ( - - - - - - + <> + + + + + + + + + {Array.isArray(queueDetailsData) ? queueDetailsData.map((queueEntry) => ( +
+
+
{moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title}
+
{formatDownloadState(queueEntry.trackedDownloadState)}
+
+
+
+
+
+
{queueEntry.timeLeft}
+
+
+ )) : undefined} + + + ); } diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js index 780542195..0f53ab14e 100644 --- a/src/widgets/radarr/widget.js +++ b/src/widgets/radarr/widget.js @@ -1,5 +1,5 @@ import genericProxyHandler from "utils/proxy/handlers/generic"; -import { jsonArrayFilter } from "utils/proxy/api-helpers"; +import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v3/{endpoint}?apikey={key}", @@ -12,6 +12,7 @@ const widget = { wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length, have: jsonArrayFilter(data, (item) => item.hasFile).length, missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length, + all: asJson(data), }), }, "queue/status": { @@ -20,6 +21,36 @@ const widget = { "totalCount" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + movieId: entry.movieId + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + }, }, }; diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx index adbb8c305..9a0f98ae4 100644 --- a/src/widgets/sonarr/component.jsx +++ b/src/widgets/sonarr/component.jsx @@ -1,8 +1,11 @@ import { useTranslation } from "next-i18next"; +import classNames from 'classnames'; +import { useCallback } from 'react'; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +import BlockList from 'components/services/widget/block-list'; export default function Component({ service }) { const { t } = useTranslation(); @@ -11,27 +14,73 @@ export default function Component({ service }) { const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue"); const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (wantedError || queuedError || seriesError) { - const finalError = wantedError ?? queuedError ?? seriesError; + // information taken from the Sonarr docs: https://sonarr.tv/docs/api/ + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (wantedError || queuedError || seriesError || queueDetailsError) { + const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError; return ; } - if (!wantedData || !queuedData || !seriesData) { + if (!wantedData || !queuedData || !seriesData || !queueDetailsData) { return ( - - - - - + <> + + + + + + + + + ); } return ( - - - - - + <> + + + + + + + + {Array.isArray(queueDetailsData) ? queueDetailsData.map((queueEntry) => ( +
+
+
{seriesData.find((entry) => entry.id === queueEntry.seriesId).title} • {queueEntry.episodeTitle}
+
{formatDownloadState(queueEntry.trackedDownloadState)}
+
+
+
+
+
+
{queueEntry.timeLeft}
+
+
+ )) : undefined} + + + ); } diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js index c14139757..80afdb991 100644 --- a/src/widgets/sonarr/widget.js +++ b/src/widgets/sonarr/widget.js @@ -8,9 +8,10 @@ const widget = { mappings: { series: { endpoint: "series", - map: (data) => ({ - total: asJson(data).length, - }) + map: (data) => asJson(data).map((entry) => ({ + title: entry.title, + id: entry.id + })) }, queue: { endpoint: "queue", @@ -24,6 +25,38 @@ const widget = { "totalRecords" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + seriesId: entry.seriesId, + episodeTitle: entry.episode?.title, + episodeId: entry.episodeId + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + } }, }; diff --git a/tailwind.config.js b/tailwind.config.js index b981051b7..96c9e6415 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -29,6 +29,9 @@ module.exports = { '3xl': '1800px', // => @media (min-width: 1800px) { ... } }, + transitionProperty: { + 'height': 'height' + }, }, }, plugins: [tailwindForms, tailwindScrollbars],