mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack
This commit is contained in:
64
src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
Normal file
64
src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Box, Indicator, IndicatorProps, Popover } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { MediaList } from './MediaList';
|
||||
import { MediasType } from './type';
|
||||
|
||||
interface CalendarDayProps {
|
||||
date: Date;
|
||||
medias: MediasType;
|
||||
}
|
||||
|
||||
export const CalendarDay = ({ date, medias }: CalendarDayProps) => {
|
||||
const [opened, { close, open }] = useDisclosure(false);
|
||||
|
||||
if (medias.totalCount === 0) {
|
||||
return <div>{date.getDate()}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="bottom"
|
||||
withArrow
|
||||
withinPortal
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
transition="pop"
|
||||
onClose={close}
|
||||
opened={opened}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Box onClick={open}>
|
||||
<DayIndicator color={'red'} position="bottom-start" medias={medias.books}>
|
||||
<DayIndicator color={'yellow'} position="top-start" medias={medias.movies}>
|
||||
<DayIndicator color={'blue'} position="top-end" medias={medias.tvShows}>
|
||||
<DayIndicator color={'green'} position="bottom-end" medias={medias.musics}>
|
||||
<div>{date.getDate()}</div>
|
||||
</DayIndicator>
|
||||
</DayIndicator>
|
||||
</DayIndicator>
|
||||
</DayIndicator>
|
||||
</Box>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<MediaList medias={medias} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
interface DayIndicatorProps {
|
||||
color: string;
|
||||
medias: any[];
|
||||
children: JSX.Element;
|
||||
position: IndicatorProps['position'];
|
||||
}
|
||||
|
||||
const DayIndicator = ({ color, medias, children, position }: DayIndicatorProps) => {
|
||||
if (medias.length === 0) return children;
|
||||
|
||||
return (
|
||||
<Indicator size={10} withBorder offset={5} color={color} position={position}>
|
||||
{children}
|
||||
</Indicator>
|
||||
);
|
||||
};
|
||||
100
src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
Normal file
100
src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Card, createStyles, MantineThemeColors, useMantineTheme } from '@mantine/core';
|
||||
import { Calendar } from '@mantine/dates';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useColorTheme } from '../../../../tools/color';
|
||||
import { isToday } from '../../../../tools/isToday';
|
||||
import { CalendarIntegrationType } from '../../../../types/integration';
|
||||
import { BaseTileProps } from '../type';
|
||||
import { CalendarDay } from './CalendarDay';
|
||||
import { MediasType } from './type';
|
||||
|
||||
interface CalendarTileProps extends BaseTileProps {
|
||||
module: CalendarIntegrationType | undefined;
|
||||
}
|
||||
|
||||
export const CalendarTile = ({ className, module }: CalendarTileProps) => {
|
||||
const { secondaryColor } = useColorTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const { classes, cx } = useStyles(secondaryColor);
|
||||
const { colorScheme, colors } = useMantineTheme();
|
||||
const [month, setMonth] = useState(new Date());
|
||||
|
||||
const { data: medias } = useQuery({
|
||||
queryKey: ['calendar/medias', { month: month.getMonth(), year: month.getFullYear() }],
|
||||
queryFn: async () =>
|
||||
(await (
|
||||
await fetch(
|
||||
`/api/modules/calendar?year=${month.getFullYear()}&month=${
|
||||
month.getMonth() + 1
|
||||
}&configName=${configName}`
|
||||
)
|
||||
).json()) as MediasType,
|
||||
});
|
||||
|
||||
if (!module) return <></>;
|
||||
|
||||
return (
|
||||
<Card className={className} withBorder p="xs">
|
||||
<Calendar
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
size="xs"
|
||||
fullWidth
|
||||
onChange={() => {}}
|
||||
firstDayOfWeek={module.properties?.isWeekStartingAtSunday ? 'sunday' : 'monday'}
|
||||
dayStyle={(date) => ({
|
||||
margin: 1,
|
||||
backgroundColor: isToday(date)
|
||||
? colorScheme === 'dark'
|
||||
? colors.dark[5]
|
||||
: colors.gray[0]
|
||||
: undefined,
|
||||
})}
|
||||
styles={{
|
||||
calendarHeader: {
|
||||
marginRight: 40,
|
||||
marginLeft: 40,
|
||||
},
|
||||
}}
|
||||
allowLevelChange={false}
|
||||
dayClassName={(_, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
|
||||
renderDay={(date) => (
|
||||
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date)} />
|
||||
)}
|
||||
></Calendar>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme, secondaryColor: keyof MantineThemeColors) => ({
|
||||
weekend: {
|
||||
color: `${secondaryColor} !important`,
|
||||
},
|
||||
}));
|
||||
|
||||
const getReleasedMediasForDate = (medias: MediasType | undefined, date: Date): MediasType => {
|
||||
const books =
|
||||
medias?.books.filter((b) => new Date(b.releaseDate).toDateString() === date.toDateString()) ??
|
||||
[];
|
||||
const movies =
|
||||
medias?.movies.filter((m) => new Date(m.inCinemas).toDateString() === date.toDateString()) ??
|
||||
[];
|
||||
const musics =
|
||||
medias?.musics.filter((m) => new Date(m.releaseDate).toDateString() === date.toDateString()) ??
|
||||
[];
|
||||
const tvShows =
|
||||
medias?.tvShows.filter(
|
||||
(tv) => new Date(tv.airDateUtc).toDateString() === date.toDateString()
|
||||
) ?? [];
|
||||
const totalCount = medias ? books.length + movies.length + musics.length + tvShows.length : 0;
|
||||
|
||||
return {
|
||||
books,
|
||||
movies,
|
||||
musics,
|
||||
tvShows,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
66
src/components/Dashboard/Tiles/Calendar/MediaList.tsx
Normal file
66
src/components/Dashboard/Tiles/Calendar/MediaList.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createStyles, Divider, ScrollArea } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import {
|
||||
LidarrMediaDisplay,
|
||||
RadarrMediaDisplay,
|
||||
ReadarrMediaDisplay,
|
||||
SonarrMediaDisplay,
|
||||
} from '../../../../modules/common';
|
||||
import { MediasType } from './type';
|
||||
|
||||
interface MediaListProps {
|
||||
medias: MediasType;
|
||||
}
|
||||
|
||||
export const MediaList = ({ medias }: MediaListProps) => {
|
||||
const { classes } = useStyles();
|
||||
const lastMediaType = getLastMediaType(medias);
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars
|
||||
scrollbarSize={5}
|
||||
pt={5}
|
||||
className={classes.scrollArea}
|
||||
styles={{
|
||||
viewport: {
|
||||
maxHeight: 450,
|
||||
minHeight: 210,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{mapMedias(medias.tvShows, SonarrMediaDisplay, lastMediaType === 'tv-show')}
|
||||
{mapMedias(medias.movies, RadarrMediaDisplay, lastMediaType === 'movie')}
|
||||
{mapMedias(medias.musics, LidarrMediaDisplay, lastMediaType === 'music')}
|
||||
{mapMedias(medias.books, ReadarrMediaDisplay, lastMediaType === 'book')}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
const mapMedias = (
|
||||
medias: any[],
|
||||
MediaComponent: (props: { media: any }) => JSX.Element | null,
|
||||
containsLastItem: boolean
|
||||
) => {
|
||||
return medias.map((media, index) => (
|
||||
<React.Fragment>
|
||||
<MediaComponent media={media} />
|
||||
{containsLastItem && index === medias.length - 1 ? null : <MediaDivider />}
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
const MediaDivider = () => <Divider variant="dashed" size="sm" my="xl" />;
|
||||
|
||||
const getLastMediaType = (medias: MediasType) => {
|
||||
if (medias.books.length >= 1) return 'book';
|
||||
if (medias.musics.length >= 1) return 'music';
|
||||
if (medias.movies.length >= 1) return 'movie';
|
||||
return 'tv-show';
|
||||
};
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
scrollArea: {
|
||||
width: 400,
|
||||
},
|
||||
}));
|
||||
7
src/components/Dashboard/Tiles/Calendar/type.ts
Normal file
7
src/components/Dashboard/Tiles/Calendar/type.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface MediasType {
|
||||
tvShows: any[]; // Sonarr
|
||||
movies: any[]; // Radarr
|
||||
musics: any[]; // Lidarr
|
||||
books: any[]; // Readarr
|
||||
totalCount: number;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { openContextModalGeneric } from '../../../../tools/mantineModalManagerEx
|
||||
import { IntegrationsType } from '../../../../types/integration';
|
||||
import { TileBaseType } from '../../../../types/tile';
|
||||
import { GenericTileMenu } from '../GenericTileMenu';
|
||||
import { IntegrationChangePositionModalInnerProps } from '../IntegrationChangePositionModal';
|
||||
import { IntegrationChangePositionModalInnerProps } from '../../Modals/ChangePosition/ChangeIntegrationPositionModal';
|
||||
import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal';
|
||||
import {
|
||||
IntegrationEditModalInnerProps,
|
||||
|
||||
64
src/components/Dashboard/Tiles/Service/ServicePing.tsx
Normal file
64
src/components/Dashboard/Tiles/Service/ServicePing.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Indicator, Tooltip } from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { ServiceType } from '../../../../types/service';
|
||||
|
||||
interface ServicePingProps {
|
||||
service: ServiceType;
|
||||
}
|
||||
|
||||
export const ServicePing = ({ service }: ServicePingProps) => {
|
||||
const { t } = useTranslation('modules/ping');
|
||||
const { config } = useConfigContext();
|
||||
const active =
|
||||
(config?.settings.customization.layout.enabledPing && service.network.enabledStatusChecker) ??
|
||||
false;
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: [`ping/${service.id}`],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/modules/ping?url=${encodeURI(service.url)}`);
|
||||
const isOk = service.network.okStatus.includes(response.status);
|
||||
return {
|
||||
status: response.status,
|
||||
state: isOk ? 'online' : 'down',
|
||||
};
|
||||
},
|
||||
enabled: active,
|
||||
});
|
||||
|
||||
const isOnline = data?.state === 'online';
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
style={{ position: 'absolute', bottom: 20, right: 20 }}
|
||||
animate={{
|
||||
scale: isOnline ? [1, 0.7, 1] : 1,
|
||||
}}
|
||||
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
|
||||
>
|
||||
<Tooltip
|
||||
withinPortal
|
||||
radius="lg"
|
||||
label={
|
||||
isLoading
|
||||
? t('states.loading')
|
||||
: isOnline
|
||||
? t('states.online', { response: data.status })
|
||||
: t('states.offline', { response: data?.status })
|
||||
}
|
||||
>
|
||||
<Indicator
|
||||
size={15}
|
||||
color={isLoading ? 'yellow' : isOnline ? 'green' : 'red'}
|
||||
children={null}
|
||||
/>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
type PingState = 'loading' | 'down' | 'online';
|
||||
@@ -7,6 +7,7 @@ import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||
import { BaseTileProps } from '../type';
|
||||
import { ServiceMenu } from './ServiceMenu';
|
||||
import { ServicePing } from './ServicePing';
|
||||
|
||||
interface ServiceTileProps extends BaseTileProps {
|
||||
service: ServiceType;
|
||||
@@ -59,7 +60,7 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
||||
{inner}
|
||||
</UnstyledButton>
|
||||
)}
|
||||
{/*<ServicePing service={service} />*/}
|
||||
<ServicePing service={service} />
|
||||
</HomarrCardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { WeatherIcon } from './WeatherIcon';
|
||||
import { BaseTileProps } from '../type';
|
||||
import { useWeatherForCity } from './useWeatherForCity';
|
||||
import { WeatherIntegrationType } from '../../../../types/integration';
|
||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||
import { IntegrationsMenu } from '../Integrations/IntegrationsMenu';
|
||||
|
||||
interface WeatherTileProps extends BaseTileProps {
|
||||
module: WeatherIntegrationType | undefined;
|
||||
@@ -45,8 +45,17 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => {
|
||||
|
||||
return (
|
||||
<HomarrCardWrapper className={className}>
|
||||
<IntegrationsMenu
|
||||
integration="weather"
|
||||
module={module}
|
||||
options={module?.properties}
|
||||
labels={{
|
||||
isFahrenheit: 'descriptor.settings.displayInFahrenheit.label',
|
||||
location: 'descriptor.settings.location.label',
|
||||
}}
|
||||
/>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Group spacing="xl" noWrap align="center">
|
||||
<Group spacing="md" noWrap align="center">
|
||||
<WeatherIcon code={weather!.current_weather.weathercode} />
|
||||
<Stack p={0} spacing={4}>
|
||||
<Title order={2}>
|
||||
@@ -55,7 +64,7 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => {
|
||||
module?.properties.isFahrenheit
|
||||
)}
|
||||
</Title>
|
||||
<Group spacing="sm">
|
||||
<Group spacing="xs" noWrap>
|
||||
<div>
|
||||
<span>
|
||||
{getPerferedUnit(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IntegrationsType } from '../../../types/integration';
|
||||
import { CalendarTile } from './Calendar/CalendarTile';
|
||||
import { ClockTile } from './Clock/ClockTile';
|
||||
import { EmptyTile } from './EmptyTile';
|
||||
import { ServiceTile } from './Service/ServiceTile';
|
||||
@@ -34,7 +35,7 @@ export const Tiles: TileDefinitionProps = {
|
||||
maxHeight: 12,
|
||||
},
|
||||
calendar: {
|
||||
component: EmptyTile, //CalendarTile,
|
||||
component: CalendarTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 5,
|
||||
|
||||
Reference in New Issue
Block a user