feat: add media requests widget (#774)
Co-authored-by: SeDemal <Tagaishi@hotmail.ch> Co-authored-by: SeDemal <demal.sebastien@bluewin.ch> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -30,7 +30,7 @@ export default function ClockWidget({ options }: WidgetComponentProps<"clock">)
|
||||
{dayjs(time).tz(timezone).format(timeFormat)}
|
||||
</Text>
|
||||
{options.showDate && (
|
||||
<Text className="clock-date-text" size="12.5cqmin" lineClamp={1}>
|
||||
<Text className="clock-date-text" size="12.5cqmin" pt="1cqmin" lineClamp={1}>
|
||||
{dayjs(time).tz(timezone).format(dateFormat)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,8 @@ import * as dnsHoleControls from "./dns-hole/controls";
|
||||
import * as dnsHoleSummary from "./dns-hole/summary";
|
||||
import * as iframe from "./iframe";
|
||||
import type { WidgetImportRecord } from "./import";
|
||||
import * as mediaRequestsList from "./media-requests/list";
|
||||
import * as mediaRequestsStats from "./media-requests/stats";
|
||||
import * as mediaServer from "./media-server";
|
||||
import * as notebook from "./notebook";
|
||||
import * as rssFeed from "./rssFeed";
|
||||
@@ -42,6 +44,8 @@ export const widgetImports = {
|
||||
"smartHome-executeAutomation": smartHomeExecuteAutomation,
|
||||
mediaServer,
|
||||
calendar,
|
||||
"mediaRequests-requestList": mediaRequestsList,
|
||||
"mediaRequests-requestStats": mediaRequestsStats,
|
||||
rssFeed,
|
||||
} satisfies WidgetImportRecord;
|
||||
|
||||
@@ -64,3 +68,9 @@ export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
|
||||
loadedComponents.set(kind, newlyLoadedComponent as never);
|
||||
return newlyLoadedComponent;
|
||||
};
|
||||
|
||||
export type inferSupportedIntegrations<TKind extends WidgetKind> = (WidgetImports[TKind]["definition"] extends {
|
||||
supportedIntegrations: string[];
|
||||
}
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: string[])[number];
|
||||
|
||||
230
packages/widgets/src/media-requests/list/component.tsx
Normal file
230
packages/widgets/src/media-requests/list/component.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Avatar,
|
||||
Badge,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { MediaAvailability, MediaRequestStatus } from "@homarr/integrations/types";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
|
||||
export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
options,
|
||||
serverData,
|
||||
itemId,
|
||||
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const isQueryEnabled = Boolean(itemId);
|
||||
const { data: mediaRequests, isError: _isError } = clientApi.widget.mediaRequests.getLatestRequests.useQuery(
|
||||
{
|
||||
integrationIds,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
itemId: itemId!,
|
||||
},
|
||||
{
|
||||
initialData: !serverData ? undefined : serverData.initialData,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
enabled: integrationIds.length > 0 && isQueryEnabled,
|
||||
},
|
||||
);
|
||||
|
||||
const sortedMediaRequests = useMemo(
|
||||
() =>
|
||||
mediaRequests
|
||||
?.filter((group) => group != null)
|
||||
.flatMap((group) => group.data)
|
||||
.flatMap(({ medias, integration }) => medias.map((media) => ({ ...media, integrationId: integration.id })))
|
||||
.sort(({ status: statusA }, { status: statusB }) => {
|
||||
if (statusA === MediaRequestStatus.PendingApproval) {
|
||||
return -1;
|
||||
}
|
||||
if (statusB === MediaRequestStatus.PendingApproval) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}) ?? [],
|
||||
[mediaRequests, integrationIds],
|
||||
);
|
||||
|
||||
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
|
||||
|
||||
if (integrationIds.length === 0) return <Center h="100%">{tCommon("errors.noIntegration")}</Center>;
|
||||
|
||||
if (sortedMediaRequests.length === 0) return <Center h="100%">{tCommon("errors.noData")}</Center>;
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="mediaRequests-list-scrollArea"
|
||||
scrollbarSize="2cqmin"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack className="mediaRequests-list-list" gap="2cqmin" p="2cqmin">
|
||||
{sortedMediaRequests.map((mediaRequest) => (
|
||||
<Card
|
||||
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`}
|
||||
key={mediaRequest.id}
|
||||
h="20cqmin"
|
||||
radius="2cqmin"
|
||||
p="2cqmin"
|
||||
withBorder
|
||||
>
|
||||
<Image
|
||||
className="mediaRequests-list-item-background"
|
||||
src={mediaRequest.backdropImageUrl}
|
||||
pos="absolute"
|
||||
w="100%"
|
||||
h="100%"
|
||||
opacity={0.2}
|
||||
top={0}
|
||||
left={0}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<Group
|
||||
className="mediaRequests-list-item-contents"
|
||||
h="100%"
|
||||
style={{ zIndex: 1 }}
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
gap={0}
|
||||
>
|
||||
<Group className="mediaRequests-list-item-left-side" h="100%" gap="4cqmin" wrap="nowrap" flex={1}>
|
||||
<Image
|
||||
className="mediaRequests-list-item-poster"
|
||||
src={mediaRequest.posterImagePath}
|
||||
h="100%"
|
||||
w="10cqmin"
|
||||
radius="1cqmin"
|
||||
/>
|
||||
<Stack className="mediaRequests-list-item-media-infos" gap="1cqmin">
|
||||
<Group className="mediaRequests-list-item-info-first-line" gap="2cqmin" wrap="nowrap">
|
||||
<Text className="mediaRequests-list-item-media-year" size="3.5cqmin" pt="0.75cqmin">
|
||||
{mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
|
||||
</Text>
|
||||
<Badge
|
||||
className="mediaRequests-list-item-media-status"
|
||||
color={getAvailabilityProperties(mediaRequest.availability, t).color}
|
||||
variant="light"
|
||||
fz="3.5cqmin"
|
||||
lh="4cqmin"
|
||||
size="5cqmin"
|
||||
pt="0.75cqmin"
|
||||
px="2cqmin"
|
||||
>
|
||||
{getAvailabilityProperties(mediaRequest.availability, t).label}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Anchor
|
||||
className="mediaRequests-list-item-info-second-line mediaRequests-list-item-media-title"
|
||||
href={mediaRequest.href}
|
||||
c="var(--mantine-color-text)"
|
||||
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
||||
fz="5cqmin"
|
||||
lineClamp={1}
|
||||
>
|
||||
{mediaRequest.name || "unknown"}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Stack className="mediaRequests-list-item-right-side" gap="1cqmin" align="end">
|
||||
<Group className="mediaRequests-list-item-request-user" gap="2cqmin" wrap="nowrap">
|
||||
<Avatar
|
||||
className="mediaRequests-list-item-request-user-avatar"
|
||||
src={mediaRequest.requestedBy?.avatar}
|
||||
size="6cqmin"
|
||||
/>
|
||||
<Anchor
|
||||
className="mediaRequests-list-item-request-user-name"
|
||||
href={mediaRequest.requestedBy?.link}
|
||||
c="var(--mantine-color-text)"
|
||||
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
||||
fz="5cqmin"
|
||||
lineClamp={1}
|
||||
style={{ wordBreak: "break-all" }}
|
||||
>
|
||||
{(mediaRequest.requestedBy?.displayName ?? "") || "unknown"}
|
||||
</Anchor>
|
||||
</Group>
|
||||
{mediaRequest.status === MediaRequestStatus.PendingApproval && (
|
||||
<Group className="mediaRequests-list-item-pending-buttons" gap="2cqmin">
|
||||
<Tooltip label={t("pending.approve")}>
|
||||
<ActionIcon
|
||||
className="mediaRequests-list-item-pending-button-approve"
|
||||
variant="light"
|
||||
color="green"
|
||||
size="5cqmin"
|
||||
onClick={() => {
|
||||
mutateRequestAnswer({
|
||||
integrationId: mediaRequest.integrationId,
|
||||
requestId: mediaRequest.id,
|
||||
answer: "approve",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconThumbUp size="4cqmin" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("pending.decline")}>
|
||||
<ActionIcon
|
||||
className="mediaRequests-list-item-pending-button-decline"
|
||||
variant="light"
|
||||
color="red"
|
||||
size="5cqmin"
|
||||
onClick={() => {
|
||||
mutateRequestAnswer({
|
||||
integrationId: mediaRequest.integrationId,
|
||||
requestId: mediaRequest.id,
|
||||
answer: "decline",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconThumbDown size="4cqmin" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
function getAvailabilityProperties(
|
||||
mediaRequestAvailability: MediaAvailability,
|
||||
t: ScopedTranslationFunction<"widget.mediaRequests-requestList">,
|
||||
) {
|
||||
switch (mediaRequestAvailability) {
|
||||
case MediaAvailability.Available:
|
||||
return { color: "green", label: t("availability.available") };
|
||||
case MediaAvailability.PartiallyAvailable:
|
||||
return { color: "yellow", label: t("availability.partiallyAvailable") };
|
||||
case MediaAvailability.Pending:
|
||||
return { color: "violet", label: t("availability.pending") };
|
||||
case MediaAvailability.Processing:
|
||||
return { color: "blue", label: t("availability.processing") };
|
||||
default:
|
||||
return { color: "red", label: t("availability.unknown") };
|
||||
}
|
||||
}
|
||||
16
packages/widgets/src/media-requests/list/index.ts
Normal file
16
packages/widgets/src/media-requests/list/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IconZoomQuestion } from "@tabler/icons-react";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaRequests-requestList", {
|
||||
icon: IconZoomQuestion,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
linksTargetNewTab: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
})),
|
||||
supportedIntegrations: ["overseerr", "jellyseerr"],
|
||||
})
|
||||
.withServerData(() => import("./serverData"))
|
||||
.withDynamicImport(() => import("./component"));
|
||||
22
packages/widgets/src/media-requests/list/serverData.ts
Normal file
22
packages/widgets/src/media-requests/list/serverData.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
|
||||
import type { WidgetProps } from "../../definition";
|
||||
|
||||
export default async function getServerDataAsync({ integrationIds, itemId }: WidgetProps<"mediaRequests-requestList">) {
|
||||
if (integrationIds.length === 0 || !itemId) {
|
||||
return {
|
||||
initialData: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const requests = await api.widget.mediaRequests.getLatestRequests({
|
||||
integrationIds,
|
||||
itemId,
|
||||
});
|
||||
|
||||
return {
|
||||
initialData: requests.filter((group) => group != null),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.gridElement:not(:nth-child(8n)) {
|
||||
border-right: 0.5cqmin solid var(--app-shell-border-color);
|
||||
}
|
||||
|
||||
.gridElement:not(:nth-last-child(-n + 8)) {
|
||||
border-bottom: 0.5cqmin solid var(--app-shell-border-color);
|
||||
}
|
||||
220
packages/widgets/src/media-requests/stats/component.tsx
Normal file
220
packages/widgets/src/media-requests/stats/component.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useMemo } from "react";
|
||||
import { ActionIcon, Avatar, Card, Center, Grid, Group, Space, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import type { Icon } from "@tabler/icons-react";
|
||||
import {
|
||||
IconDeviceTv,
|
||||
IconExternalLink,
|
||||
IconHourglass,
|
||||
IconLoaderQuarter,
|
||||
IconMovie,
|
||||
IconPlayerPlay,
|
||||
IconReceipt,
|
||||
IconThumbDown,
|
||||
IconThumbUp,
|
||||
} from "@tabler/icons-react";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { RequestStats } from "../../../../integrations/src/interfaces/media-requests/media-request";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
serverData,
|
||||
itemId,
|
||||
}: WidgetComponentProps<"mediaRequests-requestStats">) {
|
||||
const t = useScopedI18n("widget.mediaRequests-requestStats");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const isQueryEnabled = Boolean(itemId);
|
||||
const { data: requestStats, isError: _isError } = clientApi.widget.mediaRequests.getStats.useQuery(
|
||||
{
|
||||
integrationIds,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
itemId: itemId!,
|
||||
},
|
||||
{
|
||||
initialData: !serverData ? undefined : serverData.initialData,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
enabled: integrationIds.length > 0 && isQueryEnabled,
|
||||
},
|
||||
);
|
||||
|
||||
const { width, height, ref } = useElementSize();
|
||||
|
||||
const baseData = useMemo(
|
||||
() => requestStats?.filter((group) => group != null).flatMap((group) => group.data) ?? [],
|
||||
[requestStats],
|
||||
);
|
||||
|
||||
const stats = useMemo(() => baseData.flatMap(({ stats }) => stats), [baseData]);
|
||||
const users = useMemo(
|
||||
() =>
|
||||
baseData
|
||||
.flatMap(({ integration, users }) =>
|
||||
users.flatMap((user) => ({ ...user, appKind: integration.kind, appName: integration.name })),
|
||||
)
|
||||
.sort(({ requestCount: countA }, { requestCount: countB }) => countB - countA),
|
||||
[baseData],
|
||||
);
|
||||
|
||||
if (integrationIds.length === 0)
|
||||
return (
|
||||
<Center ref={ref} h="100%">
|
||||
{tCommon("errors.noIntegration")}
|
||||
</Center>
|
||||
);
|
||||
|
||||
if (users.length === 0 || stats.length === 0)
|
||||
return (
|
||||
<Center ref={ref} h="100%">
|
||||
{tCommon("errors.noData")}
|
||||
</Center>
|
||||
);
|
||||
|
||||
//Add processing and available
|
||||
const data = [
|
||||
{
|
||||
name: "approved",
|
||||
icon: IconThumbUp,
|
||||
number: stats.reduce((count, { approved }) => count + approved, 0),
|
||||
},
|
||||
{
|
||||
name: "pending",
|
||||
icon: IconHourglass,
|
||||
number: stats.reduce((count, { pending }) => count + pending, 0),
|
||||
},
|
||||
{
|
||||
name: "processing",
|
||||
icon: IconLoaderQuarter,
|
||||
number: stats.reduce((count, { processing }) => count + processing, 0),
|
||||
},
|
||||
{
|
||||
name: "declined",
|
||||
icon: IconThumbDown,
|
||||
number: stats.reduce((count, { declined }) => count + declined, 0),
|
||||
},
|
||||
{
|
||||
name: "available",
|
||||
icon: IconPlayerPlay,
|
||||
number: stats.reduce((count, { available }) => count + available, 0),
|
||||
},
|
||||
{
|
||||
name: "tv",
|
||||
icon: IconDeviceTv,
|
||||
number: stats.reduce((count, { tv }) => count + tv, 0),
|
||||
},
|
||||
{
|
||||
name: "movie",
|
||||
icon: IconMovie,
|
||||
number: stats.reduce((count, { movie }) => count + movie, 0),
|
||||
},
|
||||
{
|
||||
name: "total",
|
||||
icon: IconReceipt,
|
||||
number: stats.reduce((count, { total }) => count + total, 0),
|
||||
},
|
||||
] satisfies { name: keyof RequestStats; icon: Icon; number: number }[];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="mediaRequests-stats-layout"
|
||||
display="flex"
|
||||
h="100%"
|
||||
gap="2cqmin"
|
||||
p="2cqmin"
|
||||
align="center"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Text className="mediaRequests-stats-stats-title" size="6.5cqmin">
|
||||
{t("titles.stats.main")}
|
||||
</Text>
|
||||
<Grid className="mediaRequests-stats-stats-grid" gutter={0} w="100%">
|
||||
{data.map((stat) => (
|
||||
<Grid.Col
|
||||
className={combineClasses(
|
||||
classes.gridElement,
|
||||
"mediaRequests-stats-stat-wrapper",
|
||||
`mediaRequests-stats-stat-${stat.name}`,
|
||||
)}
|
||||
key={stat.name}
|
||||
span={3}
|
||||
>
|
||||
<Tooltip label={t(`titles.stats.${stat.name}`)}>
|
||||
<Stack className="mediaRequests-stats-stat-stack" align="center" gap="2cqmin" p="2cqmin">
|
||||
<stat.icon className="mediaRequests-stats-stat-icon" size="7.5cqmin" />
|
||||
<Text className="mediaRequests-stats-stat-value" size="5cqmin">
|
||||
{stat.number}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
<Text className="mediaRequests-stats-users-title" size="6.5cqmin">
|
||||
{t("titles.users.main")}
|
||||
</Text>
|
||||
<Stack
|
||||
className="mediaRequests-stats-users-wrapper"
|
||||
flex={1}
|
||||
w="100%"
|
||||
ref={ref}
|
||||
display="flex"
|
||||
gap="2cqmin"
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
{users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => (
|
||||
<Card
|
||||
className={combineClasses(
|
||||
"mediaRequests-stats-users-user-wrapper",
|
||||
`mediaRequests-stats-users-user-${user.id}`,
|
||||
)}
|
||||
key={user.id}
|
||||
withBorder
|
||||
p="2cqmin"
|
||||
flex={1}
|
||||
mah="38.5cqmin"
|
||||
radius="2.5cqmin"
|
||||
>
|
||||
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="2cqmin" display="flex">
|
||||
<Tooltip label={user.appName}>
|
||||
<Avatar
|
||||
className="mediaRequests-stats-users-user-avatar"
|
||||
size="12.5cqmin"
|
||||
src={user.avatar}
|
||||
bd={`0.5cqmin solid ${user.appKind === "overseerr" ? "#ECB000" : "#6677CC"}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Stack className="mediaRequests-stats-users-user-infos" gap="2cqmin">
|
||||
<Text className="mediaRequests-stats-users-user-userName" size="6cqmin">
|
||||
{user.displayName}
|
||||
</Text>
|
||||
<Text className="mediaRequests-stats-users-user-request-count" size="4cqmin">
|
||||
{tCommon("rtl", { value: t("titles.users.requests"), symbol: tCommon("symbols.colon") }) +
|
||||
user.requestCount}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Space flex={1} />
|
||||
<ActionIcon
|
||||
className="mediaRequests-stats-users-user-link-button"
|
||||
variant="light"
|
||||
color="var(--mantine-color-text)"
|
||||
size="10cqmin"
|
||||
component="a"
|
||||
href={user.link}
|
||||
>
|
||||
<IconExternalLink className="mediaRequests-stats-users-user-link-icon" size="7.5cqmin" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
11
packages/widgets/src/media-requests/stats/index.ts
Normal file
11
packages/widgets/src/media-requests/stats/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IconChartBar } from "@tabler/icons-react";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
|
||||
export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaRequests-requestStats", {
|
||||
icon: IconChartBar,
|
||||
options: {},
|
||||
supportedIntegrations: ["overseerr", "jellyseerr"],
|
||||
})
|
||||
.withServerData(() => import("./serverData"))
|
||||
.withDynamicImport(() => import("./component"));
|
||||
25
packages/widgets/src/media-requests/stats/serverData.ts
Normal file
25
packages/widgets/src/media-requests/stats/serverData.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
|
||||
import type { WidgetProps } from "../../definition";
|
||||
|
||||
export default async function getServerDataAsync({
|
||||
integrationIds,
|
||||
itemId,
|
||||
}: WidgetProps<"mediaRequests-requestStats">) {
|
||||
if (integrationIds.length === 0 || !itemId) {
|
||||
return {
|
||||
initialData: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await api.widget.mediaRequests.getStats({
|
||||
integrationIds,
|
||||
itemId,
|
||||
});
|
||||
|
||||
return {
|
||||
initialData: stats.filter((group) => group != null),
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { WeatherDescription, WeatherIcon } from "./icon";
|
||||
|
||||
export default function WeatherWidget({ options }: WidgetComponentProps<"weather">) {
|
||||
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
|
||||
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(
|
||||
{
|
||||
latitude: options.location.latitude,
|
||||
@@ -23,7 +23,14 @@ export default function WeatherWidget({ options }: WidgetComponentProps<"weather
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack align="center" justify="center" gap="0" w="100%" h="100%">
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="0"
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
{options.hasForecast ? (
|
||||
<WeeklyForecast weather={weather} options={options} />
|
||||
) : (
|
||||
@@ -51,15 +58,15 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
|
||||
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
<Text fz="20cqmin">{getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)}</Text>
|
||||
<Text fz="17.5cqmin">{getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)}</Text>
|
||||
</Group>
|
||||
<Space h="1cqmin" />
|
||||
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="1cqmin">
|
||||
<IconArrowUpRight size="12.5cqmin" />
|
||||
<Text fz="12.5cqmin">{getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit)}</Text>
|
||||
<Text fz="10cqmin">{getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit)}</Text>
|
||||
<Space w="2.5cqmin" />
|
||||
<IconArrowDownRight size="12.5cqmin" />
|
||||
<Text fz="12.5cqmin">{getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit)}</Text>
|
||||
<Text fz="10cqmin">{getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit)}</Text>
|
||||
</Group>
|
||||
{options.showCity && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user