diff --git a/.eslintrc.js b/.eslintrc.js index 2490d3526..678a2d166 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,5 +28,6 @@ module.exports = { '@typescript-eslint/no-shadow': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-non-null-assertion': 'off', + 'linebreak-style': 0 }, }; diff --git a/src/components/Config/LoadConfig.tsx b/src/components/Config/LoadConfig.tsx index a41f0e027..47c4bbef4 100644 --- a/src/components/Config/LoadConfig.tsx +++ b/src/components/Config/LoadConfig.tsx @@ -1,15 +1,14 @@ import { Group, Text, useMantineTheme } from '@mantine/core'; -import { IconX as X, IconCheck as Check, IconX, IconPhoto, IconUpload } from '@tabler/icons'; -import { showNotification } from '@mantine/notifications'; -import { setCookie } from 'cookies-next'; import { Dropzone } from '@mantine/dropzone'; +import { showNotification } from '@mantine/notifications'; +import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons'; +import { setCookie } from 'cookies-next'; import { useTranslation } from 'next-i18next'; -import { useConfig } from '../../tools/state'; +import { useConfigStore } from '../../config/store'; import { Config } from '../../tools/types'; -import { migrateToIdConfig } from '../../tools/migrate'; export default function LoadConfigComponent(props: any) { - const { setConfig } = useConfig(); + const { updateConfig } = useConfigStore(); const theme = useMantineTheme(); const { t } = useTranslation('settings/general/config-changer'); @@ -48,8 +47,7 @@ export default function LoadConfigComponent(props: any) { maxAge: 60 * 60 * 24 * 30, sameSite: 'strict', }); - const migratedConfig = migrateToIdConfig(newConfig); - setConfig(migratedConfig); + updateConfig(newConfig.name, (previousConfig) => ({ ...previousConfig, newConfig })); }); }} accept={['application/json']} diff --git a/src/components/Dashboard/Tiles/TileWrapper.tsx b/src/components/Dashboard/Tiles/TileWrapper.tsx index 78bb754ea..dfa3a62dc 100644 --- a/src/components/Dashboard/Tiles/TileWrapper.tsx +++ b/src/components/Dashboard/Tiles/TileWrapper.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unknown-property */ import { ReactNode, RefObject } from 'react'; interface GridstackTileWrapperProps { diff --git a/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx b/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx index 2ecbd9740..1a80f63b7 100644 --- a/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx +++ b/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx @@ -6,10 +6,10 @@ import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; import { CategoryType } from '../../../../types/category'; -export interface CategoryEditModalInnerProps { +export type CategoryEditModalInnerProps = { category: CategoryType; onSuccess: (category: CategoryType) => Promise; -} +}; export const CategoryEditModal = ({ context, diff --git a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx index c90ef9999..cefbf3538 100644 --- a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx +++ b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx @@ -25,6 +25,7 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => { className="grid-stack grid-stack-sidebar" style={{ transitionDuration: '0s', height: '100%' }} data-sidebar={location} + // eslint-disable-next-line react/no-unknown-property gs-min-row={minRow} ref={refs.wrapper} > diff --git a/src/components/layout/Aside.tsx b/src/components/layout/Aside.tsx deleted file mode 100644 index 6c4810572..000000000 --- a/src/components/layout/Aside.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Aside as MantineAside, createStyles } from '@mantine/core'; -import Widgets from './Widgets'; - -const useStyles = createStyles((theme) => ({ - hide: { - [theme.fn.smallerThan('xs')]: { - display: 'none', - }, - }, - burger: { - [theme.fn.largerThan('sm')]: { - display: 'none', - }, - }, -})); - -export default function Aside(props: any) { - const { classes, cx } = useStyles(); - return ( - - ); -} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx deleted file mode 100644 index 0bf44237b..000000000 --- a/src/components/layout/Navbar.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { createStyles, Navbar as MantineNavbar } from '@mantine/core'; -import Widgets from './Widgets'; - -const useStyles = createStyles((theme) => ({ - hide: { - [theme.fn.smallerThan('xs')]: { - display: 'none', - }, - }, - burger: { - [theme.fn.largerThan('sm')]: { - display: 'none', - }, - }, -})); - -export default function Navbar() { - const { classes, cx } = useStyles(); - - return ( - - ); -} diff --git a/src/components/layout/Widgets.tsx b/src/components/layout/Widgets.tsx deleted file mode 100644 index 70a6759ef..000000000 --- a/src/components/layout/Widgets.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Stack } from '@mantine/core'; -import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../../modules'; -import { DashdotModule } from '../../modules/dashdot'; -import { ModuleWrapper } from '../../modules/moduleWrapper'; - -export default function Widgets(props: any) { - return ( - - - - - - - - ); -} diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index e505cac06..619b10fce 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -58,7 +58,9 @@ export function Search() { // TODO: ask manuel-rw about overseerr // Answer: We can simply check if there is a app of the type overseer and display results if there is one. // Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results - const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr; + const isOverseerrEnabled = config?.apps.some( + (x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr' + ); const overseerrApp = config?.apps.find( (app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr' ); diff --git a/src/modules/calendar/CalendarModule.tsx b/src/modules/calendar/CalendarModule.tsx deleted file mode 100644 index b5aa42b03..000000000 --- a/src/modules/calendar/CalendarModule.tsx +++ /dev/null @@ -1,318 +0,0 @@ -/* eslint-disable react/no-children-prop */ -import { - Box, - Divider, - Indicator, - Popover, - ScrollArea, - createStyles, - useMantineTheme, - Space, -} from '@mantine/core'; -import React, { useEffect, useState } from 'react'; -import { Calendar } from '@mantine/dates'; -import { IconCalendar as CalendarIcon } from '@tabler/icons'; -import axios from 'axios'; -import { useDisclosure } from '@mantine/hooks'; -import { useConfig } from '../../tools/state'; -import { IModule } from '../ModuleTypes'; -import { - SonarrMediaDisplay, - RadarrMediaDisplay, - LidarrMediaDisplay, - ReadarrMediaDisplay, -} from '../common'; -import { serviceItem } from '../../tools/types'; -import { useColorTheme } from '../../tools/color'; - -export const CalendarModule: IModule = { - title: 'Calendar', - icon: CalendarIcon, - component: CalendarComponent, - options: { - sundaystart: { - name: 'descriptor.settings.sundayStart.label', - value: false, - }, - }, - id: 'calendar', -}; - -export default function CalendarComponent(props: any) { - const { config } = useConfig(); - const theme = useMantineTheme(); - const { secondaryColor } = useColorTheme(); - const useStyles = createStyles((theme) => ({ - weekend: { - color: `${secondaryColor} !important`, - }, - })); - - const [sonarrMedias, setSonarrMedias] = useState([] as any); - const [lidarrMedias, setLidarrMedias] = useState([] as any); - const [radarrMedias, setRadarrMedias] = useState([] as any); - const [readarrMedias, setReadarrMedias] = useState([] as any); - const sonarrServices = config.apps.filter((service) => service.type === 'Sonarr'); - const radarrServices = config.apps.filter((service) => service.type === 'Radarr'); - const lidarrServices = config.apps.filter((service) => service.type === 'Lidarr'); - const readarrServices = config.apps.filter((service) => service.type === 'Readarr'); - const today = new Date(); - - const { classes, cx } = useStyles(); - - function getMedias(service: serviceItem | undefined, type: string) { - if (!service || !service.apiKey) { - return Promise.resolve({ data: [] }); - } - return axios.post(`/api/modules/calendar?type=${type}`, { id: service.id }); - } - - useEffect(() => { - // Create each Sonarr service and get the medias - const currentSonarrMedias: any[] = []; - Promise.all( - sonarrServices.map((service) => - getMedias(service, 'sonarr') - .then((res) => { - currentSonarrMedias.push(...res.data); - }) - .catch(() => { - currentSonarrMedias.push([]); - }) - ) - ).then(() => { - setSonarrMedias(currentSonarrMedias); - }); - const currentRadarrMedias: any[] = []; - Promise.all( - radarrServices.map((service) => - getMedias(service, 'radarr') - .then((res) => { - currentRadarrMedias.push(...res.data); - }) - .catch(() => { - currentRadarrMedias.push([]); - }) - ) - ).then(() => { - setRadarrMedias(currentRadarrMedias); - }); - const currentLidarrMedias: any[] = []; - Promise.all( - lidarrServices.map((service) => - getMedias(service, 'lidarr') - .then((res) => { - currentLidarrMedias.push(...res.data); - }) - .catch(() => { - currentLidarrMedias.push([]); - }) - ) - ).then(() => { - setLidarrMedias(currentLidarrMedias); - }); - const currentReadarrMedias: any[] = []; - Promise.all( - readarrServices.map((service) => - getMedias(service, 'readarr') - .then((res) => { - currentReadarrMedias.push(...res.data); - }) - .catch(() => { - currentReadarrMedias.push([]); - }) - ) - ).then(() => { - setReadarrMedias(currentReadarrMedias); - }); - }, [config.apps]); - - const weekStartsAtSunday = - (config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false; - return ( - {}} - dayStyle={(date) => - date.getDay() === today.getDay() && date.getDate() === today.getDate() - ? { - backgroundColor: - theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0], - margin: 1, - } - : { - margin: 1, - } - } - allowLevelChange={false} - dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })} - renderDay={(renderdate) => ( - - )} - /> - ); -} - -function DayComponent(props: any) { - const { - renderdate, - sonarrmedias, - radarrmedias, - lidarrmedias, - readarrmedias, - }: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } = - props; - const [opened, { close, open }] = useDisclosure(false); - - const day = renderdate.getDate(); - - const readarrFiltered = readarrmedias.filter((media: any) => { - const date = new Date(media.releaseDate); - return date.toDateString() === renderdate.toDateString(); - }); - - const lidarrFiltered = lidarrmedias.filter((media: any) => { - const date = new Date(media.releaseDate); - return date.toDateString() === renderdate.toDateString(); - }); - const sonarrFiltered = sonarrmedias.filter((media: any) => { - const date = new Date(media.airDateUtc); - return date.toDateString() === renderdate.toDateString(); - }); - const radarrFiltered = radarrmedias.filter((media: any) => { - const date = new Date(media.inCinemas); - return date.toDateString() === renderdate.toDateString(); - }); - const totalFiltered = [ - ...readarrFiltered, - ...lidarrFiltered, - ...sonarrFiltered, - ...radarrFiltered, - ]; - if (totalFiltered.length === 0) { - return
{day}
; - } - - return ( - - - - {readarrFiltered.length > 0 && ( - - )} - {radarrFiltered.length > 0 && ( - - )} - {sonarrFiltered.length > 0 && ( - - )} - {lidarrFiltered.length > 0 && ( - - )} -
{day}
-
-
- - 1 ? totalFiltered.slice(0, 2).length * 150 : 220, - width: 400, - }} - > - - {sonarrFiltered.map((media: any, index: number) => ( - - - {index < sonarrFiltered.length - 1 && } - - ))} - {radarrFiltered.length > 0 && sonarrFiltered.length > 0 && ( - - )} - {radarrFiltered.map((media: any, index: number) => ( - - - {index < radarrFiltered.length - 1 && } - - ))} - {sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && ( - - )} - {lidarrFiltered.map((media: any, index: number) => ( - - - {index < lidarrFiltered.length - 1 && } - - ))} - {lidarrFiltered.length > 0 && readarrFiltered.length > 0 && ( - - )} - {readarrFiltered.map((media: any, index: number) => ( - - - {index < readarrFiltered.length - 1 && } - - ))} - - -
- ); -} diff --git a/src/modules/calendar/index.ts b/src/modules/calendar/index.ts deleted file mode 100644 index ccb16b383..000000000 --- a/src/modules/calendar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CalendarModule } from './CalendarModule'; diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 0a653554a..6c32ccfba 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -2,9 +2,8 @@ import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core'; import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; +import { useConfigContext } from '../../config/provider'; import { useColorTheme } from '../../tools/color'; -import { useConfig } from '../../tools/state'; -import { serviceItem } from '../../tools/types'; import { RequestModal } from '../overseerr/RequestModal'; import { Result } from '../overseerr/SearchResult'; @@ -27,9 +26,15 @@ export interface IMedia { export function OverseerrMediaDisplay(props: any) { const { media }: { media: Result } = props; - const { config } = useConfig(); + const { config } = useConfigContext(); + + if (!config) { + return null; + } + const service = config.apps.find( - (service) => service.type === 'Overseerr' || service.type === 'Jellyseerr' + (service) => + service.integration.type === 'overseerr' || service.integration.type === 'jellyseerr' ); return ( @@ -45,9 +50,9 @@ export function OverseerrMediaDisplay(props: any) { plexUrl: media.mediaInfo?.plexUrl ?? media.mediaInfo?.mediaUrl, voteAverage: media.voteAverage?.toString(), overseerrResult: media, - overseerrId: `${service?.openedUrl ? service?.openedUrl : service?.url}/${ - media.mediaType - }/${media.id}`, + overseerrId: `${ + service?.behaviour.externalUrl ? service.behaviour.externalUrl : service?.url + }/${media.mediaType}/${media.id}`, type: 'overseer', }} /> @@ -56,16 +61,21 @@ export function OverseerrMediaDisplay(props: any) { export function ReadarrMediaDisplay(props: any) { const { media }: { media: any } = props; - const { config } = useConfig(); + const { config } = useConfigContext(); + + if (!config) { + return null; + } + // Find lidarr in services - const readarr = config.apps.find((service: serviceItem) => service.type === 'Readarr'); + const readarr = config.apps.find((service) => service.integration.type === 'readarr'); // Find a poster CoverType const poster = media.images.find((image: any) => image.coverType === 'cover'); if (!readarr) { return null; } - const baseUrl = readarr.openedUrl - ? new URL(readarr.openedUrl).origin + const baseUrl = readarr.behaviour.externalUrl + ? new URL(readarr.behaviour.externalUrl).origin : new URL(readarr.url).origin; // Remove '/' from the end of the lidarr url const fullLink = poster ? `${baseUrl}${poster.url}` : undefined; @@ -88,15 +98,22 @@ export function ReadarrMediaDisplay(props: any) { export function LidarrMediaDisplay(props: any) { const { media }: { media: any } = props; - const { config } = useConfig(); + const { config } = useConfigContext(); + + if (!config) { + return null; + } + // Find lidarr in services - const lidarr = config.apps.find((service: serviceItem) => service.type === 'Lidarr'); + const lidarr = config.apps.find((service) => service.integration.type === 'lidarr'); // Find a poster CoverType const poster = media.images.find((image: any) => image.coverType === 'cover'); if (!lidarr) { return null; } - const baseUrl = lidarr.openedUrl ? new URL(lidarr.openedUrl).origin : new URL(lidarr.url).origin; + const baseUrl = lidarr.behaviour.externalUrl + ? new URL(lidarr.behaviour.externalUrl).origin + : new URL(lidarr.url).origin; // Remove '/' from the end of the lidarr url const fullLink = poster ? `${baseUrl}${poster.url}` : undefined; // Return a movie poster containting the title and the description diff --git a/src/modules/dashdot/DashdotModule.tsx b/src/modules/dashdot/DashdotModule.tsx deleted file mode 100644 index 1d3b5174d..000000000 --- a/src/modules/dashdot/DashdotModule.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core'; -import { IconCalendar as CalendarIcon } from '@tabler/icons'; -import axios from 'axios'; -import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; -import { useConfig } from '../../tools/state'; -import { serviceItem } from '../../tools/types'; -import { IModule } from '../ModuleTypes'; - -const asModule = (t: T) => t; -export const DashdotModule = asModule({ - title: 'Dash.', - icon: CalendarIcon, - component: DashdotComponent, - options: { - cpuMultiView: { - name: 'descriptor.settings.cpuMultiView.label', - value: false, - }, - storageMultiView: { - name: 'descriptor.settings.storageMultiView.label', - value: false, - }, - useCompactView: { - name: 'descriptor.settings.useCompactView.label', - value: false, - }, - graphs: { - name: 'descriptor.settings.graphs.label', - value: ['CPU', 'RAM', 'Storage', 'Network'], - options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'], - }, - url: { - name: 'descriptor.settings.url.label', - value: '', - }, - }, - id: 'dashdot', -}); - -const useStyles = createStyles((theme, _params, getRef) => ({ - heading: { - marginTop: 0, - marginBottom: 10, - }, - table: { - display: 'table', - }, - tableRow: { - display: 'table-row', - }, - tableLabel: { - display: 'table-cell', - paddingRight: 10, - }, - tableValue: { - display: 'table-cell', - whiteSpace: 'pre-wrap', - paddingBottom: 5, - }, - graphsContainer: { - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - rowGap: 10, - columnGap: 10, - }, - iframe: { - flex: '1 0 auto', - maxWidth: '100%', - height: '140px', - borderRadius: theme.radius.lg, - border: 'none', - colorScheme: 'none', - }, - graphTitle: { - ref: getRef('graphTitle'), - position: 'absolute', - right: 0, - opacity: 0, - transition: 'opacity .1s ease-in-out', - pointerEvents: 'none', - }, - graphStack: { - [`&:hover .${getRef('graphTitle')}`]: { - opacity: 0.5, - }, - }, -})); - -const bpsPrettyPrint = (bits?: number) => - !bits - ? '-' - : bits > 1000 * 1000 * 1000 - ? `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s` - : bits > 1000 * 1000 - ? `${(bits / 1000 / 1000).toFixed(1)} Mb/s` - : bits > 1000 - ? `${(bits / 1000).toFixed(1)} Kb/s` - : `${bits.toFixed(1)} b/s`; - -const bytePrettyPrint = (byte: number): string => - byte > 1024 * 1024 * 1024 - ? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB` - : byte > 1024 * 1024 - ? `${(byte / 1024 / 1024).toFixed(1)} MiB` - : byte > 1024 - ? `${(byte / 1024).toFixed(1)} KiB` - : `${byte.toFixed(1)} B`; - -const useJson = (targetUrl: string, url: string) => { - const [data, setData] = useState(); - - const doRequest = async () => { - try { - const resp = await axios.get('/api/modules/dashdot', { params: { url, base: targetUrl } }); - - setData(resp.data); - // eslint-disable-next-line no-empty - } catch (e) {} - }; - - useEffect(() => { - if (targetUrl) { - doRequest(); - } - }, [targetUrl]); - - return data; -}; - -export function DashdotComponent() { - const { config } = useConfig(); - const theme = useMantineTheme(); - const { classes } = useStyles(); - const { colorScheme } = useMantineColorScheme(); - - const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options']; - const isCompact = dashConfig?.useCompactView?.value ?? false; - const dashdotService: serviceItem | undefined = config.apps.filter( - (service) => service.type === 'Dash.' - )[0]; - const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? ''; - const enabledGraphs = dashConfig?.graphs?.value ?? ['CPU', 'RAM', 'Storage', 'Network']; - const cpuEnabled = enabledGraphs.includes('CPU'); - const storageEnabled = enabledGraphs.includes('Storage'); - const ramEnabled = enabledGraphs.includes('RAM'); - const networkEnabled = enabledGraphs.includes('Network'); - const gpuEnabled = enabledGraphs.includes('GPU'); - - const info = useJson(dashdotUrl, '/info'); - const storageLoad = useJson(dashdotUrl, '/load/storage'); - - const totalUsed = - (storageLoad?.layout as any[])?.reduce((acc, curr) => (curr.load ?? 0) + acc, 0) ?? 0; - const totalSize = - (info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0; - - const { t } = useTranslation('modules/dashdot'); - - const graphs = [ - { - id: 'cpu', - name: t('card.graphs.cpu.title'), - enabled: cpuEnabled, - params: { - multiView: dashConfig?.cpuMultiView?.value ?? false, - }, - }, - { - id: 'storage', - name: t('card.graphs.storage.title'), - enabled: storageEnabled && !isCompact, - params: { - multiView: dashConfig?.storageMultiView?.value ?? false, - }, - }, - { - id: 'ram', - name: t('card.graphs.memory.title'), - enabled: ramEnabled, - }, - { - id: 'network', - name: t('card.graphs.network.title'), - enabled: networkEnabled, - spanTwo: true, - }, - { - id: 'gpu', - name: t('card.graphs.gpu.title'), - enabled: gpuEnabled, - spanTwo: true, - }, - ].filter((g) => g.enabled); - - if (dashdotUrl === '') { - return ( -
-

{t('card.title')}

-

{t('card.errors.noService')}

-
- ); - } - - return ( -
-

{t('card.title')}

- - {!info ? ( -

{t('card.errors.noInformation')}

- ) : ( -
-
- {storageEnabled && isCompact && ( -
-

{t('card.graphs.storage.label')}

-

- {((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'} - {bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)} -

-
- )} - {networkEnabled && ( -
-

{t('card.graphs.network.label')}

-

- {bpsPrettyPrint(info?.network?.speedUp)} {t('card.graphs.network.metrics.upload')} - {'\n'} - {bpsPrettyPrint(info?.network?.speedDown)} - {t('card.graphs.network.metrics.download')} -

-
- )} -
- - {graphs.map((graph) => ( - - - {graph.name} - -