From 21ed073f294e8ab4d245f64dd6ae62f1c940b9f4 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 5 Oct 2019 23:21:55 -0400 Subject: [PATCH] New: Add List and Discovery Pages (#3803) --- .../AddListMovie/AddDiscoverMovieConnector.js | 163 ++++++++ .../src/AddMovie/AddListMovie/AddListMovie.js | 363 ++++++++++++++++++ .../AddListMovie/AddListMovieConnector.js | 163 ++++++++ .../AddListMovieFilterModalConnector.js | 24 ++ .../AddListMovie/AddListMovieItemConnector.js | 67 ++++ .../Menus/AddListMovieFilterMenu.js | 41 ++ .../Menus/AddListMovieSortMenu.js | 78 ++++ .../Menus/AddListMovieViewMenu.js | 55 +++ .../Overview/AddListMovieOverview.css | 98 +++++ .../Overview/AddListMovieOverview.js | 158 ++++++++ .../Overview/AddListMovieOverviewConnector.js | 23 ++ .../Overview/AddListMovieOverviews.css | 3 + .../Overview/AddListMovieOverviews.js | 281 ++++++++++++++ .../AddListMovieOverviewsConnector.js | 25 ++ .../AddListMovieOverviewOptionsModal.js | 25 ++ ...AddListMovieOverviewOptionsModalContent.js | 132 +++++++ ...vieOverviewOptionsModalContentConnector.js | 23 ++ .../Posters/AddListMoviePoster.css | 94 +++++ .../Posters/AddListMoviePoster.js | 163 ++++++++ .../Posters/AddListMoviePosterConnector.js | 23 ++ .../Posters/AddListMoviePosterInfo.css | 5 + .../Posters/AddListMoviePosterInfo.js | 30 ++ .../Posters/AddListMoviePosters.css | 3 + .../Posters/AddListMoviePosters.js | 307 +++++++++++++++ .../Posters/AddListMoviePostersConnector.js | 24 ++ .../Options/AddListMoviePosterOptionsModal.js | 25 ++ .../AddListMoviePosterOptionsModalContent.js | 133 +++++++ ...MoviePosterOptionsModalContentConnector.js | 23 ++ .../Table/AddListMovieActionsCell.js | 54 +++ .../AddListMovie/Table/AddListMovieHeader.css | 44 +++ .../AddListMovie/Table/AddListMovieHeader.js | 106 +++++ .../Table/AddListMovieHeaderConnector.js | 13 + .../AddListMovie/Table/AddListMovieRow.css | 56 +++ .../AddListMovie/Table/AddListMovieRow.js | 224 +++++++++++ .../Table/AddListMovieRowConnector.js | 23 ++ .../AddListMovie/Table/AddListMovieTable.css | 5 + .../AddListMovie/Table/AddListMovieTable.js | 128 ++++++ .../Table/AddListMovieTableConnector.js | 27 ++ .../AddListMovie/Table/MovieStatusCell.css | 9 + .../AddListMovie/Table/MovieStatusCell.js | 62 +++ .../AddNewMovie/AddNewMovieSearchResult.js | 1 + frontend/src/App/AppRoutes.js | 12 + .../Components/Page/Sidebar/PageSidebar.js | 2 +- frontend/src/Store/Actions/addMovieActions.js | 287 +++++++++++++- .../Middleware/createSentryMiddleware.js | 2 - .../Selectors/createAddListMovieSelector.js | 13 + ...dMovieClientSideCollectionItemsSelector.js | 36 ++ frontend/src/Store/scrollPositions.js | 3 +- .../Movies/FetchMovieListModule.cs | 6 +- .../Movies/MovieDiscoverModule.cs | 18 +- 50 files changed, 3659 insertions(+), 24 deletions(-) create mode 100644 frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/AddListMovie.js create mode 100644 frontend/src/AddMovie/AddListMovie/AddListMovieConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/AddListMovieFilterModalConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/AddListMovieItemConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Menus/AddListMovieFilterMenu.js create mode 100644 frontend/src/AddMovie/AddListMovie/Menus/AddListMovieSortMenu.js create mode 100644 frontend/src/AddMovie/AddListMovie/Menus/AddListMovieViewMenu.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.css create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.css create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewsConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModal.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContent.js create mode 100644 frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContentConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.css create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.css create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.css create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePostersConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModal.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContent.js create mode 100644 frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContentConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieActionsCell.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.css create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeaderConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.css create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieRowConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.css create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/AddListMovieTableConnector.js create mode 100644 frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.css create mode 100644 frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.js create mode 100644 frontend/src/Store/Selectors/createAddListMovieSelector.js create mode 100644 frontend/src/Store/Selectors/createAddMovieClientSideCollectionItemsSelector.js diff --git a/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js b/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js new file mode 100644 index 000000000..819b459ef --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import dimensions from 'Styles/Variables/dimensions'; +import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import { fetchDiscoverMovies, clearAddMovie, setListMovieSort, setListMovieFilter, setListMovieView, setListMovieTableOption } from 'Store/Actions/addMovieActions'; +import scrollPositions from 'Store/scrollPositions'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import withScrollPosition from 'Components/withScrollPosition'; +import AddListMovie from './AddListMovie'; + +const POSTERS_PADDING = 15; +const POSTERS_PADDING_SMALL_SCREEN = 5; +const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding); +const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen); + +// If the scrollTop is greater than zero it needs to be offset +// by the padding so when it is set initially so it is correct +// after React Virtualized takes the padding into account. + +function getScrollTop(view, scrollTop, isSmallScreen) { + if (scrollTop === 0) { + return 0; + } + + let padding = isSmallScreen ? TABLE_PADDING_SMALL_SCREEN : TABLE_PADDING; + + if (view === 'posters') { + padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING; + } + + return scrollTop + padding; +} + +function createMapStateToProps() { + return createSelector( + createAddMovieClientSideCollectionItemsSelector('addMovie'), + createDimensionsSelector(), + ( + movies, + dimensionsState + ) => { + return { + ...movies, + isSmallScreen: dimensionsState.isSmallScreen + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchRootFolders() { + dispatch(fetchRootFolders()); + }, + + dispatchClearListMovie() { + dispatch(clearAddMovie()); + }, + + dispatchFetchListMovies() { + dispatch(fetchDiscoverMovies()); + }, + + onTableOptionChange(payload) { + dispatch(setListMovieTableOption(payload)); + }, + + onSortSelect(sortKey) { + dispatch(setListMovieSort({ sortKey })); + }, + + onFilterSelect(selectedFilterKey) { + dispatch(setListMovieFilter({ selectedFilterKey })); + }, + + dispatchSetListMovieView(view) { + dispatch(setListMovieView({ view })); + } + }; +} + +class AddDiscoverMovieConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + view, + scrollTop, + isSmallScreen + } = props; + + this.state = { + scrollTop: getScrollTop(view, scrollTop, isSmallScreen) + }; + } + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.dispatchFetchRootFolders(); + this.props.dispatchFetchListMovies(); + } + + componentWillUnmount() { + this.props.dispatchClearListMovie(); + unregisterPagePopulator(this.repopulate); + } + + // + // Listeners + + onViewSelect = (view) => { + // Reset the scroll position before changing the view + this.setState({ scrollTop: 0 }, () => { + this.props.dispatchSetListMovieView(view); + }); + } + + onScroll = ({ scrollTop }) => { + this.setState({ + scrollTop + }, () => { + scrollPositions.addMovie = scrollTop; + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddDiscoverMovieConnector.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, + view: PropTypes.string.isRequired, + scrollTop: PropTypes.number.isRequired, + dispatchFetchRootFolders: PropTypes.func.isRequired, + dispatchFetchListMovies: PropTypes.func.isRequired, + dispatchClearListMovie: PropTypes.func.isRequired, + dispatchSetListMovieView: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, createMapDispatchToProps)(AddDiscoverMovieConnector), + 'addMovie' +); diff --git a/frontend/src/AddMovie/AddListMovie/AddListMovie.js b/frontend/src/AddMovie/AddListMovie/AddListMovie.js new file mode 100644 index 000000000..61bd63162 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/AddListMovie.js @@ -0,0 +1,363 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import { align, icons, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import AddListMovieTableConnector from './Table/AddListMovieTableConnector'; +import AddListMoviePosterOptionsModal from './Posters/Options/AddListMoviePosterOptionsModal'; +import AddListMoviePostersConnector from './Posters/AddListMoviePostersConnector'; +import AddListMovieOverviewOptionsModal from './Overview/Options/AddListMovieOverviewOptionsModal'; +import AddListMovieOverviewsConnector from './Overview/AddListMovieOverviewsConnector'; +import AddListMovieFilterMenu from 'AddMovie/AddListMovie/Menus/AddListMovieFilterMenu'; +import AddListMovieSortMenu from 'AddMovie/AddListMovie/Menus/AddListMovieSortMenu'; +import AddListMovieViewMenu from 'AddMovie/AddListMovie/Menus/AddListMovieViewMenu'; +import styles from 'Movie/Index/MovieIndex.css'; + +function getViewComponent(view) { + if (view === 'posters') { + return AddListMoviePostersConnector; + } + + if (view === 'overview') { + return AddListMovieOverviewsConnector; + } + + return AddListMovieTableConnector; +} + +class AddListMovie extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + contentBody: null, + jumpBarItems: [], + jumpToCharacter: null, + isPosterOptionsModalOpen: false, + isOverviewOptionsModalOpen: false, + isConfirmSearchModalOpen: false, + searchType: null, + lastToggled: null, + isRendered: false + }; + } + + componentDidMount() { + this.setJumpBarItems(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection, + scrollTop + } = this.props; + + if ( + hasDifferentItems(prevProps.items, items) || + sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection + ) { + this.setJumpBarItems(); + } + + if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) { + this.setState({ jumpToCharacter: null }); + } + } + + // + // Control + + setContentBodyRef = (ref) => { + this.setState({ contentBody: ref }); + } + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortTitle + if (sortKey !== 'sortTitle') { + this.setState({ jumpBarItems: [] }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + + const firstCharacter = item.sortTitle.charAt(0); + + if (isNaN(firstCharacter)) { + acc.push(firstCharacter); + } else { + acc.push('#'); + } + + return acc; + }, []).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + characters.reverse(); + } + + this.setState({ jumpBarItems: _.sortedUniq(characters) }); + } + + // + // Listeners + + onPosterOptionsPress = () => { + this.setState({ isPosterOptionsModalOpen: true }); + } + + onPosterOptionsModalClose = () => { + this.setState({ isPosterOptionsModalOpen: false }); + } + + onOverviewOptionsPress = () => { + this.setState({ isOverviewOptionsModalOpen: true }); + } + + onOverviewOptionsModalClose = () => { + this.setState({ isOverviewOptionsModalOpen: false }); + } + + onJumpBarItemPress = (jumpToCharacter) => { + this.setState({ jumpToCharacter }); + } + + onRender = () => { + this.setState({ isRendered: true }, () => { + const { + scrollTop, + isSmallScreen + } = this.props; + + if (isSmallScreen) { + // Seems to result in the view being off by 125px (distance to the top of the page) + // document.documentElement.scrollTop = document.body.scrollTop = scrollTop; + + // This works, but then jumps another 1px after scrolling + document.documentElement.scrollTop = scrollTop; + } + }); + } + + onScroll = ({ scrollTop }) => { + this.props.onScroll({ scrollTop }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + totalItems, + items, + columns, + selectedFilterKey, + filters, + customFilters, + sortKey, + sortDirection, + view, + scrollTop, + onSortSelect, + onFilterSelect, + onViewSelect, + ...otherProps + } = this.props; + + const { + contentBody, + jumpBarItems, + jumpToCharacter, + isPosterOptionsModalOpen, + isOverviewOptionsModalOpen, + isRendered + } = this.state; + + const ViewComponent = getViewComponent(view); + const isLoaded = !!(!error && isPopulated && items.length && contentBody); + const hasNoMovie = !totalItems; + + return ( + + + + { + view === 'table' ? + + + : + null + } + + { + view === 'posters' ? + : + null + } + + { + view === 'overview' ? + : + null + } + + { + (view === 'posters' || view === 'overview') && + + } + + + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load movies
+ } + + { + isLoaded && +
+ +
+ } + + { + !error && isPopulated && !items.length && +
+
Couldn't find any results
+
+ } +
+ + { + isLoaded && !!jumpBarItems.length && + + } +
+ + + + +
+ ); + } +} + +AddListMovie.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + totalItems: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + view: PropTypes.string.isRequired, + scrollTop: PropTypes.number.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onViewSelect: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default AddListMovie; diff --git a/frontend/src/AddMovie/AddListMovie/AddListMovieConnector.js b/frontend/src/AddMovie/AddListMovie/AddListMovieConnector.js new file mode 100644 index 000000000..e8873e548 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/AddListMovieConnector.js @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector'; +import dimensions from 'Styles/Variables/dimensions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import { fetchListMovies, clearAddMovie, setListMovieSort, setListMovieFilter, setListMovieView, setListMovieTableOption } from 'Store/Actions/addMovieActions'; +import scrollPositions from 'Store/scrollPositions'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import withScrollPosition from 'Components/withScrollPosition'; +import AddListMovie from './AddListMovie'; + +const POSTERS_PADDING = 15; +const POSTERS_PADDING_SMALL_SCREEN = 5; +const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding); +const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen); + +// If the scrollTop is greater than zero it needs to be offset +// by the padding so when it is set initially so it is correct +// after React Virtualized takes the padding into account. + +function getScrollTop(view, scrollTop, isSmallScreen) { + if (scrollTop === 0) { + return 0; + } + + let padding = isSmallScreen ? TABLE_PADDING_SMALL_SCREEN : TABLE_PADDING; + + if (view === 'posters') { + padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING; + } + + return scrollTop + padding; +} + +function createMapStateToProps() { + return createSelector( + createAddMovieClientSideCollectionItemsSelector('addMovie'), + createDimensionsSelector(), + ( + movies, + dimensionsState + ) => { + return { + ...movies, + isSmallScreen: dimensionsState.isSmallScreen + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchRootFolders() { + dispatch(fetchRootFolders()); + }, + + dispatchFetchListMovies() { + dispatch(fetchListMovies()); + }, + + onTableOptionChange(payload) { + dispatch(setListMovieTableOption(payload)); + }, + + onSortSelect(sortKey) { + dispatch(setListMovieSort({ sortKey })); + }, + + onFilterSelect(selectedFilterKey) { + dispatch(setListMovieFilter({ selectedFilterKey })); + }, + + dispatchSetListMovieView(view) { + dispatch(setListMovieView({ view })); + }, + + dispatchClearListMovie() { + dispatch(clearAddMovie()); + } + }; +} + +class AddListMovieConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + view, + scrollTop, + isSmallScreen + } = props; + + this.state = { + scrollTop: getScrollTop(view, scrollTop, isSmallScreen) + }; + } + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.dispatchFetchRootFolders(); + this.props.dispatchFetchListMovies(); + } + + componentWillUnmount() { + this.props.dispatchClearListMovie(); + unregisterPagePopulator(this.repopulate); + } + + // + // Listeners + + onViewSelect = (view) => { + // Reset the scroll position before changing the view + this.setState({ scrollTop: 0 }, () => { + this.props.dispatchSetListMovieView(view); + }); + } + + onScroll = ({ scrollTop }) => { + this.setState({ + scrollTop + }, () => { + scrollPositions.addMovie = scrollTop; + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddListMovieConnector.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, + view: PropTypes.string.isRequired, + scrollTop: PropTypes.number.isRequired, + dispatchFetchRootFolders: PropTypes.func.isRequired, + dispatchFetchListMovies: PropTypes.func.isRequired, + dispatchClearListMovie: PropTypes.func.isRequired, + dispatchSetListMovieView: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, createMapDispatchToProps)(AddListMovieConnector), + 'addMovie' +); diff --git a/frontend/src/AddMovie/AddListMovie/AddListMovieFilterModalConnector.js b/frontend/src/AddMovie/AddListMovie/AddListMovieFilterModalConnector.js new file mode 100644 index 000000000..c509d18e8 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/AddListMovieFilterModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setListMovieFilter } from 'Store/Actions/addMovieActions'; +import FilterModal from 'Components/Filter/FilterModal'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addMovie.items, + (state) => state.addMovie.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps, + customFilterType: 'addMovie' + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetFilter: setListMovieFilter +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/AddMovie/AddListMovie/AddListMovieItemConnector.js b/frontend/src/AddMovie/AddListMovie/AddListMovieItemConnector.js new file mode 100644 index 000000000..2dde915cf --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/AddListMovieItemConnector.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAddListMovieSelector from 'Store/Selectors/createAddListMovieSelector'; +import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector'; + +function createMapStateToProps() { + return createSelector( + createAddListMovieSelector(), + createMovieQualityProfileSelector(), + ( + movie, + qualityProfile + ) => { + + // If a movie is deleted this selector may fire before the parent + // selecors, which will result in an undefined movie, if that happens + // we want to return early here and again in the render function to avoid + // trying to show a movie that has no information available. + + if (!movie) { + return {}; + } + + return { + ...movie, + qualityProfile + }; + } + ); +} + +const mapDispatchToProps = { +}; + +class AddListMovieItemConnector extends Component { + + // + // Render + + render() { + const { + tmdbId, + component: ItemComponent, + ...otherProps + } = this.props; + + if (!tmdbId) { + return null; + } + + return ( + + ); + } +} + +AddListMovieItemConnector.propTypes = { + tmdbId: PropTypes.number, + component: PropTypes.elementType.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddListMovieItemConnector); diff --git a/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieFilterMenu.js b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieFilterMenu.js new file mode 100644 index 000000000..25e2f14fa --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieFilterMenu.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import AddListMovieFilterModalConnector from 'AddMovie/AddListMovie/AddListMovieFilterModalConnector'; + +function AddListMovieFilterMenu(props) { + const { + selectedFilterKey, + filters, + customFilters, + isDisabled, + onFilterSelect + } = props; + + return ( + + ); +} + +AddListMovieFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired +}; + +AddListMovieFilterMenu.defaultProps = { + showCustomFilters: false +}; + +export default AddListMovieFilterMenu; diff --git a/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieSortMenu.js b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieSortMenu.js new file mode 100644 index 000000000..b95996a0e --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieSortMenu.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align, sortDirections } from 'Helpers/Props'; +import SortMenu from 'Components/Menu/SortMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import SortMenuItem from 'Components/Menu/SortMenuItem'; + +function AddListMovieSortMenu(props) { + const { + sortKey, + sortDirection, + isDisabled, + onSortSelect + } = props; + + return ( + + + + Status + + + + Title + + + + Studio + + + + In Cinemas + + + + Physical Release + + + + ); +} + +AddListMovieSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + isDisabled: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired +}; + +export default AddListMovieSortMenu; diff --git a/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieViewMenu.js b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieViewMenu.js new file mode 100644 index 000000000..5987a4e15 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Menus/AddListMovieViewMenu.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import ViewMenu from 'Components/Menu/ViewMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; + +function AddListMovieViewMenu(props) { + const { + view, + isDisabled, + onViewSelect + } = props; + + return ( + + + + Table + + + + Posters + + + + Overview + + + + ); +} + +AddListMovieViewMenu.propTypes = { + view: PropTypes.string.isRequired, + isDisabled: PropTypes.bool.isRequired, + onViewSelect: PropTypes.func.isRequired +}; + +export default AddListMovieViewMenu; diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.css b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.css new file mode 100644 index 000000000..0a5cdfa00 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.css @@ -0,0 +1,98 @@ +$hoverScale: 1.05; + +.container { + &:hover { + .content { + background-color: $tableRowHoverBackgroundColor; + } + } +} + +.content { + display: flex; + flex-grow: 1; +} + +.poster { + position: relative; +} + +.posterContainer { + position: relative; +} + +.link { + composes: link from '~Components/Link/Link.css'; + + display: block; + color: $defaultColor; + + &:hover { + color: $defaultColor; + text-decoration: none; + } +} + +.ended { + position: absolute; + top: 0; + right: 0; + z-index: 1; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.info { + display: flex; + flex: 1 0 1px; + flex-direction: column; + overflow: hidden; + padding-left: 10px; +} + +.titleRow { + display: flex; + justify-content: space-between; + flex: 0 0 auto; + margin-bottom: 10px; + font-weight: 300; + font-size: 30px; + line-height: 32px; +} + +.title { + @add-mixin truncate; + composes: link; + + flex: 1 0 1px; + font-weight: 300; + font-size: 30px; +} + +.actions { + white-space: nowrap; +} + +.details { + display: flex; + justify-content: space-between; + flex: 1 0 auto; +} + +.overview { + composes: link; + + flex: 0 1 1000px; + overflow: hidden; + min-height: 0; +} + +@media only screen and (max-width: $breakpointSmall) { + .overview { + display: none; + } +} diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.js b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.js new file mode 100644 index 000000000..310eb4d91 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverview.js @@ -0,0 +1,158 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import dimensions from 'Styles/Variables/dimensions'; +import fonts from 'Styles/Variables/fonts'; +import MoviePoster from 'Movie/MoviePoster'; +import Link from 'Components/Link/Link'; +import AddNewMovieModal from 'AddMovie/AddNewMovie/AddNewMovieModal'; +import styles from './AddListMovieOverview.css'; + +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); +const defaultFontSize = parseInt(fonts.defaultFontSize); +const lineHeight = parseFloat(fonts.lineHeight); + +// Hardcoded height beased on line-height of 32 + bottom margin of 10. +// Less side-effecty than using react-measure. +const titleRowHeight = 42; + +function getContentHeight(rowHeight, isSmallScreen) { + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + + return rowHeight - padding; +} + +class AddListMovieOverview extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNewAddMovieModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddMovieModalOpen: true }); + } + + onAddMovieModalClose = () => { + this.setState({ isNewAddMovieModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + tmdbId, + title, + titleSlug, + folder, + year, + overview, + images, + posterWidth, + posterHeight, + rowHeight, + isSmallScreen, + isExistingMovie + } = this.props; + + const { + isNewAddMovieModalOpen + } = this.state; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress }; + + const contentHeight = getContentHeight(rowHeight, isSmallScreen); + const overviewHeight = contentHeight - titleRowHeight; + + return ( +
+ +
+
+ + +
+
+ +
+
+ {title} ({year}) +
+ +
+ + +
+
+ + + +
+ ); + } +} + +AddListMovieOverview.propTypes = { + style: PropTypes.object.isRequired, + tmdbId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + folder: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + overviewOptions: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isExistingMovie: PropTypes.bool.isRequired, + isExclusionMovie: PropTypes.bool.isRequired +}; + +export default AddListMovieOverview; diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewConnector.js b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewConnector.js new file mode 100644 index 000000000..50e0b4080 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector'; +import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddListMovieOverview from './AddListMovieOverview'; + +function createMapStateToProps() { + return createSelector( + createExistingMovieSelector(), + createExclusionMovieSelector(), + createDimensionsSelector(), + (isExistingMovie, isExclusionMovie, dimensions) => { + return { + isExistingMovie, + isExclusionMovie, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddListMovieOverview); diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.css b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.js b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.js new file mode 100644 index 000000000..906e7af92 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviews.js @@ -0,0 +1,281 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { Grid, WindowScroller } from 'react-virtualized'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import Measure from 'Components/Measure'; +import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector'; +import AddListMovieOverviewConnector from './AddListMovieOverviewConnector'; +import styles from './AddListMovieOverviews.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); + +function calculatePosterWidth(posterSize, isSmallScreen) { + const maxiumPosterWidth = isSmallScreen ? 152 : 162; + + if (posterSize === 'large') { + return maxiumPosterWidth; + } + + if (posterSize === 'medium') { + return Math.floor(maxiumPosterWidth * 0.75); + } + + return Math.floor(maxiumPosterWidth * 0.5); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { + + const heights = [ + posterHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil((250 / 170) * posterWidth); +} + +class AddListMovieOverviews extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnCount: 1, + posterWidth: 162, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filters, + sortKey, + sortDirection, + overviewOptions, + jumpToCharacter + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions); + + if ( + prevProps.sortKey !== sortKey || + prevProps.overviewOptions !== overviewOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filters !== filters || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged || + overviewOptionsChanged + ) { + this._grid.recomputeGridSize(); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (index != null) { + const { + rowHeight + } = this.state; + + const scrollTop = rowHeight * index; + + this.props.onScroll({ scrollTop }); + } + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + const { + rowHeight + } = this.state; + + const index = getIndexOfFirstCharacter(items, character); + + if (index != null) { + const scrollTop = rowHeight * index; + + this.props.onScroll({ scrollTop }); + } + } + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + overviewOptions + } = this.props; + + const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen); + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); + + this.setState({ + width, + posterWidth, + posterHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, style }) => { + const { + items, + sortKey, + overviewOptions, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + isSmallScreen + } = this.props; + + const { + posterWidth, + posterHeight, + rowHeight + } = this.state; + + const movie = items[rowIndex]; + + if (!movie) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + rowHeight + } = this.state; + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +AddListMovieOverviews.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + overviewOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + jumpToCharacter: PropTypes.string, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default AddListMovieOverviews; diff --git a/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewsConnector.js b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewsConnector.js new file mode 100644 index 000000000..c61a12638 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/AddListMovieOverviewsConnector.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddListMovieOverviews from './AddListMovieOverviews'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addMovie.overviewOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (overviewOptions, uiSettings, dimensions) => { + return { + overviewOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddListMovieOverviews); diff --git a/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModal.js b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModal.js new file mode 100644 index 000000000..c342d71ce --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddListMovieOverviewOptionsModalContentConnector from './AddListMovieOverviewOptionsModalContentConnector'; + +function AddListMovieOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddListMovieOverviewOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddListMovieOverviewOptionsModal; diff --git a/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContent.js b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContent.js new file mode 100644 index 000000000..a5673f8c8 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContent.js @@ -0,0 +1,132 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class AddListMovieOverviewOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + size: props.size, + showStudio: props.showStudio + }; + } + + componentDidUpdate(prevProps) { + const { + size, + showStudio + } = this.props; + + const state = {}; + + if (size !== prevProps.size) { + state.size = size; + } + + if (showStudio !== prevProps.showStudio) { + state.showStudio = showStudio; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeOverviewOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOverviewOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + size, + showStudio + } = this.state; + + return ( + + + Overview Options + + + +
+ + Poster Size + + + + + + Show Studio + + + +
+
+ + + + +
+ ); + } +} + +AddListMovieOverviewOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showStudio: PropTypes.bool.isRequired, + onChangeOverviewOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddListMovieOverviewOptionsModalContent; diff --git a/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContentConnector.js b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContentConnector.js new file mode 100644 index 000000000..2974dc670 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Overview/Options/AddListMovieOverviewOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setListMovieOverviewOption } from 'Store/Actions/addMovieActions'; +import AddListMovieOverviewOptionsModalContent from './AddListMovieOverviewOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addMovie, + (addMovie) => { + return addMovie.overviewOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeOverviewOption(payload) { + dispatch(setListMovieOverviewOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(AddListMovieOverviewOptionsModalContent); diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.css b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.css new file mode 100644 index 000000000..6b97b322e --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.css @@ -0,0 +1,94 @@ +$hoverScale: 1.05; + +.container { + padding: 10px; +} + +.content { + transition: all 200ms ease-in; + + &:hover { + z-index: 2; + box-shadow: 0 0 12px $black; + transition: all 200ms ease-in; + + .controls { + opacity: 0.9; + transition: opacity 200ms linear 150ms; + } + } +} + +.posterContainer { + position: relative; +} + +.link { + composes: link from '~Components/Link/Link.css'; + + position: relative; + display: block; + background-color: $defaultColor; +} + +.overlayTitle { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 5px; + width: 100%; + height: 100%; + color: $offWhite; + text-align: center; + font-size: 20px; +} + +.title { + @add-mixin truncate; + + background-color: #fafbfc; + text-align: center; + font-size: $smallFontSize; +} + +.ended { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.controls { + position: absolute; + bottom: 10px; + left: 10px; + z-index: 3; + border-radius: 4px; + background-color: #707070; + color: $white; + font-size: $smallFontSize; + opacity: 0; + transition: opacity 0; +} + +.action { + composes: button from '~Components/Link/IconButton.css'; + + &:hover { + color: $radarrYellow; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .container { + padding: 5px; + } +} diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js new file mode 100644 index 000000000..72fec309f --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import MoviePoster from 'Movie/MoviePoster'; +import AddNewMovieModal from 'AddMovie/AddNewMovie/AddNewMovieModal'; +import styles from './AddListMoviePoster.css'; + +class AddListMoviePoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPosterError: false, + isNewAddMovieModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddMovieModalOpen: true }); + } + + onAddMovieModalClose = () => { + this.setState({ isNewAddMovieModalOpen: false }); + } + + onPosterLoad = () => { + if (this.state.hasPosterError) { + this.setState({ hasPosterError: false }); + } + } + + onPosterLoadError = () => { + if (!this.state.hasPosterError) { + this.setState({ hasPosterError: true }); + } + } + + // + // Render + + render() { + const { + style, + tmdbId, + title, + year, + overview, + folder, + status, + titleSlug, + images, + posterWidth, + posterHeight, + showTitle, + isExistingMovie + } = this.props; + + const { + hasPosterError, + isNewAddMovieModalOpen + } = this.state; + + const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress }; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + return ( +
+
+
+ { + status === 'ended' && +
+ } + + + + + { + hasPosterError && +
+ {title} +
+ } + +
+ + { + showTitle && +
+ {title} +
+ } + + +
+
+ ); + } +} + +AddListMoviePoster.propTypes = { + style: PropTypes.object.isRequired, + tmdbId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string.isRequired, + folder: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + showTitle: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isExistingMovie: PropTypes.bool.isRequired, + isExclusionMovie: PropTypes.bool.isRequired +}; + +AddListMoviePoster.defaultProps = { + statistics: { + movieFileCount: 0 + } +}; + +export default AddListMoviePoster; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterConnector.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterConnector.js new file mode 100644 index 000000000..beef0b083 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector'; +import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddListMoviePoster from './AddListMoviePoster'; + +function createMapStateToProps() { + return createSelector( + createExistingMovieSelector(), + createExclusionMovieSelector(), + createDimensionsSelector(), + (isExistingMovie, isExclusionMovie, dimensions) => { + return { + isExistingMovie, + isExclusionMovie, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddListMoviePoster); diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.css b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.css new file mode 100644 index 000000000..aab27d827 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.css @@ -0,0 +1,5 @@ +.info { + background-color: #fafbfc; + text-align: center; + font-size: $smallFontSize; +} diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.js new file mode 100644 index 000000000..732032cf1 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosterInfo.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './AddListMoviePosterInfo.css'; + +function AddListMoviePosterInfo(props) { + const { + studio, + sortKey + } = props; + + if (sortKey === 'studio' && studio) { + return ( +
+ {studio} +
+ ); + } + + return null; +} + +AddListMoviePosterInfo.propTypes = { + studio: PropTypes.string, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default AddListMoviePosterInfo; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.css b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js new file mode 100644 index 000000000..b123330ae --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js @@ -0,0 +1,307 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { Grid, WindowScroller } from 'react-virtualized'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import Measure from 'Components/Measure'; +import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector'; +import AddListMoviePosterConnector from './AddListMoviePosterConnector'; +import styles from './AddListMoviePosters.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, posterSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 172 : 182; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && posterSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[posterSize])); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) { + const { + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile + } = posterOptions; + + const nextAiringHeight = 19; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showMonitored) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + switch (sortKey) { + case 'studio': + default: + // No need to add a height of 0 + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil((250 / 170) * posterWidth); +} + +class AddListMoviePosters extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 182, + columnCount: 1, + posterWidth: 162, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filters, + sortKey, + sortDirection, + posterOptions, + jumpToCharacter + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + + if ( + prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filters !== filters || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged + ) { + this._grid.recomputeGridSize(); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (index != null) { + const { + columnCount, + rowHeight + } = this.state; + + const row = Math.floor(index / columnCount); + const scrollTop = rowHeight * row; + + this.props.onScroll({ scrollTop }); + } + } + } + + // + // Control + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + posterOptions + } = this.props; + + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const posterWidth = columnWidth - padding; + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions); + + this.setState({ + width, + columnWidth, + columnCount, + posterWidth, + posterHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + posterOptions, + showRelativeDates, + shortDateFormat, + timeFormat + } = this.props; + + const { + posterWidth, + posterHeight, + columnCount + } = this.state; + + const { + showTitle + } = posterOptions; + + const movie = items[rowIndex * columnCount + columnIndex]; + + if (!movie) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +AddListMoviePosters.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + posterOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + jumpToCharacter: PropTypes.string, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default AddListMoviePosters; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePostersConnector.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePostersConnector.js new file mode 100644 index 000000000..73fadcdcc --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePostersConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddListMoviePosters from './AddListMoviePosters'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addMovie.posterOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (posterOptions, uiSettings, dimensions) => { + return { + posterOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddListMoviePosters); diff --git a/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModal.js b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModal.js new file mode 100644 index 000000000..0f0884a20 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddListMoviePosterOptionsModalContentConnector from './AddListMoviePosterOptionsModalContentConnector'; + +function AddListMoviePosterOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddListMoviePosterOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddListMoviePosterOptionsModal; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContent.js b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContent.js new file mode 100644 index 000000000..c26314c24 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContent.js @@ -0,0 +1,133 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class AddListMoviePosterOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + size: props.size, + showTitle: props.showTitle + }; + } + + componentDidUpdate(prevProps) { + const { + size, + showTitle + } = this.props; + + const state = {}; + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangePosterOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangePosterOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + size, + showTitle + } = this.state; + + return ( + + + Poster Options + + + +
+ + Poster Size + + + + + + Show Title + + + +
+
+ + + + +
+ ); + } +} + +AddListMoviePosterOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + onChangePosterOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddListMoviePosterOptionsModalContent; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContentConnector.js b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContentConnector.js new file mode 100644 index 000000000..edfc388be --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Posters/Options/AddListMoviePosterOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setListMoviePosterOption } from 'Store/Actions/addMovieActions'; +import AddListMoviePosterOptionsModalContent from './AddListMoviePosterOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addMovie, + (addMovie) => { + return addMovie.posterOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangePosterOption(payload) { + dispatch(setListMoviePosterOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(AddListMoviePosterOptionsModalContent); diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieActionsCell.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieActionsCell.js new file mode 100644 index 000000000..4b030e195 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieActionsCell.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; + +class AddListMovieActionsCell extends Component { + + // + // Render + + render() { + const { + id, + ...otherProps + } = this.props; + + return ( + + {/* + + */} + + {/* + + */} + + ); + } +} + +AddListMovieActionsCell.propTypes = { + id: PropTypes.number.isRequired +}; + +export default AddListMovieActionsCell; diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.css b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.css new file mode 100644 index 000000000..c897aee7d --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.css @@ -0,0 +1,44 @@ +.status { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 60px; +} + +.sortTitle { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 4 0 110px; +} + +.studio { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 2 0 90px; +} + +.inCinemas, +.physicalRelease, +.genres { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 180px; +} + +.movieStatus, +.certification { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 100px; +} + +.ratings { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 80px; +} + +.actions { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 90px; +} diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.js new file mode 100644 index 000000000..71cb685c8 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeader.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; +import styles from './AddListMovieHeader.css'; + +class AddListMovieHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isTableOptionsModalOpen: false + }; + } + + // + // Listeners + + onTableOptionsPress = () => { + this.setState({ isTableOptionsModalOpen: true }); + } + + onTableOptionsModalClose = () => { + this.setState({ isTableOptionsModalOpen: false }); + } + + // + // Render + + render() { + const { + columns, + onTableOptionChange, + ...otherProps + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + label, + isSortable, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'actions') { + return ( + + + + ); + } + + return ( + + {label} + + ); + }) + } + + + + ); + } +} + +AddListMovieHeader.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onTableOptionChange: PropTypes.func.isRequired +}; + +export default AddListMovieHeader; diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeaderConnector.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeaderConnector.js new file mode 100644 index 000000000..64109721f --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieHeaderConnector.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { setListMovieTableOption } from 'Store/Actions/addMovieActions'; +import AddListMovieHeader from './AddListMovieHeader'; + +function createMapDispatchToProps(dispatch, props) { + return { + onTableOptionChange(payload) { + dispatch(setListMovieTableOption(payload)); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(AddListMovieHeader); diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.css b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.css new file mode 100644 index 000000000..f8eb16625 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.css @@ -0,0 +1,56 @@ +.status { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 60px; +} + +.sortTitle { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 4 0 110px; +} + +.studio { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 2 0 90px; +} + +.inCinemas, +.physicalRelease, +.genres { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 180px; +} + +.movieStatus, +.certification { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 100px; +} + +.ratings { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 80px; +} + +.tags { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 60px; +} + +.actions { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 90px; +} + +.checkInput { + composes: input from '~Components/Form/CheckInput.css'; + + margin-top: 0; +} diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js new file mode 100644 index 000000000..b789b7c7c --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js @@ -0,0 +1,224 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import HeartRating from 'Components/HeartRating'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import MovieStatusCell from './MovieStatusCell'; +import Link from 'Components/Link/Link'; +import AddNewMovieModal from 'AddMovie/AddNewMovie/AddNewMovieModal'; +import styles from './AddListMovieRow.css'; + +class AddListMovieRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNewAddMovieModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddMovieModalOpen: true }); + } + + onAddMovieModalClose = () => { + this.setState({ isNewAddMovieModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + status, + tmdbId, + title, + titleSlug, + studio, + inCinemas, + physicalRelease, + year, + overview, + folder, + images, + genres, + ratings, + certification, + columns, + isExistingMovie + } = this.props; + + const { + isNewAddMovieModalOpen + } = this.state; + + const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress }; + + return ( +
+ + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortTitle') { + return ( + + + {title} + + + ); + } + + if (name === 'studio') { + return ( + + {studio} + + ); + } + + if (name === 'inCinemas') { + return ( + + ); + } + + if (name === 'physicalRelease') { + return ( + + ); + } + + if (name === 'genres') { + const joinedGenres = genres.join(', '); + + return ( + + + {joinedGenres} + + + ); + } + + if (name === 'ratings') { + return ( + + + + ); + } + + if (name === 'certification') { + return ( + + {certification} + + ); + } + + return null; + }) + } + + + +
+ ); + } +} + +AddListMovieRow.propTypes = { + style: PropTypes.object.isRequired, + tmdbId: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string.isRequired, + folder: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + titleSlug: PropTypes.string.isRequired, + studio: PropTypes.string, + inCinemas: PropTypes.string, + physicalRelease: PropTypes.string, + genres: PropTypes.arrayOf(PropTypes.string).isRequired, + ratings: PropTypes.object.isRequired, + certification: PropTypes.string, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isExistingMovie: PropTypes.bool.isRequired +}; + +AddListMovieRow.defaultProps = { + genres: [], + tags: [] +}; + +export default AddListMovieRow; diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRowConnector.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRowConnector.js new file mode 100644 index 000000000..684b7efc9 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRowConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector'; +import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddListMovieRow from './AddListMovieRow'; + +function createMapStateToProps() { + return createSelector( + createExistingMovieSelector(), + createExclusionMovieSelector(), + createDimensionsSelector(), + (isExistingMovie, isExclusionMovie, dimensions) => { + return { + isExistingMovie, + isExclusionMovie, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddListMovieRow); diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.css b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.css new file mode 100644 index 000000000..23ab127b5 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.css @@ -0,0 +1,5 @@ +.tableContainer { + composes: tableContainer from '~Components/Table/VirtualTable.css'; + + flex: 1 0 auto; +} diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js new file mode 100644 index 000000000..535c3bb07 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import { sortDirections } from 'Helpers/Props'; +import VirtualTable from 'Components/Table/VirtualTable'; +import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector'; +import AddListMovieHeaderConnector from './AddListMovieHeaderConnector'; +import AddListMovieRowConnector from './AddListMovieRowConnector'; +import styles from './AddListMovieTable.css'; + +class AddListMovieTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + scrollIndex: null + }; + } + + componentDidUpdate(prevProps) { + const { + items + } = this.props; + + const jumpToCharacter = this.props.jumpToCharacter; + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + + const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (scrollIndex != null) { + this.setState({ scrollIndex }); + } + } else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) { + this.setState({ scrollIndex: null }); + } + } + + // + // Control + + rowRenderer = ({ key, rowIndex, style }) => { + const { + items, + columns + } = this.props; + + const movie = items[rowIndex]; + + return ( + + ); + } + + // + // Render + + render() { + const { + items, + columns, + filters, + sortKey, + sortDirection, + isSmallScreen, + scrollTop, + contentBody, + onSortPress, + onRender, + onScroll + } = this.props; + + return ( + + } + columns={columns} + filters={filters} + sortKey={sortKey} + sortDirection={sortDirection} + onRender={onRender} + onScroll={onScroll} + /> + ); + } +} + +AddListMovieTable.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + scrollTop: PropTypes.number.isRequired, + jumpToCharacter: PropTypes.string, + contentBody: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default AddListMovieTable; diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTableConnector.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTableConnector.js new file mode 100644 index 000000000..f0988c2d9 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTableConnector.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setListMovieSort } from 'Store/Actions/addMovieActions'; +import AddListMovieTable from './AddListMovieTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.dimensions, + (state) => state.addMovie.columns, + (dimensions, columns) => { + return { + isSmallScreen: dimensions.isSmallScreen, + columns + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSortPress(sortKey) { + dispatch(setListMovieSort({ sortKey })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(AddListMovieTable); diff --git a/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.css b/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.css new file mode 100644 index 000000000..fbcd5eee9 --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.css @@ -0,0 +1,9 @@ +.status { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 60px; +} + +.statusIcon { + width: 20px !important; +} diff --git a/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.js b/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.js new file mode 100644 index 000000000..e2b7f7bfe --- /dev/null +++ b/frontend/src/AddMovie/AddListMovie/Table/MovieStatusCell.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './MovieStatusCell.css'; + +function MovieStatusCell(props) { + const { + className, + status, + component: Component, + ...otherProps + } = props; + + return ( + + { + status === 'announced' ? + : null + } + + { + status === 'inCinemas' ? + : null + } + + { + status === 'released' ? + : null + } + + ); +} + +MovieStatusCell.propTypes = { + className: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + component: PropTypes.elementType +}; + +MovieStatusCell.defaultProps = { + className: styles.status, + component: VirtualTableRowCell +}; + +export default MovieStatusCell; diff --git a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js index 618894527..0c18c644b 100644 --- a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js +++ b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js @@ -58,6 +58,7 @@ class AddNewMovieSearchResult extends Component { isExclusionMovie, isSmallScreen } = this.props; + const { isNewAddMovieModalOpen } = this.state; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c37b1c9b7..ce0ba11a3 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -6,6 +6,8 @@ import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import MovieIndexConnector from 'Movie/Index/MovieIndexConnector'; import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector'; +import AddListMovieConnector from 'AddMovie/AddListMovie/AddListMovieConnector'; +import AddDiscoverMovieConnector from 'AddMovie/AddListMovie/AddDiscoverMovieConnector'; import ImportMovies from 'AddMovie/ImportMovie/ImportMovies'; import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector'; import CalendarPageConnector from 'Calendar/CalendarPageConnector'; @@ -76,6 +78,16 @@ function AppRoutes(props) { component={ImportMovies} /> + + + + { + acc.push({ + id: movie.studio, + name: movie.studio + }); + + return acc; + }, []); + + return tagList.sort(sortByName); + } + }, + { + name: 'inCinemas', + label: 'In Cinemas', + type: filterBuilderTypes.DATE, + valueType: filterBuilderValueTypes.DATE + }, + { + name: 'physicalRelease', + label: 'Physical Release', + type: filterBuilderTypes.DATE, + valueType: filterBuilderValueTypes.DATE + }, + { + name: 'genres', + label: 'Genres', + type: filterBuilderTypes.ARRAY, + optionsSelector: function(items) { + const tagList = items.reduce((acc, movie) => { + movie.genres.forEach((genre) => { + acc.push({ + id: genre, + name: genre + }); + }); + + return acc; + }, []); + + return tagList.sort(sortByName); + } + }, + { + name: 'ratings', + label: 'Rating', + type: filterBuilderTypes.NUMBER + }, + { + name: 'certification', + label: 'Certification', + type: filterBuilderTypes.EXACT + }, + { + name: 'tags', + label: 'Tags', + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.TAG + } + ] }; export const persistState = [ - 'addMovie.defaults' + 'addMovie.defaults', + 'addMovie.sortKey', + 'addMovie.sortDirection', + 'addMovie.selectedFilterKey', + 'addMovie.customFilters', + 'addMovie.view', + 'addMovie.columns', + 'addMovie.posterOptions', + 'addMovie.overviewOptions', + 'addMovie.tableOptions' ]; // @@ -49,6 +253,16 @@ export const SET_ADD_MOVIE_VALUE = 'addMovie/setAddMovieValue'; export const CLEAR_ADD_MOVIE = 'addMovie/clearAddMovie'; export const SET_ADD_MOVIE_DEFAULT = 'addMovie/setAddMovieDefault'; +export const FETCH_LIST_MOVIES = 'addMovie/fetchListMovies'; +export const FETCH_DISCOVER_MOVIES = 'addMovie/fetchDiscoverMovies'; + +export const SET_LIST_MOVIE_SORT = 'addMovie/setListMovieSort'; +export const SET_LIST_MOVIE_FILTER = 'addMovie/setListMovieFilter'; +export const SET_LIST_MOVIE_VIEW = 'addMovie/setListMovieView'; +export const SET_LIST_MOVIE_TABLE_OPTION = 'addMovie/setListMovieTableOption'; +export const SET_LIST_MOVIE_POSTER_OPTION = 'addMovie/setListMoviePosterOption'; +export const SET_LIST_MOVIE_OVERVIEW_OPTION = 'addMovie/setListMovieOverviewOption'; + // // Action Creators @@ -57,6 +271,16 @@ export const addMovie = createThunk(ADD_MOVIE); export const clearAddMovie = createAction(CLEAR_ADD_MOVIE); export const setAddMovieDefault = createAction(SET_ADD_MOVIE_DEFAULT); +export const fetchListMovies = createThunk(FETCH_LIST_MOVIES); +export const fetchDiscoverMovies = createThunk(FETCH_DISCOVER_MOVIES); + +export const setListMovieSort = createAction(SET_LIST_MOVIE_SORT); +export const setListMovieFilter = createAction(SET_LIST_MOVIE_FILTER); +export const setListMovieView = createAction(SET_LIST_MOVIE_VIEW); +export const setListMovieTableOption = createAction(SET_LIST_MOVIE_TABLE_OPTION); +export const setListMoviePosterOption = createAction(SET_LIST_MOVIE_POSTER_OPTION); +export const setListMovieOverviewOption = createAction(SET_LIST_MOVIE_OVERVIEW_OPTION); + export const setAddMovieValue = createAction(SET_ADD_MOVIE_VALUE, (payload) => { return { section, @@ -69,6 +293,10 @@ export const setAddMovieValue = createAction(SET_ADD_MOVIE_VALUE, (payload) => { export const actionHandlers = handleThunks({ + [FETCH_LIST_MOVIES]: createFetchHandler(section, '/netimport/movies'), + + [FETCH_DISCOVER_MOVIES]: createFetchHandler(section, '/movies/discover'), + [LOOKUP_MOVIE]: function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); @@ -164,13 +392,54 @@ export const reducers = createHandleActions({ return updateSectionState(state, section, newState); }, - [CLEAR_ADD_MOVIE]: function(state) { - const { - defaults, - ...otherDefaultState - } = defaultState; + [SET_LIST_MOVIE_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_LIST_MOVIE_FILTER]: createSetClientSideCollectionFilterReducer(section), - return Object.assign({}, state, otherDefaultState); - } + [SET_LIST_MOVIE_VIEW]: function(state, { payload }) { + return Object.assign({}, state, { view: payload.view }); + }, + + [SET_LIST_MOVIE_TABLE_OPTION]: createSetTableOptionReducer(section), + + [SET_LIST_MOVIE_POSTER_OPTION]: function(state, { payload }) { + const posterOptions = state.posterOptions; + + return { + ...state, + posterOptions: { + ...posterOptions, + ...payload + } + }; + }, + + [SET_LIST_MOVIE_OVERVIEW_OPTION]: function(state, { payload }) { + const overviewOptions = state.overviewOptions; + + return { + ...state, + overviewOptions: { + ...overviewOptions, + ...payload + } + }; + }, + + // [CLEAR_ADD_MOVIE]: function(state) { + // const { + // defaults, + // view, + // ...otherDefaultState + // } = defaultState; + + // return Object.assign({}, state, otherDefaultState); + // } + + [CLEAR_ADD_MOVIE]: createClearReducer(section, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) }, defaultState, section); diff --git a/frontend/src/Store/Middleware/createSentryMiddleware.js b/frontend/src/Store/Middleware/createSentryMiddleware.js index 6cf590a8a..02f6163cd 100644 --- a/frontend/src/Store/Middleware/createSentryMiddleware.js +++ b/frontend/src/Store/Middleware/createSentryMiddleware.js @@ -68,8 +68,6 @@ export default function createSentryMiddleware() { isProduction } = window.Radarr; - console.log(window.Radarr); - if (!analytics) { return; } diff --git a/frontend/src/Store/Selectors/createAddListMovieSelector.js b/frontend/src/Store/Selectors/createAddListMovieSelector.js new file mode 100644 index 000000000..99fd1b39e --- /dev/null +++ b/frontend/src/Store/Selectors/createAddListMovieSelector.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; + +function createAddListMovieSelector() { + return createSelector( + (state, { movieId }) => movieId, + (state) => state.addMovie, + (movieId, allMovies) => { + return allMovies.items.find((movie) => movie.tmdbId === movieId); + } + ); +} + +export default createAddListMovieSelector; diff --git a/frontend/src/Store/Selectors/createAddMovieClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createAddMovieClientSideCollectionItemsSelector.js new file mode 100644 index 000000000..24ec81bbe --- /dev/null +++ b/frontend/src/Store/Selectors/createAddMovieClientSideCollectionItemsSelector.js @@ -0,0 +1,36 @@ +import { createSelector } from 'reselect'; +import createDeepEqualSelector from './createDeepEqualSelector'; +import createClientSideCollectionSelector from './createClientSideCollectionSelector'; + +function createUnoptimizedSelector(uiSection) { + return createSelector( + createClientSideCollectionSelector('movies', uiSection), + (movies) => { + const items = movies.items.map((s) => { + const { + tmdbId, + sortTitle + } = s; + + return { + tmdbId, + sortTitle + }; + }); + + return { + ...movies, + items + }; + } + ); +} + +function createAddMovieClientSideCollectionItemsSelector(uiSection) { + return createDeepEqualSelector( + createUnoptimizedSelector(uiSection), + (movies) => movies + ); +} + +export default createAddMovieClientSideCollectionItemsSelector; diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js index 4f926b7c0..5e822c568 100644 --- a/frontend/src/Store/scrollPositions.js +++ b/frontend/src/Store/scrollPositions.js @@ -1,5 +1,6 @@ const scrollPositions = { - movieIndex: 0 + movieIndex: 0, + addMovie: 0 }; export default scrollPositions; diff --git a/src/Radarr.Api.V2/Movies/FetchMovieListModule.cs b/src/Radarr.Api.V2/Movies/FetchMovieListModule.cs index 2c66fa538..f96de05ee 100644 --- a/src/Radarr.Api.V2/Movies/FetchMovieListModule.cs +++ b/src/Radarr.Api.V2/Movies/FetchMovieListModule.cs @@ -30,7 +30,7 @@ namespace Radarr.Api.V2.Movies List realResults = new List(); - /*foreach (var movie in results) + foreach (var movie in results) { var mapped = _movieSearch.MapMovieToTmdbMovie(movie); @@ -38,9 +38,9 @@ namespace Radarr.Api.V2.Movies { realResults.Add(mapped); } - }*/ + } - return MapToResource(results); + return MapToResource(realResults); } diff --git a/src/Radarr.Api.V2/Movies/MovieDiscoverModule.cs b/src/Radarr.Api.V2/Movies/MovieDiscoverModule.cs index 5cb3e4df9..a0b3effe9 100644 --- a/src/Radarr.Api.V2/Movies/MovieDiscoverModule.cs +++ b/src/Radarr.Api.V2/Movies/MovieDiscoverModule.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; -using Nancy; -using Radarr.Http.Extensions; using NzbDrone.Core.Movies; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using System.Linq; -using System; using Radarr.Http; using NzbDrone.Core.NetImport; using Radarr.Api.V2.NetImport; +using NzbDrone.Core.Organizer; namespace Radarr.Api.V2.Movies { @@ -16,12 +14,14 @@ namespace Radarr.Api.V2.Movies { private readonly IDiscoverNewMovies _searchProxy; private readonly INetImportFactory _netImportFactory; + private readonly IBuildFileNames _fileNameBuilder; - public MovieDiscoverModule(IDiscoverNewMovies searchProxy, INetImportFactory netImportFactory) + public MovieDiscoverModule(IDiscoverNewMovies searchProxy, INetImportFactory netImportFactory, IBuildFileNames fileNameBuilder) : base("/movies/discover") { _searchProxy = searchProxy; _netImportFactory = netImportFactory; + _fileNameBuilder = fileNameBuilder; Get("/lists", x => GetLists()); Get("/{action?recommendations}", x => Search(x.action)); } @@ -46,17 +46,19 @@ namespace Radarr.Api.V2.Movies }); } - private static IEnumerable MapToResource(IEnumerable movies) + private IEnumerable MapToResource(IEnumerable movies) { - foreach (var currentSeries in movies) + foreach (var currentMovie in movies) { - var resource = currentSeries.ToResource(); - var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + var resource = currentMovie.ToResource(); + var poster = currentMovie.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); if (poster != null) { resource.RemotePoster = poster.Url; } + resource.Folder = _fileNameBuilder.GetMovieFolder(currentMovie); + yield return resource; } }