feat: radarr release type to calendar widget (#1256)

* feat: add release type

* fix: type check

* fix: deepSource

* fix: new approach

* fix: deepSource

* fix: typecheck

* fix: reviewed changes
This commit is contained in:
Yossi Hillali
2024-10-14 11:38:13 +03:00
committed by GitHub
parent 1bc470a39f
commit 98b62a9f91
7 changed files with 78 additions and 15 deletions

View File

@@ -1,7 +1,11 @@
export const radarrReleaseTypes = ["inCinemas", "digitalRelease", "physicalRelease"] as const;
type ReleaseType = (typeof radarrReleaseTypes)[number];
export interface CalendarEvent { export interface CalendarEvent {
name: string; name: string;
subName: string; subName: string;
date: Date; date: Date;
dates?: { type: ReleaseType; date: Date }[];
description?: string; description?: string;
thumbnail?: string; thumbnail?: string;
mediaInformation?: { mediaInformation?: {

View File

@@ -1,8 +1,10 @@
import type { AtLeastOneOf } from "@homarr/common/types";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { z } from "@homarr/validation"; import { z } from "@homarr/validation";
import { Integration } from "../../base/integration"; import { Integration } from "../../base/integration";
import type { CalendarEvent } from "../../calendar-types"; import type { CalendarEvent } from "../../calendar-types";
import { radarrReleaseTypes } from "../../calendar-types";
export class RadarrIntegration extends Integration { export class RadarrIntegration extends Integration {
/** /**
@@ -37,19 +39,23 @@ export class RadarrIntegration extends Integration {
}); });
const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json()); const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json());
return radarrCalendarEvents.map( return radarrCalendarEvents.map((radarrCalendarEvent): CalendarEvent => {
(radarrCalendarEvent): CalendarEvent => ({ const dates = radarrReleaseTypes
.map((type) => (radarrCalendarEvent[type] ? { type, date: radarrCalendarEvent[type] } : undefined))
.filter((date) => date) as AtLeastOneOf<Exclude<CalendarEvent["dates"], undefined>[number]>;
return {
name: radarrCalendarEvent.title, name: radarrCalendarEvent.title,
subName: radarrCalendarEvent.originalTitle, subName: radarrCalendarEvent.originalTitle,
description: radarrCalendarEvent.overview, description: radarrCalendarEvent.overview,
thumbnail: this.chooseBestImageAsURL(radarrCalendarEvent), thumbnail: this.chooseBestImageAsURL(radarrCalendarEvent),
date: radarrCalendarEvent.inCinemas, date: dates[0].date,
dates,
mediaInformation: { mediaInformation: {
type: "movie", type: "movie",
}, },
links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent), links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent),
}), };
); });
} }
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => { private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
@@ -118,7 +124,18 @@ const radarrCalendarEventImageSchema = z.array(
const radarrCalendarEventSchema = z.object({ const radarrCalendarEventSchema = z.object({
title: z.string(), title: z.string(),
originalTitle: z.string(), originalTitle: z.string(),
inCinemas: z.string().transform((value) => new Date(value)), inCinemas: z
.string()
.transform((value) => new Date(value))
.optional(),
physicalRelease: z
.string()
.transform((value) => new Date(value))
.optional(),
digitalRelease: z
.string()
.transform((value) => new Date(value))
.optional(),
overview: z.string().optional(), overview: z.string().optional(),
titleSlug: z.string(), titleSlug: z.string(),
images: radarrCalendarEventImageSchema, images: radarrCalendarEventImageSchema,

View File

@@ -23,6 +23,7 @@ const optionMapping: OptionMapping = {
}, },
"mediaRequests-requestStats": {}, "mediaRequests-requestStats": {},
calendar: { calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
filterFutureMonths: () => undefined, filterFutureMonths: () => undefined,
filterPastMonths: () => undefined, filterPastMonths: () => undefined,
}, },

View File

@@ -1031,6 +1031,14 @@ export default {
name: "Calendar", name: "Calendar",
description: "Display events from your integrations in a calendar view within a certain relative time period", description: "Display events from your integrations in a calendar view within a certain relative time period",
option: { option: {
releaseType: {
label: "Radarr release type",
options: {
inCinemas: "In cinemas",
digitalRelease: "Digital release",
physicalRelease: "Physical release",
},
},
filterPastMonths: { filterPastMonths: {
label: "Start from", label: "Start from",
}, },

View File

@@ -15,6 +15,7 @@ import { IconClock } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { CalendarEvent } from "@homarr/integrations/types"; import type { CalendarEvent } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import classes from "./calendar-event-list.module.css"; import classes from "./calendar-event-list.module.css";
@@ -24,6 +25,7 @@ interface CalendarEventListProps {
export const CalendarEventList = ({ events }: CalendarEventListProps) => { export const CalendarEventList = ({ events }: CalendarEventListProps) => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const t = useI18n();
return ( return (
<ScrollArea <ScrollArea
offsetScrollbars offsetScrollbars
@@ -57,14 +59,24 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
{event.subName} {event.subName}
</Text> </Text>
)} )}
<Text fw={"bold"} lineClamp={1}> <Text fw={"bold"} lineClamp={1} size="sm">
{event.name} {event.name}
</Text> </Text>
</Stack> </Stack>
<Group gap={3} wrap="nowrap"> {event.dates ? (
<IconClock opacity={0.7} size={"1rem"} /> <Group wrap="nowrap">
<Text c={"dimmed"}>{dayjs(event.date.toString()).format("HH:mm")}</Text> <Text c="dimmed" size="sm">
</Group> {t(
`widget.calendar.option.releaseType.options.${event.dates.find(({ date }) => event.date === date)?.type ?? "inCinemas"}`,
)}
</Text>
</Group>
) : (
<Group gap={3} wrap="nowrap">
<IconClock opacity={0.7} size={"1rem"} />
<Text c={"dimmed"}>{dayjs(event.date).format("HH:mm")}</Text>
</Group>
)}
</Group> </Group>
{event.description && ( {event.description && (
<Text size={"xs"} c={"dimmed"} lineClamp={2}> <Text size={"xs"} c={"dimmed"} lineClamp={2}>

View File

@@ -6,12 +6,18 @@ import { Calendar } from "@mantine/dates";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import type { CalendarEvent } from "@homarr/integrations/types";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import { CalendarDay } from "./calender-day"; import { CalendarDay } from "./calender-day";
import classes from "./component.module.css"; import classes from "./component.module.css";
export default function CalendarWidget({ isEditMode, integrationIds, itemId }: WidgetComponentProps<"calendar">) { export default function CalendarWidget({
isEditMode,
integrationIds,
itemId,
options,
}: WidgetComponentProps<"calendar">) {
const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery( const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(
{ {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -80,9 +86,16 @@ export default function CalendarWidget({ isEditMode, integrationIds, itemId }: W
padding: 0, padding: 0,
}, },
}} }}
renderDay={(date) => { renderDay={(tileDate) => {
const eventsForDate = events.filter((event) => dayjs(event.date).isSame(date, "day")); const eventsForDate = events
return <CalendarDay date={date} events={eventsForDate} disabled={isEditMode} />; .map((event) => ({
...event,
date: (event.dates?.filter(({ type }) => options.releaseType.includes(type)) ?? [event]).find(({ date }) =>
dayjs(date).isSame(tileDate, "day"),
)?.date,
}))
.filter((event): event is CalendarEvent => Boolean(event.date));
return <CalendarDay date={tileDate} events={eventsForDate} disabled={isEditMode} />;
}} }}
/> />
); );

View File

@@ -1,6 +1,7 @@
import { IconCalendar } from "@tabler/icons-react"; import { IconCalendar } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { radarrReleaseTypes } from "@homarr/integrations/types";
import { z } from "@homarr/validation"; import { z } from "@homarr/validation";
import { createWidgetDefinition } from "../definition"; import { createWidgetDefinition } from "../definition";
@@ -9,6 +10,13 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("calendar", { export const { definition, componentLoader } = createWidgetDefinition("calendar", {
icon: IconCalendar, icon: IconCalendar,
options: optionsBuilder.from((factory) => ({ options: optionsBuilder.from((factory) => ({
releaseType: factory.multiSelect({
defaultValue: ["inCinemas", "digitalRelease"],
options: radarrReleaseTypes.map((value) => ({
value,
label: (t) => t(`widget.calendar.option.releaseType.options.${value}`),
})),
}),
filterPastMonths: factory.number({ filterPastMonths: factory.number({
validate: z.number().min(2).max(9999), validate: z.number().min(2).max(9999),
defaultValue: 2, defaultValue: 2,