mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Use react-query for Calendar UI
This commit is contained in:
parent
6a3e1278a5
commit
ccb7f07c26
28 changed files with 637 additions and 797 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { SetFilter } from 'Components/Filter/Filter';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setQueueOption } from './queueOptionsStore';
|
||||
import useQueue, { FILTER_BUILDER } from './useQueue';
|
||||
|
|
@ -8,12 +9,9 @@ type QueueFilterModalProps = FilterModalProps<History>;
|
|||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const { records } = useQueue();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
({ selectedFilterKey }: { selectedFilterKey: string | number }) => {
|
||||
setQueueOption('selectedFilterKey', selectedFilterKey);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const dispatchSetFilter = useCallback(({ selectedFilterKey }: SetFilter) => {
|
||||
setQueueOption('selectedFilterKey', selectedFilterKey);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ function QueueRow(props: QueueRowProps) {
|
|||
} = props;
|
||||
|
||||
const series = useSeries(seriesId);
|
||||
const episodes = useEpisodes(episodeIds, 'episodes');
|
||||
const episodes = useEpisodes(episodeIds);
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { FilterBuilderTypes } from 'Helpers/Props/filterBuilderTypes';
|
|||
import { DateFilterValue, FilterType } from 'Helpers/Props/filterTypes';
|
||||
import { Error } from './AppSectionState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CaptchaAppState from './CaptchaAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import CustomFiltersAppState from './CustomFiltersAppState';
|
||||
|
|
@ -81,7 +80,6 @@ export interface AppSectionState {
|
|||
interface AppState {
|
||||
app: AppSectionState;
|
||||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
captcha: CaptchaAppState;
|
||||
commands: CommandAppState;
|
||||
customFilters: CustomFiltersAppState;
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { CalendarView } from 'Calendar/calendarViews';
|
||||
import { CalendarItem } from 'typings/Calendar';
|
||||
|
||||
interface CalendarOptions {
|
||||
showEpisodeInformation: boolean;
|
||||
showFinaleIcon: boolean;
|
||||
showSpecialIcon: boolean;
|
||||
showCutoffUnmetIcon: boolean;
|
||||
collapseMultipleEpisodes: boolean;
|
||||
fullColorEvents: boolean;
|
||||
}
|
||||
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<CalendarItem>,
|
||||
AppSectionFilterState<CalendarItem> {
|
||||
searchMissingCommandId: number | null;
|
||||
start: moment.Moment;
|
||||
end: moment.Moment;
|
||||
dates: string[];
|
||||
time: string;
|
||||
view: CalendarView;
|
||||
options: CalendarOptions;
|
||||
}
|
||||
|
||||
export default CalendarAppState;
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import useCalendar from 'Calendar/useCalendar';
|
||||
import AgendaEvent from './AgendaEvent';
|
||||
import styles from './Agenda.css';
|
||||
|
||||
function Agenda() {
|
||||
const { items } = useSelector((state: AppState) => state.calendar);
|
||||
const { data } = useCalendar();
|
||||
|
||||
return (
|
||||
<div className={styles.agenda}>
|
||||
{items.map((item, index) => {
|
||||
{data.map((item, index) => {
|
||||
const momentDate = moment(item.airDateUtc);
|
||||
const showDate =
|
||||
index === 0 ||
|
||||
!moment(items[index - 1].airDateUtc).isSame(momentDate, 'day');
|
||||
!moment(data[index - 1].airDateUtc).isSame(momentDate, 'day');
|
||||
|
||||
return <AgendaEvent key={item.id} showDate={showDate} {...item} />;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
|
||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
|
|
@ -66,7 +66,7 @@ function AgendaEvent(props: AgendaEventProps) {
|
|||
showFinaleIcon,
|
||||
showSpecialIcon,
|
||||
showCutoffUnmetIcon,
|
||||
} = useSelector((state: AppState) => state.calendar.options);
|
||||
} = useCalendarOptions();
|
||||
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
|
@ -8,17 +7,11 @@ import Episode from 'Episode/Episode';
|
|||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearCalendar,
|
||||
fetchCalendar,
|
||||
gotoCalendarToday,
|
||||
} from 'Store/Actions/calendarActions';
|
||||
import {
|
||||
clearEpisodeFiles,
|
||||
fetchEpisodeFiles,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
|
|
@ -26,9 +19,11 @@ import {
|
|||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Agenda from './Agenda/Agenda';
|
||||
import { useCalendarOption } from './calendarOptionsStore';
|
||||
import CalendarDays from './Day/CalendarDays';
|
||||
import DaysOfWeek from './Day/DaysOfWeek';
|
||||
import CalendarHeader from './Header/CalendarHeader';
|
||||
import useCalendar, { goToToday } from './useCalendar';
|
||||
import styles from './Calendar.css';
|
||||
|
||||
const UPDATE_DELAY = 3600000; // 1 hour
|
||||
|
|
@ -38,54 +33,44 @@ function Calendar() {
|
|||
const requestCurrentPage = useCurrentPage();
|
||||
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const { isFetching, isPopulated, error, items, time, view } = useSelector(
|
||||
(state: AppState) => state.calendar
|
||||
);
|
||||
const { data, isFetching, isLoading, error, refetch } = useCalendar();
|
||||
const view = useCalendarOption('view');
|
||||
|
||||
const isRefreshingSeries = useSelector(
|
||||
createCommandExecutingSelector(commandNames.REFRESH_SERIES)
|
||||
);
|
||||
|
||||
const firstDayOfWeek = useSelector(
|
||||
(state: AppState) => state.settings.ui.item.firstDayOfWeek
|
||||
);
|
||||
|
||||
const wasRefreshingSeries = usePrevious(isRefreshingSeries);
|
||||
const previousFirstDayOfWeek = usePrevious(firstDayOfWeek);
|
||||
const previousItems = usePrevious(items);
|
||||
|
||||
const handleScheduleUpdate = useCallback(() => {
|
||||
clearTimeout(updateTimeout.current);
|
||||
|
||||
function updateCalendar() {
|
||||
dispatch(gotoCalendarToday());
|
||||
goToToday();
|
||||
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||
}
|
||||
|
||||
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleScheduleUpdate();
|
||||
|
||||
return () => {
|
||||
dispatch(clearCalendar());
|
||||
dispatch(clearEpisodeFiles());
|
||||
clearTimeout(updateTimeout.current);
|
||||
};
|
||||
}, [dispatch, handleScheduleUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchCalendar());
|
||||
} else {
|
||||
dispatch(gotoCalendarToday());
|
||||
if (!requestCurrentPage) {
|
||||
goToToday();
|
||||
}
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
refetch();
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate, [
|
||||
|
|
@ -96,53 +81,42 @@ function Calendar() {
|
|||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [time, view, dispatch]);
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
handleScheduleUpdate();
|
||||
}, [time, handleScheduleUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previousFirstDayOfWeek != null &&
|
||||
firstDayOfWeek !== previousFirstDayOfWeek
|
||||
) {
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
}
|
||||
}, [time, view, firstDayOfWeek, previousFirstDayOfWeek, dispatch]);
|
||||
}, [handleScheduleUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasRefreshingSeries && !isRefreshingSeries) {
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
refetch();
|
||||
}
|
||||
}, [time, view, isRefreshingSeries, wasRefreshingSeries, dispatch]);
|
||||
}, [isRefreshingSeries, wasRefreshingSeries, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previousItems || hasDifferentItems(items, previousItems)) {
|
||||
const episodeFileIds = selectUniqueIds<Episode, number>(
|
||||
items,
|
||||
'episodeFileId'
|
||||
);
|
||||
const episodeFileIds = selectUniqueIds<Episode, number>(
|
||||
data,
|
||||
'episodeFileId'
|
||||
);
|
||||
|
||||
if (episodeFileIds.length) {
|
||||
dispatch(fetchEpisodeFiles({ episodeFileIds }));
|
||||
}
|
||||
if (episodeFileIds.length) {
|
||||
dispatch(fetchEpisodeFiles({ episodeFileIds }));
|
||||
}
|
||||
}, [items, previousItems, dispatch]);
|
||||
}, [data, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.calendar}>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
{isLoading ? <LoadingIndicator /> : null}
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
||||
) : null}
|
||||
{!error && isPopulated && view === 'agenda' ? (
|
||||
{!error && !isLoading && view === 'agenda' ? (
|
||||
<div className={styles.calendarContent}>
|
||||
<CalendarHeader />
|
||||
<Agenda />
|
||||
</div>
|
||||
) : null}
|
||||
{!error && isPopulated && view !== 'agenda' ? (
|
||||
{!error && !isLoading && view !== 'agenda' ? (
|
||||
<div className={styles.calendarContent}>
|
||||
<CalendarHeader />
|
||||
<DaysOfWeek />
|
||||
|
|
|
|||
|
|
@ -1,49 +1,24 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { SetFilter } from 'Components/Filter/Filter';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
|
||||
function createCalendarSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items,
|
||||
(calendar) => {
|
||||
return calendar;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
import { setCalendarOption } from './calendarOptionsStore';
|
||||
import useCalendar, { FILTER_BUILDER } from './useCalendar';
|
||||
|
||||
type CalendarFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const { data } = useCalendar();
|
||||
const customFilterType = 'calendar';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setCalendarFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const dispatchSetFilter = useCallback(({ selectedFilterKey }: SetFilter) => {
|
||||
setCalendarOption('selectedFilterKey', selectedFilterKey);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
sectionItems={data}
|
||||
filterBuilderProps={FILTER_BUILDER}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,66 +3,61 @@ import React, { useCallback } from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useQueueDetails } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import Queue from 'typings/Queue';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import useCalendar, {
|
||||
useCalendarRange,
|
||||
useCalendarSearchMissingCommandId,
|
||||
} from './useCalendar';
|
||||
|
||||
function createIsSearchingSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.searchMissingCommandId,
|
||||
createCommandsSelector(),
|
||||
(searchMissingCommandId, commands) => {
|
||||
if (searchMissingCommandId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isCommandExecuting(
|
||||
commands.find((command) => {
|
||||
return command.id === searchMissingCommandId;
|
||||
})
|
||||
);
|
||||
function createIsSearchingSelector(searchMissingCommandId: number | undefined) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
if (searchMissingCommandId == null) {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return isCommandExecuting(
|
||||
commands.find((command) => {
|
||||
return command.id === searchMissingCommandId;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createMissingEpisodeIdsSelector(queueDetails: Queue[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.start,
|
||||
(state: AppState) => state.calendar.end,
|
||||
(state: AppState) => state.calendar.items,
|
||||
(start, end, episodes) => {
|
||||
return episodes.reduce<number[]>((acc, episode) => {
|
||||
const airDateUtc = episode.airDateUtc;
|
||||
const useMissingEpisodeIdsSelector = () => {
|
||||
const { start, end } = useCalendarRange();
|
||||
const { data } = useCalendar();
|
||||
const queueDetails = useQueueDetails();
|
||||
|
||||
if (
|
||||
!episode.episodeFileId &&
|
||||
moment(airDateUtc).isAfter(start) &&
|
||||
moment(airDateUtc).isBefore(end) &&
|
||||
isBefore(episode.airDateUtc) &&
|
||||
!queueDetails.some(
|
||||
(details) => !!details.episode && details.episode.id === episode.id
|
||||
)
|
||||
) {
|
||||
acc.push(episode.id);
|
||||
}
|
||||
return data.reduce<number[]>((acc, episode) => {
|
||||
const airDateUtc = episode.airDateUtc;
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
if (
|
||||
!episode.episodeFileId &&
|
||||
moment(airDateUtc).isAfter(start) &&
|
||||
moment(airDateUtc).isBefore(end) &&
|
||||
isBefore(episode.airDateUtc) &&
|
||||
!queueDetails.some(
|
||||
(details) => !!details.episode && details.episode.id === episode.id
|
||||
)
|
||||
) {
|
||||
acc.push(episode.id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default function CalendarMissingEpisodeSearchButton() {
|
||||
const queueDetails = useQueueDetails();
|
||||
const missingEpisodeIds = useSelector(
|
||||
createMissingEpisodeIdsSelector(queueDetails)
|
||||
const searchMissingCommandId = useCalendarSearchMissingCommandId();
|
||||
const missingEpisodeIds = useMissingEpisodeIdsSelector();
|
||||
const isSearchingForMissing = useSelector(
|
||||
createIsSearchingSelector(searchMissingCommandId)
|
||||
);
|
||||
const isSearchingForMissing = useSelector(createIsSearchingSelector());
|
||||
|
||||
const handlePress = useCallback(() => {}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import QueueDetails from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
|
@ -14,10 +13,6 @@ import Episode from 'Episode/Episode';
|
|||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
import {
|
||||
setCalendarDaysCount,
|
||||
setCalendarFilter,
|
||||
} from 'Store/Actions/calendarActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
|
|
@ -27,9 +22,15 @@ import translate from 'Utilities/String/translate';
|
|||
import Calendar from './Calendar';
|
||||
import CalendarFilterModal from './CalendarFilterModal';
|
||||
import CalendarMissingEpisodeSearchButton from './CalendarMissingEpisodeSearchButton';
|
||||
import { setCalendarOption, useCalendarOption } from './calendarOptionsStore';
|
||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||
import Legend from './Legend/Legend';
|
||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||
import useCalendar, {
|
||||
FILTERS,
|
||||
setCalendarDayCount,
|
||||
useCalendarPage,
|
||||
} from './useCalendar';
|
||||
import styles from './CalendarPage.css';
|
||||
|
||||
const MINIMUM_DAY_WIDTH = 120;
|
||||
|
|
@ -37,9 +38,11 @@ const MINIMUM_DAY_WIDTH = 120;
|
|||
function CalendarPage() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { selectedFilterKey, filters, items } = useSelector(
|
||||
(state: AppState) => state.calendar
|
||||
);
|
||||
const selectedFilterKey = useCalendarOption('selectedFilterKey');
|
||||
const { data } = useCalendar();
|
||||
|
||||
useCalendarPage();
|
||||
|
||||
const isRssSyncExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC)
|
||||
);
|
||||
|
|
@ -77,16 +80,13 @@ function CalendarPage() {
|
|||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(key: string | number) => {
|
||||
dispatch(setCalendarFilter({ selectedFilterKey: key }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleFilterSelect = useCallback((key: string | number) => {
|
||||
setCalendarOption('selectedFilterKey', key);
|
||||
}, []);
|
||||
|
||||
const episodeIds = useMemo(() => {
|
||||
return selectUniqueIds<Episode, number>(items, 'id');
|
||||
}, [items]);
|
||||
return selectUniqueIds<Episode, number>(data, 'id');
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (width === 0) {
|
||||
|
|
@ -98,8 +98,8 @@ function CalendarPage() {
|
|||
Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))
|
||||
);
|
||||
|
||||
dispatch(setCalendarDaysCount({ dayCount }));
|
||||
}, [width, dispatch]);
|
||||
setCalendarDayCount(dayCount);
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<QueueDetails episodeIds={episodeIds}>
|
||||
|
|
@ -135,7 +135,7 @@ function CalendarPage() {
|
|||
alignMenu={align.RIGHT}
|
||||
isDisabled={!hasSeries}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
filters={FILTERS}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CalendarFilterModal}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
|
||||
import * as calendarViews from 'Calendar/calendarViews';
|
||||
import CalendarEvent from 'Calendar/Events/CalendarEvent';
|
||||
import CalendarEventGroup from 'Calendar/Events/CalendarEventGroup';
|
||||
import useCalendar, { useCalendarTime } from 'Calendar/useCalendar';
|
||||
import {
|
||||
CalendarEvent as CalendarEventModel,
|
||||
CalendarEventGroup as CalendarEventGroupModel,
|
||||
|
|
@ -28,63 +27,61 @@ function sort(items: (CalendarEventModel | CalendarEventGroupModel)[]) {
|
|||
});
|
||||
}
|
||||
|
||||
function createCalendarEventsConnector(date: string) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items,
|
||||
(state: AppState) => state.calendar.options.collapseMultipleEpisodes,
|
||||
(items, collapseMultipleEpisodes) => {
|
||||
const momentDate = moment(date);
|
||||
|
||||
const filtered = items.filter((item) => {
|
||||
return momentDate.isSame(moment(item.airDateUtc), 'day');
|
||||
});
|
||||
|
||||
if (!collapseMultipleEpisodes) {
|
||||
return sort(
|
||||
filtered.map((item) => ({
|
||||
isGroup: false,
|
||||
...item,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const groupedObject = Object.groupBy(
|
||||
filtered,
|
||||
(item: CalendarItem) => `${item.seriesId}-${item.seasonNumber}`
|
||||
);
|
||||
|
||||
const grouped = Object.entries(groupedObject).reduce<
|
||||
(CalendarEventModel | CalendarEventGroupModel)[]
|
||||
>((acc, [, events]) => {
|
||||
if (!events) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (events.length === 1) {
|
||||
acc.push({
|
||||
isGroup: false,
|
||||
...events[0],
|
||||
});
|
||||
} else {
|
||||
acc.push({
|
||||
isGroup: true,
|
||||
seriesId: events[0].seriesId,
|
||||
seasonNumber: events[0].seasonNumber,
|
||||
episodeIds: events.map((event) => event.id),
|
||||
events: events.sort(
|
||||
(a, b) =>
|
||||
moment(a.airDateUtc).unix() - moment(b.airDateUtc).unix()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return sort(grouped);
|
||||
}
|
||||
const useCalendarEvents = (date: string) => {
|
||||
const { data } = useCalendar();
|
||||
const collapseMultipleEpisodes = useCalendarOption(
|
||||
'collapseMultipleEpisodes'
|
||||
);
|
||||
}
|
||||
|
||||
const momentDate = moment(date);
|
||||
|
||||
const filtered = data.filter((item) => {
|
||||
return momentDate.isSame(moment(item.airDateUtc), 'day');
|
||||
});
|
||||
|
||||
if (!collapseMultipleEpisodes) {
|
||||
return sort(
|
||||
filtered.map((item) => ({
|
||||
isGroup: false,
|
||||
...item,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const groupedObject = Object.groupBy(
|
||||
filtered,
|
||||
(item: CalendarItem) => `${item.seriesId}-${item.seasonNumber}`
|
||||
);
|
||||
|
||||
const grouped = Object.entries(groupedObject).reduce<
|
||||
(CalendarEventModel | CalendarEventGroupModel)[]
|
||||
>((acc, [, events]) => {
|
||||
if (!events) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (events.length === 1) {
|
||||
acc.push({
|
||||
isGroup: false,
|
||||
...events[0],
|
||||
});
|
||||
} else {
|
||||
acc.push({
|
||||
isGroup: true,
|
||||
seriesId: events[0].seriesId,
|
||||
seasonNumber: events[0].seasonNumber,
|
||||
episodeIds: events.map((event) => event.id),
|
||||
events: events.sort(
|
||||
(a, b) => moment(a.airDateUtc).unix() - moment(b.airDateUtc).unix()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return sort(grouped);
|
||||
};
|
||||
|
||||
interface CalendarDayProps {
|
||||
date: string;
|
||||
|
|
@ -97,8 +94,9 @@ function CalendarDay({
|
|||
isTodaysDate,
|
||||
onEventModalOpenToggle,
|
||||
}: CalendarDayProps) {
|
||||
const { time, view } = useSelector((state: AppState) => state.calendar);
|
||||
const events = useSelector(createCalendarEventsConnector(date));
|
||||
const view = useCalendarOption('view');
|
||||
const time = useCalendarTime();
|
||||
const events = useCalendarEvents(date);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@ import moment from 'moment';
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
|
||||
import * as calendarViews from 'Calendar/calendarViews';
|
||||
import {
|
||||
gotoCalendarNextRange,
|
||||
gotoCalendarPreviousRange,
|
||||
} from 'Store/Actions/calendarActions';
|
||||
goToNextRange,
|
||||
goToPreviousRange,
|
||||
useCalendarDates,
|
||||
} from 'Calendar/useCalendar';
|
||||
import CalendarDay from './CalendarDay';
|
||||
import styles from './CalendarDays.css';
|
||||
|
||||
function CalendarDays() {
|
||||
const dispatch = useDispatch();
|
||||
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||
const view = useCalendarOption('view');
|
||||
const dates = useCalendarDates();
|
||||
const isSidebarVisible = useSelector(
|
||||
(state: AppState) => state.app.isSidebarVisible
|
||||
);
|
||||
|
|
@ -71,12 +74,12 @@ function CalendarDays() {
|
|||
currentTouch > touchStart.current &&
|
||||
currentTouch - touchStart.current > 100
|
||||
) {
|
||||
dispatch(gotoCalendarPreviousRange());
|
||||
dispatch(goToPreviousRange());
|
||||
} else if (
|
||||
currentTouch < touchStart.current &&
|
||||
touchStart.current - currentTouch > 100
|
||||
) {
|
||||
dispatch(gotoCalendarNextRange());
|
||||
dispatch(goToNextRange());
|
||||
}
|
||||
|
||||
touchStart.current = null;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
|
||||
import * as calendarViews from 'Calendar/calendarViews';
|
||||
import { useCalendarDates } from 'Calendar/useCalendar';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import DayOfWeek from './DayOfWeek';
|
||||
import styles from './DaysOfWeek.css';
|
||||
|
||||
function DaysOfWeek() {
|
||||
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||
const view = useCalendarOption('view');
|
||||
const dates = useCalendarDates();
|
||||
const { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
|
@ -70,7 +70,7 @@ function CalendarEvent(props: CalendarEventProps) {
|
|||
showSpecialIcon,
|
||||
showCutoffUnmetIcon,
|
||||
fullColorEvents,
|
||||
} = useSelector((state: AppState) => state.calendar.options);
|
||||
} = useCalendarOptions();
|
||||
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIsDownloadingEpisodes } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
|
@ -39,7 +39,7 @@ function CalendarEventGroup({
|
|||
);
|
||||
|
||||
const { showEpisodeInformation, showFinaleIcon, fullColorEvents } =
|
||||
useSelector((state: AppState) => state.calendar.options);
|
||||
useCalendarOptions();
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
import moment from 'moment';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
setCalendarOption,
|
||||
useCalendarOption,
|
||||
} from 'Calendar/calendarOptionsStore';
|
||||
import { CalendarView } from 'Calendar/calendarViews';
|
||||
import useCalendar, {
|
||||
goToNextRange,
|
||||
goToPreviousRange,
|
||||
goToToday,
|
||||
useCalendarRange,
|
||||
useCalendarTime,
|
||||
} from 'Calendar/useCalendar';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
|
@ -10,12 +21,6 @@ import MenuButton from 'Components/Menu/MenuButton';
|
|||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import {
|
||||
gotoCalendarNextRange,
|
||||
gotoCalendarPreviousRange,
|
||||
gotoCalendarToday,
|
||||
setCalendarView,
|
||||
} from 'Store/Actions/calendarActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
|
@ -23,11 +28,10 @@ import CalendarHeaderViewButton from './CalendarHeaderViewButton';
|
|||
import styles from './CalendarHeader.css';
|
||||
|
||||
function CalendarHeader() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, view, time, start, end } = useSelector(
|
||||
(state: AppState) => state.calendar
|
||||
);
|
||||
const { isFetching } = useCalendar();
|
||||
const view = useCalendarOption('view');
|
||||
const time = useCalendarTime();
|
||||
const { start, end } = useCalendarRange();
|
||||
|
||||
const { isSmallScreen, isLargeScreen } = useSelector(
|
||||
createDimensionsSelector()
|
||||
|
|
@ -35,24 +39,21 @@ function CalendarHeader() {
|
|||
|
||||
const { longDateFormat } = useSelector(createUISettingsSelector());
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
(newView: string) => {
|
||||
dispatch(setCalendarView({ view: newView }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleViewChange = useCallback((newView: string) => {
|
||||
setCalendarOption('view', newView as CalendarView);
|
||||
}, []);
|
||||
|
||||
const handleTodayPress = useCallback(() => {
|
||||
dispatch(gotoCalendarToday());
|
||||
}, [dispatch]);
|
||||
goToToday();
|
||||
}, []);
|
||||
|
||||
const handlePreviousPress = useCallback(() => {
|
||||
dispatch(gotoCalendarPreviousRange());
|
||||
}, [dispatch]);
|
||||
goToPreviousRange();
|
||||
}, []);
|
||||
|
||||
const handleNextPress = useCallback(() => {
|
||||
dispatch(gotoCalendarNextRange());
|
||||
}, [dispatch]);
|
||||
goToNextRange();
|
||||
}, []);
|
||||
|
||||
const title = useMemo(() => {
|
||||
const timeMoment = moment(time);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import {
|
||||
useCalendarOption,
|
||||
useCalendarOptions,
|
||||
} from 'Calendar/calendarOptionsStore';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
|
@ -9,13 +12,13 @@ import LegendItem from './LegendItem';
|
|||
import styles from './Legend.css';
|
||||
|
||||
function Legend() {
|
||||
const view = useSelector((state: AppState) => state.calendar.view);
|
||||
const view = useCalendarOption('view');
|
||||
const {
|
||||
showFinaleIcon,
|
||||
showSpecialIcon,
|
||||
showCutoffUnmetIcon,
|
||||
fullColorEvents,
|
||||
} = useSelector((state: AppState) => state.calendar.options);
|
||||
} = useCalendarOptions();
|
||||
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
||||
|
||||
const iconsToShow = [];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import {
|
||||
CalendarOptions,
|
||||
setCalendarOption,
|
||||
useCalendarOptions,
|
||||
} from 'Calendar/calendarOptionsStore';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
|
|
@ -11,13 +15,13 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { OptionChanged } from 'Helpers/Hooks/useOptionsStore';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import {
|
||||
firstDayOfWeekOptions,
|
||||
timeFormatOptions,
|
||||
weekColumnOptions,
|
||||
} from 'Settings/UI/UISettings';
|
||||
import { setCalendarOption } from 'Store/Actions/calendarActions';
|
||||
import { saveUISettings } from 'Store/Actions/settingsActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
|
|
@ -40,7 +44,7 @@ function CalendarOptionsModalContent({
|
|||
showSpecialIcon,
|
||||
showCutoffUnmetIcon,
|
||||
fullColorEvents,
|
||||
} = useSelector((state: AppState) => state.calendar.options);
|
||||
} = useCalendarOptions();
|
||||
|
||||
const uiSettings = useSelector(createUISettingsSelector());
|
||||
|
||||
|
|
@ -59,10 +63,10 @@ function CalendarOptionsModalContent({
|
|||
} = state;
|
||||
|
||||
const handleOptionInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
dispatch(setCalendarOption({ [name]: value }));
|
||||
({ name, value }: OptionChanged<CalendarOptions>) => {
|
||||
setCalendarOption(name, value);
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleGlobalInputChange = useCallback(
|
||||
|
|
@ -98,6 +102,7 @@ function CalendarOptionsModalContent({
|
|||
name="collapseMultipleEpisodes"
|
||||
value={collapseMultipleEpisodes}
|
||||
helpText={translate('CollapseMultipleEpisodesHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -110,6 +115,7 @@ function CalendarOptionsModalContent({
|
|||
name="showEpisodeInformation"
|
||||
value={showEpisodeInformation}
|
||||
helpText={translate('ShowEpisodeInformationHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -122,6 +128,7 @@ function CalendarOptionsModalContent({
|
|||
name="showFinaleIcon"
|
||||
value={showFinaleIcon}
|
||||
helpText={translate('IconForFinalesHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -134,6 +141,7 @@ function CalendarOptionsModalContent({
|
|||
name="showSpecialIcon"
|
||||
value={showSpecialIcon}
|
||||
helpText={translate('IconForSpecialsHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -146,6 +154,7 @@ function CalendarOptionsModalContent({
|
|||
name="showCutoffUnmetIcon"
|
||||
value={showCutoffUnmetIcon}
|
||||
helpText={translate('IconForCutoffUnmetHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -158,6 +167,7 @@ function CalendarOptionsModalContent({
|
|||
name="fullColorEvents"
|
||||
value={fullColorEvents}
|
||||
helpText={translate('FullColorEventsHelpText')}
|
||||
// @ts-expect-error - The typing for inputs needs more work
|
||||
onChange={handleOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
|||
35
frontend/src/Calendar/calendarOptionsStore.ts
Normal file
35
frontend/src/Calendar/calendarOptionsStore.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { SelectedFilterKey } from 'Components/Filter/Filter';
|
||||
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||
import { CalendarView } from './calendarViews';
|
||||
|
||||
export interface CalendarOptions {
|
||||
collapseMultipleEpisodes: boolean;
|
||||
showEpisodeInformation: boolean;
|
||||
showFinaleIcon: boolean;
|
||||
showSpecialIcon: boolean;
|
||||
showCutoffUnmetIcon: boolean;
|
||||
fullColorEvents: boolean;
|
||||
selectedFilterKey: SelectedFilterKey;
|
||||
view: CalendarView;
|
||||
}
|
||||
|
||||
const { useOptions, useOption, getOptions, getOption, setOptions, setOption } =
|
||||
createOptionsStore<CalendarOptions>('calendar_options', () => {
|
||||
return {
|
||||
collapseMultipleEpisodes: false,
|
||||
showEpisodeInformation: true,
|
||||
showFinaleIcon: false,
|
||||
showSpecialIcon: false,
|
||||
showCutoffUnmetIcon: false,
|
||||
fullColorEvents: false,
|
||||
selectedFilterKey: 'monitored',
|
||||
view: window.innerWidth > 768 ? 'week' : 'day',
|
||||
};
|
||||
});
|
||||
|
||||
export const useCalendarOptions = useOptions;
|
||||
export const getCalendarOptions = getOptions;
|
||||
export const setCalendarOptions = setOptions;
|
||||
export const useCalendarOption = useOption;
|
||||
export const getCalendarOption = getOption;
|
||||
export const setCalendarOption = setOption;
|
||||
304
frontend/src/Calendar/useCalendar.ts
Normal file
304
frontend/src/Calendar/useCalendar.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
import { keepPreviousData } from '@tanstack/react-query';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { create } from 'zustand';
|
||||
import AppState, { Filter, FilterBuilderProp } from 'App/State/AppState';
|
||||
import Command from 'Commands/Command';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { setEpisodeQueryKey } from 'Episode/useEpisode';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import { filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import { executeCommandHelper } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
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: '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 = useSelector(createCustomFiltersSelector('calendar'));
|
||||
|
||||
const { start, end } = useMemo(() => {
|
||||
return getPopulatableRange(dates[0], dates[dates.length - 1], view);
|
||||
}, [dates, view]);
|
||||
|
||||
const { unmonitored, tags } = useMemo(() => {
|
||||
const selectedFilters = findSelectedFilters(
|
||||
selectedFilterKey,
|
||||
FILTERS,
|
||||
customFilters
|
||||
);
|
||||
|
||||
return selectedFilters.reduce<{
|
||||
unmonitored: boolean;
|
||||
tags?: number[] | undefined;
|
||||
}>(
|
||||
(acc, filter) => {
|
||||
if (filter.key === 'unmonitored' && Array.isArray(filter.value)) {
|
||||
acc.unmonitored = (filter.value as boolean[]).includes(true);
|
||||
}
|
||||
|
||||
if (filter.key === 'tags' && filter.type === 'contains') {
|
||||
acc.tags = filter.value as number[];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
unmonitored: false,
|
||||
}
|
||||
);
|
||||
}, [customFilters, selectedFilterKey]);
|
||||
|
||||
const { queryKey, ...result } = useApiQuery<CalendarItem[]>({
|
||||
path: '/calendar',
|
||||
queryParams: {
|
||||
start,
|
||||
end,
|
||||
unmonitored,
|
||||
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 = useSelector(
|
||||
(state: AppState) => state.settings.ui.item.firstDayOfWeek
|
||||
);
|
||||
|
||||
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 useSearchMissing = (episodeIds: number[]) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const commandPayload = {
|
||||
name: commandNames.EPISODE_SEARCH,
|
||||
episodeIds,
|
||||
};
|
||||
|
||||
executeCommandHelper(commandPayload, dispatch).then((data: Command) => {
|
||||
calendarStore.setState({ searchMissingCommandId: data.id });
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
5
frontend/src/Components/Filter/Filter.ts
Normal file
5
frontend/src/Components/Filter/Filter.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type SelectedFilterKey = string | number;
|
||||
|
||||
export interface SetFilter {
|
||||
selectedFilterKey: SelectedFilterKey;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { CustomFilter, FilterBuilderProp } from 'App/State/AppState';
|
|||
import Modal from 'Components/Modal/Modal';
|
||||
import FilterBuilderModalContent from './Builder/FilterBuilderModalContent';
|
||||
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
||||
import { SetFilter } from './Filter';
|
||||
|
||||
export interface FilterModalProps<T> {
|
||||
isOpen: boolean;
|
||||
|
|
@ -10,7 +11,7 @@ export interface FilterModalProps<T> {
|
|||
customFilterType: string;
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
sectionItems: T[];
|
||||
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
|
||||
dispatchSetFilter: (payload: SetFilter) => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { create } from 'zustand';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Episode from './Episode';
|
||||
import { CalendarItem } from 'typings/Calendar';
|
||||
|
||||
export type EpisodeEntity =
|
||||
| 'calendar'
|
||||
|
|
@ -10,6 +12,14 @@ export type EpisodeEntity =
|
|||
| 'wanted.cutoffUnmet'
|
||||
| 'wanted.missing';
|
||||
|
||||
interface EpisodeQueryKeyStore {
|
||||
calendar: QueryKey | null;
|
||||
}
|
||||
|
||||
const episodeQueryKeyStore = create<EpisodeQueryKeyStore>(() => ({
|
||||
calendar: null,
|
||||
}));
|
||||
|
||||
function createEpisodeSelector(episodeId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
|
|
@ -19,11 +29,12 @@ function createEpisodeSelector(episodeId?: number) {
|
|||
);
|
||||
}
|
||||
|
||||
function createCalendarEpisodeSelector(episodeId?: number) {
|
||||
// No-op...ish
|
||||
function createCalendarEpisodeSelector(_episodeId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items as Episode[],
|
||||
(episodes) => {
|
||||
return episodes.find(({ id }) => id === episodeId);
|
||||
(state: AppState) => state.episodes.items,
|
||||
(_episodes) => {
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -46,10 +57,24 @@ function createWantedMissingEpisodeSelector(episodeId?: number) {
|
|||
);
|
||||
}
|
||||
|
||||
function useEpisode(
|
||||
export const setEpisodeQueryKey = (
|
||||
episodeEntity: EpisodeEntity,
|
||||
queryKey: QueryKey | null
|
||||
) => {
|
||||
switch (episodeEntity) {
|
||||
case 'calendar':
|
||||
episodeQueryKeyStore.setState({ calendar: queryKey });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const useEpisode = (
|
||||
episodeId: number | undefined,
|
||||
episodeEntity: EpisodeEntity
|
||||
) {
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
let selector = createEpisodeSelector;
|
||||
|
||||
switch (episodeEntity) {
|
||||
|
|
@ -66,7 +91,19 @@ function useEpisode(
|
|||
break;
|
||||
}
|
||||
|
||||
return useSelector(selector(episodeId));
|
||||
}
|
||||
const result = useSelector(selector(episodeId));
|
||||
|
||||
if (episodeEntity === 'calendar') {
|
||||
const queryKey = episodeQueryKeyStore((state) => state.calendar);
|
||||
|
||||
return queryKey
|
||||
? queryClient
|
||||
.getQueryData<CalendarItem[]>(queryKey)
|
||||
?.find((e) => e.id === episodeId)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default useEpisode;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,6 @@ import { createSelector } from 'reselect';
|
|||
import AppState from 'App/State/AppState';
|
||||
import Episode from './Episode';
|
||||
|
||||
export type EpisodeEntity =
|
||||
| 'calendar'
|
||||
| 'episodes'
|
||||
| 'interactiveImport.episodes'
|
||||
| 'wanted.cutoffUnmet'
|
||||
| 'wanted.missing';
|
||||
|
||||
function getEpisodes(episodeIds: number[], episodes: Episode[]) {
|
||||
return episodeIds.reduce<Episode[]>((acc, id) => {
|
||||
const episode = episodes.find((episode) => episode.id === id);
|
||||
|
|
@ -31,52 +24,6 @@ function createEpisodeSelector(episodeIds: number[]) {
|
|||
);
|
||||
}
|
||||
|
||||
function createCalendarEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items as Episode[],
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createWantedCutoffUnmetEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.wanted.cutoffUnmet.items,
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createWantedMissingEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.wanted.missing.items,
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function useEpisodes(
|
||||
episodeIds: number[],
|
||||
episodeEntity: EpisodeEntity
|
||||
) {
|
||||
let selector = createEpisodeSelector;
|
||||
|
||||
switch (episodeEntity) {
|
||||
case 'calendar':
|
||||
selector = createCalendarEpisodeSelector;
|
||||
break;
|
||||
case 'wanted.cutoffUnmet':
|
||||
selector = createWantedCutoffUnmetEpisodeSelector;
|
||||
break;
|
||||
case 'wanted.missing':
|
||||
selector = createWantedMissingEpisodeSelector;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return useSelector(selector(episodeIds));
|
||||
export default function useEpisodes(episodeIds: number[]) {
|
||||
return useSelector(createEpisodeSelector(episodeIds));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,15 @@ const useApiQuery = <T>(options: QueryOptions<T>) => {
|
|||
};
|
||||
}, [options]);
|
||||
|
||||
return useQuery({
|
||||
...options.queryOptions,
|
||||
return {
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) =>
|
||||
fetchJson<T, unknown>({ ...requestOptions, signal }),
|
||||
});
|
||||
...useQuery({
|
||||
...options.queryOptions,
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) =>
|
||||
fetchJson<T, unknown>({ ...requestOptions, signal }),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export default useApiQuery;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@ export const createOptionsStore = <T extends TSettingd>(
|
|||
return store((state) => state[key]);
|
||||
};
|
||||
|
||||
const getOptions = () => {
|
||||
return store.getState();
|
||||
};
|
||||
|
||||
const getOption = <K extends keyof T>(key: K) => {
|
||||
return store.getState()[key];
|
||||
};
|
||||
|
||||
const setOptions = (options: Partial<T>) => {
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
|
|
@ -61,6 +69,8 @@ export const createOptionsStore = <T extends TSettingd>(
|
|||
store,
|
||||
useOptions,
|
||||
useOption,
|
||||
getOptions,
|
||||
getOption,
|
||||
setOptions,
|
||||
setOption,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,427 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import * as calendarViews from 'Calendar/calendarViews';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update } from './baseActions';
|
||||
import { executeCommandHelper } from './commandActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'calendar';
|
||||
|
||||
const viewRanges = {
|
||||
[calendarViews.DAY]: 'day',
|
||||
[calendarViews.WEEK]: 'week',
|
||||
[calendarViews.MONTH]: 'month',
|
||||
[calendarViews.FORECAST]: 'day'
|
||||
};
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
start: null,
|
||||
end: null,
|
||||
dates: [],
|
||||
dayCount: 7,
|
||||
view: window.innerWidth > 768 ? 'week' : 'day',
|
||||
error: null,
|
||||
items: [],
|
||||
searchMissingCommandId: null,
|
||||
|
||||
options: {
|
||||
collapseMultipleEpisodes: false,
|
||||
showEpisodeInformation: true,
|
||||
showFinaleIcon: false,
|
||||
showSpecialIcon: false,
|
||||
showCutoffUnmetIcon: false,
|
||||
fullColorEvents: false
|
||||
},
|
||||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: () => translate('All'),
|
||||
filters: [
|
||||
{
|
||||
key: 'unmonitored',
|
||||
value: [true],
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
label: () => translate('MonitoredOnly'),
|
||||
filters: [
|
||||
{
|
||||
key: 'unmonitored',
|
||||
value: [false],
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'unmonitored',
|
||||
label: () => translate('IncludeUnmonitored'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: () => translate('Tags'),
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'calendar.view',
|
||||
'calendar.selectedFilterKey',
|
||||
'calendar.options',
|
||||
'calendar.customFilters'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_CALENDAR = 'calendar/fetchCalendar';
|
||||
export const SET_CALENDAR_DAYS_COUNT = 'calendar/setCalendarDaysCount';
|
||||
export const SET_CALENDAR_FILTER = 'calendar/setCalendarFilter';
|
||||
export const SET_CALENDAR_VIEW = 'calendar/setCalendarView';
|
||||
export const GOTO_CALENDAR_TODAY = 'calendar/gotoCalendarToday';
|
||||
export const GOTO_CALENDAR_NEXT_RANGE = 'calendar/gotoCalendarNextRange';
|
||||
export const CLEAR_CALENDAR = 'calendar/clearCalendar';
|
||||
export const SET_CALENDAR_OPTION = 'calendar/setCalendarOption';
|
||||
export const SEARCH_MISSING = 'calendar/searchMissing';
|
||||
export const GOTO_CALENDAR_PREVIOUS_RANGE = 'calendar/gotoCalendarPreviousRange';
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
||||
function getDays(start, end) {
|
||||
const startTime = moment(start);
|
||||
const endTime = moment(end);
|
||||
const difference = endTime.diff(startTime, 'days');
|
||||
|
||||
// Difference is one less than the number of days we need to account for.
|
||||
return _.times(difference + 1, (i) => {
|
||||
return startTime.clone().add(i, 'days').toISOString();
|
||||
});
|
||||
}
|
||||
|
||||
function getDates(time, view, firstDayOfWeek, dayCount) {
|
||||
const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek';
|
||||
|
||||
let start = time.clone().startOf('day');
|
||||
let end = time.clone().endOf('day');
|
||||
|
||||
if (view === calendarViews.WEEK) {
|
||||
start = time.clone().startOf(weekName);
|
||||
end = time.clone().endOf(weekName);
|
||||
}
|
||||
|
||||
if (view === calendarViews.FORECAST) {
|
||||
start = time.clone().subtract(1, 'day').startOf('day');
|
||||
end = time.clone().add(dayCount - 2, 'days').endOf('day');
|
||||
}
|
||||
|
||||
if (view === calendarViews.MONTH) {
|
||||
start = time.clone().startOf('month').startOf(weekName);
|
||||
end = time.clone().endOf('month').endOf(weekName);
|
||||
}
|
||||
|
||||
if (view === calendarViews.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, endDate, view) {
|
||||
switch (view) {
|
||||
case calendarViews.DAY:
|
||||
return {
|
||||
start: moment(startDate).subtract(1, 'day').toISOString(),
|
||||
end: moment(endDate).add(1, 'day').toISOString()
|
||||
};
|
||||
case calendarViews.WEEK:
|
||||
case calendarViews.FORECAST:
|
||||
return {
|
||||
start: moment(startDate).subtract(1, 'week').toISOString(),
|
||||
end: moment(endDate).add(1, 'week').toISOString()
|
||||
};
|
||||
default:
|
||||
return {
|
||||
start: startDate,
|
||||
end: endDate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isRangePopulated(start, end, state) {
|
||||
const {
|
||||
start: currentStart,
|
||||
end: currentEnd,
|
||||
view: currentView
|
||||
} = state;
|
||||
|
||||
if (!currentStart || !currentEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
start: currentPopulatedStart,
|
||||
end: currentPopulatedEnd
|
||||
} = getPopulatableRange(currentStart, currentEnd, currentView);
|
||||
|
||||
if (
|
||||
moment(start).isAfter(currentPopulatedStart) &&
|
||||
moment(start).isBefore(currentPopulatedEnd)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCustomFilters(state, type) {
|
||||
return state.customFilters.items.filter((customFilter) => customFilter.type === type);
|
||||
}
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchCalendar = createThunk(FETCH_CALENDAR);
|
||||
export const setCalendarDaysCount = createThunk(SET_CALENDAR_DAYS_COUNT);
|
||||
export const setCalendarFilter = createThunk(SET_CALENDAR_FILTER);
|
||||
export const setCalendarView = createThunk(SET_CALENDAR_VIEW);
|
||||
export const gotoCalendarToday = createThunk(GOTO_CALENDAR_TODAY);
|
||||
export const gotoCalendarPreviousRange = createThunk(GOTO_CALENDAR_PREVIOUS_RANGE);
|
||||
export const gotoCalendarNextRange = createThunk(GOTO_CALENDAR_NEXT_RANGE);
|
||||
export const clearCalendar = createAction(CLEAR_CALENDAR);
|
||||
export const setCalendarOption = createAction(SET_CALENDAR_OPTION);
|
||||
export const searchMissing = createThunk(SEARCH_MISSING);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_CALENDAR]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const calendar = state.calendar;
|
||||
const customFilters = getCustomFilters(state, section);
|
||||
const selectedFilters = findSelectedFilters(calendar.selectedFilterKey, calendar.filters, customFilters);
|
||||
|
||||
const {
|
||||
time = calendar.time,
|
||||
view = calendar.view
|
||||
} = payload;
|
||||
|
||||
const dayCount = state.calendar.dayCount;
|
||||
const dates = getDates(moment(time), view, state.settings.ui.item.firstDayOfWeek, dayCount);
|
||||
const { start, end } = getPopulatableRange(dates.start, dates.end, view);
|
||||
const isPrePopulated = isRangePopulated(start, end, state.calendar);
|
||||
|
||||
const basesAttrs = {
|
||||
section,
|
||||
isFetching: true
|
||||
};
|
||||
|
||||
const attrs = isPrePopulated ?
|
||||
{
|
||||
view,
|
||||
...basesAttrs,
|
||||
...dates
|
||||
} :
|
||||
basesAttrs;
|
||||
|
||||
dispatch(set(attrs));
|
||||
|
||||
const requestParams = {
|
||||
start,
|
||||
end
|
||||
};
|
||||
|
||||
selectedFilters.forEach((selectedFilter) => {
|
||||
if (selectedFilter.key === 'unmonitored') {
|
||||
requestParams.unmonitored = selectedFilter.value.includes(true);
|
||||
}
|
||||
|
||||
if (selectedFilter.key === 'tags') {
|
||||
requestParams.tags = selectedFilter.value.join(',');
|
||||
}
|
||||
});
|
||||
|
||||
requestParams.unmonitored = requestParams.unmonitored ?? false;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/calendar',
|
||||
data: requestParams
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
view,
|
||||
...dates,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[SET_CALENDAR_DAYS_COUNT]: function(getState, payload, dispatch) {
|
||||
if (payload.dayCount === getState().calendar.dayCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
dayCount: payload.dayCount
|
||||
}));
|
||||
|
||||
const state = getState();
|
||||
const { time, view } = state.calendar;
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[SET_CALENDAR_FILTER]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
selectedFilterKey: payload.selectedFilterKey
|
||||
}));
|
||||
|
||||
const state = getState();
|
||||
const { time, view } = state.calendar;
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[SET_CALENDAR_VIEW]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const view = payload.view;
|
||||
const time = view === calendarViews.FORECAST || calendarViews.AGENDA ?
|
||||
moment() :
|
||||
state.calendar.time;
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[GOTO_CALENDAR_TODAY]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const view = state.calendar.view;
|
||||
const time = moment();
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[GOTO_CALENDAR_PREVIOUS_RANGE]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
|
||||
const {
|
||||
view,
|
||||
dayCount
|
||||
} = state.calendar;
|
||||
|
||||
const amount = view === calendarViews.FORECAST ? dayCount : 1;
|
||||
const time = moment(state.calendar.time).subtract(amount, viewRanges[view]);
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[GOTO_CALENDAR_NEXT_RANGE]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
|
||||
const {
|
||||
view,
|
||||
dayCount
|
||||
} = state.calendar;
|
||||
|
||||
const amount = view === calendarViews.FORECAST ? dayCount : 1;
|
||||
const time = moment(state.calendar.time).add(amount, viewRanges[view]);
|
||||
|
||||
dispatch(fetchCalendar({ time, view }));
|
||||
},
|
||||
|
||||
[SEARCH_MISSING]: function(getState, payload, dispatch) {
|
||||
const { episodeIds } = payload;
|
||||
|
||||
const commandPayload = {
|
||||
name: commandNames.EPISODE_SEARCH,
|
||||
episodeIds
|
||||
};
|
||||
|
||||
executeCommandHelper(commandPayload, dispatch).then((data) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
searchMissingCommandId: data.id
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_CALENDAR]: createClearReducer(section, {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
}),
|
||||
|
||||
[SET_CALENDAR_OPTION]: function(state, { payload }) {
|
||||
const options = state.options;
|
||||
|
||||
return {
|
||||
...state,
|
||||
options: {
|
||||
...options,
|
||||
...payload
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import * as app from './appActions';
|
||||
import * as calendar from './calendarActions';
|
||||
import * as captcha from './captchaActions';
|
||||
import * as commands from './commandActions';
|
||||
import * as customFilters from './customFilterActions';
|
||||
|
|
@ -25,7 +24,6 @@ import * as wanted from './wantedActions';
|
|||
|
||||
export default [
|
||||
app,
|
||||
calendar,
|
||||
captcha,
|
||||
commands,
|
||||
customFilters,
|
||||
|
|
|
|||
Loading…
Reference in a new issue