From 7f814a3cb9a1f72b36028dcdac101fcc84de4082 Mon Sep 17 00:00:00 2001 From: nitsua Date: Mon, 7 Sep 2020 21:12:47 -0400 Subject: [PATCH] New: Add support for left/right arrows on the movie details to navigate through movies New: Add support for ctrl+home and ctrl+end to jump to the top and bottom of the movie index New: Add redirect to previous movie instead of index when deleting one --- .../Header/KeyboardShortcutsModalContent.js | 14 +++++++-- frontend/src/Components/keyboardShortcuts.js | 31 ++++++++++++++++--- frontend/src/Movie/Delete/DeleteMovieModal.js | 5 ++- .../DeleteMovieModalContentConnector.js | 12 +++++-- frontend/src/Movie/Details/MovieDetails.js | 12 +++++++ frontend/src/Movie/Index/MovieIndex.js | 15 +++++++++ frontend/src/Utilities/Constants/keyCodes.js | 4 +++ src/NzbDrone.Core/Localization/Core/en.json | 9 ++++++ 8 files changed, 91 insertions(+), 11 deletions(-) diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js index 5918479e8..1dc4cc159 100644 --- a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js @@ -20,18 +20,26 @@ function getShortcuts() { } function getShortcutKey(combo, isOsx) { - const comboMatch = combo.match(/(.+?)\+(.)/); + const comboMatch = combo.match(/(.+?)\+(.*)/); if (!comboMatch) { return combo; } const modifier = comboMatch[1]; - const key = comboMatch[2]; + let key = comboMatch[2]; let osModifier = modifier; if (modifier === 'mod') { - osModifier = isOsx ? 'cmd' : 'ctrl'; + osModifier = isOsx ? 'cmd' : 'Ctrl'; + } + + if (key === 'home') { + key = isOsx ? '↑' : 'Home'; + } + + if (key === 'end') { + key = isOsx ? '↓' : 'End'; } return `${osModifier} + ${key}`; diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js index d2609aa58..ebf4c9e49 100644 --- a/frontend/src/Components/keyboardShortcuts.js +++ b/frontend/src/Components/keyboardShortcuts.js @@ -1,31 +1,52 @@ import Mousetrap from 'mousetrap'; import React, { Component } from 'react'; import getDisplayName from 'Helpers/getDisplayName'; +import translate from 'Utilities/String/translate'; export const shortcuts = { OPEN_KEYBOARD_SHORTCUTS_MODAL: { key: '?', - name: 'Open This Modal' + name: translate('OpenThisModal') }, CLOSE_MODAL: { key: 'Esc', - name: 'Close Current Modal' + name: translate('CloseCurrentModal') }, ACCEPT_CONFIRM_MODAL: { key: 'Enter', - name: 'Accept Confirmation Modal' + name: translate('AcceptConfirmationModal') }, MOVIE_SEARCH_INPUT: { key: 's', - name: 'Focus Search Box' + name: translate('FocusSearchBox') }, SAVE_SETTINGS: { key: 'mod+s', - name: 'Save Settings' + name: translate('SaveSettings') + }, + + SCROLL_TOP: { + key: 'mod+home', + name: translate('MovieIndexScrollTop') + }, + + SCROLL_BOTTOM: { + key: 'mod+end', + name: translate('MovieIndexScrollBottom') + }, + + DETAILS_NEXT: { + key: '→', + name: translate('MovieDetailsNextMovie') + }, + + DETAILS_PREVIOUS: { + key: '←', + name: translate('MovieDetailsPreviousMovie') } }; diff --git a/frontend/src/Movie/Delete/DeleteMovieModal.js b/frontend/src/Movie/Delete/DeleteMovieModal.js index 51dd5bb53..dc4e1de04 100644 --- a/frontend/src/Movie/Delete/DeleteMovieModal.js +++ b/frontend/src/Movie/Delete/DeleteMovieModal.js @@ -8,6 +8,7 @@ function DeleteMovieModal(props) { const { isOpen, onModalClose, + previousMovie, ...otherProps } = props; @@ -20,6 +21,7 @@ function DeleteMovieModal(props) { ); @@ -27,7 +29,8 @@ function DeleteMovieModal(props) { DeleteMovieModal.propTypes = { isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired + onModalClose: PropTypes.func.isRequired, + previousMovie: PropTypes.string }; export default DeleteMovieModal; diff --git a/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js b/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js index 8aef87c2b..e93b7daf4 100644 --- a/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js +++ b/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js @@ -1,3 +1,4 @@ +import { push } from 'connected-react-router'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -16,7 +17,8 @@ function createMapStateToProps() { } const mapDispatchToProps = { - deleteMovie + deleteMovie, + push }; class DeleteMovieModalContentConnector extends Component { @@ -32,6 +34,10 @@ class DeleteMovieModalContentConnector extends Component { }); this.props.onModalClose(true); + + if (this.props.previousMovie) { + this.props.push(this.props.previousMovie); + } } // @@ -50,7 +56,9 @@ class DeleteMovieModalContentConnector extends Component { DeleteMovieModalContentConnector.propTypes = { movieId: PropTypes.number.isRequired, onModalClose: PropTypes.func.isRequired, - deleteMovie: PropTypes.func.isRequired + deleteMovie: PropTypes.func.isRequired, + push: PropTypes.func.isRequired, + previousMovie: PropTypes.string }; export default connect(createMapStateToProps, mapDispatchToProps)(DeleteMovieModalContentConnector); diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 77ace6349..35d6894e5 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -30,6 +30,7 @@ import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import fonts from 'Styles/Variables/fonts'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; import formatRuntime from 'Utilities/Date/formatRuntime'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; @@ -91,6 +92,7 @@ class MovieDetails extends Component { window.addEventListener('touchend', this.onTouchEnd); window.addEventListener('touchcancel', this.onTouchCancel); window.addEventListener('touchmove', this.onTouchMove); + window.addEventListener('keyup', this.onKeyUp); } componentWillUnmount() { @@ -173,6 +175,15 @@ class MovieDetails extends Component { this.setState({ titleWidth: width }); } + onKeyUp = (event) => { + if (event.keyCode === keyCodes.LEFT_ARROW) { + this.props.onGoToMovie(this.props.previousMovie.titleSlug); + } + if (event.keyCode === keyCodes.RIGHT_ARROW) { + this.props.onGoToMovie(this.props.nextMovie.titleSlug); + } + } + onTouchStart = (event) => { const touches = event.touches; const touchStart = touches[0].pageX; @@ -716,6 +727,7 @@ class MovieDetails extends Component { isOpen={isDeleteMovieModalOpen} movieId={id} onModalClose={this.onDeleteMovieModalClose} + previousMovie={`/movie/${previousMovie.titleSlug}`} /> { + const jumpBarItems = this.state.jumpBarItems.order; + if (event.path.length === 4) { + if (event.keyCode === keyCodes.HOME && event.ctrlKey) { + this.setState({ jumpToCharacter: jumpBarItems[0] }); + } + if (event.keyCode === keyCodes.END && event.ctrlKey) { + this.setState({ jumpToCharacter: jumpBarItems[jumpBarItems.length - 1] }); + } + } + } + onSelectAllChange = ({ value }) => { this.setState(selectAll(this.state.selectedState, value)); } diff --git a/frontend/src/Utilities/Constants/keyCodes.js b/frontend/src/Utilities/Constants/keyCodes.js index 9285b10fe..8ca1195f9 100644 --- a/frontend/src/Utilities/Constants/keyCodes.js +++ b/frontend/src/Utilities/Constants/keyCodes.js @@ -3,5 +3,9 @@ export const ENTER = 13; export const SHIFT = 16; export const CONTROL = 17; export const ESCAPE = 27; +export const HOME = 36; +export const END = 35; export const UP_ARROW = 38; export const DOWN_ARROW = 40; +export const LEFT_ARROW = 37; +export const RIGHT_ARROW = 39; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a6114d260..77544fe49 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1,5 +1,6 @@ { "About": "About", + "AcceptConfirmationModal": "Accept Confirmation Modal", "Actions": "Actions", "Activity": "Activity", "Added": "Added", @@ -96,6 +97,7 @@ "CloneIndexer": "Clone Indexer", "CloneProfile": "Clone Profile", "Close": "Close", + "CloseCurrentModal": "Close Current Modal", "Collection": "Collection", "ColonReplacement": "Colon Replacement", "ColonReplacementFormatHelpText": "Change how Radarr handles colon replacement", @@ -251,6 +253,7 @@ "FilterPlaceHolder": "Search movies", "FirstDayOfWeek": "First Day of Week", "Fixed": "Fixed", + "FocusSearchBox": "Focus Search Box", "Folder": "Folder", "Folders": "Folders", "FollowPerson": "Follow Person", @@ -436,6 +439,10 @@ "MovieNaming": "Movie Naming", "Movies": "Movies", "MoviesSelectedInterp": "{0} Movie(s) Selected", + "MovieDetailsNextMovie": "Movie Details: Next Movie", + "MovieDetailsPreviousMovie": "Movie Details: Previous Movie", + "MovieIndexScrollBottom": "Movie Index: Scroll Bottom", + "MovieIndexScrollTop": "Movie Index: Scroll Top", "MovieTitle": "Movie Title", "MovieTitleHelpText": "The title of the movie to exclude (can be anything meaningful)", "MovieYear": "Movie Year", @@ -469,6 +476,7 @@ "OnRenameHelpText": "On Rename", "OnUpgradeHelpText": "Be notified when movies are upgraded to a better quality", "OpenBrowserOnStart": "Open browser on start", + "OpenThisModal": "Open This Modal", "Options": "Options", "Organize": "Organize", "OrganizeAndRename": "Organize & Rename", @@ -615,6 +623,7 @@ "Runtime": "Runtime", "Save": "Save", "SaveChanges": "Save Changes", + "SaveSettings": "Save Settings", "Scheduled": "Scheduled", "ScriptPath": "Script Path", "Search": "Search",