diff --git a/docs/widgets/services/calendar.md b/docs/widgets/services/calendar.md
index 794c0ed2..990c01c2 100644
--- a/docs/widgets/services/calendar.md
+++ b/docs/widgets/services/calendar.md
@@ -3,6 +3,8 @@ title: Calendar
description: Calendar widget
---
+## Monthly view
+
This widget shows monthly calendar, with optional integrations to show events from supported widgets.
@@ -11,6 +13,8 @@ This widget shows monthly calendar, with optional integrations to show events fr
widget:
type: calendar
firstDayInWeek: sunday # optional - defaults to monday
+ view: monthly # optional - possible values monthly, agenda
+ maxEvents: 10 # optional - defaults to 10
integrations: # optional
- type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr
service_group: Media # group name where widget exists
@@ -20,6 +24,20 @@ widget:
unmonitored: true # optional - defaults to false, used with *arr stack
```
+## Agenda
+
+This view shows only list of events from configured integrations
+
+```yaml
+widget:
+ type: calendar
+ view: agenda
+ maxEvents: 10 # optional - defaults to 10
+ integrations: # same as in Monthly view example
+```
+
+## Integrations
+
Currently integrated widgets are [sonarr](sonarr.md), [radarr](radarr.md), [lidarr](lidarr.md) and [readarr](readarr.md).
Supported colors can be found on [color palette](../../configs/settings.md#color-palette).
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 68a6578a..25f84341 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -12,7 +12,7 @@ import { ColorProvider } from "utils/contexts/color";
import { ThemeProvider } from "utils/contexts/theme";
import { SettingsProvider } from "utils/contexts/settings";
import { TabProvider } from "utils/contexts/tab";
-import { EventProvider, ShowDateProvider } from "utils/contexts/calendar";
+import { EventProvider } from "utils/contexts/calendar";
function MyApp({ Component, pageProps }) {
return (
@@ -33,9 +33,7 @@ function MyApp({ Component, pageProps }) {
-
-
-
+
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 96c2f17d..04f9e883 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -364,6 +364,8 @@ export function cleanServiceGroups(groups) {
refreshInterval,
integrations, // calendar widget
firstDayInWeek,
+ view,
+ maxEvents,
} = cleanedService.widget;
let fieldsList = fields;
@@ -450,6 +452,8 @@ export function cleanServiceGroups(groups) {
if (type === "calendar") {
if (integrations) cleanedService.widget.integrations = integrations;
if (firstDayInWeek) cleanedService.widget.firstDayInWeek = firstDayInWeek;
+ if (view) cleanedService.widget.view = view;
+ if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
}
}
diff --git a/src/utils/contexts/calendar.jsx b/src/utils/contexts/calendar.jsx
index 70616d5f..578563a5 100644
--- a/src/utils/contexts/calendar.jsx
+++ b/src/utils/contexts/calendar.jsx
@@ -1,7 +1,6 @@
import { createContext, useState, useMemo } from "react";
export const EventContext = createContext();
-export const ShowDateContext = createContext();
export function EventProvider({ initialEvent, children }) {
const [events, setEvents] = useState({});
@@ -14,15 +13,3 @@ export function EventProvider({ initialEvent, children }) {
return {children};
}
-
-export function ShowDateProvider({ initialDate, children }) {
- const [showDate, setShowDate] = useState(null);
-
- if (initialDate) {
- setShowDate(initialDate);
- }
-
- const value = useMemo(() => ({ showDate, setShowDate }), [showDate]);
-
- return {children};
-}
diff --git a/src/widgets/calendar/agenda.jsx b/src/widgets/calendar/agenda.jsx
new file mode 100644
index 00000000..dc7761b9
--- /dev/null
+++ b/src/widgets/calendar/agenda.jsx
@@ -0,0 +1,101 @@
+import { useContext, useState } from "react";
+import { DateTime } from "luxon";
+import classNames from "classnames";
+import { useTranslation } from "next-i18next";
+import { IoMdCheckmarkCircleOutline } from "react-icons/io";
+
+import { EventContext } from "../../utils/contexts/calendar";
+
+export function Event({ event, colorVariants, showDate = false }) {
+ const [hover, setHover] = useState(false);
+ const { i18n } = useTranslation();
+
+ return (
+
setHover(!hover)}
+ onMouseLeave={() => setHover(!hover)}
+ >
+
+
+ {showDate &&
+ event.date.setLocale(i18n.language).startOf("day").toLocaleString({ month: "short", day: "numeric" })}
+
+
+
+
+
+
+
{hover && event.additional ? event.additional : event.title}
+
+ {event.isCompleted && (
+
+
+
+ )}
+
+ );
+}
+
+export default function Agenda({ service, colorVariants, showDate }) {
+ const { widget } = service;
+ const { events } = useContext(EventContext);
+ const { i18n } = useTranslation();
+
+ if (!showDate) {
+ return ;
+ }
+
+ const eventsArray = Object.keys(events)
+ .filter(
+ (eventKey) => showDate.startOf("day").toUnixInteger() <= events[eventKey].date?.startOf("day").toUnixInteger(),
+ )
+ .map((eventKey) => events[eventKey])
+ .sort((a, b) => a.date - b.date)
+ .slice(0, widget?.maxEvents ?? 10);
+
+ if (!eventsArray.length) {
+ return (
+
+ );
+ }
+
+ const days = Array.from(new Set(eventsArray.map((e) => e.date.startOf("day").ts)));
+ const eventsByDay = days.map((d) => eventsArray.filter((e) => e.date.startOf("day").ts === d));
+
+ return (
+
+
+ {eventsByDay.map((eventsDay, i) => (
+
+ {eventsDay.map((event, j) => (
+
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/src/widgets/calendar/component.jsx b/src/widgets/calendar/component.jsx
index 915ebc9e..688915e2 100644
--- a/src/widgets/calendar/component.jsx
+++ b/src/widgets/calendar/component.jsx
@@ -1,15 +1,51 @@
-import { useContext, useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
import dynamic from "next/dynamic";
+import { DateTime } from "luxon";
+import { useTranslation } from "next-i18next";
-import { ShowDateContext } from "../../utils/contexts/calendar";
-
-import MonthlyView from "./monthly-view";
+import Monthly from "./monthly";
+import Agenda from "./agenda";
import Container from "components/services/widget/container";
+const colorVariants = {
+ // https://tailwindcss.com/docs/content-configuration#dynamic-class-names
+ amber: "bg-amber-500",
+ blue: "bg-blue-500",
+ cyan: "bg-cyan-500",
+ emerald: "bg-emerald-500",
+ fuchsia: "bg-fuchsia-500",
+ gray: "bg-gray-500",
+ green: "bg-green-500",
+ indigo: "bg-indigo-500",
+ lime: "bg-lime-500",
+ neutral: "bg-neutral-500",
+ orange: "bg-orange-500",
+ pink: "bg-pink-500",
+ purple: "bg-purple-500",
+ red: "bg-red-500",
+ rose: "bg-rose-500",
+ sky: "bg-sky-500",
+ slate: "bg-slate-500",
+ stone: "bg-stone-500",
+ teal: "bg-teal-500",
+ violet: "bg-violet-500",
+ white: "bg-white-500",
+ yellow: "bg-yellow-500",
+ zinc: "bg-zinc-500",
+};
+
export default function Component({ service }) {
const { widget } = service;
- const { showDate } = useContext(ShowDateContext);
+ const { i18n } = useTranslation();
+ const [showDate, setShowDate] = useState(null);
+ const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
+
+ useEffect(() => {
+ if (!showDate) {
+ setShowDate(currentDate);
+ }
+ }, [showDate, currentDate]);
// params for API fetch
const params = useMemo(() => {
@@ -27,10 +63,12 @@ export default function Component({ service }) {
// Load active integrations
const integrations = useMemo(
() =>
- widget.integrations?.map((integration) => ({
- service: dynamic(() => import(`./integrations/${integration?.type}`)),
- widget: integration,
- })) ?? [],
+ widget.integrations
+ ?.filter((integration) => integration?.type)
+ .map((integration) => ({
+ service: dynamic(() => import(`./integrations/${integration.type}`)),
+ widget: integration,
+ })) ?? [],
[widget.integrations],
);
@@ -52,7 +90,24 @@ export default function Component({ service }) {
);
})}
-
+ {(!widget?.view || widget?.view === "monthly") && (
+
+ )}
+ {widget?.view === "agenda" && (
+
+ )}
);
diff --git a/src/widgets/calendar/integrations/lidarr.jsx b/src/widgets/calendar/integrations/lidarr.jsx
index d472bb48..8e407b89 100644
--- a/src/widgets/calendar/integrations/lidarr.jsx
+++ b/src/widgets/calendar/integrations/lidarr.jsx
@@ -27,6 +27,8 @@ export default function Integration({ config, params }) {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? "green",
+ isCompleted: event.grabbed,
+ additional: "",
};
});
diff --git a/src/widgets/calendar/integrations/radarr.jsx b/src/widgets/calendar/integrations/radarr.jsx
index 4cbe5b2a..7fa01140 100644
--- a/src/widgets/calendar/integrations/radarr.jsx
+++ b/src/widgets/calendar/integrations/radarr.jsx
@@ -29,16 +29,22 @@ export default function Integration({ config, params }) {
title: cinemaTitle,
date: DateTime.fromISO(event.inCinemas),
color: config?.color ?? "amber",
+ isCompleted: event.isAvailable,
+ additional: "",
};
eventsToAdd[physicalTitle] = {
title: physicalTitle,
date: DateTime.fromISO(event.physicalRelease),
color: config?.color ?? "cyan",
+ isCompleted: event.isAvailable,
+ additional: "",
};
eventsToAdd[digitalTitle] = {
title: digitalTitle,
date: DateTime.fromISO(event.digitalRelease),
color: config?.color ?? "emerald",
+ isCompleted: event.isAvailable,
+ additional: "",
};
});
diff --git a/src/widgets/calendar/integrations/readarr.jsx b/src/widgets/calendar/integrations/readarr.jsx
index 5fefa896..98a5752b 100644
--- a/src/widgets/calendar/integrations/readarr.jsx
+++ b/src/widgets/calendar/integrations/readarr.jsx
@@ -28,6 +28,8 @@ export default function Integration({ config, params }) {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? "rose",
+ isCompleted: event.grabbed,
+ additional: "",
};
});
diff --git a/src/widgets/calendar/integrations/sonarr.jsx b/src/widgets/calendar/integrations/sonarr.jsx
index a7201dd5..0c46fa57 100644
--- a/src/widgets/calendar/integrations/sonarr.jsx
+++ b/src/widgets/calendar/integrations/sonarr.jsx
@@ -26,9 +26,11 @@ export default function Integration({ config, params }) {
const title = `${event.series.title ?? event.title} - S${event.seasonNumber}E${event.episodeNumber}`;
eventsToAdd[title] = {
- title,
+ title: `${event.series.title ?? event.title}`,
date: DateTime.fromISO(event.airDateUtc),
color: config?.color ?? "teal",
+ isCompleted: event.hasFile,
+ additional: `S${event.seasonNumber} E${event.episodeNumber}`,
};
});
diff --git a/src/widgets/calendar/monthly-view.jsx b/src/widgets/calendar/monthly.jsx
similarity index 83%
rename from src/widgets/calendar/monthly-view.jsx
rename to src/widgets/calendar/monthly.jsx
index da3f201b..e3c9e1fd 100644
--- a/src/widgets/calendar/monthly-view.jsx
+++ b/src/widgets/calendar/monthly.jsx
@@ -1,43 +1,16 @@
-import { useContext, useEffect, useMemo } from "react";
+import { useContext, useMemo } from "react";
import { DateTime, Info } from "luxon";
import classNames from "classnames";
import { useTranslation } from "next-i18next";
+import { IoMdCheckmarkCircleOutline } from "react-icons/io";
-import { EventContext, ShowDateContext } from "../../utils/contexts/calendar";
-
-const colorVariants = {
- // https://tailwindcss.com/docs/content-configuration#dynamic-class-names
- amber: "bg-amber-500",
- blue: "bg-blue-500",
- cyan: "bg-cyan-500",
- emerald: "bg-emerald-500",
- fuchsia: "bg-fuchsia-500",
- gray: "bg-gray-500",
- green: "bg-green-500",
- indigo: "bg-indigo-500",
- lime: "bg-lime-500",
- neutral: "bg-neutral-500",
- orange: "bg-orange-500",
- pink: "bg-pink-500",
- purple: "bg-purple-500",
- red: "bg-red-500",
- rose: "bg-rose-500",
- sky: "bg-sky-500",
- slate: "bg-slate-500",
- stone: "bg-stone-500",
- teal: "bg-teal-500",
- violet: "bg-violet-500",
- white: "bg-white-500",
- yellow: "bg-yellow-500",
- zinc: "bg-zinc-500",
-};
+import { EventContext } from "../../utils/contexts/calendar";
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
-export function Day({ weekNumber, weekday, events }) {
+export function Day({ weekNumber, weekday, events, colorVariants, showDate, setShowDate }) {
const currentDate = DateTime.now();
- const { showDate, setShowDate } = useContext(ShowDateContext);
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
const filteredEvents = events?.filter(
@@ -105,7 +78,13 @@ export function Event({ event }) {
>
{event.title}
+ {event.additional ? ` - ${event.additional}` : ""}
+ {event.isCompleted && (
+
+
+
+ )}
);
}
@@ -120,19 +99,12 @@ const dayInWeekId = {
sunday: 7,
};
-export default function MonthlyView({ service }) {
+export default function Monthly({ service, colorVariants, showDate, setShowDate }) {
const { widget } = service;
const { i18n } = useTranslation();
- const { showDate, setShowDate } = useContext(ShowDateContext);
const { events } = useContext(EventContext);
const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
- useEffect(() => {
- if (!showDate) {
- setShowDate(currentDate);
- }
- });
-
const dayNames = Info.weekdays("short", { locale: i18n.language });
const firstDayInWeekCalendar = widget?.firstDayInWeek ? widget?.firstDayInWeek?.toLowerCase() : "monday";
@@ -211,6 +183,9 @@ export default function MonthlyView({ service }) {
weekNumber={weekNumber}
weekday={dayInWeek}
events={eventsArray}
+ colorVariants={colorVariants}
+ showDate={showDate}
+ setShowDate={setShowDate}
/>
)),
)}
@@ -219,8 +194,9 @@ export default function MonthlyView({ service }) {
{eventsArray
?.filter((event) => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
+ .slice(0, widget?.maxEvents ?? 10)
.map((event) => (
-
+
))}