mirror of
https://github.com/ajnart/homarr.git
synced 2026-05-13 01:26:43 +02:00
213 lines
7.0 KiB
TypeScript
213 lines
7.0 KiB
TypeScript
import { useMemo } from "react";
|
|
import { ActionIcon, Avatar, Card, 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 type { RequestStats } from "@homarr/integrations/types";
|
|
import { useScopedI18n } from "@homarr/translation/client";
|
|
|
|
import type { WidgetComponentProps } from "../../definition";
|
|
import { NoIntegrationSelectedError } from "../../errors";
|
|
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
|
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) throw new NoIntegrationSelectedError();
|
|
|
|
if (users.length === 0 || stats.length === 0) throw new NoIntegrationDataError();
|
|
|
|
//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>
|
|
);
|
|
}
|