Add overseerr widget

This commit is contained in:
Manuel
2023-04-04 22:32:08 +02:00
parent 7cf6fe53fc
commit c1463b3aa6
10 changed files with 410 additions and 0 deletions

View File

@@ -9,6 +9,8 @@ import torrent from './torrent/TorrentTile';
import usenet from './useNet/UseNetTile';
import videoStream from './video/VideoStreamTile';
import weather from './weather/WeatherTile';
import mediaRequestsList from './media-requests/MediaRequestListTile';
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
export default {
calendar,
@@ -22,4 +24,6 @@ export default {
'video-stream': videoStream,
iframe,
'media-server': mediaServer,
'media-requests-list': mediaRequestsList,
'media-requests-stats': mediaRequestsStats,
};

7
src/widgets/loading.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { Center, Loader } from '@mantine/core';
export const WidgetLoading = () => (
<Center h="100%">
<Loader variant="bars" />
</Center>
);

View File

@@ -0,0 +1,123 @@
import { Badge, Card, Center, Flex, Group, Image, Stack, Text } from '@mantine/core';
import { IconGitPullRequest } from '@tabler/icons';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useMediaRequestQuery } from './media-request-query';
import { MediaRequestStatus } from './media-request-types';
const definition = defineWidget({
id: 'media-requests-list',
icon: IconGitPullRequest,
options: {},
component: MediaRequestListTile,
gridstack: {
minWidth: 3,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
});
export type MediaRequestListWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface MediaRequestListWidgetProps {
widget: MediaRequestListWidget;
}
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
const { data, isFetching } = useMediaRequestQuery();
if (!data || isFetching) {
return <WidgetLoading />;
}
if (data.length === 0) {
return (
<Center h="100%">
<Text>There are no requests. Ensure that you&apos;ve configured your apps correctly.</Text>
</Center>
);
}
const countPendingApproval = data.filter(
(x) => x.status === MediaRequestStatus.PendingApproval
).length;
return (
<Stack>
{countPendingApproval > 0 ? (
<Text>There are {countPendingApproval} requests waiting for an approval.</Text>
) : (
<Text>There are currently no pending approvals. You&apos;re good to go!</Text>
)}
{data.map((item) => (
<Card pos="relative" withBorder>
<Flex justify="space-between" gap="md">
<Flex gap="md">
<Image
src={item.posterPath}
width={30}
height={50}
alt="poster"
radius="xs"
withPlaceholder
/>
<Stack spacing={0}>
<Group spacing="xs">
<Text>{item.airDate.split('-')[0]}</Text>
<MediaRequestStatusBadge status={item.status} />
</Group>
<Text
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
lineClamp={1}
weight="bold"
component="a"
href={item.href}
>
{item.name}
</Text>
</Stack>
</Flex>
<Flex gap="xs">
<Image src={item.userProfilePicture} width={25} height={25} alt="requester avatar" />
<Text
component="a"
href={item.userLink}
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
>
{item.userName}
</Text>
</Flex>
</Flex>
<Image
src={item.backdropPath}
pos="absolute"
w="100%"
h="100%"
opacity={0.1}
top={0}
left={0}
style={{ pointerEvents: 'none' }}
/>
</Card>
))}
</Stack>
);
}
const MediaRequestStatusBadge = ({ status }: { status: MediaRequestStatus }) => {
switch (status) {
case MediaRequestStatus.Approved:
return <Badge color="green">Approved</Badge>;
case MediaRequestStatus.Declined:
return <Badge color="red">Declined</Badge>;
case MediaRequestStatus.PendingApproval:
return <Badge color="orange">Pending approval</Badge>;
default:
return <></>;
}
};
export default definition;

View File

@@ -0,0 +1,73 @@
import { Card, Center, Flex, Stack, Text } from '@mantine/core';
import { IconChartBar } from '@tabler/icons';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useMediaRequestQuery } from './media-request-query';
import { MediaRequestStatus } from './media-request-types';
const definition = defineWidget({
id: 'media-requests-stats',
icon: IconChartBar,
options: {},
component: MediaRequestStatsTile,
gridstack: {
minWidth: 1,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
});
export type MediaRequestStatsWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface MediaRequestStatsWidgetProps {
widget: MediaRequestStatsWidget;
}
function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
const { data, isFetching } = useMediaRequestQuery();
if (!data || isFetching) {
return <WidgetLoading />;
}
return (
<Flex gap="md" wrap="wrap">
<Card w={100} h={100} withBorder>
<Center h="100%">
<Stack spacing={0} align="center">
<Text>
{data.filter((x) => x.status === MediaRequestStatus.PendingApproval).length}
</Text>
<Text color="dimmed" align="center" size="xs">
Pending approvals
</Text>
</Stack>
</Center>
</Card>
<Card w={100} h={100} withBorder>
<Center h="100%">
<Stack spacing={0} align="center">
<Text align="center">{data.filter((x) => x.type === 'tv').length}</Text>
<Text color="dimmed" align="center" size="xs">
TV requests
</Text>
</Stack>
</Center>
</Card>
<Card w={100} h={100} withBorder>
<Center h="100%">
<Stack spacing={0} align="center">
<Text align="center">{data.filter((x) => x.type === 'movie').length}</Text>
<Text color="dimmed" align="center" size="xs">
Movie requests
</Text>
</Stack>
</Center>
</Card>
</Flex>
);
}
export default definition;

View File

@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { MediaRequest } from './media-request-types';
export const useMediaRequestQuery = () => useQuery({
queryKey: ['media-requests'],
queryFn: async () => {
const response = await fetch('/api/modules/media-requests');
return (await response.json()) as MediaRequest[];
},
});

View File

@@ -0,0 +1,22 @@
export type MediaRequest = {
appId: string;
id: number;
createdAt: string;
rootFolder: string;
type: 'movie' | 'tv';
name: string;
userName: string;
userProfilePicture: string;
userLink: string;
airDate: string;
status: MediaRequestStatus;
backdropPath: string;
posterPath: string;
href: string;
};
export enum MediaRequestStatus {
PendingApproval = 1,
Approved = 2,
Declined = 3
}