1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-08-16 15:29:40 +02:00

New: Add table options for movie files details

This commit is contained in:
Bogdan 2023-08-20 16:48:39 +03:00
parent 329e43c331
commit 4b9107465c
9 changed files with 315 additions and 192 deletions

View File

@ -2,6 +2,7 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import translate from 'Utilities/String/translate';
import Link from './Link'; import Link from './Link';
import styles from './IconButton.css'; import styles from './IconButton.css';
@ -23,7 +24,7 @@ function IconButton(props) {
className, className,
isDisabled && styles.isDisabled isDisabled && styles.isDisabled
)} )}
aria-label="Table Options Button" aria-label={translate('TableOptionsButton')}
isDisabled={isDisabled} isDisabled={isDisabled}
{...otherProps} {...otherProps}
> >

View File

@ -654,7 +654,7 @@ class MovieDetails extends Component {
</div> </div>
} }
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}> <Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
<TabList <TabList
className={styles.tabList} className={styles.tabList}
> >

View File

@ -28,6 +28,9 @@
white-space: nowrap; white-space: nowrap;
} }
.languages,
.audio,
.video,
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@ -3,17 +3,20 @@
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'age': string; 'age': string;
'audio': string;
'audioLanguages': string; 'audioLanguages': string;
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'formats': string; 'formats': string;
'language': string; 'language': string;
'languages': string;
'quality': string; 'quality': string;
'rejected': string; 'rejected': string;
'relativePath': string; 'relativePath': string;
'releaseGroup': string; 'releaseGroup': string;
'size': string; 'size': string;
'subtitles': string; 'subtitles': string;
'video': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@ -81,7 +81,8 @@ class MovieFileEditorRow extends Component {
qualityCutoffNotMet, qualityCutoffNotMet,
customFormats, customFormats,
customFormatScore, customFormatScore,
languages languages,
columns
} = this.props; } = this.props;
const { const {
@ -91,138 +92,214 @@ class MovieFileEditorRow extends Component {
} = this.state; } = this.state;
const showQualityPlaceholder = !quality; const showQualityPlaceholder = !quality;
const showLanguagePlaceholder = !languages; const showLanguagePlaceholder = !languages;
return ( return (
<TableRow> <TableRow>
<TableRowCell {
className={styles.relativePath} columns.map((column) => {
title={relativePath} const {
> name,
{relativePath} isVisible
</TableRowCell> } = column;
<TableRowCell> if (!isVisible) {
<MediaInfoConnector return null;
movieFileId={id} }
type={mediaInfoTypes.VIDEO}
/>
</TableRowCell>
<TableRowCell> if (name === 'relativePath') {
<MediaInfoConnector return (
movieFileId={id} <TableRowCell
type={mediaInfoTypes.AUDIO} key={name}
/> className={styles.relativePath}
</TableRowCell> title={relativePath}
>
{relativePath}
</TableRowCell>
);
}
<TableRowCell if (name === 'customFormats') {
className={styles.audioLanguages} return (
> <TableRowCell key={name}>
<MediaInfoConnector <MovieFormats
type={mediaInfoTypes.AUDIO_LANGUAGES} formats={customFormats}
movieFileId={id} />
/> </TableRowCell>
</TableRowCell> );
}
<TableRowCell if (name === 'customFormatScore') {
className={styles.subtitles} return (
> <TableRowCell
<MediaInfoConnector key={name}
type={mediaInfoTypes.SUBTITLES} className={styles.customFormatScore}
movieFileId={id} >
/> <Tooltip
</TableRowCell> anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
<TableRowCell if (name === 'languages') {
className={styles.size} return (
title={size} <TableRowCell
> key={name}
{formatBytes(size)} className={styles.languages}
</TableRowCell> >
{
showLanguagePlaceholder ?
<MovieFileRowCellPlaceholder /> :
null
}
<TableRowCell {
className={styles.language} !showLanguagePlaceholder && !!languages &&
> <MovieLanguage
{ className={styles.label}
showLanguagePlaceholder && languages={languages}
<MovieFileRowCellPlaceholder /> />
} }
</TableRowCell>
);
}
{ if (name === 'quality') {
!showLanguagePlaceholder && !!languages && return (
<MovieLanguage <TableRowCell
className={styles.label} key={name}
languages={languages} className={styles.quality}
/> >
} {
</TableRowCell> showQualityPlaceholder ?
<MovieFileRowCellPlaceholder /> :
null
}
<TableRowCell {
className={styles.quality} !showQualityPlaceholder && !!quality &&
> <MovieQuality
{ className={styles.label}
showQualityPlaceholder && quality={quality}
<MovieFileRowCellPlaceholder /> isCutoffNotMet={qualityCutoffNotMet}
} />
}
</TableRowCell>
);
}
{ if (name === 'audioInfo') {
!showQualityPlaceholder && !!quality && return (
<MovieQuality <TableRowCell
className={styles.label} key={name}
quality={quality} className={styles.audio}
isCutoffNotMet={qualityCutoffNotMet} >
/> <MediaInfoConnector
} movieFileId={id}
</TableRowCell> type={mediaInfoTypes.AUDIO}
/>
</TableRowCell>
);
}
<TableRowCell if (name === 'audioLanguages') {
className={styles.releaseGroup} return (
> <TableRowCell
{releaseGroup} key={name}
</TableRowCell> className={styles.audioLanguages}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO_LANGUAGES}
movieFileId={id}
/>
</TableRowCell>
);
}
<TableRowCell if (name === 'subtitleLanguages') {
className={styles.formats} return (
> <TableRowCell
<MovieFormats key={name}
formats={customFormats} className={styles.subtitles}
/> >
</TableRowCell> <MediaInfoConnector
type={mediaInfoTypes.SUBTITLES}
movieFileId={id}
/>
</TableRowCell>
);
}
<TableRowCell if (name === 'videoCodec') {
className={styles.customFormatScore} return (
> <TableRowCell
<Tooltip key={name}
anchor={formatCustomFormatScore( className={styles.video}
customFormatScore, >
customFormats.length <MediaInfoConnector
)} movieFileId={id}
tooltip={<MovieFormats formats={customFormats} />} type={mediaInfoTypes.VIDEO}
position={tooltipPositions.TOP} />
/> </TableRowCell>
</TableRowCell> );
}
<TableRowCell className={styles.actions}> if (name === 'size') {
<IconButton return (
title={translate('EditMovieFile')} <TableRowCell
name={icons.EDIT} key={name}
onPress={this.onFileEditPress} className={styles.size}
/> title={size}
>
{formatBytes(size)}
</TableRowCell>
);
}
<IconButton if (name === 'releaseGroup') {
title={translate('Details')} return (
name={icons.MEDIA_INFO} <TableRowCell
onPress={this.onFileDetailsPress} key={name}
/> className={styles.releaseGroup}
>
{releaseGroup}
</TableRowCell>
);
}
<IconButton if (name === 'actions') {
title={translate('DeleteFile')} return (
name={icons.REMOVE} <TableRowCell key={name} className={styles.actions}>
onPress={this.onDeletePress} <IconButton
/> title={translate('EditMovieFile')}
</TableRowCell> name={icons.EDIT}
onPress={this.onFileEditPress}
/>
<IconButton
title={translate('Details')}
name={icons.MEDIA_INFO}
onPress={this.onFileDetailsPress}
/>
<IconButton
title={translate('DeleteFile')}
name={icons.REMOVE}
onPress={this.onDeletePress}
/>
</TableRowCell>
);
}
return null;
})
}
<FileDetailsModal <FileDetailsModal
isOpen={isFileDetailsModalOpen} isOpen={isFileDetailsModalOpen}
@ -263,7 +340,12 @@ MovieFileEditorRow.propTypes = {
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
mediaInfo: PropTypes.object, mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired onDeletePress: PropTypes.func.isRequired
}; };
MovieFileEditorRow.defaultProps = {
customFormats: []
};
export default MovieFileEditorRow; export default MovieFileEditorRow;

View File

@ -1,80 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieFileEditorRow from './MovieFileEditorRow'; import MovieFileEditorRow from './MovieFileEditorRow';
import styles from './MovieFileEditorTableContent.css'; import styles from './MovieFileEditorTableContent.css';
const columns = [
{
name: 'title',
label: () => translate('RelativePath'),
isVisible: true
},
{
name: 'videoCodec',
label: () => translate('VideoCodec'),
isVisible: true
},
{
name: 'audioInfo',
label: () => translate('AudioInfo'),
isVisible: true
},
{
name: 'audioLanguages',
label: () => translate('AudioLanguages'),
isVisible: true
},
{
name: 'subtitleLanguages',
label: () => translate('SubtitleLanguages'),
isVisible: true
},
{
name: 'size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'releaseGroup',
label: () => translate('ReleaseGroup'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('Formats'),
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isVisible: true
},
{
name: 'action',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieFileEditorTableContent extends Component { class MovieFileEditorTableContent extends Component {
// //
@ -82,7 +12,9 @@ class MovieFileEditorTableContent extends Component {
render() { render() {
const { const {
items items,
columns,
onTableOptionChange
} = this.props; } = this.props;
return ( return (
@ -96,13 +28,17 @@ class MovieFileEditorTableContent extends Component {
{ {
!!items.length && !!items.length &&
<Table columns={columns}> <Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody> <TableBody>
{ {
items.map((item) => { items.map((item) => {
return ( return (
<MovieFileEditorRow <MovieFileEditorRow
key={item.id} key={item.id}
columns={columns}
{...item} {...item}
onDeletePress={this.props.onDeletePress} onDeletePress={this.props.onDeletePress}
/> />
@ -122,6 +58,8 @@ MovieFileEditorTableContent.propTypes = {
movieId: PropTypes.number, movieId: PropTypes.number,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired onDeletePress: PropTypes.func.isRequired
}; };

View File

@ -1,9 +1,8 @@
/* eslint max-params: 0 */
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions'; import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
@ -30,6 +29,7 @@ function createMapStateToProps() {
return { return {
items: filesForMovie, items: filesForMovie,
columns: movieFiles.columns,
isDeleting: movieFiles.isDeleting, isDeleting: movieFiles.isDeleting,
isSaving: movieFiles.isSaving, isSaving: movieFiles.isSaving,
error: null, error: null,
@ -54,6 +54,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(updateMovieFiles(updateProps)); dispatch(updateMovieFiles(updateProps));
}, },
onTableOptionChange(payload) {
dispatch(setMovieFilesTableOption(payload));
},
onDeletePress(movieFileId) { onDeletePress(movieFileId) {
dispatch(deleteMovieFile({ dispatch(deleteMovieFile({
id: movieFileId id: movieFileId

View File

@ -1,9 +1,15 @@
import _ from 'lodash'; import _ from 'lodash';
import React from 'react';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import movieEntities from 'Movie/movieEntities'; import movieEntities from 'Movie/movieEntities';
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import translate from 'Utilities/String/translate';
import { removeItem, set, updateItem } from './baseActions'; import { removeItem, set, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
@ -25,9 +31,82 @@ export const defaultState = {
deleteError: null, deleteError: null,
isSaving: false, isSaving: false,
saveError: null, saveError: null,
items: [] items: [],
columns: [
{
name: 'relativePath',
label: () => translate('RelativePath'),
isVisible: true
},
{
name: 'videoCodec',
label: () => translate('VideoCodec'),
isVisible: true
},
{
name: 'audioInfo',
label: () => translate('AudioInfo'),
isVisible: true
},
{
name: 'audioLanguages',
label: () => translate('AudioLanguages'),
isVisible: false
},
{
name: 'subtitleLanguages',
label: () => translate('SubtitleLanguages'),
isVisible: false
},
{
name: 'size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'releaseGroup',
label: () => translate('ReleaseGroup'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('Formats'),
isVisible: true
},
{
name: 'customFormatScore',
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isVisible: true
},
{
name: 'actions',
columnLabel: () => translate('Actions'),
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true,
isModifiable: false
}
]
}; };
export const persistState = [
'movieFiles.columns'
];
// //
// Actions Types // Actions Types
@ -36,6 +115,7 @@ export const DELETE_MOVIE_FILE = 'movieFiles/deleteMovieFile';
export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles'; export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles';
export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles'; export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles';
export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles'; export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles';
export const SET_MOVIE_FILES_TABLE_OPTION = 'movieFiles/setMovieFilesTableOption';
// //
// Action Creators // Action Creators
@ -45,6 +125,7 @@ export const deleteMovieFile = createThunk(DELETE_MOVIE_FILE);
export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES); export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES);
export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES); export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES);
export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES); export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES);
export const setMovieFilesTableOption = createAction(SET_MOVIE_FILES_TABLE_OPTION);
// //
// Helpers // Helpers
@ -234,9 +315,19 @@ export const actionHandlers = handleThunks({
// Reducers // Reducers
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[SET_MOVIE_FILES_TABLE_OPTION]: createSetTableOptionReducer(section),
[CLEAR_MOVIE_FILES]: (state) => { [CLEAR_MOVIE_FILES]: (state) => {
return Object.assign({}, state, defaultState); return Object.assign({}, state, {
isFetching: false,
isPopulated: false,
error: null,
isDeleting: false,
deleteError: null,
isSaving: false,
saveError: null,
items: []
});
} }
}, defaultState, section); }, defaultState, section);

View File

@ -1109,6 +1109,7 @@
"TMDb": "TMDb", "TMDb": "TMDb",
"Table": "Table", "Table": "Table",
"TableOptions": "Table Options", "TableOptions": "Table Options",
"TableOptionsButton": "Table Options Button",
"TableOptionsColumnsMessage": "Choose which columns are visible and which order they appear in", "TableOptionsColumnsMessage": "Choose which columns are visible and which order they appear in",
"TagCannotBeDeletedWhileInUse": "Cannot be deleted while in use", "TagCannotBeDeletedWhileInUse": "Cannot be deleted while in use",
"TagDetails": "Tag Details - {label}", "TagDetails": "Tag Details - {label}",