From f37c7a974842429e34b10283b24c2dd163a780a0 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 4 Jul 2020 03:13:24 -0400 Subject: [PATCH] New: Swipe on Movie Details Page --- frontend/src/Movie/Details/MovieDetails.css | 3 +- frontend/src/Movie/Details/MovieDetails.js | 74 ++++++++++- .../Movie/Details/MovieDetailsConnector.js | 125 +++++++++++------- 3 files changed, 154 insertions(+), 48 deletions(-) diff --git a/frontend/src/Movie/Details/MovieDetails.css b/frontend/src/Movie/Details/MovieDetails.css index d78ba1e16..46d4bdca9 100644 --- a/frontend/src/Movie/Details/MovieDetails.css +++ b/frontend/src/Movie/Details/MovieDetails.css @@ -202,7 +202,8 @@ } @media only screen and (max-width: $breakpointLarge) { - .poster { + .poster, + .movieNavigationButtons { display: none; } } diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 6b01d7a58..97b8d746f 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -83,6 +83,20 @@ class MovieDetails extends Component { }; } + componentDidMount() { + window.addEventListener('touchstart', this.onTouchStart); + window.addEventListener('touchend', this.onTouchEnd); + window.addEventListener('touchcancel', this.onTouchCancel); + window.addEventListener('touchmove', this.onTouchMove); + } + + componentWillUnmount() { + window.removeEventListener('touchstart', this.onTouchStart); + window.removeEventListener('touchend', this.onTouchEnd); + window.removeEventListener('touchcancel', this.onTouchCancel); + window.removeEventListener('touchmove', this.onTouchMove); + } + // // Listeners @@ -156,6 +170,58 @@ class MovieDetails extends Component { this.setState({ titleWidth: width }); } + onTouchStart = (event) => { + const touches = event.touches; + const touchStart = touches[0].pageX; + const touchY = touches[0].pageY; + + // Only change when swipe is on header, we need horizontal scroll on tables + if (touchY > 470) { + return; + } + + 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.onGoToMovie(this.props.previousMovie.titleSlug); + } else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) { + this.props.onGoToMovie(this.props.nextMovie.titleSlug); + } + + this._touchStart = null; + } + + onTouchCancel = (event) => { + this._touchStart = null; + } + + onTouchMove = (event) => { + if (!this._touchStart) { + return; + } + } + // // Render @@ -207,6 +273,8 @@ class MovieDetails extends Component { selectedTabIndex } = this.state; + const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150); + return ( @@ -293,7 +361,7 @@ class MovieDetails extends Component { /> -
+
@@ -665,6 +733,7 @@ MovieDetails.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, movieFilesError: PropTypes.object, movieCreditsError: PropTypes.object, extraFilesError: PropTypes.object, @@ -673,7 +742,8 @@ MovieDetails.propTypes = { nextMovie: PropTypes.object.isRequired, onMonitorTogglePress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired, - onSearchPress: PropTypes.func.isRequired + onSearchPress: PropTypes.func.isRequired, + onGoToMovie: PropTypes.func.isRequired }; MovieDetails.defaultProps = { diff --git a/frontend/src/Movie/Details/MovieDetailsConnector.js b/frontend/src/Movie/Details/MovieDetailsConnector.js index b656a0a02..1f7f9cf93 100644 --- a/frontend/src/Movie/Details/MovieDetailsConnector.js +++ b/frontend/src/Movie/Details/MovieDetailsConnector.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { push } from 'connected-react-router'; import { createSelector } from 'reselect'; import { findCommand, isCommandExecuting } from 'Utilities/Command'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; @@ -86,7 +87,8 @@ function createMapStateToProps() { createAllMoviesSelector(), createCommandsSelector(), createDimensionsSelector(), - (titleSlug, movieFiles, movieCredits, extraFiles, allMovies, commands, dimensions) => { + (state) => state.app.isSidebarVisible, + (titleSlug, movieFiles, movieCredits, extraFiles, allMovies, commands, dimensions, isSidebarVisible) => { const sortedMovies = _.orderBy(allMovies, 'sortTitle'); const movieIndex = _.findIndex(sortedMovies, { titleSlug }); const movie = sortedMovies[movieIndex]; @@ -157,27 +159,59 @@ function createMapStateToProps() { sizeOnDisk, previousMovie, nextMovie, - isSmallScreen: dimensions.isSmallScreen + isSmallScreen: dimensions.isSmallScreen, + isSidebarVisible }; } ); } -const mapDispatchToProps = { - fetchMovieFiles, - clearMovieFiles, - fetchMovieCredits, - clearMovieCredits, - fetchExtraFiles, - clearExtraFiles, - clearReleases, - cancelFetchReleases, - fetchNetImportSchema, - toggleMovieMonitored, - fetchQueueDetails, - clearQueueDetails, - executeCommand -}; +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchMovieFiles({ movieId }) { + dispatch(fetchMovieFiles({ movieId })); + }, + dispatchClearMovieFiles() { + dispatch(clearMovieFiles()); + }, + dispatchFetchMovieCredits({ movieId }) { + dispatch(fetchMovieCredits({ movieId })); + }, + dispatchClearMovieCredits() { + dispatch(clearMovieCredits()); + }, + dispatchFetchExtraFiles({ movieId }) { + dispatch(fetchExtraFiles({ movieId })); + }, + dispatchClearExtraFiles() { + dispatch(clearExtraFiles()); + }, + dispatchClearReleases() { + dispatch(clearReleases()); + }, + dispatchCancelFetchReleases() { + dispatch(cancelFetchReleases()); + }, + dispatchFetchQueueDetails({ movieId }) { + dispatch(fetchQueueDetails({ movieId })); + }, + dispatchClearQueueDetails() { + dispatch(clearQueueDetails()); + }, + dispatchFetchNetImportSchema() { + dispatch(fetchNetImportSchema()); + }, + dispatchToggleMovieMonitored(payload) { + dispatch(toggleMovieMonitored(payload)); + }, + dispatchExecuteCommand(payload) { + dispatch(executeCommand(payload)); + }, + onGoToMovie(titleSlug) { + dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`)); + } + }; +} class MovieDetailsConnector extends Component { @@ -227,41 +261,41 @@ class MovieDetailsConnector extends Component { populate = () => { const movieId = this.props.id; - this.props.fetchMovieFiles({ movieId }); - this.props.fetchExtraFiles({ movieId }); - this.props.fetchMovieCredits({ movieId }); - this.props.fetchQueueDetails({ movieId }); - this.props.fetchNetImportSchema(); + this.props.dispatchFetchMovieFiles({ movieId }); + this.props.dispatchFetchExtraFiles({ movieId }); + this.props.dispatchFetchMovieCredits({ movieId }); + this.props.dispatchFetchQueueDetails({ movieId }); + this.props.dispatchFetchNetImportSchema(); } unpopulate = () => { - this.props.cancelFetchReleases(); - this.props.clearMovieFiles(); - this.props.clearExtraFiles(); - this.props.clearMovieCredits(); - this.props.clearQueueDetails(); - this.props.clearReleases(); + this.props.dispatchCancelFetchReleases(); + this.props.dispatchClearMovieFiles(); + this.props.dispatchClearExtraFiles(); + this.props.dispatchClearMovieCredits(); + this.props.dispatchClearQueueDetails(); + this.props.dispatchClearReleases(); } // // Listeners onMonitorTogglePress = (monitored) => { - this.props.toggleMovieMonitored({ + this.props.dispatchToggleMovieMonitored({ movieId: this.props.id, monitored }); } onRefreshPress = () => { - this.props.executeCommand({ + this.props.dispatchExecuteCommand({ name: commandNames.REFRESH_MOVIE, movieId: this.props.id }); } onSearchPress = () => { - this.props.executeCommand({ + this.props.dispatchExecuteCommand({ name: commandNames.MOVIE_SEARCH, movieIds: [this.props.id] }); @@ -291,19 +325,20 @@ MovieDetailsConnector.propTypes = { isRenamingFiles: PropTypes.bool.isRequired, isRenamingMovie: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired, - fetchMovieFiles: PropTypes.func.isRequired, - clearMovieFiles: PropTypes.func.isRequired, - fetchExtraFiles: PropTypes.func.isRequired, - clearExtraFiles: PropTypes.func.isRequired, - fetchMovieCredits: PropTypes.func.isRequired, - clearMovieCredits: PropTypes.func.isRequired, - clearReleases: PropTypes.func.isRequired, - cancelFetchReleases: PropTypes.func.isRequired, - toggleMovieMonitored: PropTypes.func.isRequired, - fetchQueueDetails: PropTypes.func.isRequired, - clearQueueDetails: PropTypes.func.isRequired, - fetchNetImportSchema: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired + dispatchFetchMovieFiles: PropTypes.func.isRequired, + dispatchClearMovieFiles: PropTypes.func.isRequired, + dispatchFetchExtraFiles: PropTypes.func.isRequired, + dispatchClearExtraFiles: PropTypes.func.isRequired, + dispatchFetchMovieCredits: PropTypes.func.isRequired, + dispatchClearMovieCredits: PropTypes.func.isRequired, + dispatchClearReleases: PropTypes.func.isRequired, + dispatchCancelFetchReleases: PropTypes.func.isRequired, + dispatchToggleMovieMonitored: PropTypes.func.isRequired, + dispatchFetchQueueDetails: PropTypes.func.isRequired, + dispatchClearQueueDetails: PropTypes.func.isRequired, + dispatchFetchNetImportSchema: PropTypes.func.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired, + onGoToMovie: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(MovieDetailsConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(MovieDetailsConnector);