mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
300 lines
7.4 KiB
TypeScript
300 lines
7.4 KiB
TypeScript
import { keepPreviousData } from '@tanstack/react-query';
|
|
import moment from 'moment';
|
|
import { useEffect, useMemo } from 'react';
|
|
import { create } from 'zustand';
|
|
import { setEpisodeQueryKey } from 'Episode/useEpisode';
|
|
import { Filter, FilterBuilderProp } from 'Filters/Filter';
|
|
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
|
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
|
import { filterBuilderValueTypes } from 'Helpers/Props';
|
|
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
|
|
import { CalendarItem } from 'typings/Calendar';
|
|
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
|
import translate from 'Utilities/String/translate';
|
|
import { getCalendarOption, useCalendarOption } from './calendarOptionsStore';
|
|
import { CalendarView } from './calendarViews';
|
|
|
|
export const FILTERS: Filter[] = [
|
|
{
|
|
key: 'all',
|
|
label: () => translate('All'),
|
|
filters: [],
|
|
},
|
|
{
|
|
key: 'monitored',
|
|
label: () => translate('MonitoredOnly'),
|
|
filters: [
|
|
{
|
|
key: 'unmonitored',
|
|
value: [false],
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const FILTER_BUILDER: FilterBuilderProp<CalendarItem>[] = [
|
|
{
|
|
name: 'unmonitored',
|
|
label: () => translate('IncludeUnmonitored'),
|
|
type: 'equal',
|
|
valueType: filterBuilderValueTypes.BOOL,
|
|
},
|
|
{
|
|
name: 'includeSpecials',
|
|
label: () => translate('IncludeSpecials'),
|
|
type: 'equal',
|
|
valueType: filterBuilderValueTypes.BOOL,
|
|
},
|
|
{
|
|
name: 'tags',
|
|
label: () => translate('Tags'),
|
|
type: 'contains',
|
|
valueType: filterBuilderValueTypes.TAG,
|
|
},
|
|
];
|
|
|
|
interface CalendarStore {
|
|
time: moment.Moment;
|
|
dates: string[];
|
|
dayCount: number;
|
|
searchMissingCommandId?: number;
|
|
}
|
|
|
|
const calendarStore = create<CalendarStore>(() => ({
|
|
time: moment(),
|
|
dates: [],
|
|
dayCount: 7,
|
|
queryKey: null,
|
|
}));
|
|
|
|
const VIEW_RANGES: Record<
|
|
CalendarView,
|
|
moment.unitOfTime.DurationConstructor | undefined
|
|
> = {
|
|
agenda: undefined,
|
|
day: 'day',
|
|
week: 'week',
|
|
month: 'month',
|
|
forecast: 'day',
|
|
};
|
|
|
|
const useCalendar = () => {
|
|
const dates = useCalendarDates();
|
|
const time = useCalendarTime();
|
|
const selectedFilterKey = useCalendarOption('selectedFilterKey');
|
|
const view = useCalendarOption('view');
|
|
const customFilters = useCustomFiltersList('calendar');
|
|
|
|
const { start, end } = useMemo(() => {
|
|
return getPopulatableRange(dates[0], dates[dates.length - 1], view);
|
|
}, [dates, view]);
|
|
|
|
const { includeUnmonitored, includeSpecials, tags } = useMemo(() => {
|
|
const selectedFilters = findSelectedFilters(
|
|
selectedFilterKey,
|
|
FILTERS,
|
|
customFilters
|
|
);
|
|
|
|
return selectedFilters.reduce<{
|
|
includeUnmonitored: boolean;
|
|
includeSpecials: boolean;
|
|
tags?: number[] | undefined;
|
|
}>(
|
|
(acc, filter) => {
|
|
if (filter.key === 'unmonitored' && Array.isArray(filter.value)) {
|
|
acc.includeUnmonitored = (filter.value as boolean[]).includes(true);
|
|
}
|
|
|
|
if (filter.key === 'includeSpecials' && Array.isArray(filter.value)) {
|
|
acc.includeSpecials = (filter.value as boolean[]).includes(true);
|
|
}
|
|
|
|
if (filter.key === 'tags' && filter.type === 'contains') {
|
|
acc.tags = filter.value as number[];
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{
|
|
includeUnmonitored: false,
|
|
includeSpecials: true,
|
|
}
|
|
);
|
|
}, [customFilters, selectedFilterKey]);
|
|
|
|
const { queryKey, ...result } = useApiQuery<CalendarItem[]>({
|
|
path: '/calendar',
|
|
queryParams: {
|
|
start,
|
|
end,
|
|
includeUnmonitored,
|
|
includeSpecials,
|
|
tags,
|
|
},
|
|
queryOptions: {
|
|
enabled: !!time && !!start && !!end,
|
|
placeholderData: keepPreviousData,
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
setEpisodeQueryKey('calendar', queryKey);
|
|
}, [queryKey]);
|
|
|
|
return {
|
|
...result,
|
|
data: result.data ?? [],
|
|
};
|
|
};
|
|
|
|
export default useCalendar;
|
|
|
|
export const useCalendarPage = () => {
|
|
const dayCount = useCalendarDayCount();
|
|
const time = useCalendarTime();
|
|
const view = useCalendarOption('view');
|
|
const { firstDayOfWeek } = useUiSettingsValues();
|
|
|
|
useEffect(() => {
|
|
const { dates } = getDates(time, view, firstDayOfWeek, dayCount);
|
|
|
|
calendarStore.setState({ dates });
|
|
}, [firstDayOfWeek, dayCount, time, view]);
|
|
};
|
|
|
|
export const useCalendarTime = () => {
|
|
return calendarStore((state) => state.time);
|
|
};
|
|
|
|
export const useCalendarDates = () => {
|
|
return calendarStore((state) => state.dates);
|
|
};
|
|
|
|
export const useCalendarDayCount = () => {
|
|
return calendarStore((state) => state.dayCount);
|
|
};
|
|
|
|
export const useCalendarRange = () => {
|
|
const dates = useCalendarDates();
|
|
|
|
return {
|
|
start: dates[0],
|
|
end: dates[dates.length - 1],
|
|
};
|
|
};
|
|
|
|
export const useCalendarSearchMissingCommandId = () => {
|
|
return calendarStore((state) => state.searchMissingCommandId);
|
|
};
|
|
|
|
export const setCalendarDayCount = (dayCount: number) => {
|
|
calendarStore.setState({ dayCount });
|
|
};
|
|
|
|
export const goToToday = () => {
|
|
setCalendarTime(moment());
|
|
};
|
|
|
|
export const goToPreviousRange = () => {
|
|
const { dayCount, time } = calendarStore.getState();
|
|
const view = getCalendarOption('view');
|
|
|
|
const amount = view === 'forecast' ? dayCount : 1;
|
|
const newTime = moment(time).subtract(amount, VIEW_RANGES[view]);
|
|
|
|
setCalendarTime(newTime);
|
|
};
|
|
|
|
export const goToNextRange = () => {
|
|
const { dayCount, time } = calendarStore.getState();
|
|
const view = getCalendarOption('view');
|
|
|
|
const amount = view === 'forecast' ? dayCount : 1;
|
|
const newTime = moment(time).add(amount, VIEW_RANGES[view]);
|
|
|
|
setCalendarTime(newTime);
|
|
};
|
|
|
|
const setCalendarTime = (time: moment.Moment) => {
|
|
calendarStore.setState({ time });
|
|
};
|
|
|
|
const getDays = (start: moment.Moment, end: moment.Moment) => {
|
|
const startTime = moment(start);
|
|
const endTime = moment(end);
|
|
const difference = endTime.diff(startTime, 'days');
|
|
|
|
return Array(difference + 1)
|
|
.fill(0)
|
|
.map((_, i) => startTime.clone().add(i, 'days').toISOString());
|
|
};
|
|
|
|
const getDates = (
|
|
time: moment.Moment,
|
|
view: CalendarView,
|
|
firstDayOfWeek: number,
|
|
dayCount: number
|
|
) => {
|
|
const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek';
|
|
|
|
let start = time.clone().startOf('day');
|
|
let end = time.clone().endOf('day');
|
|
|
|
if (view === 'week') {
|
|
start = time.clone().startOf(weekName);
|
|
end = time.clone().endOf(weekName);
|
|
}
|
|
|
|
if (view === 'forecast') {
|
|
start = time.clone().subtract(1, 'day').startOf('day');
|
|
end = time
|
|
.clone()
|
|
.add(dayCount - 2, 'days')
|
|
.endOf('day');
|
|
}
|
|
|
|
if (view === 'month') {
|
|
start = time.clone().startOf('month').startOf(weekName);
|
|
end = time.clone().endOf('month').endOf(weekName);
|
|
}
|
|
|
|
if (view === 'agenda') {
|
|
start = time.clone().subtract(1, 'day').startOf('day');
|
|
end = time.clone().add(1, 'month').endOf('day');
|
|
}
|
|
|
|
return {
|
|
start: start.toISOString(),
|
|
end: end.toISOString(),
|
|
time: time.toISOString(),
|
|
dates: getDays(start, end),
|
|
};
|
|
};
|
|
|
|
function getPopulatableRange(
|
|
startDate: string,
|
|
endDate: string,
|
|
view: CalendarView
|
|
) {
|
|
switch (view) {
|
|
case 'day':
|
|
return {
|
|
start: moment(startDate).subtract(1, 'day').toISOString(),
|
|
end: moment(endDate).add(1, 'day').toISOString(),
|
|
};
|
|
case 'week':
|
|
case 'forecast':
|
|
return {
|
|
start: moment(startDate).subtract(1, 'week').toISOString(),
|
|
end: moment(endDate).add(1, 'week').toISOString(),
|
|
};
|
|
default:
|
|
return {
|
|
start: startDate,
|
|
end: endDate,
|
|
};
|
|
}
|
|
}
|