- {translate('LibraryImportSeriesHeader')}
+ Import series you already have
- {translate('LibraryImportTips')}
+ Some tips to ensure the import goes smoothly:
-
+ Make sure that your files include the quality in their filenames. eg. episode.s02e15.bluray.mkv
-
+ Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. "{isWindows ? 'C:\\tv shows' : '/tv shows'}" and not "{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}" Additionally, each series must be in its own folder within the root/library folder.
- {translate('LibraryImportTipsDontUseDownloadsFolder')}
+ Do not use for importing downloads from your download client, this is only for existing organized libraries, not unsorted files.
@@ -100,7 +96,7 @@ class ImportSeriesSelectFolder extends Component {
{
hasRootFolders ?
-
+
- {translate('AddRootFolderError')}
+ Unable to add root folder
{
- Array.isArray(saveError.responseJSON) ?
- saveError.responseJSON.map((e, index) => {
- return (
-
- {e.errorMessage}
-
- );
- }) :
-
- {
- JSON.stringify(saveError.responseJSON)
- }
-
+ saveError.responseJSON.map((e, index) => {
+ return (
+
+ {e.errorMessage}
+
+ );
+ })
}
:
@@ -153,8 +143,8 @@ class ImportSeriesSelectFolder extends Component {
/>
{
hasRootFolders ?
- translate('ChooseAnotherFolder') :
- translate('StartImport')
+ 'Choose another folder' :
+ 'Start Import'
}
diff --git a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js
index 1df231f4e..5ef79ec4e 100644
--- a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js
+++ b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js
@@ -1,17 +1,16 @@
-import { push } from 'connected-react-router';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { addRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
-import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
+import { push } from 'connected-react-router';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
+import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
function createMapStateToProps() {
return createSelector(
- createRootFoldersSelector(),
+ (state) => state.rootFolders,
createSystemStatusSelector(),
(rootFolders, systemStatus) => {
return {
@@ -58,7 +57,7 @@ class ImportSeriesSelectFolderConnector extends Component {
onNewRootFolderSelect = (path) => {
this.props.addRootFolder({ path });
- };
+ }
//
// Render
diff --git a/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js b/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js
deleted file mode 100644
index c70ec0dec..000000000
--- a/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import DescriptionList from 'Components/DescriptionList/DescriptionList';
-import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
-import translate from 'Utilities/String/translate';
-
-function SeriesMonitorNewItemsOptionsPopoverContent() {
- return (
-
-
-
-
-
- );
-}
-
-export default SeriesMonitorNewItemsOptionsPopoverContent;
diff --git a/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js b/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js
index 21289fcb8..e889fbb09 100644
--- a/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js
+++ b/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js
@@ -1,64 +1,43 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
-import translate from 'Utilities/String/translate';
function SeriesMonitoringOptionsPopoverContent() {
return (
-
-
-
-
-
-
-
-
);
diff --git a/frontend/src/AddSeries/SeriesTypePopoverContent.js b/frontend/src/AddSeries/SeriesTypePopoverContent.js
index 9771bd8db..e57d49a9e 100644
--- a/frontend/src/AddSeries/SeriesTypePopoverContent.js
+++ b/frontend/src/AddSeries/SeriesTypePopoverContent.js
@@ -1,24 +1,23 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
-import translate from 'Utilities/String/translate';
function SeriesTypePopoverContent() {
return (
);
diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js
new file mode 100644
index 000000000..2baaf9fe9
--- /dev/null
+++ b/frontend/src/App/App.js
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import DocumentTitle from 'react-document-title';
+import { Provider } from 'react-redux';
+import { ConnectedRouter } from 'connected-react-router';
+import PageConnector from 'Components/Page/PageConnector';
+import AppRoutes from './AppRoutes';
+
+function App({ store, history }) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+App.propTypes = {
+ store: PropTypes.object.isRequired,
+ history: PropTypes.object.isRequired
+};
+
+export default App;
diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.tsx
deleted file mode 100644
index b71199bb3..000000000
--- a/frontend/src/App/App.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
-import React from 'react';
-import DocumentTitle from 'react-document-title';
-import { Provider } from 'react-redux';
-import { Store } from 'redux';
-import PageConnector from 'Components/Page/PageConnector';
-import ApplyTheme from './ApplyTheme';
-import AppRoutes from './AppRoutes';
-
-interface AppProps {
- store: Store;
- history: ConnectedRouterProps['history'];
-}
-
-const queryClient = new QueryClient();
-
-function App({ store, history }: AppProps) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default App;
diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js
new file mode 100644
index 000000000..102681071
--- /dev/null
+++ b/frontend/src/App/AppRoutes.js
@@ -0,0 +1,254 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Route, Redirect } from 'react-router-dom';
+import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
+import NotFound from 'Components/NotFound';
+import Switch from 'Components/Router/Switch';
+import SeriesIndexConnector from 'Series/Index/SeriesIndexConnector';
+import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
+import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
+import SeriesEditorConnector from 'Series/Editor/SeriesEditorConnector';
+import SeasonPassConnector from 'SeasonPass/SeasonPassConnector';
+import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
+import CalendarPageConnector from 'Calendar/CalendarPageConnector';
+import HistoryConnector from 'Activity/History/HistoryConnector';
+import QueueConnector from 'Activity/Queue/QueueConnector';
+import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
+import MissingConnector from 'Wanted/Missing/MissingConnector';
+import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
+import Settings from 'Settings/Settings';
+import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
+import Profiles from 'Settings/Profiles/Profiles';
+import Quality from 'Settings/Quality/Quality';
+import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
+import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
+import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
+import NotificationSettings from 'Settings/Notifications/NotificationSettings';
+import MetadataSettings from 'Settings/Metadata/MetadataSettings';
+import TagSettings from 'Settings/Tags/TagSettings';
+import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
+import UISettingsConnector from 'Settings/UI/UISettingsConnector';
+import Status from 'System/Status/Status';
+import Tasks from 'System/Tasks/Tasks';
+import BackupsConnector from 'System/Backup/BackupsConnector';
+import UpdatesConnector from 'System/Updates/UpdatesConnector';
+import LogsTableConnector from 'System/Events/LogsTableConnector';
+import Logs from 'System/Logs/Logs';
+
+function AppRoutes(props) {
+ const {
+ app
+ } = props;
+
+ return (
+
+ {/*
+ Series
+ */}
+
+
+
+ {
+ window.Sonarr.urlBase &&
+ {
+ return (
+
+ );
+ }}
+ />
+ }
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ Calendar
+ */}
+
+
+
+ {/*
+ Activity
+ */}
+
+
+
+
+
+
+
+ {/*
+ Wanted
+ */}
+
+
+
+
+
+ {/*
+ Settings
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ System
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ Not Found
+ */}
+
+
+
+ );
+}
+
+AppRoutes.propTypes = {
+ app: PropTypes.func.isRequired
+};
+
+export default AppRoutes;
diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx
deleted file mode 100644
index fbe4a15bb..000000000
--- a/frontend/src/App/AppRoutes.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React from 'react';
-import { Redirect, Route } from 'react-router-dom';
-import Blocklist from 'Activity/Blocklist/Blocklist';
-import History from 'Activity/History/History';
-import Queue from 'Activity/Queue/Queue';
-import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
-import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
-import CalendarPage from 'Calendar/CalendarPage';
-import NotFound from 'Components/NotFound';
-import Switch from 'Components/Router/Switch';
-import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
-import SeriesIndex from 'Series/Index/SeriesIndex';
-import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
-import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
-import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
-import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
-import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
-import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
-import MetadataSettings from 'Settings/Metadata/MetadataSettings';
-import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings';
-import NotificationSettings from 'Settings/Notifications/NotificationSettings';
-import Profiles from 'Settings/Profiles/Profiles';
-import QualityConnector from 'Settings/Quality/QualityConnector';
-import Settings from 'Settings/Settings';
-import TagSettings from 'Settings/Tags/TagSettings';
-import UISettingsConnector from 'Settings/UI/UISettingsConnector';
-import BackupsConnector from 'System/Backup/BackupsConnector';
-import LogsTableConnector from 'System/Events/LogsTableConnector';
-import Logs from 'System/Logs/Logs';
-import Status from 'System/Status/Status';
-import Tasks from 'System/Tasks/Tasks';
-import Updates from 'System/Updates/Updates';
-import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
-import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
-import MissingConnector from 'Wanted/Missing/MissingConnector';
-
-function RedirectWithUrlBase() {
- return
;
-}
-
-function AppRoutes() {
- return (
-
- {/*
- Series
- */}
-
-
-
- {window.Sonarr.urlBase && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {/*
- Calendar
- */}
-
-
-
- {/*
- Activity
- */}
-
-
-
-
-
-
-
- {/*
- Wanted
- */}
-
-
-
-
-
- {/*
- Settings
- */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
- System
- */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
- Not Found
- */}
-
-
-
- );
-}
-
-export default AppRoutes;
diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js
new file mode 100644
index 000000000..abc7f8832
--- /dev/null
+++ b/frontend/src/App/AppUpdatedModal.js
@@ -0,0 +1,30 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Modal from 'Components/Modal/Modal';
+import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
+
+function AppUpdatedModal(props) {
+ const {
+ isOpen,
+ onModalClose
+ } = props;
+
+ return (
+
+
+
+ );
+}
+
+AppUpdatedModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AppUpdatedModal;
diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx
deleted file mode 100644
index 696d36fb2..000000000
--- a/frontend/src/App/AppUpdatedModal.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { useCallback } from 'react';
-import Modal from 'Components/Modal/Modal';
-import AppUpdatedModalContent from './AppUpdatedModalContent';
-
-interface AppUpdatedModalProps {
- isOpen: boolean;
- onModalClose: (...args: unknown[]) => unknown;
-}
-
-function AppUpdatedModal(props: AppUpdatedModalProps) {
- const { isOpen, onModalClose } = props;
-
- const handleModalClose = useCallback(() => {
- location.reload();
- }, []);
-
- return (
-
-
-
- );
-}
-
-export default AppUpdatedModal;
diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js
new file mode 100644
index 000000000..a21afbc5a
--- /dev/null
+++ b/frontend/src/App/AppUpdatedModalConnector.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import AppUpdatedModal from './AppUpdatedModal';
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onModalClose() {
+ location.reload();
+ }
+ };
+}
+
+export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
diff --git a/frontend/src/App/AppUpdatedModalContent.css b/frontend/src/App/AppUpdatedModalContent.css
index 0df4183a6..37b89c9be 100644
--- a/frontend/src/App/AppUpdatedModalContent.css
+++ b/frontend/src/App/AppUpdatedModalContent.css
@@ -1,7 +1,6 @@
.version {
margin: 0 3px;
font-weight: bold;
- font-family: var(--defaultFontFamily);
}
.maintenance {
diff --git a/frontend/src/App/AppUpdatedModalContent.css.d.ts b/frontend/src/App/AppUpdatedModalContent.css.d.ts
deleted file mode 100644
index 70ddbf6a1..000000000
--- a/frontend/src/App/AppUpdatedModalContent.css.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'changes': string;
- 'maintenance': string;
- 'version': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js
new file mode 100644
index 000000000..562467288
--- /dev/null
+++ b/frontend/src/App/AppUpdatedModalContent.js
@@ -0,0 +1,137 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { kinds } from 'Helpers/Props';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Button from 'Components/Link/Button';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import UpdateChanges from 'System/Updates/UpdateChanges';
+import styles from './AppUpdatedModalContent.css';
+
+function mergeUpdates(items, version, prevVersion) {
+ let installedIndex = items.findIndex((u) => u.version === version);
+ let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
+
+ if (installedIndex === -1) {
+ installedIndex = 0;
+ }
+
+ if (installedPreviouslyIndex === -1) {
+ installedPreviouslyIndex = items.length;
+ } else if (installedPreviouslyIndex === installedIndex && items.length) {
+ installedPreviouslyIndex += 1;
+ }
+
+ const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
+
+ if (!appliedUpdates.length) {
+ return null;
+ }
+
+ const appliedChanges = { new: [], fixed: [] };
+ appliedUpdates.forEach((u) => {
+ if (u.changes) {
+ appliedChanges.new.push(... u.changes.new);
+ appliedChanges.fixed.push(... u.changes.fixed);
+ }
+ });
+
+ const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
+
+ if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
+ mergedUpdate.changes = null;
+ }
+
+ return mergedUpdate;
+}
+
+function AppUpdatedModalContent(props) {
+ const {
+ version,
+ prevVersion,
+ isPopulated,
+ error,
+ items,
+ onSeeChangesPress,
+ onModalClose
+ } = props;
+
+ const update = mergeUpdates(items, version, prevVersion);
+
+ return (
+
+
+ Sonarr Updated
+
+
+
+
+ Sonarr has been updated to version {version} , in order to get the latest changes you'll need to reload Sonarr.
+
+
+ {
+ isPopulated && !error && !!update &&
+
+ {
+ !update.changes &&
+
Maintenance release
+ }
+
+ {
+ !!update.changes &&
+
+
+ What's new?
+
+
+
+
+
+
+ }
+
+ }
+
+ {
+ !isPopulated && !error &&
+
+ }
+
+
+
+
+ Recent Changes
+
+
+
+ Reload
+
+
+
+ );
+}
+
+AppUpdatedModalContent.propTypes = {
+ version: PropTypes.string.isRequired,
+ prevVersion: PropTypes.string,
+ isPopulated: PropTypes.bool.isRequired,
+ error: PropTypes.object,
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onSeeChangesPress: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AppUpdatedModalContent;
diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx
deleted file mode 100644
index 6553d6270..000000000
--- a/frontend/src/App/AppUpdatedModalContent.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React, { useCallback, useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import Button from 'Components/Link/Button';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
-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 usePrevious from 'Helpers/Hooks/usePrevious';
-import { kinds } from 'Helpers/Props';
-import { fetchUpdates } from 'Store/Actions/systemActions';
-import UpdateChanges from 'System/Updates/UpdateChanges';
-import Update from 'typings/Update';
-import translate from 'Utilities/String/translate';
-import AppState from './State/AppState';
-import styles from './AppUpdatedModalContent.css';
-
-function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
- let installedIndex = items.findIndex((u) => u.version === version);
- let installedPreviouslyIndex = items.findIndex(
- (u) => u.version === prevVersion
- );
-
- if (installedIndex === -1) {
- installedIndex = 0;
- }
-
- if (installedPreviouslyIndex === -1) {
- installedPreviouslyIndex = items.length;
- } else if (installedPreviouslyIndex === installedIndex && items.length) {
- installedPreviouslyIndex += 1;
- }
-
- const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
-
- if (!appliedUpdates.length) {
- return null;
- }
-
- const appliedChanges: Update['changes'] = { new: [], fixed: [] };
-
- appliedUpdates.forEach((u: Update) => {
- if (u.changes) {
- appliedChanges.new.push(...u.changes.new);
- appliedChanges.fixed.push(...u.changes.fixed);
- }
- });
-
- const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
- changes: appliedChanges,
- });
-
- if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
- mergedUpdate.changes = null;
- }
-
- return mergedUpdate;
-}
-
-interface AppUpdatedModalContentProps {
- onModalClose: () => void;
-}
-
-function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
- const dispatch = useDispatch();
- const { version, prevVersion } = useSelector((state: AppState) => state.app);
- const { isPopulated, error, items } = useSelector(
- (state: AppState) => state.system.updates
- );
- const previousVersion = usePrevious(version);
-
- const { onModalClose } = props;
-
- const update = mergeUpdates(items, version, prevVersion);
-
- const handleSeeChangesPress = useCallback(() => {
- window.location.href = `${window.Sonarr.urlBase}/system/updates`;
- }, []);
-
- useEffect(() => {
- dispatch(fetchUpdates());
- }, [dispatch]);
-
- useEffect(() => {
- if (version !== previousVersion) {
- dispatch(fetchUpdates());
- }
- }, [version, previousVersion, dispatch]);
-
- return (
-
- {translate('AppUpdated')}
-
-
-
-
-
-
- {isPopulated && !error && !!update ? (
-
- {update.changes ? (
-
- {translate('MaintenanceRelease')}
-
- ) : null}
-
- {update.changes ? (
-
-
{translate('WhatsNew')}
-
-
-
-
-
- ) : null}
-
- ) : null}
-
- {!isPopulated && !error ? : null}
-
-
-
-
- {translate('RecentChanges')}
-
-
-
- {translate('Reload')}
-
-
-
- );
-}
-
-export default AppUpdatedModalContent;
diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js
new file mode 100644
index 000000000..4100ee674
--- /dev/null
+++ b/frontend/src/App/AppUpdatedModalContentConnector.js
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchUpdates } from 'Store/Actions/systemActions';
+import AppUpdatedModalContent from './AppUpdatedModalContent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.app.version,
+ (state) => state.app.prevVersion,
+ (state) => state.system.updates,
+ (version, prevVersion, updates) => {
+ const {
+ isPopulated,
+ error,
+ items
+ } = updates;
+
+ return {
+ version,
+ prevVersion,
+ isPopulated,
+ error,
+ items
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ dispatchFetchUpdates() {
+ dispatch(fetchUpdates());
+ },
+
+ onSeeChangesPress() {
+ window.location = `${window.Sonarr.urlBase}/system/updates`;
+ }
+ };
+}
+
+class AppUpdatedModalContentConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ this.props.dispatchFetchUpdates();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.version !== this.props.version) {
+ this.props.dispatchFetchUpdates();
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ dispatchFetchUpdates,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+AppUpdatedModalContentConnector.propTypes = {
+ version: PropTypes.string.isRequired,
+ dispatchFetchUpdates: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx
deleted file mode 100644
index ce598f2dc..000000000
--- a/frontend/src/App/ApplyTheme.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useCallback, useEffect } from 'react';
-import { useSelector } from 'react-redux';
-import { createSelector } from 'reselect';
-import themes from 'Styles/Themes';
-import AppState from './State/AppState';
-
-function createThemeSelector() {
- return createSelector(
- (state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
- (theme) => {
- return theme;
- }
- );
-}
-
-function ApplyTheme() {
- const theme = useSelector(createThemeSelector());
-
- const updateCSSVariables = useCallback(() => {
- Object.entries(themes[theme]).forEach(([key, value]) => {
- document.documentElement.style.setProperty(`--${key}`, value);
- });
- }, [theme]);
-
- // On Component Mount and Component Update
- useEffect(() => {
- updateCSSVariables();
- }, [updateCSSVariables, theme]);
-
- return null;
-}
-
-export default ApplyTheme;
diff --git a/frontend/src/App/ColorImpairedContext.ts b/frontend/src/App/ColorImpairedContext.js
similarity index 100%
rename from frontend/src/App/ColorImpairedContext.ts
rename to frontend/src/App/ColorImpairedContext.js
diff --git a/frontend/src/App/ConnectionLostModal.css.d.ts b/frontend/src/App/ConnectionLostModal.css.d.ts
deleted file mode 100644
index 027f2a9a3..000000000
--- a/frontend/src/App/ConnectionLostModal.css.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'automatic': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.js
new file mode 100644
index 000000000..b54f81d50
--- /dev/null
+++ b/frontend/src/App/ConnectionLostModal.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { kinds } from 'Helpers/Props';
+import Button from 'Components/Link/Button';
+import Modal from 'Components/Modal/Modal';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import styles from './ConnectionLostModal.css';
+
+function ConnectionLostModal(props) {
+ const {
+ isOpen,
+ onModalClose
+ } = props;
+
+ return (
+
+
+
+ Connection Lost
+
+
+
+
+ Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
+
+
+
+ Sonarr will try to connect automatically, or you can click reload below.
+
+
+
+
+ Reload
+
+
+
+
+ );
+}
+
+ConnectionLostModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default ConnectionLostModal;
diff --git a/frontend/src/App/ConnectionLostModal.tsx b/frontend/src/App/ConnectionLostModal.tsx
deleted file mode 100644
index f08f2c0e2..000000000
--- a/frontend/src/App/ConnectionLostModal.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { useCallback } from 'react';
-import Button from 'Components/Link/Button';
-import Modal from 'Components/Modal/Modal';
-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 { kinds } from 'Helpers/Props';
-import translate from 'Utilities/String/translate';
-import styles from './ConnectionLostModal.css';
-
-interface ConnectionLostModalProps {
- isOpen: boolean;
-}
-
-function ConnectionLostModal(props: ConnectionLostModalProps) {
- const { isOpen } = props;
-
- const handleModalClose = useCallback(() => {
- location.reload();
- }, []);
-
- return (
-
-
- {translate('ConnectionLost')}
-
-
- {translate('ConnectionLostToBackend')}
-
-
- {translate('ConnectionLostReconnect')}
-
-
-
-
- {translate('Reload')}
-
-
-
-
- );
-}
-
-export default ConnectionLostModal;
diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js
new file mode 100644
index 000000000..8ab8e3cd0
--- /dev/null
+++ b/frontend/src/App/ConnectionLostModalConnector.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import ConnectionLostModal from './ConnectionLostModal';
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onModalClose() {
+ location.reload();
+ }
+ };
+}
+
+export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
diff --git a/frontend/src/App/ModelBase.ts b/frontend/src/App/ModelBase.ts
deleted file mode 100644
index 187b12fb2..000000000
--- a/frontend/src/App/ModelBase.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-interface ModelBase {
- id: number;
-}
-
-export default ModelBase;
diff --git a/frontend/src/App/SelectContext.tsx b/frontend/src/App/SelectContext.tsx
deleted file mode 100644
index 66be388ce..000000000
--- a/frontend/src/App/SelectContext.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { cloneDeep } from 'lodash';
-import React, { useCallback, useEffect } from 'react';
-import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState';
-import ModelBase from './ModelBase';
-
-export type SelectContextAction =
- | { type: 'reset' }
- | { type: 'selectAll' }
- | { type: 'unselectAll' }
- | {
- type: 'toggleSelected';
- id: number;
- isSelected: boolean;
- shiftKey: boolean;
- }
- | {
- type: 'removeItem';
- id: number;
- }
- | {
- type: 'updateItems';
- items: ModelBase[];
- };
-
-export type SelectDispatch = (action: SelectContextAction) => void;
-
-interface SelectProviderOptions
{
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- children: any;
- items: Array;
-}
-
-const SelectContext = React.createContext<
- [SelectState, SelectDispatch] | undefined
->(cloneDeep(undefined));
-
-export function SelectProvider(
- props: SelectProviderOptions
-) {
- const { items } = props;
- const [state, dispatch] = useSelectState();
-
- const dispatchWrapper = useCallback(
- (action: SelectContextAction) => {
- switch (action.type) {
- case 'reset':
- case 'removeItem':
- dispatch(action);
- break;
-
- default:
- dispatch({
- ...action,
- items,
- });
- break;
- }
- },
- [items, dispatch]
- );
-
- const value: [SelectState, SelectDispatch] = [state, dispatchWrapper];
-
- useEffect(() => {
- dispatch({ type: 'updateItems', items });
- }, [items, dispatch]);
-
- return (
-
- {props.children}
-
- );
-}
-
-export function useSelect() {
- const context = React.useContext(SelectContext);
-
- if (context === undefined) {
- throw new Error('useSelect must be used within a SelectProvider');
- }
-
- return context;
-}
diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts
deleted file mode 100644
index 4e9dbe7a0..000000000
--- a/frontend/src/App/State/AppSectionState.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import Column from 'Components/Table/Column';
-import { SortDirection } from 'Helpers/Props/sortDirections';
-import { ValidationFailure } from 'typings/pending';
-import { FilterBuilderProp, PropertyFilter } from './AppState';
-
-export interface Error {
- status?: number;
- responseJSON:
- | {
- message: string | undefined;
- }
- | ValidationFailure[]
- | undefined;
-}
-
-export interface AppSectionDeleteState {
- isDeleting: boolean;
- deleteError: Error;
-}
-
-export interface AppSectionSaveState {
- isSaving: boolean;
- saveError: Error;
-}
-
-export interface PagedAppSectionState {
- page: number;
- pageSize: number;
- totalPages: number;
- totalRecords?: number;
-}
-export interface TableAppSectionState {
- columns: Column[];
-}
-
-export interface AppSectionFilterState {
- selectedFilterKey: string;
- filters: PropertyFilter[];
- filterBuilderProps: FilterBuilderProp[];
-}
-
-export interface AppSectionSchemaState {
- isSchemaFetching: boolean;
- isSchemaPopulated: boolean;
- schemaError: Error;
- schema: {
- items: T[];
- };
-}
-
-export interface AppSectionItemSchemaState {
- isSchemaFetching: boolean;
- isSchemaPopulated: boolean;
- schemaError: Error;
- schema: T;
-}
-
-export interface AppSectionItemState {
- isFetching: boolean;
- isPopulated: boolean;
- error: Error;
- pendingChanges: Partial;
- item: T;
-}
-
-export interface AppSectionProviderState
- extends AppSectionDeleteState,
- AppSectionSaveState {
- isFetching: boolean;
- isPopulated: boolean;
- error: Error;
- items: T[];
- pendingChanges: Partial;
-}
-
-interface AppSectionState {
- isFetching: boolean;
- isPopulated: boolean;
- error: Error;
- items: T[];
- sortKey: string;
- sortDirection: SortDirection;
-}
-
-export default AppSectionState;
diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts
deleted file mode 100644
index 84bd5d0b4..000000000
--- a/frontend/src/App/State/AppState.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import BlocklistAppState from './BlocklistAppState';
-import CalendarAppState from './CalendarAppState';
-import CaptchaAppState from './CaptchaAppState';
-import CommandAppState from './CommandAppState';
-import EpisodeFilesAppState from './EpisodeFilesAppState';
-import EpisodesAppState from './EpisodesAppState';
-import HistoryAppState from './HistoryAppState';
-import InteractiveImportAppState from './InteractiveImportAppState';
-import OAuthAppState from './OAuthAppState';
-import ParseAppState from './ParseAppState';
-import PathsAppState from './PathsAppState';
-import ProviderOptionsAppState from './ProviderOptionsAppState';
-import QueueAppState from './QueueAppState';
-import ReleasesAppState from './ReleasesAppState';
-import RootFolderAppState from './RootFolderAppState';
-import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
-import SettingsAppState from './SettingsAppState';
-import SystemAppState from './SystemAppState';
-import TagsAppState from './TagsAppState';
-import WantedAppState from './WantedAppState';
-
-interface FilterBuilderPropOption {
- id: string;
- name: string;
-}
-
-export interface FilterBuilderProp {
- name: string;
- label: string;
- type: string;
- valueType?: string;
- optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
-}
-
-export interface PropertyFilter {
- key: string;
- value: boolean | string | number | string[] | number[];
- type: string;
-}
-
-export interface Filter {
- key: string;
- label: string;
- filters: PropertyFilter[];
-}
-
-export interface CustomFilter {
- id: number;
- type: string;
- label: string;
- filters: PropertyFilter[];
-}
-
-export interface AppSectionState {
- isConnected: boolean;
- isReconnecting: boolean;
- isSidebarVisible: boolean;
- version: string;
- prevVersion?: string;
- dimensions: {
- isSmallScreen: boolean;
- isLargeScreen: boolean;
- width: number;
- height: number;
- };
-}
-
-interface AppState {
- app: AppSectionState;
- blocklist: BlocklistAppState;
- calendar: CalendarAppState;
- captcha: CaptchaAppState;
- commands: CommandAppState;
- episodeFiles: EpisodeFilesAppState;
- episodeHistory: HistoryAppState;
- episodes: EpisodesAppState;
- episodesSelection: EpisodesAppState;
- history: HistoryAppState;
- interactiveImport: InteractiveImportAppState;
- oAuth: OAuthAppState;
- parse: ParseAppState;
- paths: PathsAppState;
- providerOptions: ProviderOptionsAppState;
- queue: QueueAppState;
- releases: ReleasesAppState;
- rootFolders: RootFolderAppState;
- series: SeriesAppState;
- seriesIndex: SeriesIndexAppState;
- settings: SettingsAppState;
- system: SystemAppState;
- tags: TagsAppState;
- wanted: WantedAppState;
-}
-
-export default AppState;
diff --git a/frontend/src/App/State/BlocklistAppState.ts b/frontend/src/App/State/BlocklistAppState.ts
deleted file mode 100644
index 004a30732..000000000
--- a/frontend/src/App/State/BlocklistAppState.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import Blocklist from 'typings/Blocklist';
-import AppSectionState, {
- AppSectionFilterState,
- PagedAppSectionState,
- TableAppSectionState,
-} from './AppSectionState';
-
-interface BlocklistAppState
- extends AppSectionState,
- AppSectionFilterState,
- PagedAppSectionState,
- TableAppSectionState {
- isRemoving: boolean;
-}
-
-export default BlocklistAppState;
diff --git a/frontend/src/App/State/CalendarAppState.ts b/frontend/src/App/State/CalendarAppState.ts
deleted file mode 100644
index 75c8b5e50..000000000
--- a/frontend/src/App/State/CalendarAppState.ts
+++ /dev/null
@@ -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,
- AppSectionFilterState {
- searchMissingCommandId: number | null;
- start: moment.Moment;
- end: moment.Moment;
- dates: string[];
- time: string;
- view: CalendarView;
- options: CalendarOptions;
-}
-
-export default CalendarAppState;
diff --git a/frontend/src/App/State/CaptchaAppState.ts b/frontend/src/App/State/CaptchaAppState.ts
deleted file mode 100644
index 7252937eb..000000000
--- a/frontend/src/App/State/CaptchaAppState.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-interface CaptchaAppState {
- refreshing: false;
- token: string;
- siteKey: unknown;
- secretToken: unknown;
- ray: unknown;
- stoken: unknown;
- responseUrl: unknown;
-}
-
-export default CaptchaAppState;
diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts
deleted file mode 100644
index f4110ef73..000000000
--- a/frontend/src/App/State/ClientSideCollectionAppState.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { CustomFilter } from './AppState';
-
-interface ClientSideCollectionAppState {
- totalItems: number;
- customFilters: CustomFilter[];
-}
-
-export default ClientSideCollectionAppState;
diff --git a/frontend/src/App/State/CommandAppState.ts b/frontend/src/App/State/CommandAppState.ts
deleted file mode 100644
index 1bde37371..000000000
--- a/frontend/src/App/State/CommandAppState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import AppSectionState from 'App/State/AppSectionState';
-import Command from 'Commands/Command';
-
-export type CommandAppState = AppSectionState;
-
-export default CommandAppState;
diff --git a/frontend/src/App/State/CustomFiltersAppState.ts b/frontend/src/App/State/CustomFiltersAppState.ts
deleted file mode 100644
index 6ac4820c7..000000000
--- a/frontend/src/App/State/CustomFiltersAppState.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import AppSectionState, {
- AppSectionDeleteState,
-} from 'App/State/AppSectionState';
-import { CustomFilter } from './AppState';
-
-interface CustomFiltersAppState
- extends AppSectionState,
- AppSectionDeleteState {}
-
-export default CustomFiltersAppState;
diff --git a/frontend/src/App/State/EpisodeFilesAppState.ts b/frontend/src/App/State/EpisodeFilesAppState.ts
deleted file mode 100644
index 5e6e94a06..000000000
--- a/frontend/src/App/State/EpisodeFilesAppState.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import AppSectionState, {
- AppSectionDeleteState,
-} from 'App/State/AppSectionState';
-import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
-
-interface EpisodeFilesAppState
- extends AppSectionState,
- AppSectionDeleteState {}
-
-export default EpisodeFilesAppState;
diff --git a/frontend/src/App/State/EpisodesAppState.ts b/frontend/src/App/State/EpisodesAppState.ts
deleted file mode 100644
index 4234c0bcb..000000000
--- a/frontend/src/App/State/EpisodesAppState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import AppSectionState from 'App/State/AppSectionState';
-import Episode from 'Episode/Episode';
-
-type EpisodesAppState = AppSectionState;
-
-export default EpisodesAppState;
diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts
deleted file mode 100644
index 632b82179..000000000
--- a/frontend/src/App/State/HistoryAppState.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import AppSectionState, {
- AppSectionFilterState,
- PagedAppSectionState,
- TableAppSectionState,
-} from 'App/State/AppSectionState';
-import History from 'typings/History';
-
-interface HistoryAppState
- extends AppSectionState,
- AppSectionFilterState,
- PagedAppSectionState,
- TableAppSectionState {}
-
-export default HistoryAppState;
diff --git a/frontend/src/App/State/InteractiveImportAppState.ts b/frontend/src/App/State/InteractiveImportAppState.ts
deleted file mode 100644
index 84fd9f4c1..000000000
--- a/frontend/src/App/State/InteractiveImportAppState.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import AppSectionState from 'App/State/AppSectionState';
-import ImportMode from 'InteractiveImport/ImportMode';
-import InteractiveImport from 'InteractiveImport/InteractiveImport';
-
-interface FavoriteFolder {
- folder: string;
-}
-
-interface RecentFolder {
- folder: string;
- lastUsed: string;
-}
-
-interface InteractiveImportAppState extends AppSectionState {
- originalItems: InteractiveImport[];
- importMode: ImportMode;
- favoriteFolders: FavoriteFolder[];
- recentFolders: RecentFolder[];
-}
-
-export default InteractiveImportAppState;
diff --git a/frontend/src/App/State/MetadataAppState.ts b/frontend/src/App/State/MetadataAppState.ts
deleted file mode 100644
index 495f109d8..000000000
--- a/frontend/src/App/State/MetadataAppState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { AppSectionProviderState } from 'App/State/AppSectionState';
-import Metadata from 'typings/Metadata';
-
-type MetadataAppState = AppSectionProviderState;
-
-export default MetadataAppState;
diff --git a/frontend/src/App/State/OAuthAppState.ts b/frontend/src/App/State/OAuthAppState.ts
deleted file mode 100644
index 619767929..000000000
--- a/frontend/src/App/State/OAuthAppState.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Error } from './AppSectionState';
-
-interface OAuthAppState {
- authorizing: boolean;
- result: Record | null;
- error: Error;
-}
-
-export default OAuthAppState;
diff --git a/frontend/src/App/State/ParseAppState.ts b/frontend/src/App/State/ParseAppState.ts
deleted file mode 100644
index 67fb4cc63..000000000
--- a/frontend/src/App/State/ParseAppState.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import ModelBase from 'App/ModelBase';
-import { AppSectionItemState } from 'App/State/AppSectionState';
-import Episode from 'Episode/Episode';
-import Language from 'Language/Language';
-import { QualityModel } from 'Quality/Quality';
-import Series from 'Series/Series';
-import CustomFormat from 'typings/CustomFormat';
-
-export interface SeriesTitleInfo {
- title: string;
- titleWithoutYear: string;
- year: number;
- allTitles: string[];
-}
-
-export interface ParsedEpisodeInfo {
- releaseTitle: string;
- seriesTitle: string;
- seriesTitleInfo: SeriesTitleInfo;
- quality: QualityModel;
- seasonNumber: number;
- episodeNumbers: number[];
- absoluteEpisodeNumbers: number[];
- specialAbsoluteEpisodeNumbers: number[];
- languages: Language[];
- fullSeason: boolean;
- isPartialSeason: boolean;
- isMultiSeason: boolean;
- isSeasonExtra: boolean;
- special: boolean;
- releaseHash: string;
- seasonPart: number;
- releaseGroup?: string;
- releaseTokens: string;
- airDate?: string;
- isDaily: boolean;
- isAbsoluteNumbering: boolean;
- isPossibleSpecialEpisode: boolean;
- isPossibleSceneSeasonSpecial: boolean;
-}
-
-export interface ParseModel extends ModelBase {
- title: string;
- parsedEpisodeInfo: ParsedEpisodeInfo;
- series?: Series;
- episodes: Episode[];
- languages?: Language[];
- customFormats?: CustomFormat[];
- customFormatScore?: number;
-}
-
-type ParseAppState = AppSectionItemState;
-
-export default ParseAppState;
diff --git a/frontend/src/App/State/PathsAppState.ts b/frontend/src/App/State/PathsAppState.ts
deleted file mode 100644
index 068a48dc0..000000000
--- a/frontend/src/App/State/PathsAppState.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-interface BasePath {
- name: string;
- path: string;
- size: number;
- lastModified: string;
-}
-
-interface File extends BasePath {
- type: 'file';
-}
-
-interface Folder extends BasePath {
- type: 'folder';
-}
-
-export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
-export type Path = File | Folder;
-
-interface PathsAppState {
- currentPath: string;
- isFetching: boolean;
- isPopulated: boolean;
- error: Error;
- directories: Folder[];
- files: File[];
- parent: string | null;
-}
-
-export default PathsAppState;
diff --git a/frontend/src/App/State/ProviderOptionsAppState.ts b/frontend/src/App/State/ProviderOptionsAppState.ts
deleted file mode 100644
index 7fb5df02b..000000000
--- a/frontend/src/App/State/ProviderOptionsAppState.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import AppSectionState from 'App/State/AppSectionState';
-import Field, { FieldSelectOption } from 'typings/Field';
-
-export interface ProviderOptions {
- fields?: Field[];
-}
-
-interface ProviderOptionsDevice {
- id: string;
- name: string;
-}
-
-interface ProviderOptionsAppState {
- devices: AppSectionState;
- servers: AppSectionState>;
- newznabCategories: AppSectionState>;
- getProfiles: AppSectionState>;
- getTags: AppSectionState>;
- getRootFolders: AppSectionState>;
-}
-
-export default ProviderOptionsAppState;
diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts
deleted file mode 100644
index 954d649a2..000000000
--- a/frontend/src/App/State/QueueAppState.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import Queue from 'typings/Queue';
-import AppSectionState, {
- AppSectionFilterState,
- AppSectionItemState,
- Error,
- PagedAppSectionState,
- TableAppSectionState,
-} from './AppSectionState';
-
-export interface QueueStatus {
- totalCount: number;
- count: number;
- unknownCount: number;
- errors: boolean;
- warnings: boolean;
- unknownErrors: boolean;
- unknownWarnings: boolean;
-}
-
-export interface QueueDetailsAppState extends AppSectionState {
- params: unknown;
-}
-
-export interface QueuePagedAppState
- extends AppSectionState,
- AppSectionFilterState,
- PagedAppSectionState,
- TableAppSectionState {
- isGrabbing: boolean;
- grabError: Error;
- isRemoving: boolean;
- removeError: Error;
-}
-
-interface QueueAppState {
- status: AppSectionItemState;
- details: QueueDetailsAppState;
- paged: QueuePagedAppState;
- options: {
- includeUnknownSeriesItems: boolean;
- };
-}
-
-export default QueueAppState;
diff --git a/frontend/src/App/State/ReleasesAppState.ts b/frontend/src/App/State/ReleasesAppState.ts
deleted file mode 100644
index 350f6eac8..000000000
--- a/frontend/src/App/State/ReleasesAppState.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import AppSectionState, {
- AppSectionFilterState,
-} from 'App/State/AppSectionState';
-import Release from 'typings/Release';
-
-interface ReleasesAppState
- extends AppSectionState,
- AppSectionFilterState {}
-
-export default ReleasesAppState;
diff --git a/frontend/src/App/State/RootFolderAppState.ts b/frontend/src/App/State/RootFolderAppState.ts
deleted file mode 100644
index 9e636c95f..000000000
--- a/frontend/src/App/State/RootFolderAppState.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import AppSectionState, {
- AppSectionDeleteState,
- AppSectionSaveState,
-} from 'App/State/AppSectionState';
-import RootFolder from 'typings/RootFolder';
-
-interface RootFolderAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {}
-
-export default RootFolderAppState;
diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts
deleted file mode 100644
index 5da5987dd..000000000
--- a/frontend/src/App/State/SeriesAppState.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import AppSectionState, {
- AppSectionDeleteState,
- AppSectionSaveState,
-} from 'App/State/AppSectionState';
-import Column from 'Components/Table/Column';
-import { SortDirection } from 'Helpers/Props/sortDirections';
-import Series from 'Series/Series';
-import { Filter, FilterBuilderProp } from './AppState';
-
-export interface SeriesIndexAppState {
- sortKey: string;
- sortDirection: SortDirection;
- secondarySortKey: string;
- secondarySortDirection: SortDirection;
- view: string;
-
- posterOptions: {
- detailedProgressBar: boolean;
- size: string;
- showTitle: boolean;
- showMonitored: boolean;
- showQualityProfile: boolean;
- showTags: boolean;
- showSearchAction: boolean;
- };
-
- overviewOptions: {
- detailedProgressBar: boolean;
- size: string;
- showMonitored: boolean;
- showNetwork: boolean;
- showQualityProfile: boolean;
- showPreviousAiring: boolean;
- showAdded: boolean;
- showSeasonCount: boolean;
- showPath: boolean;
- showSizeOnDisk: boolean;
- showTags: boolean;
- showSearchAction: boolean;
- };
-
- tableOptions: {
- showBanners: boolean;
- showSearchAction: boolean;
- };
-
- selectedFilterKey: string;
- filterBuilderProps: FilterBuilderProp[];
- filters: Filter[];
- columns: Column[];
-}
-
-interface SeriesAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {
- itemMap: Record;
-
- deleteOptions: {
- addImportListExclusion: boolean;
- };
-
- pendingChanges: Partial;
-}
-
-export default SeriesAppState;
diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts
deleted file mode 100644
index b8e6f4954..000000000
--- a/frontend/src/App/State/SettingsAppState.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import AppSectionState, {
- AppSectionDeleteState,
- AppSectionItemSchemaState,
- AppSectionItemState,
- AppSectionSaveState,
- PagedAppSectionState,
-} from 'App/State/AppSectionState';
-import Language from 'Language/Language';
-import CustomFormat from 'typings/CustomFormat';
-import DownloadClient from 'typings/DownloadClient';
-import ImportList from 'typings/ImportList';
-import ImportListExclusion from 'typings/ImportListExclusion';
-import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
-import Indexer from 'typings/Indexer';
-import IndexerFlag from 'typings/IndexerFlag';
-import Notification from 'typings/Notification';
-import QualityProfile from 'typings/QualityProfile';
-import General from 'typings/Settings/General';
-import NamingConfig from 'typings/Settings/NamingConfig';
-import NamingExample from 'typings/Settings/NamingExample';
-import ReleaseProfile from 'typings/Settings/ReleaseProfile';
-import UiSettings from 'typings/Settings/UiSettings';
-import MetadataAppState from './MetadataAppState';
-
-export interface DownloadClientAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {
- isTestingAll: boolean;
-}
-
-export interface GeneralAppState
- extends AppSectionItemState,
- AppSectionSaveState {}
-
-export interface NamingAppState
- extends AppSectionItemState,
- AppSectionSaveState {}
-
-export type NamingExamplesAppState = AppSectionItemState;
-
-export interface ImportListAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {}
-
-export interface IndexerAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {
- isTestingAll: boolean;
-}
-
-export interface NotificationAppState
- extends AppSectionState,
- AppSectionDeleteState {}
-
-export interface QualityProfilesAppState
- extends AppSectionState,
- AppSectionItemSchemaState {}
-
-export interface ReleaseProfilesAppState
- extends AppSectionState,
- AppSectionSaveState {
- pendingChanges: Partial;
-}
-
-export interface CustomFormatAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {}
-
-export interface ImportListOptionsSettingsAppState
- extends AppSectionItemState,
- AppSectionSaveState {}
-
-export interface ImportListExclusionsSettingsAppState
- extends AppSectionState,
- AppSectionSaveState,
- PagedAppSectionState,
- AppSectionDeleteState {
- pendingChanges: Partial;
-}
-
-export type IndexerFlagSettingsAppState = AppSectionState;
-export type LanguageSettingsAppState = AppSectionState;
-export type UiSettingsAppState = AppSectionItemState;
-
-interface SettingsAppState {
- advancedSettings: boolean;
- customFormats: CustomFormatAppState;
- downloadClients: DownloadClientAppState;
- general: GeneralAppState;
- importListExclusions: ImportListExclusionsSettingsAppState;
- importListOptions: ImportListOptionsSettingsAppState;
- importLists: ImportListAppState;
- indexerFlags: IndexerFlagSettingsAppState;
- indexers: IndexerAppState;
- languages: LanguageSettingsAppState;
- metadata: MetadataAppState;
- naming: NamingAppState;
- namingExamples: NamingExamplesAppState;
- notifications: NotificationAppState;
- qualityProfiles: QualityProfilesAppState;
- releaseProfiles: ReleaseProfilesAppState;
- ui: UiSettingsAppState;
-}
-
-export default SettingsAppState;
diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts
deleted file mode 100644
index 1161f0e1e..000000000
--- a/frontend/src/App/State/SystemAppState.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import DiskSpace from 'typings/DiskSpace';
-import Health from 'typings/Health';
-import SystemStatus from 'typings/SystemStatus';
-import Task from 'typings/Task';
-import Update from 'typings/Update';
-import AppSectionState, { AppSectionItemState } from './AppSectionState';
-
-export type DiskSpaceAppState = AppSectionState;
-export type HealthAppState = AppSectionState;
-export type SystemStatusAppState = AppSectionItemState;
-export type TaskAppState = AppSectionState;
-export type UpdateAppState = AppSectionState;
-
-interface SystemAppState {
- diskSpace: DiskSpaceAppState;
- health: HealthAppState;
- status: SystemStatusAppState;
- tasks: TaskAppState;
- updates: UpdateAppState;
-}
-
-export default SystemAppState;
diff --git a/frontend/src/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts
deleted file mode 100644
index 914df9044..000000000
--- a/frontend/src/App/State/TagsAppState.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import ModelBase from 'App/ModelBase';
-import AppSectionState, {
- AppSectionDeleteState,
- AppSectionSaveState,
-} from 'App/State/AppSectionState';
-
-export interface Tag extends ModelBase {
- label: string;
-}
-
-export interface TagDetail extends ModelBase {
- label: string;
- autoTagIds: number[];
- delayProfileIds: number[];
- downloadClientIds: [];
- importListIds: number[];
- indexerIds: number[];
- notificationIds: number[];
- restrictionIds: number[];
- seriesIds: number[];
-}
-
-export interface TagDetailAppState
- extends AppSectionState,
- AppSectionDeleteState,
- AppSectionSaveState {}
-
-interface TagsAppState extends AppSectionState, AppSectionDeleteState {
- details: TagDetailAppState;
-}
-
-export default TagsAppState;
diff --git a/frontend/src/App/State/WantedAppState.ts b/frontend/src/App/State/WantedAppState.ts
deleted file mode 100644
index b543d3879..000000000
--- a/frontend/src/App/State/WantedAppState.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import AppSectionState from 'App/State/AppSectionState';
-import Episode from 'Episode/Episode';
-
-type WantedCutoffUnmetAppState = AppSectionState;
-
-type WantedMissingAppState = AppSectionState;
-
-interface WantedAppState {
- cutoffUnmet: WantedCutoffUnmetAppState;
- missing: WantedMissingAppState;
-}
-
-export default WantedAppState;
diff --git a/frontend/src/Calendar/Agenda/Agenda.css.d.ts b/frontend/src/Calendar/Agenda/Agenda.css.d.ts
deleted file mode 100644
index 44421cc99..000000000
--- a/frontend/src/Calendar/Agenda/Agenda.css.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'agenda': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Agenda/Agenda.js b/frontend/src/Calendar/Agenda/Agenda.js
new file mode 100644
index 000000000..89472301d
--- /dev/null
+++ b/frontend/src/Calendar/Agenda/Agenda.js
@@ -0,0 +1,38 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React from 'react';
+import AgendaEventConnector from './AgendaEventConnector';
+import styles from './Agenda.css';
+
+function Agenda(props) {
+ const {
+ items
+ } = props;
+
+ return (
+
+ {
+ items.map((item, index) => {
+ const momentDate = moment(item.airDateUtc);
+ const showDate = index === 0 ||
+ !moment(items[index - 1].airDateUtc).isSame(momentDate, 'day');
+
+ return (
+
+ );
+ })
+ }
+
+ );
+}
+
+Agenda.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object).isRequired
+};
+
+export default Agenda;
diff --git a/frontend/src/Calendar/Agenda/Agenda.tsx b/frontend/src/Calendar/Agenda/Agenda.tsx
deleted file mode 100644
index fdef40466..000000000
--- a/frontend/src/Calendar/Agenda/Agenda.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import moment from 'moment';
-import React from 'react';
-import { useSelector } from 'react-redux';
-import AppState from 'App/State/AppState';
-import AgendaEvent from './AgendaEvent';
-import styles from './Agenda.css';
-
-function Agenda() {
- const { items } = useSelector((state: AppState) => state.calendar);
-
- return (
-
- {items.map((item, index) => {
- const momentDate = moment(item.airDateUtc);
- const showDate =
- index === 0 ||
- !moment(items[index - 1].airDateUtc).isSame(momentDate, 'day');
-
- return
;
- })}
-
- );
-}
-
-export default Agenda;
diff --git a/frontend/src/Calendar/Agenda/AgendaConnector.js b/frontend/src/Calendar/Agenda/AgendaConnector.js
new file mode 100644
index 000000000..b6f238873
--- /dev/null
+++ b/frontend/src/Calendar/Agenda/AgendaConnector.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import Agenda from './Agenda';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar,
+ (calendar) => {
+ return calendar;
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(Agenda);
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css b/frontend/src/Calendar/Agenda/AgendaEvent.css
index 7ad9ccf6a..38b4f50f3 100644
--- a/frontend/src/Calendar/Agenda/AgendaEvent.css
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.css
@@ -1,27 +1,12 @@
.event {
- position: relative;
- padding: 5px;
- border-bottom: 1px solid var(--borderColor);
-}
-
-.underlay {
- @add-mixin cover;
-
- &:hover {
- background-color: var(--tableRowHoverBackgroundColor);
- }
-}
-
-.overlay {
- @add-mixin linkOverlay;
-
- position: relative;
display: flex;
overflow-x: hidden;
+ padding: 5px;
+ border-bottom: 1px solid $borderColor;
font-size: $defaultFontSize;
- &:global(.colorImpaired) {
- border-left-width: 5px;
+ &:hover {
+ background-color: $tableRowHoverBackgroundColor;
}
}
@@ -71,8 +56,6 @@
.statusIcon {
margin-left: 3px;
- cursor: default;
- pointer-events: all;
}
/*
@@ -103,12 +86,8 @@
composes: premiere from '~Calendar/Events/CalendarEvent.css';
}
-.unaired {
- composes: unaired from '~Calendar/Events/CalendarEvent.css';
-}
-
@media only screen and (max-width: $breakpointSmall) {
- .overlay {
+ .event {
flex-direction: column;
}
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts b/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts
deleted file mode 100644
index 288e11824..000000000
--- a/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'absoluteEpisodeNumber': string;
- 'date': string;
- 'downloaded': string;
- 'downloading': string;
- 'episodeSeparator': string;
- 'episodeTitle': string;
- 'event': string;
- 'eventWrapper': string;
- 'missing': string;
- 'onAir': string;
- 'overlay': string;
- 'premiere': string;
- 'seasonEpisodeNumber': string;
- 'seriesTitle': string;
- 'statusIcon': string;
- 'time': string;
- 'unaired': string;
- 'underlay': string;
- 'unmonitored': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js
new file mode 100644
index 000000000..3d5aa36fb
--- /dev/null
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.js
@@ -0,0 +1,253 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import formatTime from 'Utilities/Date/formatTime';
+import padNumber from 'Utilities/Number/padNumber';
+import { icons, kinds } from 'Helpers/Props';
+import getStatusStyle from 'Calendar/getStatusStyle';
+import Icon from 'Components/Icon';
+import Link from 'Components/Link/Link';
+import episodeEntities from 'Episode/episodeEntities';
+import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
+import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
+import styles from './AgendaEvent.css';
+
+class AgendaEvent extends Component {
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isDetailsModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onPress = () => {
+ this.setState({ isDetailsModalOpen: true });
+ }
+
+ onDetailsModalClose = () => {
+ this.setState({ isDetailsModalOpen: false });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ id,
+ series,
+ episodeFile,
+ title,
+ seasonNumber,
+ episodeNumber,
+ absoluteEpisodeNumber,
+ airDateUtc,
+ monitored,
+ hasFile,
+ grabbed,
+ queueItem,
+ showDate,
+ showEpisodeInformation,
+ showFinaleIcon,
+ showSpecialIcon,
+ showCutoffUnmetIcon,
+ timeFormat,
+ longDateFormat,
+ colorImpairedMode
+ } = this.props;
+
+ const startTime = moment(airDateUtc);
+ const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
+ const downloading = !!(queueItem || grabbed);
+ const isMonitored = series.monitored && monitored;
+ const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
+ const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
+ const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
+ const seasonStatistics = season.statistics || {};
+
+ return (
+
+
+
+ {
+ showDate &&
+ startTime.format(longDateFormat)
+ }
+
+
+
+
+ {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
+
+
+
+ {series.title}
+
+
+ {
+ showEpisodeInformation &&
+
+ {seasonNumber}x{padNumber(episodeNumber, 2)}
+
+ {
+ series.seriesType === 'anime' && absoluteEpisodeNumber &&
+
({absoluteEpisodeNumber})
+ }
+
+
-
+
+ }
+
+
+ {
+ showEpisodeInformation &&
+ title
+ }
+
+
+ {
+ missingAbsoluteNumber &&
+
+ }
+
+ {
+ !!queueItem &&
+
+
+
+ }
+
+ {
+ !queueItem && grabbed &&
+
+ }
+
+ {
+ showCutoffUnmetIcon &&
+ !!episodeFile &&
+ episodeFile.qualityCutoffNotMet &&
+
+ }
+
+ {
+ showCutoffUnmetIcon &&
+ !!episodeFile &&
+ episodeFile.languageCutoffNotMet &&
+ !episodeFile.qualityCutoffNotMet &&
+
+ }
+
+ {
+ episodeNumber === 1 && seasonNumber > 0 &&
+
+ }
+
+ {
+ showFinaleIcon &&
+ episodeNumber !== 1 &&
+ seasonNumber > 0 &&
+ episodeNumber === seasonStatistics.totalEpisodeCount &&
+
+ }
+
+ {
+ showSpecialIcon &&
+ (episodeNumber === 0 || seasonNumber === 0) &&
+
+ }
+
+
+
+
+
+ );
+ }
+}
+
+AgendaEvent.propTypes = {
+ id: PropTypes.number.isRequired,
+ series: PropTypes.object.isRequired,
+ episodeFile: PropTypes.object,
+ title: PropTypes.string.isRequired,
+ seasonNumber: PropTypes.number.isRequired,
+ episodeNumber: PropTypes.number.isRequired,
+ absoluteEpisodeNumber: PropTypes.number,
+ airDateUtc: PropTypes.string.isRequired,
+ monitored: PropTypes.bool.isRequired,
+ hasFile: PropTypes.bool.isRequired,
+ grabbed: PropTypes.bool,
+ queueItem: PropTypes.object,
+ showDate: PropTypes.bool.isRequired,
+ showEpisodeInformation: PropTypes.bool.isRequired,
+ showFinaleIcon: PropTypes.bool.isRequired,
+ showSpecialIcon: PropTypes.bool.isRequired,
+ showCutoffUnmetIcon: PropTypes.bool.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ colorImpairedMode: PropTypes.bool.isRequired
+};
+
+export default AgendaEvent;
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.tsx b/frontend/src/Calendar/Agenda/AgendaEvent.tsx
deleted file mode 100644
index 2fd2d7c54..000000000
--- a/frontend/src/Calendar/Agenda/AgendaEvent.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import classNames from 'classnames';
-import moment from 'moment';
-import React, { useCallback, useState } from 'react';
-import { useSelector } from 'react-redux';
-import AppState from 'App/State/AppState';
-import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
-import getStatusStyle from 'Calendar/getStatusStyle';
-import Icon from 'Components/Icon';
-import Link from 'Components/Link/Link';
-import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
-import episodeEntities from 'Episode/episodeEntities';
-import getFinaleTypeName from 'Episode/getFinaleTypeName';
-import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
-import { icons, kinds } from 'Helpers/Props';
-import useSeries from 'Series/useSeries';
-import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import formatTime from 'Utilities/Date/formatTime';
-import padNumber from 'Utilities/Number/padNumber';
-import translate from 'Utilities/String/translate';
-import styles from './AgendaEvent.css';
-
-interface AgendaEventProps {
- id: number;
- seriesId: number;
- episodeFileId: number;
- title: string;
- seasonNumber: number;
- episodeNumber: number;
- absoluteEpisodeNumber?: number;
- airDateUtc: string;
- monitored: boolean;
- unverifiedSceneNumbering?: boolean;
- finaleType?: string;
- hasFile: boolean;
- grabbed?: boolean;
- showDate: boolean;
-}
-
-function AgendaEvent(props: AgendaEventProps) {
- const {
- id,
- seriesId,
- episodeFileId,
- title,
- seasonNumber,
- episodeNumber,
- absoluteEpisodeNumber,
- airDateUtc,
- monitored,
- unverifiedSceneNumbering,
- finaleType,
- hasFile,
- grabbed,
- showDate,
- } = props;
-
- const series = useSeries(seriesId)!;
- const episodeFile = useEpisodeFile(episodeFileId);
- const queueItem = useSelector(createQueueItemSelectorForHook(id));
- const { timeFormat, longDateFormat, enableColorImpairedMode } = useSelector(
- createUISettingsSelector()
- );
-
- const {
- showEpisodeInformation,
- showFinaleIcon,
- showSpecialIcon,
- showCutoffUnmetIcon,
- } = useSelector((state: AppState) => state.calendar.options);
-
- const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
-
- const startTime = moment(airDateUtc);
- const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
- const downloading = !!(queueItem || grabbed);
- const isMonitored = series.monitored && monitored;
- const statusStyle = getStatusStyle(
- hasFile,
- downloading,
- startTime,
- endTime,
- isMonitored
- );
- const missingAbsoluteNumber =
- series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
-
- const handlePress = useCallback(() => {
- setIsDetailsModalOpen(true);
- }, []);
-
- const handleDetailsModalClose = useCallback(() => {
- setIsDetailsModalOpen(false);
- }, []);
-
- return (
-
-
-
-
-
- {showDate && startTime.format(longDateFormat)}
-
-
-
-
- {formatTime(airDateUtc, timeFormat)} -{' '}
- {formatTime(endTime.toISOString(), timeFormat, {
- includeMinuteZero: true,
- })}
-
-
-
{series.title}
-
- {showEpisodeInformation ? (
-
- {seasonNumber}x{padNumber(episodeNumber, 2)}
- {series.seriesType === 'anime' && absoluteEpisodeNumber && (
-
- ({absoluteEpisodeNumber})
-
- )}
-
-
-
- ) : null}
-
-
- {showEpisodeInformation ? title : null}
-
-
- {missingAbsoluteNumber ? (
-
- ) : null}
-
- {unverifiedSceneNumbering && !missingAbsoluteNumber ? (
-
- ) : null}
-
- {queueItem ? (
-
-
-
- ) : null}
-
- {!queueItem && grabbed ? (
-
- ) : null}
-
- {showCutoffUnmetIcon &&
- episodeFile &&
- episodeFile.qualityCutoffNotMet ? (
-
- ) : null}
-
- {episodeNumber === 1 && seasonNumber > 0 && (
-
- )}
-
- {showFinaleIcon && finaleType ? (
-
- ) : null}
-
- {showSpecialIcon && (episodeNumber === 0 || seasonNumber === 0) ? (
-
- ) : null}
-
-
-
-
-
- );
-}
-
-export default AgendaEvent;
diff --git a/frontend/src/Calendar/Agenda/AgendaEventConnector.js b/frontend/src/Calendar/Agenda/AgendaEventConnector.js
new file mode 100644
index 000000000..e1d996225
--- /dev/null
+++ b/frontend/src/Calendar/Agenda/AgendaEventConnector.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
+import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
+import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import AgendaEvent from './AgendaEvent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.options,
+ createSeriesSelector(),
+ createEpisodeFileSelector(),
+ createQueueItemSelector(),
+ createUISettingsSelector(),
+ (calendarOptions, series, episodeFile, queueItem, uiSettings) => {
+ return {
+ series,
+ episodeFile,
+ queueItem,
+ ...calendarOptions,
+ timeFormat: uiSettings.timeFormat,
+ longDateFormat: uiSettings.longDateFormat,
+ colorImpairedMode: uiSettings.enableColorImpairedMode
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(AgendaEvent);
diff --git a/frontend/src/Calendar/Calendar.css.d.ts b/frontend/src/Calendar/Calendar.css.d.ts
deleted file mode 100644
index 503034402..000000000
--- a/frontend/src/Calendar/Calendar.css.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'calendar': string;
- 'calendarContent': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Calendar.js b/frontend/src/Calendar/Calendar.js
new file mode 100644
index 000000000..6ceb1f3bb
--- /dev/null
+++ b/frontend/src/Calendar/Calendar.js
@@ -0,0 +1,64 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import * as calendarViews from './calendarViews';
+import CalendarHeaderConnector from './Header/CalendarHeaderConnector';
+import DaysOfWeekConnector from './Day/DaysOfWeekConnector';
+import CalendarDaysConnector from './Day/CalendarDaysConnector';
+import AgendaConnector from './Agenda/AgendaConnector';
+import styles from './Calendar.css';
+
+class Calendar extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ view
+ } = this.props;
+
+ return (
+
+ {
+ isFetching && !isPopulated &&
+
+ }
+
+ {
+ !isFetching && !!error &&
+
Unable to load the calendar
+ }
+
+ {
+ !error && isPopulated && view === calendarViews.AGENDA &&
+
+ }
+
+ {
+ !error && isPopulated && view !== calendarViews.AGENDA &&
+
+
+
+
+
+ }
+
+ );
+ }
+}
+
+Calendar.propTypes = {
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ error: PropTypes.object,
+ view: PropTypes.string.isRequired
+};
+
+export default Calendar;
diff --git a/frontend/src/Calendar/Calendar.tsx b/frontend/src/Calendar/Calendar.tsx
deleted file mode 100644
index caa337cf0..000000000
--- a/frontend/src/Calendar/Calendar.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-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';
-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 {
- clearQueueDetails,
- fetchQueueDetails,
-} from 'Store/Actions/queueActions';
-import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
-import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
-import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
-import {
- registerPagePopulator,
- unregisterPagePopulator,
-} from 'Utilities/pagePopulator';
-import translate from 'Utilities/String/translate';
-import Agenda from './Agenda/Agenda';
-import CalendarDays from './Day/CalendarDays';
-import DaysOfWeek from './Day/DaysOfWeek';
-import CalendarHeader from './Header/CalendarHeader';
-import styles from './Calendar.css';
-
-const UPDATE_DELAY = 3600000; // 1 hour
-
-function Calendar() {
- const dispatch = useDispatch();
- const requestCurrentPage = useCurrentPage();
- const updateTimeout = useRef>();
-
- const { isFetching, isPopulated, error, items, time, view } = useSelector(
- (state: AppState) => state.calendar
- );
-
- 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());
- updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
- }
-
- updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
- }, [dispatch]);
-
- useEffect(() => {
- handleScheduleUpdate();
-
- return () => {
- dispatch(clearCalendar());
- dispatch(clearQueueDetails());
- dispatch(clearEpisodeFiles());
- clearTimeout(updateTimeout.current);
- };
- }, [dispatch, handleScheduleUpdate]);
-
- useEffect(() => {
- if (requestCurrentPage) {
- dispatch(fetchCalendar());
- } else {
- dispatch(gotoCalendarToday());
- }
- }, [requestCurrentPage, dispatch]);
-
- useEffect(() => {
- const repopulate = () => {
- dispatch(fetchQueueDetails({ time, view }));
- dispatch(fetchCalendar({ time, view }));
- };
-
- registerPagePopulator(repopulate, [
- 'episodeFileUpdated',
- 'episodeFileDeleted',
- ]);
-
- return () => {
- unregisterPagePopulator(repopulate);
- };
- }, [time, view, dispatch]);
-
- useEffect(() => {
- handleScheduleUpdate();
- }, [time, handleScheduleUpdate]);
-
- useEffect(() => {
- if (
- previousFirstDayOfWeek != null &&
- firstDayOfWeek !== previousFirstDayOfWeek
- ) {
- dispatch(fetchCalendar({ time, view }));
- }
- }, [time, view, firstDayOfWeek, previousFirstDayOfWeek, dispatch]);
-
- useEffect(() => {
- if (wasRefreshingSeries && !isRefreshingSeries) {
- dispatch(fetchCalendar({ time, view }));
- }
- }, [time, view, isRefreshingSeries, wasRefreshingSeries, dispatch]);
-
- useEffect(() => {
- if (!previousItems || hasDifferentItems(items, previousItems)) {
- const episodeIds = selectUniqueIds(items, 'id');
- const episodeFileIds = selectUniqueIds(
- items,
- 'episodeFileId'
- );
-
- if (items.length) {
- dispatch(fetchQueueDetails({ episodeIds }));
- }
-
- if (episodeFileIds.length) {
- dispatch(fetchEpisodeFiles({ episodeFileIds }));
- }
- }
- }, [items, previousItems, dispatch]);
-
- return (
-
- {isFetching && !isPopulated ?
: null}
-
- {!isFetching && error ? (
-
{translate('CalendarLoadError')}
- ) : null}
-
- {!error && isPopulated && view === 'agenda' ? (
-
- ) : null}
-
- {!error && isPopulated && view !== 'agenda' ? (
-
-
-
-
-
- ) : null}
-
- );
-}
-
-export default Calendar;
diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js
new file mode 100644
index 000000000..636026c56
--- /dev/null
+++ b/frontend/src/Calendar/CalendarConnector.js
@@ -0,0 +1,196 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
+import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
+import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
+import * as calendarActions from 'Store/Actions/calendarActions';
+import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
+import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
+import * as commandNames from 'Commands/commandNames';
+import Calendar from './Calendar';
+
+const UPDATE_DELAY = 3600000; // 1 hour
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar,
+ (state) => state.settings.ui.item.firstDayOfWeek,
+ createCommandExecutingSelector(commandNames.REFRESH_SERIES),
+ (calendar, firstDayOfWeek, isRefreshingSeries) => {
+ return {
+ ...calendar,
+ isRefreshingSeries,
+ firstDayOfWeek
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ ...calendarActions,
+ fetchEpisodeFiles,
+ clearEpisodeFiles,
+ fetchQueueDetails,
+ clearQueueDetails
+};
+
+class CalendarConnector extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.updateTimeoutId = null;
+ }
+
+ componentDidMount() {
+ const {
+ useCurrentPage,
+ fetchCalendar,
+ gotoCalendarToday
+ } = this.props;
+
+ registerPagePopulator(this.repopulate);
+
+ if (useCurrentPage) {
+ fetchCalendar();
+ } else {
+ gotoCalendarToday();
+ }
+
+ this.scheduleUpdate();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ items,
+ time,
+ view,
+ isRefreshingSeries,
+ firstDayOfWeek
+ } = this.props;
+
+ if (hasDifferentItems(prevProps.items, items)) {
+ const episodeIds = selectUniqueIds(items, 'id');
+ const episodeFileIds = selectUniqueIds(items, 'episodeFileId');
+
+ if (items.length) {
+ this.props.fetchQueueDetails({ episodeIds });
+ }
+
+ if (episodeFileIds.length) {
+ this.props.fetchEpisodeFiles({ episodeFileIds });
+ }
+ }
+
+ if (prevProps.time !== time) {
+ this.scheduleUpdate();
+ }
+
+ if (prevProps.firstDayOfWeek !== firstDayOfWeek) {
+ this.props.fetchCalendar({ time, view });
+ }
+
+ if (prevProps.isRefreshingSeries && !isRefreshingSeries) {
+ this.props.fetchCalendar({ time, view });
+ }
+ }
+
+ componentWillUnmount() {
+ unregisterPagePopulator(this.repopulate);
+ this.props.clearCalendar();
+ this.props.clearQueueDetails();
+ this.props.clearEpisodeFiles();
+ this.clearUpdateTimeout();
+ }
+
+ //
+ // Control
+
+ repopulate = () => {
+ const {
+ time,
+ view
+ } = this.props;
+
+ this.props.fetchQueueDetails({ time, view });
+ this.props.fetchCalendar({ time, view });
+ }
+
+ scheduleUpdate = () => {
+ this.clearUpdateTimeout();
+
+ this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
+ }
+
+ clearUpdateTimeout = () => {
+ if (this.updateTimeoutId) {
+ clearTimeout(this.updateTimeoutId);
+ }
+ }
+
+ updateCalendar = () => {
+ this.props.gotoCalendarToday();
+ this.scheduleUpdate();
+ }
+
+ //
+ // Listeners
+
+ onCalendarViewChange = (view) => {
+ this.props.setCalendarView({ view });
+ }
+
+ onTodayPress = () => {
+ this.props.gotoCalendarToday();
+ }
+
+ onPreviousPress = () => {
+ this.props.gotoCalendarPreviousRange();
+ }
+
+ onNextPress = () => {
+ this.props.gotoCalendarNextRange();
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+CalendarConnector.propTypes = {
+ useCurrentPage: PropTypes.bool.isRequired,
+ time: PropTypes.string,
+ view: PropTypes.string.isRequired,
+ firstDayOfWeek: PropTypes.number.isRequired,
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ isRefreshingSeries: PropTypes.bool.isRequired,
+ setCalendarView: PropTypes.func.isRequired,
+ gotoCalendarToday: PropTypes.func.isRequired,
+ gotoCalendarPreviousRange: PropTypes.func.isRequired,
+ gotoCalendarNextRange: PropTypes.func.isRequired,
+ clearCalendar: PropTypes.func.isRequired,
+ fetchCalendar: PropTypes.func.isRequired,
+ fetchEpisodeFiles: PropTypes.func.isRequired,
+ clearEpisodeFiles: PropTypes.func.isRequired,
+ fetchQueueDetails: PropTypes.func.isRequired,
+ clearQueueDetails: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector);
diff --git a/frontend/src/Calendar/CalendarFilterModal.tsx b/frontend/src/Calendar/CalendarFilterModal.tsx
deleted file mode 100644
index e26b2928b..000000000
--- a/frontend/src/Calendar/CalendarFilterModal.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { useCallback } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
-import FilterModal 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;
- }
- );
-}
-
-interface CalendarFilterModalProps {
- isOpen: boolean;
-}
-
-export default function CalendarFilterModal(props: CalendarFilterModalProps) {
- const sectionItems = useSelector(createCalendarSelector());
- const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
- const customFilterType = 'calendar';
-
- const dispatch = useDispatch();
-
- const dispatchSetFilter = useCallback(
- (payload: unknown) => {
- dispatch(setCalendarFilter(payload));
- },
- [dispatch]
- );
-
- return (
-
- );
-}
diff --git a/frontend/src/Calendar/CalendarPage.css.d.ts b/frontend/src/Calendar/CalendarPage.css.d.ts
deleted file mode 100644
index 30befba55..000000000
--- a/frontend/src/Calendar/CalendarPage.css.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'calendarInnerPageBody': string;
- 'calendarPageBody': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js
new file mode 100644
index 000000000..5e4f1c6db
--- /dev/null
+++ b/frontend/src/Calendar/CalendarPage.js
@@ -0,0 +1,192 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { align, icons } from 'Helpers/Props';
+import PageContent from 'Components/Page/PageContent';
+import Measure from 'Components/Measure';
+import PageContentBody from 'Components/Page/PageContentBody';
+import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
+import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
+import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
+import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
+import FilterMenu from 'Components/Menu/FilterMenu';
+import NoSeries from 'Series/NoSeries';
+import CalendarLinkModal from './iCal/CalendarLinkModal';
+import CalendarOptionsModal from './Options/CalendarOptionsModal';
+import LegendConnector from './Legend/LegendConnector';
+import CalendarConnector from './CalendarConnector';
+import styles from './CalendarPage.css';
+
+const MINIMUM_DAY_WIDTH = 120;
+
+class CalendarPage extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isCalendarLinkModalOpen: false,
+ isOptionsModalOpen: false,
+ width: 0
+ };
+ }
+
+ //
+ // Listeners
+
+ onMeasure = ({ width }) => {
+ this.setState({ width });
+ const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
+
+ this.props.onDaysCountChange(days);
+ }
+
+ onGetCalendarLinkPress = () => {
+ this.setState({ isCalendarLinkModalOpen: true });
+ }
+
+ onGetCalendarLinkModalClose = () => {
+ this.setState({ isCalendarLinkModalOpen: false });
+ }
+
+ onOptionsPress = () => {
+ this.setState({ isOptionsModalOpen: true });
+ }
+
+ onOptionsModalClose = () => {
+ this.setState({ isOptionsModalOpen: false });
+ }
+
+ onSearchMissingPress = () => {
+ const {
+ missingEpisodeIds,
+ onSearchMissingPress
+ } = this.props;
+
+ onSearchMissingPress(missingEpisodeIds);
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ selectedFilterKey,
+ filters,
+ hasSeries,
+ missingEpisodeIds,
+ isRssSyncExecuting,
+ isSearchingForMissing,
+ useCurrentPage,
+ onRssSyncPress,
+ onFilterSelect
+ } = this.props;
+
+ const {
+ isCalendarLinkModalOpen,
+ isOptionsModalOpen
+ } = this.state;
+
+ const isMeasured = this.state.width > 0;
+ const PageComponent = hasSeries ? CalendarConnector : NoSeries;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ isMeasured ?
+ :
+
+ }
+
+
+ {
+ hasSeries &&
+
+ }
+
+
+
+
+
+
+ );
+ }
+}
+
+CalendarPage.propTypes = {
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ hasSeries: PropTypes.bool.isRequired,
+ missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
+ isRssSyncExecuting: PropTypes.bool.isRequired,
+ isSearchingForMissing: PropTypes.bool.isRequired,
+ useCurrentPage: PropTypes.bool.isRequired,
+ onSearchMissingPress: PropTypes.func.isRequired,
+ onDaysCountChange: PropTypes.func.isRequired,
+ onRssSyncPress: PropTypes.func.isRequired,
+ onFilterSelect: PropTypes.func.isRequired
+};
+
+export default CalendarPage;
diff --git a/frontend/src/Calendar/CalendarPage.tsx b/frontend/src/Calendar/CalendarPage.tsx
deleted file mode 100644
index f408b6a60..000000000
--- a/frontend/src/Calendar/CalendarPage.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import moment from 'moment';
-import React, { useCallback, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
-import * as commandNames from 'Commands/commandNames';
-import Measure from 'Components/Measure';
-import FilterMenu from 'Components/Menu/FilterMenu';
-import PageContent from 'Components/Page/PageContent';
-import PageContentBody from 'Components/Page/PageContentBody';
-import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
-import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
-import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
-import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
-import { align, icons } from 'Helpers/Props';
-import NoSeries from 'Series/NoSeries';
-import {
- searchMissing,
- setCalendarDaysCount,
- setCalendarFilter,
-} from 'Store/Actions/calendarActions';
-import { executeCommand } from 'Store/Actions/commandActions';
-import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
-import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
-import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
-import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
-import { isCommandExecuting } from 'Utilities/Command';
-import isBefore from 'Utilities/Date/isBefore';
-import translate from 'Utilities/String/translate';
-import Calendar from './Calendar';
-import CalendarFilterModal from './CalendarFilterModal';
-import CalendarLinkModal from './iCal/CalendarLinkModal';
-import Legend from './Legend/Legend';
-import CalendarOptionsModal from './Options/CalendarOptionsModal';
-import styles from './CalendarPage.css';
-
-const MINIMUM_DAY_WIDTH = 120;
-
-function createMissingEpisodeIdsSelector() {
- return createSelector(
- (state: AppState) => state.calendar.start,
- (state: AppState) => state.calendar.end,
- (state: AppState) => state.calendar.items,
- (state: AppState) => state.queue.details.items,
- (start, end, episodes, queueDetails) => {
- return episodes.reduce((acc, episode) => {
- const airDateUtc = episode.airDateUtc;
-
- 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;
- }, []);
- }
- );
-}
-
-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 CalendarPage() {
- const dispatch = useDispatch();
-
- const { selectedFilterKey, filters } = useSelector(
- (state: AppState) => state.calendar
- );
- const missingEpisodeIds = useSelector(createMissingEpisodeIdsSelector());
- const isSearchingForMissing = useSelector(createIsSearchingSelector());
- const isRssSyncExecuting = useSelector(
- createCommandExecutingSelector(commandNames.RSS_SYNC)
- );
- const customFilters = useSelector(createCustomFiltersSelector('calendar'));
- const hasSeries = !!useSelector(createSeriesCountSelector());
-
- const [isCalendarLinkModalOpen, setIsCalendarLinkModalOpen] = useState(false);
- const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
- const [width, setWidth] = useState(0);
-
- const isMeasured = width > 0;
- const PageComponent = hasSeries ? Calendar : NoSeries;
-
- const handleMeasure = useCallback(
- ({ width: newWidth }: { width: number }) => {
- setWidth(newWidth);
-
- const dayCount = Math.max(
- 3,
- Math.min(7, Math.floor(newWidth / MINIMUM_DAY_WIDTH))
- );
-
- dispatch(setCalendarDaysCount({ dayCount }));
- },
- [dispatch]
- );
-
- const handleGetCalendarLinkPress = useCallback(() => {
- setIsCalendarLinkModalOpen(true);
- }, []);
-
- const handleGetCalendarLinkModalClose = useCallback(() => {
- setIsCalendarLinkModalOpen(false);
- }, []);
-
- const handleOptionsPress = useCallback(() => {
- setIsOptionsModalOpen(true);
- }, []);
-
- const handleOptionsModalClose = useCallback(() => {
- setIsOptionsModalOpen(false);
- }, []);
-
- const handleRssSyncPress = useCallback(() => {
- dispatch(
- executeCommand({
- name: commandNames.RSS_SYNC,
- })
- );
- }, [dispatch]);
-
- const handleSearchMissingPress = useCallback(() => {
- dispatch(searchMissing({ episodeIds: missingEpisodeIds }));
- }, [missingEpisodeIds, dispatch]);
-
- const handleFilterSelect = useCallback(
- (key: string) => {
- dispatch(setCalendarFilter({ selectedFilterKey: key }));
- },
- [dispatch]
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isMeasured ? :
}
-
-
- {hasSeries && }
-
-
-
-
-
-
- );
-}
-
-export default CalendarPage;
diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js
new file mode 100644
index 000000000..e039b2824
--- /dev/null
+++ b/frontend/src/Calendar/CalendarPageConnector.js
@@ -0,0 +1,113 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import moment from 'moment';
+import { isCommandExecuting } from 'Utilities/Command';
+import isBefore from 'Utilities/Date/isBefore';
+import * as commandNames from 'Commands/commandNames';
+import withCurrentPage from 'Components/withCurrentPage';
+import { executeCommand } from 'Store/Actions/commandActions';
+import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
+import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
+import CalendarPage from './CalendarPage';
+
+function createMissingEpisodeIdsSelector() {
+ return createSelector(
+ (state) => state.calendar.start,
+ (state) => state.calendar.end,
+ (state) => state.calendar.items,
+ (state) => state.queue.details.items,
+ (start, end, episodes, queueDetails) => {
+ return episodes.reduce((acc, episode) => {
+ const airDateUtc = episode.airDateUtc;
+
+ 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;
+ }, []);
+ }
+ );
+}
+
+function createIsSearchingSelector() {
+ return createSelector(
+ (state) => state.calendar.searchMissingCommandId,
+ createCommandsSelector(),
+ (searchMissingCommandId, commands) => {
+ if (searchMissingCommandId == null) {
+ return false;
+ }
+
+ return isCommandExecuting(commands.find((command) => {
+ return command.id === searchMissingCommandId;
+ }));
+ }
+ );
+}
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.selectedFilterKey,
+ (state) => state.calendar.filters,
+ createSeriesCountSelector(),
+ createUISettingsSelector(),
+ createMissingEpisodeIdsSelector(),
+ createCommandExecutingSelector(commandNames.RSS_SYNC),
+ createIsSearchingSelector(),
+ (
+ selectedFilterKey,
+ filters,
+ seriesCount,
+ uiSettings,
+ missingEpisodeIds,
+ isRssSyncExecuting,
+ isSearchingForMissing
+ ) => {
+ return {
+ selectedFilterKey,
+ filters,
+ colorImpairedMode: uiSettings.enableColorImpairedMode,
+ hasSeries: !!seriesCount,
+ missingEpisodeIds,
+ isRssSyncExecuting,
+ isSearchingForMissing
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onRssSyncPress() {
+ dispatch(executeCommand({
+ name: commandNames.RSS_SYNC
+ }));
+ },
+
+ onSearchMissingPress(episodeIds) {
+ dispatch(searchMissing({ episodeIds }));
+ },
+
+ onDaysCountChange(dayCount) {
+ dispatch(setCalendarDaysCount({ dayCount }));
+ },
+
+ onFilterSelect(selectedFilterKey) {
+ dispatch(setCalendarFilter({ selectedFilterKey }));
+ }
+ };
+}
+
+export default withCurrentPage(
+ connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
+);
diff --git a/frontend/src/Calendar/Day/CalendarDay.css b/frontend/src/Calendar/Day/CalendarDay.css
index 22d1b1ccf..79eb67ae7 100644
--- a/frontend/src/Calendar/Day/CalendarDay.css
+++ b/frontend/src/Calendar/Day/CalendarDay.css
@@ -2,8 +2,8 @@
flex: 1 0 14.28%;
overflow: hidden;
min-height: 70px;
- border-bottom: 1px solid var(--calendarBorderColor);
- border-left: 1px solid var(--calendarBorderColor);
+ border-bottom: 1px solid $calendarBorderColor;
+ border-left: 1px solid $calendarBorderColor;
}
.isSingleDay {
@@ -12,14 +12,14 @@
.dayOfMonth {
padding-right: 5px;
- border-bottom: 1px solid var(--calendarBorderColor);
+ border-bottom: 1px solid $calendarBorderColor;
text-align: right;
}
.isToday {
- background-color: var(--calendarTodayBackgroundColor);
+ background-color: $calendarTodayBackgroundColor;
}
.isDifferentMonth {
- color: var(--disabledColor);
+ color: $disabledColor;
}
diff --git a/frontend/src/Calendar/Day/CalendarDay.css.d.ts b/frontend/src/Calendar/Day/CalendarDay.css.d.ts
deleted file mode 100644
index f32def3dd..000000000
--- a/frontend/src/Calendar/Day/CalendarDay.css.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'day': string;
- 'dayOfMonth': string;
- 'isDifferentMonth': string;
- 'isSingleDay': string;
- 'isToday': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Day/CalendarDay.js b/frontend/src/Calendar/Day/CalendarDay.js
new file mode 100644
index 000000000..faa45b28b
--- /dev/null
+++ b/frontend/src/Calendar/Day/CalendarDay.js
@@ -0,0 +1,74 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React from 'react';
+import classNames from 'classnames';
+import * as calendarViews from 'Calendar/calendarViews';
+import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
+import CalendarEventGroupConnector from 'Calendar/Events/CalendarEventGroupConnector';
+import styles from './CalendarDay.css';
+
+function CalendarDay(props) {
+ const {
+ date,
+ time,
+ isTodaysDate,
+ events,
+ view,
+ onEventModalOpenToggle
+ } = props;
+
+ return (
+
+ {
+ view === calendarViews.MONTH &&
+
+ {moment(date).date()}
+
+ }
+
+ {
+ events.map((event) => {
+ if (event.isGroup) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ })
+ }
+
+
+ );
+}
+
+CalendarDay.propTypes = {
+ date: PropTypes.string.isRequired,
+ time: PropTypes.string.isRequired,
+ isTodaysDate: PropTypes.bool.isRequired,
+ events: PropTypes.arrayOf(PropTypes.object).isRequired,
+ view: PropTypes.string.isRequired,
+ onEventModalOpenToggle: PropTypes.func.isRequired
+};
+
+export default CalendarDay;
diff --git a/frontend/src/Calendar/Day/CalendarDay.tsx b/frontend/src/Calendar/Day/CalendarDay.tsx
deleted file mode 100644
index a619109ca..000000000
--- a/frontend/src/Calendar/Day/CalendarDay.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-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 * as calendarViews from 'Calendar/calendarViews';
-import CalendarEvent from 'Calendar/Events/CalendarEvent';
-import CalendarEventGroup from 'Calendar/Events/CalendarEventGroup';
-import {
- CalendarEvent as CalendarEventModel,
- CalendarEventGroup as CalendarEventGroupModel,
- CalendarItem,
-} from 'typings/Calendar';
-import styles from './CalendarDay.css';
-
-function sort(items: (CalendarEventModel | CalendarEventGroupModel)[]) {
- return items.sort((a, b) => {
- const aDate = a.isGroup
- ? moment(a.events[0].airDateUtc).unix()
- : moment(a.airDateUtc).unix();
-
- const bDate = b.isGroup
- ? moment(b.events[0].airDateUtc).unix()
- : moment(b.airDateUtc).unix();
-
- return aDate - bDate;
- });
-}
-
-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);
- }
- );
-}
-
-interface CalendarDayProps {
- date: string;
- isTodaysDate: boolean;
- onEventModalOpenToggle(isOpen: boolean): unknown;
-}
-
-function CalendarDay({
- date,
- isTodaysDate,
- onEventModalOpenToggle,
-}: CalendarDayProps) {
- const { time, view } = useSelector((state: AppState) => state.calendar);
- const events = useSelector(createCalendarEventsConnector(date));
-
- const ref = React.useRef(null);
-
- React.useEffect(() => {
- if (isTodaysDate && view === calendarViews.MONTH && ref.current) {
- ref.current.scrollIntoView();
- }
- }, [time, isTodaysDate, view]);
-
- return (
-
- {view === calendarViews.MONTH && (
-
- {moment(date).date()}
-
- )}
-
- {events.map((event) => {
- if (event.isGroup) {
- return (
-
- );
- }
-
- return (
-
- );
- })}
-
-
- );
-}
-
-export default CalendarDay;
diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js
new file mode 100644
index 000000000..8fd6cc5a1
--- /dev/null
+++ b/frontend/src/Calendar/Day/CalendarDayConnector.js
@@ -0,0 +1,91 @@
+import _ from 'lodash';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import CalendarDay from './CalendarDay';
+
+function sort(items) {
+ return _.sortBy(items, (item) => {
+ if (item.isGroup) {
+ return moment(item.events[0].airDateUtc).unix();
+ }
+
+ return moment(item.airDateUtc).unix();
+ });
+}
+
+function createCalendarEventsConnector() {
+ return createSelector(
+ (state, { date }) => date,
+ (state) => state.calendar.items,
+ (state) => state.calendar.options.collapseMultipleEpisodes,
+ (date, items, collapseMultipleEpisodes) => {
+ const filtered = _.filter(items, (item) => {
+ return moment(date).isSame(moment(item.airDateUtc), 'day');
+ });
+
+ if (!collapseMultipleEpisodes) {
+ return sort(filtered);
+ }
+
+ const groupedObject = _.groupBy(filtered, (item) => `${item.seriesId}-${item.seasonNumber}`);
+ const grouped = [];
+
+ Object.keys(groupedObject).forEach((key) => {
+ const events = groupedObject[key];
+
+ if (events.length === 1) {
+ grouped.push(events[0]);
+ } else {
+ grouped.push({
+ isGroup: true,
+ seriesId: events[0].seriesId,
+ seasonNumber: events[0].seasonNumber,
+ episodeIds: events.map((event) => event.id),
+ events: _.sortBy(events, (item) => moment(item.airDateUtc).unix())
+ });
+ }
+ });
+
+ const sorted = sort(grouped);
+
+ return sorted;
+ }
+ );
+}
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar,
+ createCalendarEventsConnector(),
+ (calendar, events) => {
+ return {
+ time: calendar.time,
+ view: calendar.view,
+ events
+ };
+ }
+ );
+}
+
+class CalendarDayConnector extends Component {
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+CalendarDayConnector.propTypes = {
+ date: PropTypes.string.isRequired
+};
+
+export default connect(createMapStateToProps)(CalendarDayConnector);
diff --git a/frontend/src/Calendar/Day/CalendarDays.css b/frontend/src/Calendar/Day/CalendarDays.css
index 507dd0ede..b6dd2100c 100644
--- a/frontend/src/Calendar/Day/CalendarDays.css
+++ b/frontend/src/Calendar/Day/CalendarDays.css
@@ -1,6 +1,6 @@
.days {
display: flex;
- border-right: 1px solid var(--calendarBorderColor);
+ border-right: 1px solid $calendarBorderColor;
}
.day,
diff --git a/frontend/src/Calendar/Day/CalendarDays.css.d.ts b/frontend/src/Calendar/Day/CalendarDays.css.d.ts
deleted file mode 100644
index ae3e7aebc..000000000
--- a/frontend/src/Calendar/Day/CalendarDays.css.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'day': string;
- 'days': string;
- 'forecast': string;
- 'month': string;
- 'week': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Day/CalendarDays.js b/frontend/src/Calendar/Day/CalendarDays.js
new file mode 100644
index 000000000..0a1a36172
--- /dev/null
+++ b/frontend/src/Calendar/Day/CalendarDays.js
@@ -0,0 +1,164 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import isToday from 'Utilities/Date/isToday';
+import * as calendarViews from 'Calendar/calendarViews';
+import CalendarDayConnector from './CalendarDayConnector';
+import styles from './CalendarDays.css';
+
+class CalendarDays extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this._touchStart = null;
+
+ this.state = {
+ todaysDate: moment().startOf('day').toISOString(),
+ isEventModalOpen: false
+ };
+
+ this.updateTimeoutId = null;
+ }
+
+ // Lifecycle
+
+ componentDidMount() {
+ const view = this.props.view;
+
+ if (view === calendarViews.MONTH) {
+ this.scheduleUpdate();
+ }
+
+ window.addEventListener('touchstart', this.onTouchStart);
+ window.addEventListener('touchend', this.onTouchEnd);
+ window.addEventListener('touchcancel', this.onTouchCancel);
+ window.addEventListener('touchmove', this.onTouchMove);
+ }
+
+ componentWillUnmount() {
+ this.clearUpdateTimeout();
+
+ window.removeEventListener('touchstart', this.onTouchStart);
+ window.removeEventListener('touchend', this.onTouchEnd);
+ window.removeEventListener('touchcancel', this.onTouchCancel);
+ window.removeEventListener('touchmove', this.onTouchMove);
+ }
+
+ //
+ // Control
+
+ scheduleUpdate = () => {
+ this.clearUpdateTimeout();
+ const todaysDate = moment().startOf('day');
+ const diff = moment().diff(todaysDate.clone().add(1, 'day'));
+
+ this.setState({ todaysDate: todaysDate.toISOString() });
+
+ this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
+ }
+
+ clearUpdateTimeout = () => {
+ if (this.updateTimeoutId) {
+ clearTimeout(this.updateTimeoutId);
+ }
+ }
+
+ //
+ // Listeners
+
+ onEventModalOpenToggle = (isEventModalOpen) => {
+ this.setState({ isEventModalOpen });
+ }
+
+ onTouchStart = (event) => {
+ const touches = event.touches;
+ const touchStart = touches[0].pageX;
+
+ if (touches.length !== 1) {
+ return;
+ }
+
+ if (
+ touchStart < 50 ||
+ this.props.isSidebarVisible ||
+ this.state.isEventModalOpen
+ ) {
+ return;
+ }
+
+ this._touchStart = touchStart;
+ }
+
+ onTouchEnd = (event) => {
+ const touches = event.changedTouches;
+ const currentTouch = touches[0].pageX;
+
+ if (!this._touchStart) {
+ return;
+ }
+
+ if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) {
+ this.props.onNavigatePrevious();
+ } else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) {
+ this.props.onNavigateNext();
+ }
+
+ this._touchStart = null;
+ }
+
+ onTouchCancel = (event) => {
+ this._touchStart = null;
+ }
+
+ onTouchMove = (event) => {
+ if (!this._touchStart) {
+ return;
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ dates,
+ view
+ } = this.props;
+
+ return (
+
+ {
+ dates.map((date) => {
+ return (
+
+ );
+ })
+ }
+
+ );
+ }
+}
+
+CalendarDays.propTypes = {
+ dates: PropTypes.arrayOf(PropTypes.string).isRequired,
+ view: PropTypes.string.isRequired,
+ isSidebarVisible: PropTypes.bool.isRequired,
+ onNavigatePrevious: PropTypes.func.isRequired,
+ onNavigateNext: PropTypes.func.isRequired
+};
+
+export default CalendarDays;
diff --git a/frontend/src/Calendar/Day/CalendarDays.tsx b/frontend/src/Calendar/Day/CalendarDays.tsx
deleted file mode 100644
index 149dc1455..000000000
--- a/frontend/src/Calendar/Day/CalendarDays.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import classNames from 'classnames';
-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 * as calendarViews from 'Calendar/calendarViews';
-import {
- gotoCalendarNextRange,
- gotoCalendarPreviousRange,
-} from 'Store/Actions/calendarActions';
-import CalendarDay from './CalendarDay';
-import styles from './CalendarDays.css';
-
-function CalendarDays() {
- const dispatch = useDispatch();
- const { dates, view } = useSelector((state: AppState) => state.calendar);
- const isSidebarVisible = useSelector(
- (state: AppState) => state.app.isSidebarVisible
- );
-
- const updateTimeout = useRef>();
- const touchStart = useRef(null);
- const isEventModalOpen = useRef(false);
- const [todaysDate, setTodaysDate] = useState(
- moment().startOf('day').toISOString()
- );
-
- const handleEventModalOpenToggle = useCallback((isOpen: boolean) => {
- isEventModalOpen.current = isOpen;
- }, []);
-
- const scheduleUpdate = useCallback(() => {
- clearTimeout(updateTimeout.current);
-
- const todaysDate = moment().startOf('day');
- const diff = moment().diff(todaysDate.clone().add(1, 'day'));
-
- setTodaysDate(todaysDate.toISOString());
-
- updateTimeout.current = setTimeout(scheduleUpdate, diff);
- }, []);
-
- const handleTouchStart = useCallback(
- (event: TouchEvent) => {
- const touches = event.touches;
- const currentTouch = touches[0].pageX;
-
- if (touches.length !== 1) {
- return;
- }
-
- if (currentTouch < 50 || isSidebarVisible || isEventModalOpen.current) {
- return;
- }
-
- touchStart.current = currentTouch;
- },
- [isSidebarVisible]
- );
-
- const handleTouchEnd = useCallback(
- (event: TouchEvent) => {
- const touches = event.changedTouches;
- const currentTouch = touches[0].pageX;
-
- if (!touchStart.current) {
- return;
- }
-
- if (
- currentTouch > touchStart.current &&
- currentTouch - touchStart.current > 100
- ) {
- dispatch(gotoCalendarPreviousRange());
- } else if (
- currentTouch < touchStart.current &&
- touchStart.current - currentTouch > 100
- ) {
- dispatch(gotoCalendarNextRange());
- }
-
- touchStart.current = null;
- },
- [dispatch]
- );
-
- const handleTouchCancel = useCallback(() => {
- touchStart.current = null;
- }, []);
-
- const handleTouchMove = useCallback(() => {
- if (!touchStart.current) {
- return;
- }
- }, []);
-
- useEffect(() => {
- if (view === calendarViews.MONTH) {
- scheduleUpdate();
- }
- }, [view, scheduleUpdate]);
-
- useEffect(() => {
- window.addEventListener('touchstart', handleTouchStart);
- window.addEventListener('touchend', handleTouchEnd);
- window.addEventListener('touchcancel', handleTouchCancel);
- window.addEventListener('touchmove', handleTouchMove);
-
- return () => {
- window.removeEventListener('touchstart', handleTouchStart);
- window.removeEventListener('touchend', handleTouchEnd);
- window.removeEventListener('touchcancel', handleTouchCancel);
- window.removeEventListener('touchmove', handleTouchMove);
- };
- }, [handleTouchStart, handleTouchEnd, handleTouchCancel, handleTouchMove]);
-
- return (
-
- {dates.map((date) => {
- return (
-
- );
- })}
-
- );
-}
-
-export default CalendarDays;
diff --git a/frontend/src/Calendar/Day/CalendarDaysConnector.js b/frontend/src/Calendar/Day/CalendarDaysConnector.js
new file mode 100644
index 000000000..3dea906a7
--- /dev/null
+++ b/frontend/src/Calendar/Day/CalendarDaysConnector.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { gotoCalendarPreviousRange, gotoCalendarNextRange } from 'Store/Actions/calendarActions';
+import CalendarDays from './CalendarDays';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar,
+ (state) => state.app.isSidebarVisible,
+ (calendar, isSidebarVisible) => {
+ return {
+ dates: calendar.dates,
+ view: calendar.view,
+ isSidebarVisible
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ onNavigatePrevious: gotoCalendarPreviousRange,
+ onNavigateNext: gotoCalendarNextRange
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);
diff --git a/frontend/src/Calendar/Day/DayOfWeek.css b/frontend/src/Calendar/Day/DayOfWeek.css
index 2d31a30be..8c3552e55 100644
--- a/frontend/src/Calendar/Day/DayOfWeek.css
+++ b/frontend/src/Calendar/Day/DayOfWeek.css
@@ -1,6 +1,6 @@
.dayOfWeek {
flex: 1 0 14.28%;
- background-color: var(--calendarBackgroundColor);
+ background-color: #e4eaec;
text-align: center;
}
@@ -9,5 +9,5 @@
}
.isToday {
- background-color: var(--calendarTodayBackgroundColor);
+ background-color: $calendarTodayBackgroundColor;
}
diff --git a/frontend/src/Calendar/Day/DayOfWeek.css.d.ts b/frontend/src/Calendar/Day/DayOfWeek.css.d.ts
deleted file mode 100644
index a377e4a8e..000000000
--- a/frontend/src/Calendar/Day/DayOfWeek.css.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'dayOfWeek': string;
- 'isSingleDay': string;
- 'isToday': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Day/DayOfWeek.js b/frontend/src/Calendar/Day/DayOfWeek.js
new file mode 100644
index 000000000..d97671522
--- /dev/null
+++ b/frontend/src/Calendar/Day/DayOfWeek.js
@@ -0,0 +1,56 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import getRelativeDate from 'Utilities/Date/getRelativeDate';
+import * as calendarViews from 'Calendar/calendarViews';
+import styles from './DayOfWeek.css';
+
+class DayOfWeek extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ date,
+ view,
+ isTodaysDate,
+ calendarWeekColumnHeader,
+ shortDateFormat,
+ showRelativeDates
+ } = this.props;
+
+ const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
+ const momentDate = moment(date);
+ let formatedDate = momentDate.format('dddd');
+
+ if (view === calendarViews.WEEK) {
+ formatedDate = momentDate.format(calendarWeekColumnHeader);
+ } else if (view === calendarViews.FORECAST) {
+ formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
+ }
+
+ return (
+
+ {formatedDate}
+
+ );
+ }
+}
+
+DayOfWeek.propTypes = {
+ date: PropTypes.string.isRequired,
+ view: PropTypes.string.isRequired,
+ isTodaysDate: PropTypes.bool.isRequired,
+ calendarWeekColumnHeader: PropTypes.string.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ showRelativeDates: PropTypes.bool.isRequired
+};
+
+export default DayOfWeek;
diff --git a/frontend/src/Calendar/Day/DayOfWeek.tsx b/frontend/src/Calendar/Day/DayOfWeek.tsx
deleted file mode 100644
index c8b493b7c..000000000
--- a/frontend/src/Calendar/Day/DayOfWeek.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import classNames from 'classnames';
-import moment from 'moment';
-import React from 'react';
-import * as calendarViews from 'Calendar/calendarViews';
-import getRelativeDate from 'Utilities/Date/getRelativeDate';
-import styles from './DayOfWeek.css';
-
-interface DayOfWeekProps {
- date: string;
- view: string;
- isTodaysDate: boolean;
- calendarWeekColumnHeader: string;
- shortDateFormat: string;
- showRelativeDates: boolean;
-}
-
-function DayOfWeek(props: DayOfWeekProps) {
- const {
- date,
- view,
- isTodaysDate,
- calendarWeekColumnHeader,
- shortDateFormat,
- showRelativeDates,
- } = props;
-
- const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
- const momentDate = moment(date);
- let formatedDate = momentDate.format('dddd');
-
- if (view === calendarViews.WEEK) {
- formatedDate = momentDate.format(calendarWeekColumnHeader);
- } else if (view === calendarViews.FORECAST) {
- formatedDate = getRelativeDate({
- date,
- shortDateFormat,
- showRelativeDates,
- });
- }
-
- return (
-
- {formatedDate}
-
- );
-}
-
-export default DayOfWeek;
diff --git a/frontend/src/Calendar/Day/DaysOfWeek.css.d.ts b/frontend/src/Calendar/Day/DaysOfWeek.css.d.ts
deleted file mode 100644
index 5bc224b68..000000000
--- a/frontend/src/Calendar/Day/DaysOfWeek.css.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'daysOfWeek': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Day/DaysOfWeek.js b/frontend/src/Calendar/Day/DaysOfWeek.js
new file mode 100644
index 000000000..a67777f7c
--- /dev/null
+++ b/frontend/src/Calendar/Day/DaysOfWeek.js
@@ -0,0 +1,97 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import DayOfWeek from './DayOfWeek';
+import * as calendarViews from 'Calendar/calendarViews';
+import styles from './DaysOfWeek.css';
+
+class DaysOfWeek extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ todaysDate: moment().startOf('day').toISOString()
+ };
+
+ this.updateTimeoutId = null;
+ }
+
+ // Lifecycle
+
+ componentDidMount() {
+ const view = this.props.view;
+
+ if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) {
+ this.scheduleUpdate();
+ }
+ }
+
+ componentWillUnmount() {
+ this.clearUpdateTimeout();
+ }
+
+ //
+ // Control
+
+ scheduleUpdate = () => {
+ this.clearUpdateTimeout();
+ const todaysDate = moment().startOf('day');
+ const diff = todaysDate.clone().add(1, 'day').diff(moment());
+
+ this.setState({
+ todaysDate: todaysDate.toISOString()
+ });
+
+ this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
+ }
+
+ clearUpdateTimeout = () => {
+ if (this.updateTimeoutId) {
+ clearTimeout(this.updateTimeoutId);
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ dates,
+ view,
+ ...otherProps
+ } = this.props;
+
+ if (view === calendarViews.AGENDA) {
+ return null;
+ }
+
+ return (
+
+ {
+ dates.map((date) => {
+ return (
+
+ );
+ })
+ }
+
+ );
+ }
+}
+
+DaysOfWeek.propTypes = {
+ dates: PropTypes.arrayOf(PropTypes.string),
+ view: PropTypes.string.isRequired
+};
+
+export default DaysOfWeek;
diff --git a/frontend/src/Calendar/Day/DaysOfWeek.tsx b/frontend/src/Calendar/Day/DaysOfWeek.tsx
deleted file mode 100644
index 64bc886cc..000000000
--- a/frontend/src/Calendar/Day/DaysOfWeek.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import moment from 'moment';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { useSelector } from 'react-redux';
-import AppState from 'App/State/AppState';
-import * as calendarViews from 'Calendar/calendarViews';
-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 { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } =
- useSelector(createUISettingsSelector());
-
- const updateTimeout = useRef>();
- const [todaysDate, setTodaysDate] = useState(
- moment().startOf('day').toISOString()
- );
-
- const scheduleUpdate = useCallback(() => {
- clearTimeout(updateTimeout.current);
-
- const todaysDate = moment().startOf('day');
- const diff = moment().diff(todaysDate.clone().add(1, 'day'));
-
- setTodaysDate(todaysDate.toISOString());
-
- updateTimeout.current = setTimeout(scheduleUpdate, diff);
- }, []);
-
- useEffect(() => {
- if (view !== calendarViews.AGENDA && view !== calendarViews.MONTH) {
- scheduleUpdate();
- }
- }, [view, scheduleUpdate]);
-
- if (view === calendarViews.AGENDA) {
- return null;
- }
-
- return (
-
- {dates.map((date) => {
- return (
-
- );
- })}
-
- );
-}
-
-export default DaysOfWeek;
diff --git a/frontend/src/Calendar/Day/DaysOfWeekConnector.js b/frontend/src/Calendar/Day/DaysOfWeekConnector.js
new file mode 100644
index 000000000..7f5cdef19
--- /dev/null
+++ b/frontend/src/Calendar/Day/DaysOfWeekConnector.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import DaysOfWeek from './DaysOfWeek';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar,
+ createUISettingsSelector(),
+ (calendar, UiSettings) => {
+ return {
+ dates: calendar.dates.slice(0, 7),
+ view: calendar.view,
+ calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader,
+ shortDateFormat: UiSettings.shortDateFormat,
+ showRelativeDates: UiSettings.showRelativeDates
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(DaysOfWeek);
diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css
index 679b4cc51..fb7a39855 100644
--- a/frontend/src/Calendar/Events/CalendarEvent.css
+++ b/frontend/src/Calendar/Events/CalendarEvent.css
@@ -1,22 +1,11 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
- position: relative;
+ overflow-x: hidden;
margin: 4px 2px;
padding: 5px;
- border-bottom: 1px solid var(--calendarBorderColor);
- border-left: 4px solid var(--calendarBorderColor);
-}
-
-.underlay {
- @add-mixin cover;
-}
-
-.overlay {
- @add-mixin linkOverlay;
-
- position: relative;
- overflow-x: hidden;
+ border-bottom: 1px solid $borderColor;
+ border-left: 4px solid $borderColor;
font-size: 12px;
&:global(.colorImpaired) {
@@ -30,7 +19,7 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.episodeInfo {
- color: var(--calendarTextDim);
+ color: $calendarTextDim;
}
.seriesTitle,
@@ -41,7 +30,7 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.seriesTitle {
- color: var(--calendarTextDimAlternate);
+ color: #3a3f51;
font-size: $defaultFontSize;
}
@@ -52,20 +41,14 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.statusContainer {
display: flex;
align-items: center;
-
- &:global(.fullColor) {
- filter: var(--calendarFullColorFilter)
- }
}
.statusIcon {
margin-left: 3px;
- cursor: default;
- pointer-events: all;
}
.airTime {
- color: var(--calendarTextDim);
+ color: $calendarTextDim;
}
/*
@@ -73,19 +56,19 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
*/
.downloaded {
- border-left-color: var(--successColor) !important;
+ border-left-color: $successColor !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
- border-left-color: color(#27c24c saturation(+15%)) !important;
+ border-left-color: color($successColor, saturation(+15%)) !important;
}
}
.downloading {
- border-left-color: var(--purple) !important;
+ border-left-color: $purple !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
@@ -93,14 +76,14 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.unmonitored {
- border-left-color: var(--gray) !important;
+ border-left-color: $gray !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
- background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
+ background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
@@ -109,14 +92,14 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.onAir {
- border-left-color: var(--warningColor) !important;
+ border-left-color: $warningColor !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) {
- background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
+ background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
@@ -125,15 +108,15 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.missing {
- border-left-color: var(--dangerColor) !important;
+ border-left-color: $dangerColor !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) {
- border-left-color: color(#f05050 saturation(+15%)) !important;
- background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
+ border-left-color: color($dangerColor saturation(+15%)) !important;
+ background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
@@ -142,14 +125,14 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
}
.unaired {
- border-left-color: var(--primaryColor) !important;
+ border-left-color: $primaryColor !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
- background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
+ background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
diff --git a/frontend/src/Calendar/Events/CalendarEvent.css.d.ts b/frontend/src/Calendar/Events/CalendarEvent.css.d.ts
deleted file mode 100644
index f099df211..000000000
--- a/frontend/src/Calendar/Events/CalendarEvent.css.d.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'absoluteEpisodeNumber': string;
- 'airTime': string;
- 'downloaded': string;
- 'downloading': string;
- 'episodeInfo': string;
- 'episodeTitle': string;
- 'event': string;
- 'info': string;
- 'missing': string;
- 'onAir': string;
- 'overlay': string;
- 'seriesTitle': string;
- 'statusContainer': string;
- 'statusIcon': string;
- 'unaired': string;
- 'underlay': string;
- 'unmonitored': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js
new file mode 100644
index 000000000..00e91aa8c
--- /dev/null
+++ b/frontend/src/Calendar/Events/CalendarEvent.js
@@ -0,0 +1,260 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component, Fragment } from 'react';
+import classNames from 'classnames';
+import { icons, kinds } from 'Helpers/Props';
+import formatTime from 'Utilities/Date/formatTime';
+import padNumber from 'Utilities/Number/padNumber';
+import getStatusStyle from 'Calendar/getStatusStyle';
+import episodeEntities from 'Episode/episodeEntities';
+import Icon from 'Components/Icon';
+import Link from 'Components/Link/Link';
+import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
+import CalendarEventQueueDetails from './CalendarEventQueueDetails';
+import styles from './CalendarEvent.css';
+
+class CalendarEvent extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isDetailsModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onPress = () => {
+ this.setState({ isDetailsModalOpen: true }, () => {
+ this.props.onEventModalOpenToggle(true);
+ });
+ }
+
+ onDetailsModalClose = () => {
+ this.setState({ isDetailsModalOpen: false }, () => {
+ this.props.onEventModalOpenToggle(false);
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ id,
+ series,
+ episodeFile,
+ title,
+ seasonNumber,
+ episodeNumber,
+ absoluteEpisodeNumber,
+ airDateUtc,
+ monitored,
+ hasFile,
+ grabbed,
+ queueItem,
+ showEpisodeInformation,
+ showFinaleIcon,
+ showSpecialIcon,
+ showCutoffUnmetIcon,
+ fullColorEvents,
+ timeFormat,
+ colorImpairedMode
+ } = this.props;
+
+ if (!series) {
+ return null;
+ }
+
+ const startTime = moment(airDateUtc);
+ const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
+ const isDownloading = !!(queueItem || grabbed);
+ const isMonitored = series.monitored && monitored;
+ const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored);
+ const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
+ const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
+ const seasonStatistics = season.statistics || {};
+
+ return (
+
+
+
+
+ {series.title}
+
+
+
+ {
+ missingAbsoluteNumber ?
+ :
+ null
+ }
+
+ {
+ queueItem ?
+
+
+ :
+ null
+ }
+
+ {
+ !queueItem && grabbed ?
+ :
+ null
+ }
+
+ {
+ showCutoffUnmetIcon &&
+ !!episodeFile &&
+ episodeFile.qualityCutoffNotMet ?
+ :
+ null
+ }
+
+ {
+ showCutoffUnmetIcon &&
+ !!episodeFile &&
+ episodeFile.languageCutoffNotMet &&
+ !episodeFile.qualityCutoffNotMet ?
+ :
+ null
+ }
+
+ {
+ episodeNumber === 1 && seasonNumber > 0 ?
+ :
+ null
+ }
+
+ {
+ showFinaleIcon &&
+ episodeNumber !== 1 &&
+ seasonNumber > 0 &&
+ episodeNumber === seasonStatistics.totalEpisodeCount ?
+ :
+ null
+ }
+
+ {
+ showSpecialIcon &&
+ (episodeNumber === 0 || seasonNumber === 0) ?
+ :
+ null
+ }
+
+
+
+ {
+ showEpisodeInformation ?
+
+
+ {title}
+
+
+
+ {seasonNumber}x{padNumber(episodeNumber, 2)}
+
+ {
+ series.seriesType === 'anime' && absoluteEpisodeNumber ?
+ ({absoluteEpisodeNumber}) : null
+ }
+
+
:
+ null
+ }
+
+
+ {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
+
+
+
+
+
+ );
+ }
+}
+
+CalendarEvent.propTypes = {
+ id: PropTypes.number.isRequired,
+ series: PropTypes.object.isRequired,
+ episodeFile: PropTypes.object,
+ title: PropTypes.string.isRequired,
+ seasonNumber: PropTypes.number.isRequired,
+ episodeNumber: PropTypes.number.isRequired,
+ absoluteEpisodeNumber: PropTypes.number,
+ airDateUtc: PropTypes.string.isRequired,
+ monitored: PropTypes.bool.isRequired,
+ hasFile: PropTypes.bool.isRequired,
+ grabbed: PropTypes.bool,
+ queueItem: PropTypes.object,
+ showEpisodeInformation: PropTypes.bool.isRequired,
+ showFinaleIcon: PropTypes.bool.isRequired,
+ showSpecialIcon: PropTypes.bool.isRequired,
+ showCutoffUnmetIcon: PropTypes.bool.isRequired,
+ fullColorEvents: PropTypes.bool.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ colorImpairedMode: PropTypes.bool.isRequired,
+ onEventModalOpenToggle: PropTypes.func.isRequired
+};
+
+export default CalendarEvent;
diff --git a/frontend/src/Calendar/Events/CalendarEvent.tsx b/frontend/src/Calendar/Events/CalendarEvent.tsx
deleted file mode 100644
index 079256a0e..000000000
--- a/frontend/src/Calendar/Events/CalendarEvent.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import classNames from 'classnames';
-import moment from 'moment';
-import React, { useCallback, useState } from 'react';
-import { useSelector } from 'react-redux';
-import AppState from 'App/State/AppState';
-import getStatusStyle from 'Calendar/getStatusStyle';
-import Icon from 'Components/Icon';
-import Link from 'Components/Link/Link';
-import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
-import episodeEntities from 'Episode/episodeEntities';
-import getFinaleTypeName from 'Episode/getFinaleTypeName';
-import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
-import { icons, kinds } from 'Helpers/Props';
-import useSeries from 'Series/useSeries';
-import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import formatTime from 'Utilities/Date/formatTime';
-import padNumber from 'Utilities/Number/padNumber';
-import translate from 'Utilities/String/translate';
-import CalendarEventQueueDetails from './CalendarEventQueueDetails';
-import styles from './CalendarEvent.css';
-
-interface CalendarEventProps {
- id: number;
- episodeId: number;
- seriesId: number;
- episodeFileId?: number;
- title: string;
- seasonNumber: number;
- episodeNumber: number;
- absoluteEpisodeNumber?: number;
- airDateUtc: string;
- monitored: boolean;
- unverifiedSceneNumbering?: boolean;
- finaleType?: string;
- hasFile: boolean;
- grabbed?: boolean;
- onEventModalOpenToggle: (isOpen: boolean) => void;
-}
-
-function CalendarEvent(props: CalendarEventProps) {
- const {
- id,
- seriesId,
- episodeFileId,
- title,
- seasonNumber,
- episodeNumber,
- absoluteEpisodeNumber,
- airDateUtc,
- monitored,
- unverifiedSceneNumbering,
- finaleType,
- hasFile,
- grabbed,
- onEventModalOpenToggle,
- } = props;
-
- const series = useSeries(seriesId);
- const episodeFile = useEpisodeFile(episodeFileId);
- const queueItem = useSelector(createQueueItemSelectorForHook(id));
-
- const { timeFormat, enableColorImpairedMode } = useSelector(
- createUISettingsSelector()
- );
-
- const {
- showEpisodeInformation,
- showFinaleIcon,
- showSpecialIcon,
- showCutoffUnmetIcon,
- fullColorEvents,
- } = useSelector((state: AppState) => state.calendar.options);
-
- const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
-
- const handlePress = useCallback(() => {
- setIsDetailsModalOpen(true);
- onEventModalOpenToggle(true);
- }, [onEventModalOpenToggle]);
-
- const handleDetailsModalClose = useCallback(() => {
- setIsDetailsModalOpen(false);
- onEventModalOpenToggle(false);
- }, [onEventModalOpenToggle]);
-
- if (!series) {
- return null;
- }
-
- const startTime = moment(airDateUtc);
- const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
- const isDownloading = !!(queueItem || grabbed);
- const isMonitored = series.monitored && monitored;
- const statusStyle = getStatusStyle(
- hasFile,
- isDownloading,
- startTime,
- endTime,
- isMonitored
- );
- const missingAbsoluteNumber =
- series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
-
- return (
-
-
-
-
-
-
{series.title}
-
-
- {missingAbsoluteNumber ? (
-
- ) : null}
-
- {unverifiedSceneNumbering && !missingAbsoluteNumber ? (
-
- ) : null}
-
- {queueItem ? (
-
-
-
- ) : null}
-
- {!queueItem && grabbed ? (
-
- ) : null}
-
- {showCutoffUnmetIcon &&
- !!episodeFile &&
- episodeFile.qualityCutoffNotMet ? (
-
- ) : null}
-
- {episodeNumber === 1 && seasonNumber > 0 ? (
-
- ) : null}
-
- {showFinaleIcon && finaleType ? (
-
- ) : null}
-
- {showSpecialIcon && (episodeNumber === 0 || seasonNumber === 0) ? (
-
- ) : null}
-
-
-
- {showEpisodeInformation ? (
-
-
{title}
-
-
- {seasonNumber}x{padNumber(episodeNumber, 2)}
- {series.seriesType === 'anime' && absoluteEpisodeNumber ? (
-
- ({absoluteEpisodeNumber})
-
- ) : null}
-
-
- ) : null}
-
-
- {formatTime(airDateUtc, timeFormat)} -{' '}
- {formatTime(endTime.toISOString(), timeFormat, {
- includeMinuteZero: true,
- })}
-
-
-
-
-
- );
-}
-
-export default CalendarEvent;
diff --git a/frontend/src/Calendar/Events/CalendarEventConnector.js b/frontend/src/Calendar/Events/CalendarEventConnector.js
new file mode 100644
index 000000000..f3b663dae
--- /dev/null
+++ b/frontend/src/Calendar/Events/CalendarEventConnector.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
+import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
+import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import CalendarEvent from './CalendarEvent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.options,
+ createSeriesSelector(),
+ createEpisodeFileSelector(),
+ createQueueItemSelector(),
+ createUISettingsSelector(),
+ (calendarOptions, series, episodeFile, queueItem, uiSettings) => {
+ return {
+ series,
+ episodeFile,
+ queueItem,
+ ...calendarOptions,
+ timeFormat: uiSettings.timeFormat,
+ colorImpairedMode: uiSettings.enableColorImpairedMode
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(CalendarEvent);
diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.css b/frontend/src/Calendar/Events/CalendarEventGroup.css
index 990d994ec..e5be0fd92 100644
--- a/frontend/src/Calendar/Events/CalendarEventGroup.css
+++ b/frontend/src/Calendar/Events/CalendarEventGroup.css
@@ -2,8 +2,8 @@
overflow-x: hidden;
margin: 4px 2px;
padding: 5px;
- border-bottom: 1px solid var(--borderColor);
- border-left: 4px solid var(--borderColor);
+ border-bottom: 1px solid $borderColor;
+ border-left: 4px solid $borderColor;
font-size: 12px;
}
@@ -16,18 +16,18 @@
@add-mixin truncate;
flex: 1 0 1px;
margin-right: 10px;
- color: var(--calendarTextDimAlternate);
+ color: #3a3f51;
font-size: $defaultFontSize;
}
.airTime {
flex: 1 0 1px;
- color: var(--calendarTextDim);
+ color: $calendarTextDim;
}
.episodeInfo {
margin-left: 10px;
- color: var(--calendarTextDim);
+ color: $calendarTextDim;
}
.absoluteEpisodeNumber {
@@ -43,7 +43,6 @@
.expandContainer,
.collapseContainer {
display: flex;
- align-items: center;
justify-content: center;
}
@@ -51,15 +50,6 @@
margin-bottom: 5px;
}
-.statusContainer {
- display: flex;
- align-items: center;
-
- &:global(.fullColor) {
- filter: var(--calendarFullColorFilter)
- }
-}
-
.statusIcon {
margin-left: 3px;
}
diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts b/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts
deleted file mode 100644
index c527feff1..000000000
--- a/frontend/src/Calendar/Events/CalendarEventGroup.css.d.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'absoluteEpisodeNumber': string;
- 'airTime': string;
- 'airingInfo': string;
- 'collapseContainer': string;
- 'downloaded': string;
- 'downloading': string;
- 'episodeInfo': string;
- 'eventGroup': string;
- 'expandContainer': string;
- 'expandContainerInline': string;
- 'info': string;
- 'missing': string;
- 'onAir': string;
- 'premiere': string;
- 'seriesTitle': string;
- 'statusContainer': string;
- 'statusIcon': string;
- 'unaired': string;
- 'unmonitored': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.js b/frontend/src/Calendar/Events/CalendarEventGroup.js
new file mode 100644
index 000000000..320ae8cc6
--- /dev/null
+++ b/frontend/src/Calendar/Events/CalendarEventGroup.js
@@ -0,0 +1,249 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import formatTime from 'Utilities/Date/formatTime';
+import padNumber from 'Utilities/Number/padNumber';
+import { icons, kinds } from 'Helpers/Props';
+import Icon from 'Components/Icon';
+import Link from 'Components/Link/Link';
+import getStatusStyle from 'Calendar/getStatusStyle';
+import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
+import styles from './CalendarEventGroup.css';
+
+function getEventsInfo(events) {
+ let files = 0;
+ let queued = 0;
+ let monitored = 0;
+ let absoluteEpisodeNumbers = 0;
+
+ events.forEach((event) => {
+ if (event.episodeFileId) {
+ files++;
+ }
+
+ if (event.queued) {
+ queued++;
+ }
+
+ if (event.monitored) {
+ monitored++;
+ }
+
+ if (event.absoluteEpisodeNumber) {
+ absoluteEpisodeNumbers++;
+ }
+ });
+
+ return {
+ allDownloaded: files === events.length,
+ anyQueued: queued > 0,
+ anyMonitored: monitored > 0,
+ allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length
+ };
+}
+
+class CalendarEventGroup extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isExpanded: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onExpandPress = () => {
+ this.setState({ isExpanded: !this.state.isExpanded });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ series,
+ events,
+ isDownloading,
+ showEpisodeInformation,
+ showFinaleIcon,
+ timeFormat,
+ fullColorEvents,
+ colorImpairedMode,
+ onEventModalOpenToggle
+ } = this.props;
+
+ const { isExpanded } = this.state;
+ const {
+ allDownloaded,
+ anyQueued,
+ anyMonitored,
+ allAbsoluteEpisodeNumbers
+ } = getEventsInfo(events);
+ const anyDownloading = isDownloading || anyQueued;
+ const firstEpisode = events[0];
+ const lastEpisode = events[events.length -1];
+ const airDateUtc = firstEpisode.airDateUtc;
+ const startTime = moment(airDateUtc);
+ const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
+ const seasonNumber = firstEpisode.seasonNumber;
+ const statusStyle = getStatusStyle(allDownloaded, anyDownloading, startTime, endTime, anyMonitored);
+ const isMissingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !allAbsoluteEpisodeNumbers;
+
+ if (isExpanded) {
+ return (
+
+ {
+ events.map((event) => {
+ if (event.isGroup) {
+ return null;
+ }
+
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {series.title}
+
+
+ {
+ isMissingAbsoluteNumber &&
+
+ }
+
+ {
+ anyDownloading &&
+
+ }
+
+ {
+ firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
+
+ }
+
+ {
+ showFinaleIcon &&
+ lastEpisode.episodeNumber !== 1 &&
+ seasonNumber > 0 &&
+ lastEpisode.episodeNumber === series.seasons.find((season) => season.seasonNumber === seasonNumber).statistics.totalEpisodeCount &&
+
+ }
+
+
+
+
+ {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
+
+
+ {
+ showEpisodeInformation ?
+
+ {seasonNumber}x{padNumber(firstEpisode.episodeNumber, 2)}-{padNumber(lastEpisode.episodeNumber, 2)}
+
+ {
+ series.seriesType === 'anime' &&
+ firstEpisode.absoluteEpisodeNumber &&
+ lastEpisode.absoluteEpisodeNumber &&
+
+ ({firstEpisode.absoluteEpisodeNumber}-{lastEpisode.absoluteEpisodeNumber})
+
+ }
+
:
+
+
+
+ }
+
+
+ {
+ showEpisodeInformation &&
+
+
+
+ }
+
+ );
+ }
+}
+
+CalendarEventGroup.propTypes = {
+ series: PropTypes.object.isRequired,
+ events: PropTypes.arrayOf(PropTypes.object).isRequired,
+ isDownloading: PropTypes.bool.isRequired,
+ showEpisodeInformation: PropTypes.bool.isRequired,
+ showFinaleIcon: PropTypes.bool.isRequired,
+ fullColorEvents: PropTypes.bool.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ colorImpairedMode: PropTypes.bool.isRequired,
+ onEventModalOpenToggle: PropTypes.func.isRequired
+};
+
+export default CalendarEventGroup;
diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.tsx b/frontend/src/Calendar/Events/CalendarEventGroup.tsx
deleted file mode 100644
index 1ee981cfd..000000000
--- a/frontend/src/Calendar/Events/CalendarEventGroup.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-import classNames from 'classnames';
-import moment from 'moment';
-import React, { useCallback, useMemo, useState } from 'react';
-import { useSelector } from 'react-redux';
-import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
-import getStatusStyle from 'Calendar/getStatusStyle';
-import Icon from 'Components/Icon';
-import Link from 'Components/Link/Link';
-import getFinaleTypeName from 'Episode/getFinaleTypeName';
-import { icons, kinds } from 'Helpers/Props';
-import useSeries from 'Series/useSeries';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import { CalendarItem } from 'typings/Calendar';
-import formatTime from 'Utilities/Date/formatTime';
-import padNumber from 'Utilities/Number/padNumber';
-import translate from 'Utilities/String/translate';
-import CalendarEvent from './CalendarEvent';
-import styles from './CalendarEventGroup.css';
-
-function createIsDownloadingSelector(episodeIds: number[]) {
- return createSelector(
- (state: AppState) => state.queue.details,
- (details) => {
- return details.items.some((item) => {
- return !!(item.episodeId && episodeIds.includes(item.episodeId));
- });
- }
- );
-}
-
-interface CalendarEventGroupProps {
- episodeIds: number[];
- seriesId: number;
- events: CalendarItem[];
- onEventModalOpenToggle: (isOpen: boolean) => void;
-}
-
-function CalendarEventGroup({
- episodeIds,
- seriesId,
- events,
- onEventModalOpenToggle,
-}: CalendarEventGroupProps) {
- const isDownloading = useSelector(createIsDownloadingSelector(episodeIds));
- const series = useSeries(seriesId)!;
-
- const { timeFormat, enableColorImpairedMode } = useSelector(
- createUISettingsSelector()
- );
-
- const { showEpisodeInformation, showFinaleIcon, fullColorEvents } =
- useSelector((state: AppState) => state.calendar.options);
-
- const [isExpanded, setIsExpanded] = useState(false);
-
- const firstEpisode = events[0];
- const lastEpisode = events[events.length - 1];
- const airDateUtc = firstEpisode.airDateUtc;
- const startTime = moment(airDateUtc);
- const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
- const seasonNumber = firstEpisode.seasonNumber;
-
- const { allDownloaded, anyQueued, anyMonitored, allAbsoluteEpisodeNumbers } =
- useMemo(() => {
- let files = 0;
- let queued = 0;
- let monitored = 0;
- let absoluteEpisodeNumbers = 0;
-
- events.forEach((event) => {
- if (event.episodeFileId) {
- files++;
- }
-
- if (event.queued) {
- queued++;
- }
-
- if (series.monitored && event.monitored) {
- monitored++;
- }
-
- if (event.absoluteEpisodeNumber) {
- absoluteEpisodeNumbers++;
- }
- });
-
- return {
- allDownloaded: files === events.length,
- anyQueued: queued > 0,
- anyMonitored: monitored > 0,
- allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length,
- };
- }, [series, events]);
-
- const anyDownloading = isDownloading || anyQueued;
-
- const statusStyle = getStatusStyle(
- allDownloaded,
- anyDownloading,
- startTime,
- endTime,
- anyMonitored
- );
- const isMissingAbsoluteNumber =
- series.seriesType === 'anime' &&
- seasonNumber > 0 &&
- !allAbsoluteEpisodeNumbers;
-
- const handleExpandPress = useCallback(() => {
- setIsExpanded((state) => !state);
- }, []);
-
- if (isExpanded) {
- return (
-
- {events.map((event) => {
- return (
-
- );
- })}
-
-
-
-
-
- );
- }
-
- return (
-
-
-
{series.title}
-
-
- {isMissingAbsoluteNumber ? (
-
- ) : null}
-
- {anyDownloading ? (
-
- ) : null}
-
- {firstEpisode.episodeNumber === 1 && seasonNumber > 0 ? (
-
- ) : null}
-
- {showFinaleIcon && lastEpisode.finaleType ? (
-
- ) : null}
-
-
-
-
-
- {formatTime(airDateUtc, timeFormat)} -{' '}
- {formatTime(endTime.toISOString(), timeFormat, {
- includeMinuteZero: true,
- })}
-
-
- {showEpisodeInformation ? (
-
- {seasonNumber}x{padNumber(firstEpisode.episodeNumber, 2)}-
- {padNumber(lastEpisode.episodeNumber, 2)}
- {series.seriesType === 'anime' &&
- firstEpisode.absoluteEpisodeNumber &&
- lastEpisode.absoluteEpisodeNumber ? (
-
- ({firstEpisode.absoluteEpisodeNumber}-
- {lastEpisode.absoluteEpisodeNumber})
-
- ) : null}
-
- ) : (
-
-
-
- )}
-
-
- {showEpisodeInformation ? (
-
-
-
-
-
- ) : null}
-
- );
-}
-
-export default CalendarEventGroup;
diff --git a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js
new file mode 100644
index 000000000..dbd967784
--- /dev/null
+++ b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js
@@ -0,0 +1,37 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import CalendarEventGroup from './CalendarEventGroup';
+
+function createIsDownloadingSelector() {
+ return createSelector(
+ (state, { episodeIds }) => episodeIds,
+ (state) => state.queue.details,
+ (episodeIds, details) => {
+ return details.items.some((item) => {
+ return item.episode && episodeIds.includes(item.episode.id);
+ });
+ }
+ );
+}
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.options,
+ createSeriesSelector(),
+ createIsDownloadingSelector(),
+ createUISettingsSelector(),
+ (calendarOptions, series, isDownloading, uiSettings) => {
+ return {
+ series,
+ isDownloading,
+ ...calendarOptions,
+ timeFormat: uiSettings.timeFormat,
+ colorImpairedMode: uiSettings.enableColorImpairedMode
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(CalendarEventGroup);
diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js
new file mode 100644
index 000000000..7440b9d67
--- /dev/null
+++ b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import colors from 'Styles/Variables/colors';
+import CircularProgressBar from 'Components/CircularProgressBar';
+import QueueDetails from 'Activity/Queue/QueueDetails';
+
+function CalendarEventQueueDetails(props) {
+ const {
+ title,
+ size,
+ sizeleft,
+ estimatedCompletionTime,
+ status,
+ trackedDownloadState,
+ trackedDownloadStatus,
+ errorMessage
+ } = props;
+
+ const progress = size ? (100 - sizeleft / size * 100) : 0;
+
+ return (
+
+
+
+ }
+ />
+ );
+}
+
+CalendarEventQueueDetails.propTypes = {
+ title: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired,
+ sizeleft: PropTypes.number.isRequired,
+ estimatedCompletionTime: PropTypes.string,
+ status: PropTypes.string.isRequired,
+ trackedDownloadState: PropTypes.string.isRequired,
+ trackedDownloadStatus: PropTypes.string.isRequired,
+ errorMessage: PropTypes.string
+};
+
+export default CalendarEventQueueDetails;
diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx b/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx
deleted file mode 100644
index 2372bc78e..000000000
--- a/frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import QueueDetails from 'Activity/Queue/QueueDetails';
-import CircularProgressBar from 'Components/CircularProgressBar';
-import {
- QueueTrackedDownloadState,
- QueueTrackedDownloadStatus,
- StatusMessage,
-} from 'typings/Queue';
-
-interface CalendarEventQueueDetailsProps {
- title: string;
- size: number;
- sizeleft: number;
- estimatedCompletionTime?: string;
- status: string;
- trackedDownloadState: QueueTrackedDownloadState;
- trackedDownloadStatus: QueueTrackedDownloadStatus;
- statusMessages?: StatusMessage[];
- errorMessage?: string;
-}
-
-function CalendarEventQueueDetails({
- title,
- size,
- sizeleft,
- estimatedCompletionTime,
- status,
- trackedDownloadState,
- trackedDownloadStatus,
- statusMessages,
- errorMessage,
-}: CalendarEventQueueDetailsProps) {
- const progress = size ? 100 - (sizeleft / size) * 100 : 0;
-
- return (
-