From 6898faa3de450f18d7b8d9d4091b9cf38848e03c Mon Sep 17 00:00:00 2001 From: Denis Papec Date: Sat, 21 Oct 2023 00:31:19 +0100 Subject: [PATCH] Feature: Added agenda view for calendar, calendar improvements (#2216) * Feature: Added agenda view for calendar, calendar improvements * Fix duplicate event keys * Additional hover on title, not date * Show date once in list * Rename monthly view for consistency * Remove unneeded key props * CSS cleanup, dont slice title to arbitrary 42 chars which can break column layouts * Simplify agenda components * Fix show date once in list --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/calendar.md | 18 ++++ src/pages/_app.jsx | 6 +- src/utils/config/service-helpers.js | 4 + src/utils/contexts/calendar.jsx | 13 --- src/widgets/calendar/agenda.jsx | 101 ++++++++++++++++++ src/widgets/calendar/component.jsx | 75 +++++++++++-- src/widgets/calendar/integrations/lidarr.jsx | 2 + src/widgets/calendar/integrations/radarr.jsx | 6 ++ src/widgets/calendar/integrations/readarr.jsx | 2 + src/widgets/calendar/integrations/sonarr.jsx | 4 +- .../{monthly-view.jsx => monthly.jsx} | 56 +++------- 11 files changed, 219 insertions(+), 68 deletions(-) create mode 100644 src/widgets/calendar/agenda.jsx rename src/widgets/calendar/{monthly-view.jsx => monthly.jsx} (83%) diff --git a/docs/widgets/services/calendar.md b/docs/widgets/services/calendar.md index 794c0ed2d..990c01c26 100644 --- a/docs/widgets/services/calendar.md +++ b/docs/widgets/services/calendar.md @@ -3,6 +3,8 @@ title: Calendar description: Calendar widget --- +## Monthly view + calendar 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 68a6578a2..25f843418 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 96c2f17d1..04f9e883d 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 70616d5ff..578563a55 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 000000000..dc7761b9a --- /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 915ebc9e5..688915e26 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 d472bb48b..8e407b893 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 4cbe5b2aa..7fa01140a 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 5fefa8969..98a5752b5 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 a7201dd5f..0c46fa570 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 da3f201b2..e3c9e1fd4 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) => ( - + ))}