From 681138899121a97925a4552f5c647527ddde11de Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:16:28 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20multipl?= =?UTF-8?q?e=20RSS=20feeds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tiles/Widgets/WidgetsEditModal.tsx | 19 ++ src/widgets/rss/RssWidgetTile.tsx | 226 +++++++----------- src/widgets/widgets.ts | 13 +- 3 files changed, 123 insertions(+), 135 deletions(-) diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index e89d32176..643a8b64b 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -243,6 +243,25 @@ const WidgetOptionTypeSwitch: FC<{ ); + case 'multiple-text': + return ( + ({ value: v, label: v }))} + label={t(`descriptor.settings.${key}.label`)} + description={t(`descriptor.settings.${key}.description`)} + defaultValue={value as string[]} + withinPortal + searchable + creatable + getCreateLabel={(query) => `+ Add ${query}`} + onChange={(values) => + handleChange( + key, + values.map((item: string) => item) + ) + } + /> + ); /* eslint-enable no-case-declarations */ default: return null; diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index b1eab57cd..d2b8907ff 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -1,9 +1,9 @@ +import Link from 'next/link'; import { ActionIcon, Badge, Card, Center, - createStyles, Flex, Group, Image, @@ -14,31 +14,27 @@ import { Stack, Text, Title, + createStyles, } from '@mantine/core'; import { - IconBulldozer, - IconCalendarTime, IconClock, - IconCopyright, IconRefresh, IconRss, - IconSpeakerphone, } from '@tabler/icons'; import { useQuery } from '@tanstack/react-query'; import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; -import Link from 'next/link'; -import { useState } from 'react'; -import { IWidget } from '../widgets'; + import { defineWidget } from '../helper'; +import { IWidget } from '../widgets'; const definition = defineWidget({ id: 'rss', icon: IconRss, options: { rssFeedUrl: { - type: 'text', - defaultValue: '', + type: 'multiple-text', + defaultValue: ['https://japantimes.co.jp/feed'], }, }, gridstack: { @@ -56,34 +52,42 @@ interface RssTileProps { widget: IRssWidget; } -export const useGetRssFeed = (feedUrl: string, widgetId: string) => +export const useGetRssFeeds = (feedUrls: string[], widgetId: string) => useQuery({ - queryKey: ['rss-feed', feedUrl], + queryKey: ['rss-feeds', feedUrls], queryFn: async () => { - const response = await fetch(`/api/modules/rss?widgetId=${widgetId}`); - return response.json(); + const responses = await Promise.all( + feedUrls.map((feedUrl) => + fetch( + `/api/modules/rss?widgetId=${widgetId}&feedUrl=${encodeURIComponent(feedUrl)}` + ).then((response) => response.json()) + ) + ); + return responses; }, }); function RssTile({ widget }: RssTileProps) { const { t } = useTranslation('modules/rss'); - const { data, isLoading, isFetching, isError, refetch } = useGetRssFeed( + const { data, isLoading, isFetching, isError, refetch } = useGetRssFeeds( widget.properties.rssFeedUrl, widget.id ); const { classes } = useStyles(); - const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false); function formatDate(input: string): string { // Parse the input date as a local date - const inputDate = dayjs(new Date(input)); - const now = dayjs(); // Current date and time - - // The difference between the input date and now - const difference = now.diff(inputDate, 'ms'); - const duration = dayjs.duration(difference, 'ms'); - const humanizedDuration = duration.humanize(); - return `${humanizedDuration} ago`; + try { + const inputDate = dayjs(new Date(input)); + const now = dayjs(); // Current date and time + // The difference between the input date and now + const difference = now.diff(inputDate, 'ms'); + const duration = dayjs.duration(difference, 'ms'); + const humanizedDuration = duration.humanize(); + return `${humanizedDuration} ago`; + } catch (e) { + return 'Error'; + } } if (!data || isLoading) { @@ -94,7 +98,7 @@ function RssTile({ widget }: RssTileProps) { ); } - if (!data.success || isError) { + if (data.length < 1 || isError) { return (
@@ -109,122 +113,78 @@ function RssTile({ widget }: RssTileProps) { return ( - {data.feed.title && {data.feed.title}} - - - {data.feed.items.map((item: any, index: number) => ( - - {item.enclosure && ( - // eslint-disable-next-line @next/next/no-img-element - backdrop - )} - - + + {data.map((item, index) => ( + + {item.feed.items.map((item: any, index: number) => ( + {item.enclosure && ( - - - + // eslint-disable-next-line @next/next/no-img-element + backdrop )} - - {item.categories && ( - - {item.categories.map((category: any, categoryIndex: number) => ( - {category} - ))} - + + + {item.enclosure && ( + + + )} + + {item.categories && ( + + {item.categories.map((category: any, categoryIndex: number) => ( + {category} + ))} + + )} - {item.title} - - {item.content} - + {item.title} + + {item.content} + - {item.pubDate && } + {item.pubDate && } + - - - ))} - + + ))} + + ))} - - {data.feed.copyright && ( - - - - {data.feed.copyright} - - - )} - {data.feed.pubDate && ( - - - - {data.feed.pubDate} - - - )} - {data.feed.lastBuildDate && ( - - - - {formatDate(data.feed.lastBuildDate)} - - - )} - {data.feed.feedUrl && ( - - - - Feed URL - - - )} - refetch()} - bottom={10} - styles={{ - root: { - borderColor: 'red', - }, - }} - > - {data.feed.image ? ( - {data.feed.image.title} - ) : ( - - )} - - + refetch()} + bottom={10} + styles={{ + root: { + borderColor: 'red', + }, + }} + > + + ); } diff --git a/src/widgets/widgets.ts b/src/widgets/widgets.ts index 998413fea..a5b6b9729 100644 --- a/src/widgets/widgets.ts +++ b/src/widgets/widgets.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { MultiSelectProps, NumberInputProps, @@ -7,7 +8,7 @@ import { TextInputProps, } from '@mantine/core'; import { TablerIcon } from '@tabler/icons'; -import React from 'react'; + import { AreaType } from '../types/area'; import { ShapeType } from '../types/shape'; @@ -37,7 +38,8 @@ export type IWidgetOptionValue = | ISliderInputOptionValue | ISelectOptionValue | INumberInputOptionValue - | IDraggableListInputValue; + | IDraggableListInputValue + | IMultipleTextInputOptionValue; // Interface for data type interface DataType { @@ -105,6 +107,13 @@ export type IDraggableListInputValue = { >; }; +// will show a text-input with a button to add a new line +export type IMultipleTextInputOptionValue = { + type: 'multiple-text'; + defaultValue: string[]; + inputProps?: Partial; +}; + // is used to type the widget definitions which will be used to display all widgets export type IWidgetDefinition = { id: TKey; From 54aa5f7f4da0f13cccb8c41554a6118a59afcfe8 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:18:54 +0900 Subject: [PATCH 02/10] Update RSS widget locale --- public/locales/en/modules/rss.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/en/modules/rss.json b/public/locales/en/modules/rss.json index df52af4af..47d6e405d 100644 --- a/public/locales/en/modules/rss.json +++ b/public/locales/en/modules/rss.json @@ -5,7 +5,8 @@ "settings": { "title": "Settings for RSS widget", "rssFeedUrl": { - "label": "RSS feed url" + "label": "RSS feeds urls", + "description": "The urls of the RSS feeds you want to display from." } } }, From 405219c081860ae2019f07b0a53e2ea9f7878912 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:23:52 +0900 Subject: [PATCH 03/10] Change default value, fix API --- src/pages/api/modules/rss/index.ts | 18 +++++++----------- src/widgets/rss/RssWidgetTile.tsx | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/api/modules/rss/index.ts b/src/pages/api/modules/rss/index.ts index 7ea57572b..211e5bc37 100644 --- a/src/pages/api/modules/rss/index.ts +++ b/src/pages/api/modules/rss/index.ts @@ -1,17 +1,13 @@ -import Consola from 'consola'; - -import { getCookie } from 'cookies-next'; - -import { decode } from 'html-entities'; - import { NextApiRequest, NextApiResponse } from 'next'; - +import Consola from 'consola'; +import { getCookie } from 'cookies-next'; +import { decode } from 'html-entities'; import Parser from 'rss-parser'; - import { z } from 'zod'; + import { getConfig } from '../../../../tools/config/getConfig'; -import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile'; import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool'; +import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile'; type CustomItem = { 'media:content': string; @@ -28,6 +24,7 @@ const parser: Parser = new Parser({ const getQuerySchema = z.object({ widgetId: z.string().uuid(), + feedUrl: z.string(), }); export const Get = async (request: NextApiRequest, response: NextApiResponse) => { @@ -44,7 +41,6 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) => const rssWidget = config.widgets.find( (x) => x.type === 'rss' && x.id === parseResult.data.widgetId ) as IRssWidget | undefined; - if ( !rssWidget || !rssWidget.properties.rssFeedUrl || @@ -56,7 +52,7 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) => Consola.info('Requesting RSS feed...'); const stopWatch = new Stopwatch(); - const feed = await parser.parseURL(rssWidget.properties.rssFeedUrl); + const feed = await parser.parseURL(parseResult.data.feedUrl); Consola.info(`Retrieved RSS feed after ${stopWatch.getEllapsedMilliseconds()} milliseconds`); const orderedFeed = { diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index d2b8907ff..57a60773c 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -34,7 +34,7 @@ const definition = defineWidget({ options: { rssFeedUrl: { type: 'multiple-text', - defaultValue: ['https://japantimes.co.jp/feed'], + defaultValue: [], }, }, gridstack: { From 0c99b77843b4f55285bff6c4f90be010e9de291c Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:25:32 +0900 Subject: [PATCH 04/10] Update default rss feed value with homarr releases --- src/widgets/rss/RssWidgetTile.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index 57a60773c..c8103056f 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -16,11 +16,7 @@ import { Title, createStyles, } from '@mantine/core'; -import { - IconClock, - IconRefresh, - IconRss, -} from '@tabler/icons'; +import { IconClock, IconRefresh, IconRss } from '@tabler/icons'; import { useQuery } from '@tanstack/react-query'; import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; @@ -34,7 +30,7 @@ const definition = defineWidget({ options: { rssFeedUrl: { type: 'multiple-text', - defaultValue: [], + defaultValue: ['https://github.com/ajnart/homarr/tags.atom'], }, }, gridstack: { From 1930a4c1f683c7905f753a186c5a54f7b5df6991 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:38:38 +0900 Subject: [PATCH 05/10] Add title display if availalbe --- src/widgets/rss/RssWidgetTile.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index c8103056f..5f1ee2c57 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -110,9 +110,9 @@ function RssTile({ widget }: RssTileProps) { - {data.map((item, index) => ( + {data.map((feed, index) => ( - {item.feed.items.map((item: any, index: number) => ( + {feed.feed.items.map((item: any, index: number) => ( - {item.pubDate && } + {item.pubDate && ( + + )} @@ -185,12 +187,17 @@ function RssTile({ widget }: RssTileProps) { ); } -const TimeDisplay = ({ date }: { date: string }) => ( +const InfoDisplay = ({ date, title }: { date: string; title: string | undefined }) => ( {date} + {title && ( + + {title} + + )} ); From 1a7ae434b71d656dc89fbbd6c8c64b6959390869 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:42:22 +0900 Subject: [PATCH 06/10] Raise cache time for ReactQuery --- src/widgets/rss/RssWidgetTile.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index 5f1ee2c57..fa2ed14ff 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -51,6 +51,8 @@ interface RssTileProps { export const useGetRssFeeds = (feedUrls: string[], widgetId: string) => useQuery({ queryKey: ['rss-feeds', feedUrls], + // Cache the results for 24 hours + cacheTime: 1000 * 60 * 60 * 24, queryFn: async () => { const responses = await Promise.all( feedUrls.map((feedUrl) => From 9d51e2ce52211feeb687ee15efb7b15145a48f52 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:46:03 +0900 Subject: [PATCH 07/10] Change the loading overloay to the refresh button --- src/widgets/rss/RssWidgetTile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index fa2ed14ff..8290c3306 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -110,7 +110,6 @@ function RssTile({ widget }: RssTileProps) { return ( - {data.map((feed, index) => ( @@ -183,7 +182,7 @@ function RssTile({ widget }: RssTileProps) { }, }} > - + {isFetching ? : } ); From 31a80f5588869f61730964ed33e5673f102c3d0a Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 14:53:18 +0900 Subject: [PATCH 08/10] Add option to manually edit the refresh interval --- public/locales/en/modules/rss.json | 17 ++++++++++------- .../Tiles/Widgets/WidgetsEditModal.tsx | 1 + src/widgets/rss/RssWidgetTile.tsx | 11 ++++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/public/locales/en/modules/rss.json b/public/locales/en/modules/rss.json index 47d6e405d..56a751750 100644 --- a/public/locales/en/modules/rss.json +++ b/public/locales/en/modules/rss.json @@ -7,14 +7,17 @@ "rssFeedUrl": { "label": "RSS feeds urls", "description": "The urls of the RSS feeds you want to display from." + }, + "refreshInterval": { + "label": "Refresh interval (in seconds)" } - } - }, - "card": { - "errors": { - "general": { - "title": "Unable to retrieve RSS feed", - "text": "There was a problem reaching out the RSS feed. Make sure that you have correctly configured the RSS feed using a valid URL. URLs should match the official specification. After updating the feed, you may need to refresh the dashboard." + }, + "card": { + "errors": { + "general": { + "title": "Unable to retrieve RSS feed", + "text": "There was a problem reaching out the RSS feed. Make sure that you have correctly configured the RSS feed using a valid URL. URLs should match the official specification. After updating the feed, you may need to refresh the dashboard." + } } } } diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index 643a8b64b..805b7f805 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -184,6 +184,7 @@ const WidgetOptionTypeSwitch: FC<{ case 'slider': return ( + {t(`descriptor.settings.${key}.label`)} +export const useGetRssFeeds = (feedUrls: string[], refreshInterval: number, widgetId: string) => useQuery({ queryKey: ['rss-feeds', feedUrls], // Cache the results for 24 hours cacheTime: 1000 * 60 * 60 * 24, + staleTime: 1000 * refreshInterval, queryFn: async () => { const responses = await Promise.all( feedUrls.map((feedUrl) => @@ -69,6 +77,7 @@ function RssTile({ widget }: RssTileProps) { const { t } = useTranslation('modules/rss'); const { data, isLoading, isFetching, isError, refetch } = useGetRssFeeds( widget.properties.rssFeedUrl, + widget.properties.refreshInterval, widget.id ); const { classes } = useStyles(); From 18c84e7e1e97afaeeeb7ee6f88dd7a971c43e4b5 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 5 Apr 2023 15:25:20 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20dependen?= =?UTF-8?q?cies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/rss/RssWidgetTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index 50be54627..d9dd597a9 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -8,7 +8,6 @@ import { Group, Image, Loader, - LoadingOverlay, MediaQuery, ScrollArea, Stack, From b45a614cd80cbc10f3b2fa016221430d5f8043ba Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 10 Apr 2023 23:29:00 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=92=A1=20Address=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/common.json | 1 + public/locales/en/modules/rss.json | 2 +- .../Dashboard/Tiles/Widgets/WidgetsEditModal.tsx | 4 ++-- src/pages/api/modules/rss/index.ts | 2 +- src/widgets/rss/RssWidgetTile.tsx | 9 ++++----- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 4f2e4f0ff..c62f22acf 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -10,6 +10,7 @@ "changePosition": "Change position", "remove": "Remove", "removeConfirm": "Are you sure that you want to remove {{item}}?", + "createItem": "+ create {{item}}", "sections": { "settings": "Settings", "dangerZone": "Danger zone" diff --git a/public/locales/en/modules/rss.json b/public/locales/en/modules/rss.json index 56a751750..3c50414d2 100644 --- a/public/locales/en/modules/rss.json +++ b/public/locales/en/modules/rss.json @@ -9,7 +9,7 @@ "description": "The urls of the RSS feeds you want to display from." }, "refreshInterval": { - "label": "Refresh interval (in seconds)" + "label": "Refresh interval (in minutes)" } }, "card": { diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index 805b7f805..03c2d6b34 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -247,14 +247,14 @@ const WidgetOptionTypeSwitch: FC<{ case 'multiple-text': return ( ({ value: v, label: v }))} + data={value.map((name: any) => ({ value: name, label: name }))} label={t(`descriptor.settings.${key}.label`)} description={t(`descriptor.settings.${key}.description`)} defaultValue={value as string[]} withinPortal searchable creatable - getCreateLabel={(query) => `+ Add ${query}`} + getCreateLabel={(query) => t('common:createItem', query)} onChange={(values) => handleChange( key, diff --git a/src/pages/api/modules/rss/index.ts b/src/pages/api/modules/rss/index.ts index 211e5bc37..9c4284356 100644 --- a/src/pages/api/modules/rss/index.ts +++ b/src/pages/api/modules/rss/index.ts @@ -50,7 +50,7 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) => return; } - Consola.info('Requesting RSS feed...'); + Consola.info(`Requesting RSS feed at url ${parseResult.data.feedUrl}`); const stopWatch = new Stopwatch(); const feed = await parser.parseURL(parseResult.data.feedUrl); Consola.info(`Retrieved RSS feed after ${stopWatch.getEllapsedMilliseconds()} milliseconds`); diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index d9dd597a9..132c07b81 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -33,10 +33,10 @@ const definition = defineWidget({ }, refreshInterval: { type: 'slider', - defaultValue: 60, - min: 30, + defaultValue: 30, + min: 15, max: 300, - step: 30, + step: 15, }, }, gridstack: { @@ -59,7 +59,7 @@ export const useGetRssFeeds = (feedUrls: string[], refreshInterval: number, widg queryKey: ['rss-feeds', feedUrls], // Cache the results for 24 hours cacheTime: 1000 * 60 * 60 * 24, - staleTime: 1000 * refreshInterval, + staleTime: 1000 * 60 * refreshInterval, queryFn: async () => { const responses = await Promise.all( feedUrls.map((feedUrl) => @@ -86,7 +86,6 @@ function RssTile({ widget }: RssTileProps) { try { const inputDate = dayjs(new Date(input)); const now = dayjs(); // Current date and time - // The difference between the input date and now const difference = now.diff(inputDate, 'ms'); const duration = dayjs.duration(difference, 'ms'); const humanizedDuration = duration.humanize();