mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
136 Commits
v5.8.2.891
...
v5.10.1.91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe4793606 | ||
|
|
fa80608394 | ||
|
|
6e81d5917e | ||
|
|
8d189523c4 | ||
|
|
4d589422e6 | ||
|
|
675612e7c6 | ||
|
|
be3916f67d | ||
|
|
453f216e0d | ||
|
|
2d4846e5be | ||
|
|
2700a6cf8a | ||
|
|
674e414111 | ||
|
|
21bd21b70c | ||
|
|
fde87a38f9 | ||
|
|
0d6ba200d3 | ||
|
|
93298645e3 | ||
|
|
58f544e9e0 | ||
|
|
cf952d5c0b | ||
|
|
f6d630bdd3 | ||
|
|
657ced4772 | ||
|
|
d3a0c83f98 | ||
|
|
5833d5d4c4 | ||
|
|
2ba4562f49 | ||
|
|
d79db69644 | ||
|
|
7532dfb03c | ||
|
|
a47528aa81 | ||
|
|
a812d9f39f | ||
|
|
fc97f05850 | ||
|
|
644876123d | ||
|
|
540659a799 | ||
|
|
288668f7e6 | ||
|
|
fcf3be42d5 | ||
|
|
16e218501e | ||
|
|
caf2d33c11 | ||
|
|
bc918ed3b5 | ||
|
|
df77474314 | ||
|
|
bf84471509 | ||
|
|
d346d969de | ||
|
|
14b125ccd9 | ||
|
|
da5323a08f | ||
|
|
672b351497 | ||
|
|
fc4f4ab211 | ||
|
|
333e8281ea | ||
|
|
c278ffd8a0 | ||
|
|
5898eea3d0 | ||
|
|
5b78a1297a | ||
|
|
14e3e1fa35 | ||
|
|
c0e76544ef | ||
|
|
8c16677875 | ||
|
|
401e19547c | ||
|
|
c9f28fdc4f | ||
|
|
0ad4d7ea9a | ||
|
|
e8bb3df68e | ||
|
|
9442f1fb04 | ||
|
|
9ad6b3a611 | ||
|
|
fa1d6ad109 | ||
|
|
ccbc8f591b | ||
|
|
a4301f8db0 | ||
|
|
fe00825f2b | ||
|
|
17a9b0f7b0 | ||
|
|
62bdb66d0f | ||
|
|
7c1fedb8ce | ||
|
|
333351da45 | ||
|
|
fbbe7f7b5d | ||
|
|
edec201a6c | ||
|
|
1e783bfe07 | ||
|
|
7d5236de21 | ||
|
|
1efe7db5f3 | ||
|
|
b37cc42805 | ||
|
|
fa19f45171 | ||
|
|
4ae382cea7 | ||
|
|
37c09ba1f8 | ||
|
|
322df78f5a | ||
|
|
3a4446cc8e | ||
|
|
6c456e57d8 | ||
|
|
abc7efabea | ||
|
|
ace692aca6 | ||
|
|
882bde713f | ||
|
|
2575e3647f | ||
|
|
5cac5b6068 | ||
|
|
4628868dfa | ||
|
|
25685314bc | ||
|
|
41b1ea553e | ||
|
|
5d17f8e84d | ||
|
|
7490fc7040 | ||
|
|
f4e1f51a9c | ||
|
|
8e1016572b | ||
|
|
caabb032f3 | ||
|
|
ce9c5d4d97 | ||
|
|
967bed3161 | ||
|
|
8d9f1697ee | ||
|
|
3be2c6b0be | ||
|
|
b6d9c73a17 | ||
|
|
b1a7652753 | ||
|
|
f76c97c3ce | ||
|
|
1f5a84d202 | ||
|
|
d25bcdb043 | ||
|
|
f75497f57d | ||
|
|
2f413c68d9 | ||
|
|
68c20713e5 | ||
|
|
6eeed96d12 | ||
|
|
6f306a22e5 | ||
|
|
29ef75960d | ||
|
|
364a42424a | ||
|
|
a5b315ba83 | ||
|
|
e80e96de0e | ||
|
|
44c7c71226 | ||
|
|
04c5e6c2a6 | ||
|
|
5533528b56 | ||
|
|
74246df881 | ||
|
|
88127298ae | ||
|
|
5559fa5fa5 | ||
|
|
d503e01747 | ||
|
|
ae89ae175f | ||
|
|
df35e78e1f | ||
|
|
a3b3fee06b | ||
|
|
ae377d97a5 | ||
|
|
270df9d1dd | ||
|
|
6ed3045433 | ||
|
|
ddb7d5690b | ||
|
|
a1104b8263 | ||
|
|
358ff0c130 | ||
|
|
ff0a04c331 | ||
|
|
c12f01f919 | ||
|
|
93d661242a | ||
|
|
324dac8db3 | ||
|
|
bba69d8b22 | ||
|
|
1366f6e8b4 | ||
|
|
f79712951b | ||
|
|
101b046753 | ||
|
|
cd713e7252 | ||
|
|
a54f54eb6e | ||
|
|
f2af7a1b72 | ||
|
|
a5b48153a6 | ||
|
|
1804e486d6 | ||
|
|
b490177a77 | ||
|
|
7a90b4a6b2 |
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.8.2'
|
||||
majorVersion: '5.10.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.421'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -359,11 +359,16 @@ module.exports = {
|
||||
],
|
||||
|
||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||
'no-shadow': 'off',
|
||||
// These should be enabled after cleaning things up
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'no-shadow': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
@@ -376,7 +381,41 @@ module.exports = {
|
||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// React Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// React
|
||||
'react/function-component-definition': 'error',
|
||||
'react/hook-use-state': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'always'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' }
|
||||
],
|
||||
'react/jsx-fragments': 'error',
|
||||
'react/jsx-handler-names': [
|
||||
'error',
|
||||
{
|
||||
eventHandlerPrefix: 'on',
|
||||
eventHandlerPropPrefix: 'on'
|
||||
}
|
||||
],
|
||||
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||
'react/jsx-sort-props': [
|
||||
'error',
|
||||
{
|
||||
callbacksLast: true,
|
||||
noSortAlphabetically: true,
|
||||
reservedFirst: true
|
||||
}
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error'
|
||||
})
|
||||
},
|
||||
{
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -66,7 +66,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -201,7 +201,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -104,7 +104,7 @@ class BlocklistRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -136,7 +136,7 @@ class BlocklistRow extends Component {
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
@@ -12,7 +12,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -175,7 +175,7 @@ class QueueRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -319,7 +319,7 @@ class QueueRow extends Component {
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,11 @@ function TimeleftCell(props) {
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
@@ -40,7 +44,11 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
@@ -36,7 +35,6 @@ const mapDispatchToProps = {
|
||||
lookupMovie,
|
||||
clearAddMovie,
|
||||
fetchRootFolders,
|
||||
fetchImportExclusions,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
@@ -56,7 +54,6 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
this.props.fetchImportExclusions();
|
||||
this.props.fetchQueueDetails();
|
||||
}
|
||||
|
||||
@@ -131,7 +128,6 @@ AddNewMovieConnector.propTypes = {
|
||||
lookupMovie: PropTypes.func.isRequired,
|
||||
clearAddMovie: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
fetchImportExclusions: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.originalLanguage,
|
||||
.studio,
|
||||
.genres {
|
||||
margin-left: 5px;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CssExports {
|
||||
'genres': string;
|
||||
'icons': string;
|
||||
'links': string;
|
||||
'originalLanguage': string;
|
||||
'overlay': string;
|
||||
'overview': string;
|
||||
'poster': string;
|
||||
|
||||
@@ -62,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
titleSlug,
|
||||
year,
|
||||
studio,
|
||||
originalLanguage,
|
||||
genres,
|
||||
status,
|
||||
overview,
|
||||
@@ -70,7 +71,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
images,
|
||||
existingMovieId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isExcluded,
|
||||
isSmallScreen,
|
||||
colorImpairedMode,
|
||||
id,
|
||||
@@ -154,26 +155,27 @@ class AddNewMovieSearchResult extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.icons}>
|
||||
<div>
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExclusionMovie &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isExcluded &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,17 +215,31 @@ class AddNewMovieSearchResult extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!!studio &&
|
||||
originalLanguage?.name ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.LANGUAGE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.originalLanguage}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
studio ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.STUDIO}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.studio}>
|
||||
{studio}
|
||||
</span>
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -233,7 +249,6 @@ class AddNewMovieSearchResult extends Component {
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.genres}>
|
||||
{genres.slice(0, 3).join(', ')}
|
||||
</span>
|
||||
@@ -271,6 +286,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
@@ -311,6 +327,7 @@ AddNewMovieSearchResult.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
originalLanguage: PropTypes.object,
|
||||
genres: PropTypes.arrayOf(PropTypes.string),
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
@@ -319,7 +336,7 @@ AddNewMovieSearchResult.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
existingMovieId: PropTypes.number,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExclusionMovie: PropTypes.bool.isRequired,
|
||||
isExcluded: PropTypes.bool,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
@@ -333,7 +350,8 @@ AddNewMovieSearchResult.propTypes = {
|
||||
};
|
||||
|
||||
AddNewMovieSearchResult.defaultProps = {
|
||||
genres: []
|
||||
genres: [],
|
||||
isExcluded: false
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createExclusionMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||
@@ -17,7 +13,7 @@ function createThemeSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
function ApplyTheme() {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
@@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
|
||||
@@ -18,7 +18,10 @@ export interface AppSectionSaveState {
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
@@ -38,6 +41,7 @@ export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
pendingChanges: Partial<T>;
|
||||
item: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,14 @@ export interface MovieIndexAppState {
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showCinemaRelease: boolean;
|
||||
showDigitalRelease: boolean;
|
||||
showPhysicalRelease: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -37,6 +40,7 @@ export interface MovieIndexAppState {
|
||||
showAdded: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,21 +3,30 @@ import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
@@ -36,12 +45,34 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListExclusionsSettingsAppState
|
||||
extends AppSectionState<ImportListExclusion>,
|
||||
AppSectionSaveState,
|
||||
PagedAppSectionState,
|
||||
AppSectionDeleteState {
|
||||
pendingChanges: Partial<ImportListExclusion>;
|
||||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
|
||||
@@ -3,10 +3,10 @@ import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AgendaEvent.css';
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
|
||||
startTime = moment(startTime);
|
||||
const downloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
|
||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
|
||||
if (view === calendarViews.WEEK) {
|
||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||
} else if (view === calendarViews.FORECAST) {
|
||||
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
|
||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,10 +2,10 @@ import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||
import styles from './CalendarEvent.css';
|
||||
@@ -39,7 +39,7 @@ class CalendarEvent extends Component {
|
||||
|
||||
const isDownloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
|
||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
const eventType = [];
|
||||
|
||||
25
frontend/src/Calendar/getStatusStyle.js
Normal file
25
frontend/src/Calendar/getStatusStyle.js
Normal file
@@ -0,0 +1,25 @@
|
||||
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
|
||||
if (downloading) {
|
||||
return 'queue';
|
||||
}
|
||||
|
||||
if (hasFile && isMonitored) {
|
||||
return 'downloaded';
|
||||
}
|
||||
|
||||
if (hasFile && !isMonitored) {
|
||||
return 'unmonitored';
|
||||
}
|
||||
|
||||
if (isAvailable && isMonitored) {
|
||||
return 'missingMonitored';
|
||||
}
|
||||
|
||||
if (!isMonitored) {
|
||||
return 'missingUnmonitored';
|
||||
}
|
||||
|
||||
return 'continuing';
|
||||
}
|
||||
|
||||
export default getStatusStyle;
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CollectionMovieLabel.css';
|
||||
|
||||
@@ -45,7 +45,7 @@ class CollectionMovieLabel extends Component {
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
styles[getProgressBarKind(status, monitored, hasFile, isAvailable)]
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
<div>{info.componentStack}</div>
|
||||
)}
|
||||
|
||||
{<div className={styles.version}>Version: {window.Radarr.version}</div>}
|
||||
<div className={styles.version}>Version: {window.Radarr.version}</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
@@ -14,7 +15,7 @@ import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilte
|
||||
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
@@ -77,7 +78,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.MOVIE:
|
||||
return MovieFilterBuilderRowValue;
|
||||
@@ -228,7 +229,7 @@ class FilterBuilderRow extends Component {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
}).sort(sortByProp('value'));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByName);
|
||||
}, []).sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
@@ -11,7 +11,7 @@ function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
|
||||
const tagList = allMovies
|
||||
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||
.sort(sortByName);
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createQualityProfilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||
|
||||
const tagList = qualityProfiles
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default QualityProfileFilterBuilderRowValue;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
||||
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
|
||||
@@ -4,7 +4,8 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -22,7 +23,7 @@ function createMapStateToProps() {
|
||||
|
||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name,
|
||||
@@ -33,7 +34,7 @@ function createMapStateToProps() {
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = items.sort(sortByName).map((indexer) => ({
|
||||
const values = items.sort(sortByProp('name')).map((indexer) => ({
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
}));
|
||||
|
||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
|
||||
@@ -52,6 +52,7 @@ class SelectInput extends Component {
|
||||
const {
|
||||
key,
|
||||
value: optionValue,
|
||||
isDisabled: optionIsDisabled = false,
|
||||
...otherOptionProps
|
||||
} = option;
|
||||
|
||||
@@ -59,6 +60,7 @@ class SelectInput extends Component {
|
||||
<option
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={optionIsDisabled}
|
||||
{...otherOptionProps}
|
||||
>
|
||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-left: 3px;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'hasError': string;
|
||||
'hasWarning': string;
|
||||
'input': string;
|
||||
'internalInput': string;
|
||||
'isFocused': string;
|
||||
|
||||
@@ -225,6 +225,8 @@ class TagInput extends Component {
|
||||
const {
|
||||
className,
|
||||
inputContainerClassName,
|
||||
hasError,
|
||||
hasWarning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -241,7 +243,9 @@ class TagInput extends Component {
|
||||
className={className}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
|
||||
@@ -7,17 +7,11 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: 100%;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.linkWithEdit {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: calc(100% - 9px - 4px - 2px);
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.editContainer {
|
||||
@@ -36,5 +30,6 @@
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,11 @@ class TextTagInputConnector extends Component {
|
||||
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||
|
||||
newTags.forEach((newTag) => {
|
||||
newValue.push(newTag.trim());
|
||||
const newTagValue = newTag.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
});
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
@@ -80,7 +84,12 @@ class TextTagInputConnector extends Component {
|
||||
|
||||
const newValue = [...valueArray];
|
||||
newValue.splice(tagToReplace.index, 1);
|
||||
newValue.push(newTag.name.trim());
|
||||
|
||||
const newTagValue = newTag.name.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
47
frontend/src/Components/ImdbRating.tsx
Normal file
47
frontend/src/Components/ImdbRating.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title {
|
||||
.name {
|
||||
margin-bottom: 2px;
|
||||
color: var(--helpTextColor);
|
||||
font-size: 10px;
|
||||
|
||||
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
@@ -4,9 +4,9 @@ interface CssExports {
|
||||
'label': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
'name': string;
|
||||
'outline': string;
|
||||
'small': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './InfoLabel.css';
|
||||
function InfoLabel(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
@@ -25,8 +25,8 @@ function InfoLabel(props) {
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
@@ -37,7 +37,7 @@ function InfoLabel(props) {
|
||||
|
||||
InfoLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterMenuItem from './FilterMenuItem';
|
||||
import MenuContent from './MenuContent';
|
||||
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.sort(sortByProp('label'))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import MovieSearchInputConnector from './MovieSearchInputConnector';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
@@ -84,6 +84,7 @@ class PageHeader extends Component {
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
title={translate('SuggestTranslationChange')}
|
||||
@@ -91,7 +92,8 @@ class PageHeader extends Component {
|
||||
to="https://translate.servarr.com/projects/radarr/radarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
function PageHeaderActionsMenu(props) {
|
||||
const {
|
||||
formsAuth,
|
||||
onKeyboardShortcutsPress,
|
||||
onRestartPress,
|
||||
onShutdownPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.KEYBOARD}
|
||||
/>
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={onRestartPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.RESTART}
|
||||
/>
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={onShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<div className={styles.separator} />
|
||||
}
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<MenuItem
|
||||
to={`${window.Radarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PageHeaderActionsMenu.propTypes = {
|
||||
formsAuth: PropTypes.bool.isRequired,
|
||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||
onRestartPress: PropTypes.func.isRequired,
|
||||
onShutdownPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
interface PageHeaderActionsMenuProps {
|
||||
onKeyboardShortcutsPress(): void;
|
||||
}
|
||||
|
||||
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||
const { onKeyboardShortcutsPress } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { authentication, isDocker } = useSelector(
|
||||
(state: AppState) => state.system.status.item
|
||||
);
|
||||
|
||||
const formsAuth = authentication === 'forms';
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleShutdownPress = useCallback(() => {
|
||||
dispatch(shutdown());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
{isDocker ? null : (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={handleRestartPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={handleShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formsAuth ? (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem to={`${window.Radarr.urlBase}/logout`} noRouter={true}>
|
||||
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||
{translate('Logout')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
return {
|
||||
formsAuth: status.item.authentication === 'forms'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
restart,
|
||||
shutdown
|
||||
};
|
||||
|
||||
class PageHeaderActionsMenuConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestartPress = () => {
|
||||
this.props.restart();
|
||||
};
|
||||
|
||||
onShutdownPress = () => {
|
||||
this.props.shutdown();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageHeaderActionsMenu
|
||||
{...this.props}
|
||||
onRestartPress={this.onRestartPress}
|
||||
onShutdownPress={this.onShutdownPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHeaderActionsMenuConnector.propTypes = {
|
||||
restart: PropTypes.func.isRequired,
|
||||
shutdown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
||||
@@ -21,6 +21,10 @@
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
background-color: var(--inverseLabelColor);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
@@ -61,10 +65,18 @@
|
||||
.frontTextContainer {
|
||||
z-index: 1;
|
||||
color: var(--progressBarFrontTextColor);
|
||||
|
||||
&.inverse {
|
||||
color: var(--inverseLabelTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
.backTextContainer {
|
||||
color: var(--progressBarBackTextColor);
|
||||
|
||||
&.inverse {
|
||||
color: var(--inverseLabelTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
.backTextContainer,
|
||||
|
||||
1
frontend/src/Components/ProgressBar.css.d.ts
vendored
1
frontend/src/Components/ProgressBar.css.d.ts
vendored
@@ -9,6 +9,7 @@ interface CssExports {
|
||||
'frontText': string;
|
||||
'frontTextContainer': string;
|
||||
'info': string;
|
||||
'inverse': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
'primary': string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ProgressBar.css';
|
||||
|
||||
function ProgressBar(props) {
|
||||
@@ -57,7 +58,7 @@ function ProgressBar(props) {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
role="meter"
|
||||
aria-label={`Progress Bar at ${progress.toFixed(0)}%`}
|
||||
aria-label={translate('ProgressBarProgress', { progress: progress.toFixed(0) })}
|
||||
aria-valuenow={progress.toFixed(0)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import styles from './RottenTomatoRating.css';
|
||||
|
||||
class RottenTomatoRating extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
ratings,
|
||||
hideIcon,
|
||||
iconSize
|
||||
} = this.props;
|
||||
|
||||
const rtRotten = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNDQ1LjE4NSA0NDQuNjg0Yy03OS4zNjkgNC4xNjctOTUuNTg3LTg2LjY1Mi0xMjYuNzI2LTg2LjAwNi0xMy4yNjguMjc5LTIzLjcyNiAxNC4xNTEtMTkuMTMzIDMwLjMyIDIuNTI1IDguODg4IDkuNTMgMjEuOTIzIDEzLjk0NCAzMC4wMTEgMTUuNTcgMjguNTQ0LTcuNDQ3IDYwLjg0NS0zNC4zODMgNjMuNTc3LTQ0Ljc2IDQuNTQtNjMuNDMzLTIxLjQyNi02Mi4yNzgtNDguMDA3IDEuMy0yOS44NCAyNi42LTYwLjMzMS42NS03My4zMDUtMjcuMTk0LTEzLjU5Ny00OS4zMDEgMzkuNTcyLTc1LjMyNSA1MS40MzktMjMuNTUzIDEwLjc0MS01Ni4yNDggMi40MTMtNjcuODcyLTIzLjc0MS04LjE2NC0xOC4zNzktNi42OC01My43NjggMjkuNjctNjcuMjcgMjIuNzA2LTguNDMzIDczLjMwNSAxMS4wMjkgNzUuOS0xMy42MjMgMi45OTItMjguNDE2LTUzLjE1NS0zMC44MTItNzAuMDYtMzcuNjI2LTI5LjkxMi0xMi4wNTUtNDcuNTY3LTM3Ljg1LTMzLjczNC02NS41MjIgMTAuMzc4LTIwLjc1NyA0MC45MTUtMjkuMjAzIDY0LjIyMy0yMC4xMSAyNy45MjIgMTAuODkyIDMyLjQwNCAzOS44NTMgNDYuNzEgNTEuODk3IDEyLjMyNCAxMC4zOCAyOS4xOSAxMS42OCA0MC4yMiA0LjU0MyA4LjEzNS01LjI2NSAxMC44NDMtMTYuODI4IDcuNzc0LTI3LjM5LTQuMDctMTQuMDIzLTE0Ljg3NS0yMi43NzMtMjUuNDE1LTMxLjM0Ni0xOC43NTgtMTUuMjQ5LTQ1LjI0LTI4LjM2LTI5LjIyMi02OS45ODMgMTMuMTMtMzQuMTEgNTEuNjQyLTM1LjM0IDUxLjY0Mi0zNS4zNCAxNS4zLTEuNzIgMjkuMDAyIDIuOSA0MC4xNjcgMTIuODc1IDE0LjkyNyAxMy4zMzUgMTcuODM0IDMxLjE2IDE1LjMzNiA1MC4xNzYtMi4yODMgMTcuMzU4LTguNDI2IDMyLjU2LTExLjYzIDQ5Ljc1OS0zLjcxNyAxOS45NjYgNi45NTQgNDAuMDg2IDI3LjI0OSA0MC44NjkgMjYuNjk0IDEuMDMxIDM0LjY5OC0xOS40ODYgMzcuOTY0LTMyLjQ5MiA0Ljc4Mi0xOS4wMjggMTEuMDU4LTM2LjY5NCAyOC43MTgtNDcuODIgMjUuMzQ2LTE1Ljk3IDYwLjU1Mi0xMi40NyA3Ni44ODYgMTguMjIyIDEyLjkyIDI0LjI4NCA4Ljc3MiA1Ny43MTUtMTEuMDQ3IDc1Ljk3LTguODkyIDguMTg4LTE5LjU4NCAxMS4wNzUtMzEuMTQ4IDExLjE1Ni0xNi41ODUuMTE3LTMzLjE2Mi0uMjktNDguNTU2IDcuNDcxLTEwLjQ4IDUuMjgxLTE1LjA0NyAxMy44ODgtMTUuMDQ1IDI1LjQyMyAwIDExLjI0MiA1Ljg1MyAxOC41ODUgMTUuMzM2IDIzLjM2MyAxNy44NiA5LjAwMyAzNy41NzcgMTAuODQzIDU2Ljg3MSAxNC4yMjIgMjcuOTggNC45IDUyLjU4MSAxNC43NTUgNjguMzc1IDQwLjcyLjE0Mi4yMjguMjguNDU4LjQxNS42OSAxOC4xMzkgMzAuNzQxLS44MzEgNzUuMDA1LTM2LjQ3NiA3Ni44NzgiIGZpbGw9IiMwQUM4NTUiLz48L3N2Zz4=';
|
||||
const rtFresh = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNDc4LjI5IDI5Ni45OGMtMy45OS02My45NjYtMzYuNTItMTExLjgyLTg1LjQ2OC0xMzguNTggMC4yNzggMS41Ni0xLjEwOSAzLjUwOC0yLjY4OCAyLjgxOC0zMi4wMTYtMTQuMDA2LTg2LjMyOCAzMS4zMi0xMjQuMjggNy41ODQgMC4yODUgOC41MTktMS4zNzggNTAuMDcyLTU5LjkxNCA1Mi40ODMtMS4zODIgMC4wNTYtMi4xNDItMS4zNTUtMS4yNjgtMi4zNTQgNy44MjgtOC45MjkgMTUuNzMyLTMxLjUzNSA0LjM2Ny00My41ODYtMjQuMzM4IDIxLjgxLTM4LjQ3MiAzMC4wMTctODUuMTM4IDE5LjE4Ni0yOS44NzggMzEuMjQxLTQ2LjgwOSA3NC00My40ODUgMTI3LjI2IDYuNzggMTA4Ljc0IDEwOC42MyAxNzAuODkgMjExLjE5IDE2NC40OSAxMDIuNTYtNi4zOTUgMTkzLjQ3LTgwLjU3MiAxODYuNjgtMTg5LjMxIiBmaWxsPSIjRkEzMjBBIi8+PHBhdGggZD0iTTI5MS4zNzUgMTMyLjI5M2MyMS4wNzUtNS4wMjMgODEuNjkzLS40OSAxMDEuMTE0IDI1LjI3NCAxLjE2NiAxLjU0NS0uNDc1IDQuNDY4LTIuMzU1IDMuNjQ4LTMyLjAxNi0xNC4wMDYtODYuMzI4IDMxLjMyLTEyNC4yODIgNy41ODQuMjg1IDguNTE5LTEuMzc4IDUwLjA3Mi01OS45MTQgNTIuNDgzLTEuMzgyLjA1Ni0yLjE0Mi0xLjM1NS0xLjI2OC0yLjM1NCA3LjgyOC04LjkyOSAxNS43My0zMS41MzUgNC4zNjctNDMuNTg2LTI2LjUxMiAyMy43NTgtNDAuODg0IDMxLjM5Mi05OC40MjYgMTUuODM4LTEuODgzLS41MDgtMS4yNDEtMy41MzUuNzYyLTQuMjk4IDEwLjg3Ni00LjE1NyAzNS41MTUtMjIuMzYxIDU4LjgyNC0zMC4zODUgNC40MzgtMS41MjYgOC44NjItMi43MSAxMy4xOC0zLjQtMjUuNjY1LTIuMjkzLTM3LjIzNS01Ljg2Mi01My41NTktMy40LTEuNzg5LjI3LTMuMDA0LTEuODEzLTEuODk1LTMuMjQxIDIxLjk5NS0yOC4zMzIgNjIuNTEzLTM2Ljg4OCA4Ny41MTItMjEuODM3LTE1LjQxLTE5LjA5NC0yNy40OC0zNC4zMjEtMjcuNDgtMzQuMzIxbDI4LjYwMS0xNi4yNDZzMTEuODE3IDI2LjQgMjAuNDE0IDQ1LjYxNGMyMS4yNzUtMzEuNDM1IDYwLjg2LTM0LjMzNiA3Ny41ODUtMTIuMDMzLjk5MiAxLjMyNi0uMDQ1IDMuMjEtMS43MDIgMy4xNzEtMTMuNjEyLS4zMzEtMjEuMTA3IDEyLjA1LTIxLjY3NSAyMS40NjZsLjE5Ny4wMjMiIGZpbGw9IiMwMDkxMkQiLz48L3N2Zz4=';
|
||||
|
||||
const rating = ratings.rottenTomatoes;
|
||||
|
||||
let ratingString = '0%';
|
||||
let ratingImage = rtFresh;
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}%`;
|
||||
ratingImage = rating.value > 50 ? rtFresh : rtRotten;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{ratingString}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RottenTomatoRating.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
iconSize: PropTypes.number.isRequired,
|
||||
hideIcon: PropTypes.bool
|
||||
};
|
||||
|
||||
RottenTomatoRating.defaultProps = {
|
||||
iconSize: 14
|
||||
};
|
||||
|
||||
export default RottenTomatoRating;
|
||||
43
frontend/src/Components/RottenTomatoRating.tsx
Normal file
43
frontend/src/Components/RottenTomatoRating.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Ratings, RatingValues } from 'Movie/Movie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RottenTomatoRating.css';
|
||||
|
||||
interface RottenTomatoRatingProps {
|
||||
ratings: Ratings;
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}
|
||||
|
||||
function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const rtRotten =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNDQ1LjE4NSA0NDQuNjg0Yy03OS4zNjkgNC4xNjctOTUuNTg3LTg2LjY1Mi0xMjYuNzI2LTg2LjAwNi0xMy4yNjguMjc5LTIzLjcyNiAxNC4xNTEtMTkuMTMzIDMwLjMyIDIuNTI1IDguODg4IDkuNTMgMjEuOTIzIDEzLjk0NCAzMC4wMTEgMTUuNTcgMjguNTQ0LTcuNDQ3IDYwLjg0NS0zNC4zODMgNjMuNTc3LTQ0Ljc2IDQuNTQtNjMuNDMzLTIxLjQyNi02Mi4yNzgtNDguMDA3IDEuMy0yOS44NCAyNi42LTYwLjMzMS42NS03My4zMDUtMjcuMTk0LTEzLjU5Ny00OS4zMDEgMzkuNTcyLTc1LjMyNSA1MS40MzktMjMuNTUzIDEwLjc0MS01Ni4yNDggMi40MTMtNjcuODcyLTIzLjc0MS04LjE2NC0xOC4zNzktNi42OC01My43NjggMjkuNjctNjcuMjcgMjIuNzA2LTguNDMzIDczLjMwNSAxMS4wMjkgNzUuOS0xMy42MjMgMi45OTItMjguNDE2LTUzLjE1NS0zMC44MTItNzAuMDYtMzcuNjI2LTI5LjkxMi0xMi4wNTUtNDcuNTY3LTM3Ljg1LTMzLjczNC02NS41MjIgMTAuMzc4LTIwLjc1NyA0MC45MTUtMjkuMjAzIDY0LjIyMy0yMC4xMSAyNy45MjIgMTAuODkyIDMyLjQwNCAzOS44NTMgNDYuNzEgNTEuODk3IDEyLjMyNCAxMC4zOCAyOS4xOSAxMS42OCA0MC4yMiA0LjU0MyA4LjEzNS01LjI2NSAxMC44NDMtMTYuODI4IDcuNzc0LTI3LjM5LTQuMDctMTQuMDIzLTE0Ljg3NS0yMi43NzMtMjUuNDE1LTMxLjM0Ni0xOC43NTgtMTUuMjQ5LTQ1LjI0LTI4LjM2LTI5LjIyMi02OS45ODMgMTMuMTMtMzQuMTEgNTEuNjQyLTM1LjM0IDUxLjY0Mi0zNS4zNCAxNS4zLTEuNzIgMjkuMDAyIDIuOSA0MC4xNjcgMTIuODc1IDE0LjkyNyAxMy4zMzUgMTcuODM0IDMxLjE2IDE1LjMzNiA1MC4xNzYtMi4yODMgMTcuMzU4LTguNDI2IDMyLjU2LTExLjYzIDQ5Ljc1OS0zLjcxNyAxOS45NjYgNi45NTQgNDAuMDg2IDI3LjI0OSA0MC44NjkgMjYuNjk0IDEuMDMxIDM0LjY5OC0xOS40ODYgMzcuOTY0LTMyLjQ5MiA0Ljc4Mi0xOS4wMjggMTEuMDU4LTM2LjY5NCAyOC43MTgtNDcuODIgMjUuMzQ2LTE1Ljk3IDYwLjU1Mi0xMi40NyA3Ni44ODYgMTguMjIyIDEyLjkyIDI0LjI4NCA4Ljc3MiA1Ny43MTUtMTEuMDQ3IDc1Ljk3LTguODkyIDguMTg4LTE5LjU4NCAxMS4wNzUtMzEuMTQ4IDExLjE1Ni0xNi41ODUuMTE3LTMzLjE2Mi0uMjktNDguNTU2IDcuNDcxLTEwLjQ4IDUuMjgxLTE1LjA0NyAxMy44ODgtMTUuMDQ1IDI1LjQyMyAwIDExLjI0MiA1Ljg1MyAxOC41ODUgMTUuMzM2IDIzLjM2MyAxNy44NiA5LjAwMyAzNy41NzcgMTAuODQzIDU2Ljg3MSAxNC4yMjIgMjcuOTggNC45IDUyLjU4MSAxNC43NTUgNjguMzc1IDQwLjcyLjE0Mi4yMjguMjguNDU4LjQxNS42OSAxOC4xMzkgMzAuNzQxLS44MzEgNzUuMDA1LTM2LjQ3NiA3Ni44NzgiIGZpbGw9IiMwQUM4NTUiLz48L3N2Zz4=';
|
||||
const rtFresh =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNDc4LjI5IDI5Ni45OGMtMy45OS02My45NjYtMzYuNTItMTExLjgyLTg1LjQ2OC0xMzguNTggMC4yNzggMS41Ni0xLjEwOSAzLjUwOC0yLjY4OCAyLjgxOC0zMi4wMTYtMTQuMDA2LTg2LjMyOCAzMS4zMi0xMjQuMjggNy41ODQgMC4yODUgOC41MTktMS4zNzggNTAuMDcyLTU5LjkxNCA1Mi40ODMtMS4zODIgMC4wNTYtMi4xNDItMS4zNTUtMS4yNjgtMi4zNTQgNy44MjgtOC45MjkgMTUuNzMyLTMxLjUzNSA0LjM2Ny00My41ODYtMjQuMzM4IDIxLjgxLTM4LjQ3MiAzMC4wMTctODUuMTM4IDE5LjE4Ni0yOS44NzggMzEuMjQxLTQ2LjgwOSA3NC00My40ODUgMTI3LjI2IDYuNzggMTA4Ljc0IDEwOC42MyAxNzAuODkgMjExLjE5IDE2NC40OSAxMDIuNTYtNi4zOTUgMTkzLjQ3LTgwLjU3MiAxODYuNjgtMTg5LjMxIiBmaWxsPSIjRkEzMjBBIi8+PHBhdGggZD0iTTI5MS4zNzUgMTMyLjI5M2MyMS4wNzUtNS4wMjMgODEuNjkzLS40OSAxMDEuMTE0IDI1LjI3NCAxLjE2NiAxLjU0NS0uNDc1IDQuNDY4LTIuMzU1IDMuNjQ4LTMyLjAxNi0xNC4wMDYtODYuMzI4IDMxLjMyLTEyNC4yODIgNy41ODQuMjg1IDguNTE5LTEuMzc4IDUwLjA3Mi01OS45MTQgNTIuNDgzLTEuMzgyLjA1Ni0yLjE0Mi0xLjM1NS0xLjI2OC0yLjM1NCA3LjgyOC04LjkyOSAxNS43My0zMS41MzUgNC4zNjctNDMuNTg2LTI2LjUxMiAyMy43NTgtNDAuODg0IDMxLjM5Mi05OC40MjYgMTUuODM4LTEuODgzLS41MDgtMS4yNDEtMy41MzUuNzYyLTQuMjk4IDEwLjg3Ni00LjE1NyAzNS41MTUtMjIuMzYxIDU4LjgyNC0zMC4zODUgNC40MzgtMS41MjYgOC44NjItMi43MSAxMy4xOC0zLjQtMjUuNjY1LTIuMjkzLTM3LjIzNS01Ljg2Mi01My41NTktMy40LTEuNzg5LjI3LTMuMDA0LTEuODEzLTEuODk1LTMuMjQxIDIxLjk5NS0yOC4zMzIgNjIuNTEzLTM2Ljg4OCA4Ny41MTItMjEuODM3LTE1LjQxLTE5LjA5NC0yNy40OC0zNC4zMjEtMjcuNDgtMzQuMzIxbDI4LjYwMS0xNi4yNDZzMTEuODE3IDI2LjQgMjAuNDE0IDQ1LjYxNGMyMS4yNzUtMzEuNDM1IDYwLjg2LTM0LjMzNiA3Ny41ODUtMTIuMDMzLjk5MiAxLjMyNi0uMDQ1IDMuMjEtMS43MDIgMy4xNzEtMTMuNjEyLS4zMzEtMjEuMTA3IDEyLjA1LTIxLjY3NSAyMS40NjZsLjE5Ny4wMjMiIGZpbGw9IiMwMDkxMkQiLz48L3N2Zz4=';
|
||||
|
||||
const { rottenTomatoes: rottenTomatoesRatings = {} as RatingValues } =
|
||||
ratings;
|
||||
const { value = 0 } = rottenTomatoesRatings;
|
||||
|
||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||
|
||||
return (
|
||||
<span>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
alt={translate('RottenTomatoesRating')}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{value}%
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default RottenTomatoRating;
|
||||
@@ -202,6 +202,8 @@ class SignalRConnector extends Component {
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
|
||||
repopulatePage('movieUpdated');
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
class RelativeDateCell extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
date,
|
||||
includeSeconds,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
component: Component,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!date) {
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RelativeDateCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
date: PropTypes.string,
|
||||
includeSeconds: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
component: PropTypes.elementType,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
RelativeDateCell.defaultProps = {
|
||||
className: styles.cell,
|
||||
includeSeconds: false,
|
||||
component: TableRowCell
|
||||
};
|
||||
|
||||
export default RelativeDateCell;
|
||||
59
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
59
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
interface RelativeDateCellProps {
|
||||
className?: string;
|
||||
date?: string;
|
||||
includeSeconds?: boolean;
|
||||
includeTime?: boolean;
|
||||
timeForToday?: boolean;
|
||||
component?: React.ElementType;
|
||||
}
|
||||
|
||||
function RelativeDateCell(props: RelativeDateCellProps) {
|
||||
const {
|
||||
className = styles.cell,
|
||||
date,
|
||||
includeSeconds = false,
|
||||
includeTime = false,
|
||||
timeForToday = true,
|
||||
|
||||
component: Component = TableRowCell,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
if (!date) {
|
||||
return <Component className={className} {...otherProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, {
|
||||
includeSeconds,
|
||||
includeRelativeDay: !showRelativeDates,
|
||||
})}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate({
|
||||
date,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
timeFormat,
|
||||
includeSeconds,
|
||||
includeTime,
|
||||
timeForToday,
|
||||
})}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
export default RelativeDateCell;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import RelativeDateCell from './RelativeDateCell';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, null)(RelativeDateCell);
|
||||
@@ -5,6 +5,7 @@ type PropertyFunction<T> = () => T;
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
|
||||
@@ -66,7 +66,9 @@ function Table(props) {
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
isVisible,
|
||||
isSortable,
|
||||
...otherColumnProps
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
@@ -84,6 +86,7 @@ function Table(props) {
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
{...otherColumnProps}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
|
||||
54
frontend/src/Components/Table/usePaging.ts
Normal file
54
frontend/src/Components/Table/usePaging.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
interface PagingOptions {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
gotoPage: ({ page }: { page: number }) => void;
|
||||
}
|
||||
|
||||
function usePaging(options: PagingOptions) {
|
||||
const { page, totalPages, gotoPage } = options;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFirstPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: 1 }));
|
||||
}, [dispatch, gotoPage]);
|
||||
|
||||
const handlePreviousPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
|
||||
}, [page, dispatch, gotoPage]);
|
||||
|
||||
const handleNextPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
|
||||
}, [page, totalPages, dispatch, gotoPage]);
|
||||
|
||||
const handleLastPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: totalPages }));
|
||||
}, [totalPages, dispatch, gotoPage]);
|
||||
|
||||
const handlePageSelect = useCallback(
|
||||
(page: number) => {
|
||||
dispatch(gotoPage({ page }));
|
||||
},
|
||||
[dispatch, gotoPage]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
};
|
||||
}, [
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
]);
|
||||
}
|
||||
|
||||
export default usePaging;
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Label from './Label';
|
||||
import styles from './TagList.css';
|
||||
|
||||
@@ -8,7 +9,7 @@ function TagList({ tags, tagList }) {
|
||||
const sortedTags = tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((tag) => !!tag)
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
.sort(sortByProp('label'));
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import styles from './TmdbRating.css';
|
||||
|
||||
class TmdbRating extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
ratings,
|
||||
hideIcon,
|
||||
iconSize
|
||||
} = this.props;
|
||||
|
||||
const tmdbImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTAuMjQgODEuNTIiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeTE9IjQwLjc2IiB4Mj0iMTkwLjI0IiB5Mj0iNDAuNzYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MGNlYTEiLz48c3RvcCBvZmZzZXQ9Ii41NiIgc3RvcC1jb2xvcj0iIzNjYmVjOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYjNlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Ik0xMDUuNjcgMzYuMDZoNjYuOWExNy42NyAxNy42NyAwIDAwMTcuNjctMTcuNjZBMTcuNjcgMTcuNjcgMCAwMDE3Mi41Ny43M2gtNjYuOUExNy42NyAxNy42NyAwIDAwODggMTguNGExNy42NyAxNy42NyAwIDAwMTcuNjcgMTcuNjZ6bS04OCA0NWg3Ni45YTE3LjY3IDE3LjY3IDAgMDAxNy42Ny0xNy42NiAxNy42NyAxNy42NyAwIDAwLTE3LjY3LTE3LjY3aC03Ni45QTE3LjY3IDE3LjY3IDAgMDAwIDYzLjRhMTcuNjcgMTcuNjcgMCAwMDE3LjY3IDE3LjY2em0tNy4yNi00NS42NGg3LjhWNi45MmgxMC4xVjBoLTI4djYuOWgxMC4xem0yOC4xIDBoNy44VjguMjVoLjFsOSAyNy4xNWg2bDkuMy0yNy4xNWguMVYzNS40aDcuOFYwSDY2Ljc2bC04LjIgMjMuMWgtLjFMNTAuMzEgMGgtMTEuOHptMTEzLjkyIDIwLjI1YTE1LjA3IDE1LjA3IDAgMDAtNC41Mi01LjUyIDE4LjU3IDE4LjU3IDAgMDAtNi42OC0zLjA4IDMzLjU0IDMzLjU0IDAgMDAtOC4wNy0xaC0xMS43djM1LjRoMTIuNzVhMjQuNTggMjQuNTggMCAwMDcuNTUtMS4xNSAxOS4zNCAxOS4zNCAwIDAwNi4zNS0zLjMyIDE2LjI3IDE2LjI3IDAgMDA0LjM3LTUuNSAxNi45MSAxNi45MSAwIDAwMS42My03LjU4IDE4LjUgMTguNSAwIDAwLTEuNjgtOC4yNXpNMTQ1IDY4LjZhOC44IDguOCAwIDAxLTIuNjQgMy40IDEwLjcgMTAuNyAwIDAxLTQgMS44MiAyMS41NyAyMS41NyAwIDAxLTUgLjU1aC00LjA1di0yMWg0LjZhMTcgMTcgMCAwMTQuNjcuNjMgMTEuNjYgMTEuNjYgMCAwMTMuODggMS44N0E5LjE0IDkuMTQgMCAwMTE0NSA1OWE5Ljg3IDkuODcgMCAwMTEgNC41MiAxMS44OSAxMS44OSAwIDAxLTEgNS4wOHptNDQuNjMtLjEzYTggOCAwIDAwLTEuNTgtMi42MiA4LjM4IDguMzggMCAwMC0yLjQyLTEuODUgMTAuMzEgMTAuMzEgMCAwMC0zLjE3LTF2LS4xYTkuMjIgOS4yMiAwIDAwNC40Mi0yLjgyIDcuNDMgNy40MyAwIDAwMS42OC01IDguNDIgOC40MiAwIDAwLTEuMTUtNC42NSA4LjA5IDguMDkgMCAwMC0zLTIuNzIgMTIuNTYgMTIuNTYgMCAwMC00LjE4LTEuMyAzMi44NCAzMi44NCAwIDAwLTQuNjItLjMzaC0xMy4ydjM1LjRoMTQuNWEyMi40MSAyMi40MSAwIDAwNC43Mi0uNSAxMy41MyAxMy41MyAwIDAwNC4yOC0xLjY1IDkuNDIgOS40MiAwIDAwMy4xLTMgOC41MiA4LjUyIDAgMDAxLjItNC42OCA5LjM5IDkuMzkgMCAwMC0uNTUtMy4xOHptLTE5LjQyLTE1Ljc1aDUuM2ExMCAxMCAwIDAxMS44NS4xOCA2LjE4IDYuMTggMCAwMTEuNy41NyAzLjM5IDMuMzkgMCAwMTEuMjIgMS4xMyAzLjIyIDMuMjIgMCAwMS40OCAxLjgyIDMuNjMgMy42MyAwIDAxLS40MyAxLjggMy40IDMuNCAwIDAxLTEuMTIgMS4yIDQuOTIgNC45MiAwIDAxLTEuNTguNjUgNy41MSA3LjUxIDAgMDEtMS43Ny4yaC01LjY1em0xMS43MiAyMGEzLjkgMy45IDAgMDEtMS4yMiAxLjMgNC42NCA0LjY0IDAgMDEtMS42OC43IDguMTggOC4xOCAwIDAxLTEuODIuMmgtN3YtOGg1LjlhMTUuMzUgMTUuMzUgMCAwMTIgLjE1IDguNDcgOC40NyAwIDAxMi4wNS41NSA0IDQgMCAwMTEuNTcgMS4xOCAzLjExIDMuMTEgMCAwMS42MyAyIDMuNzEgMy43MSAwIDAxLS40MyAxLjkyeiIgZmlsbD0idXJsKCNhKSIvPjwvc3ZnPg==';
|
||||
|
||||
const rating = ratings.tmdb;
|
||||
|
||||
let ratingString = '0%';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${(rating.value * 10).toFixed()}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${rating.votes} votes`}>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={tmdbImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{ratingString}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TmdbRating.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
iconSize: PropTypes.number.isRequired,
|
||||
hideIcon: PropTypes.bool
|
||||
};
|
||||
|
||||
TmdbRating.defaultProps = {
|
||||
iconSize: 14
|
||||
};
|
||||
|
||||
export default TmdbRating;
|
||||
46
frontend/src/Components/TmdbRating.tsx
Normal file
46
frontend/src/Components/TmdbRating.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { Ratings } from 'Movie/Movie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TmdbRating.css';
|
||||
|
||||
interface TmdbRatingProps {
|
||||
ratings: Ratings;
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}
|
||||
|
||||
function TmdbRating(props: TmdbRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const tmdbImage =
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTAuMjQgODEuNTIiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeTE9IjQwLjc2IiB4Mj0iMTkwLjI0IiB5Mj0iNDAuNzYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MGNlYTEiLz48c3RvcCBvZmZzZXQ9Ii41NiIgc3RvcC1jb2xvcj0iIzNjYmVjOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYjNlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Ik0xMDUuNjcgMzYuMDZoNjYuOWExNy42NyAxNy42NyAwIDAwMTcuNjctMTcuNjZBMTcuNjcgMTcuNjcgMCAwMDE3Mi41Ny43M2gtNjYuOUExNy42NyAxNy42NyAwIDAwODggMTguNGExNy42NyAxNy42NyAwIDAwMTcuNjcgMTcuNjZ6bS04OCA0NWg3Ni45YTE3LjY3IDE3LjY3IDAgMDAxNy42Ny0xNy42NiAxNy42NyAxNy42NyAwIDAwLTE3LjY3LTE3LjY3aC03Ni45QTE3LjY3IDE3LjY3IDAgMDAwIDYzLjRhMTcuNjcgMTcuNjcgMCAwMDE3LjY3IDE3LjY2em0tNy4yNi00NS42NGg3LjhWNi45MmgxMC4xVjBoLTI4djYuOWgxMC4xem0yOC4xIDBoNy44VjguMjVoLjFsOSAyNy4xNWg2bDkuMy0yNy4xNWguMVYzNS40aDcuOFYwSDY2Ljc2bC04LjIgMjMuMWgtLjFMNTAuMzEgMGgtMTEuOHptMTEzLjkyIDIwLjI1YTE1LjA3IDE1LjA3IDAgMDAtNC41Mi01LjUyIDE4LjU3IDE4LjU3IDAgMDAtNi42OC0zLjA4IDMzLjU0IDMzLjU0IDAgMDAtOC4wNy0xaC0xMS43djM1LjRoMTIuNzVhMjQuNTggMjQuNTggMCAwMDcuNTUtMS4xNSAxOS4zNCAxOS4zNCAwIDAwNi4zNS0zLjMyIDE2LjI3IDE2LjI3IDAgMDA0LjM3LTUuNSAxNi45MSAxNi45MSAwIDAwMS42My03LjU4IDE4LjUgMTguNSAwIDAwLTEuNjgtOC4yNXpNMTQ1IDY4LjZhOC44IDguOCAwIDAxLTIuNjQgMy40IDEwLjcgMTAuNyAwIDAxLTQgMS44MiAyMS41NyAyMS41NyAwIDAxLTUgLjU1aC00LjA1di0yMWg0LjZhMTcgMTcgMCAwMTQuNjcuNjMgMTEuNjYgMTEuNjYgMCAwMTMuODggMS44N0E5LjE0IDkuMTQgMCAwMTE0NSA1OWE5Ljg3IDkuODcgMCAwMTEgNC41MiAxMS44OSAxMS44OSAwIDAxLTEgNS4wOHptNDQuNjMtLjEzYTggOCAwIDAwLTEuNTgtMi42MiA4LjM4IDguMzggMCAwMC0yLjQyLTEuODUgMTAuMzEgMTAuMzEgMCAwMC0zLjE3LTF2LS4xYTkuMjIgOS4yMiAwIDAwNC40Mi0yLjgyIDcuNDMgNy40MyAwIDAwMS42OC01IDguNDIgOC40MiAwIDAwLTEuMTUtNC42NSA4LjA5IDguMDkgMCAwMC0zLTIuNzIgMTIuNTYgMTIuNTYgMCAwMC00LjE4LTEuMyAzMi44NCAzMi44NCAwIDAwLTQuNjItLjMzaC0xMy4ydjM1LjRoMTQuNWEyMi40MSAyMi40MSAwIDAwNC43Mi0uNSAxMy41MyAxMy41MyAwIDAwNC4yOC0xLjY1IDkuNDIgOS40MiAwIDAwMy4xLTMgOC41MiA4LjUyIDAgMDAxLjItNC42OCA5LjM5IDkuMzkgMCAwMC0uNTUtMy4xOHptLTE5LjQyLTE1Ljc1aDUuM2ExMCAxMCAwIDAxMS44NS4xOCA2LjE4IDYuMTggMCAwMTEuNy41NyAzLjM5IDMuMzkgMCAwMTEuMjIgMS4xMyAzLjIyIDMuMjIgMCAwMS40OCAxLjgyIDMuNjMgMy42MyAwIDAxLS40MyAxLjggMy40IDMuNCAwIDAxLTEuMTIgMS4yIDQuOTIgNC45MiAwIDAxLTEuNTguNjUgNy41MSA3LjUxIDAgMDEtMS43Ny4yaC01LjY1em0xMS43MiAyMGEzLjkgMy45IDAgMDEtMS4yMiAxLjMgNC42NCA0LjY0IDAgMDEtMS42OC43IDguMTggOC4xOCAwIDAxLTEuODIuMmgtN3YtOGg1LjlhMTUuMzUgMTUuMzUgMCAwMTIgLjE1IDguNDcgOC40NyAwIDAxMi4wNS41NSA0IDQgMCAwMTEuNTcgMS4xOCAzLjExIDMuMTEgMCAwMS42MyAyIDMuNzEgMy43MSAwIDAxLS40MyAxLjkyeiIgZmlsbD0idXJsKCNhKSIvPjwvc3ZnPg==';
|
||||
|
||||
const { value = 0, votes = 0 } = ratings.tmdb;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
alt={translate('TmdbRating')}
|
||||
src={tmdbImage}
|
||||
style={{
|
||||
height: `${iconSize}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(value * 10).toFixed()}%
|
||||
</span>
|
||||
}
|
||||
tooltip={translate('CountVotes', { votes })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TmdbRating;
|
||||
@@ -5,9 +5,8 @@ import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { addImportExclusions, addMovies, clearAddMovie, fetchDiscoverMovies, setListMovieFilter, setListMovieSort, setListMovieTableOption, setListMovieView } from 'Store/Actions/discoverMovieActions';
|
||||
import { addImportListExclusions, addMovies, clearAddMovie, fetchDiscoverMovies, setListMovieFilter, setListMovieSort, setListMovieTableOption, setListMovieView } from 'Store/Actions/discoverMovieActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
@@ -43,10 +42,6 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(fetchRootFolders());
|
||||
},
|
||||
|
||||
dispatchFetchImportExclusions() {
|
||||
dispatch(fetchImportExclusions());
|
||||
},
|
||||
|
||||
dispatchClearListMovie() {
|
||||
dispatch(clearAddMovie());
|
||||
},
|
||||
@@ -75,8 +70,8 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(addMovies({ ids, addOptions }));
|
||||
},
|
||||
|
||||
dispatchAddImportExclusions(exclusions) {
|
||||
dispatch(addImportExclusions(exclusions));
|
||||
dispatchAddImportListExclusions(exclusions) {
|
||||
dispatch(addImportListExclusions(exclusions));
|
||||
},
|
||||
|
||||
onImportListSyncPress() {
|
||||
@@ -96,7 +91,6 @@ class DiscoverMovieConnector extends Component {
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.repopulate);
|
||||
this.props.dispatchFetchRootFolders();
|
||||
this.props.dispatchFetchImportExclusions();
|
||||
this.props.dispatchFetchListMovies();
|
||||
}
|
||||
|
||||
@@ -121,7 +115,7 @@ class DiscoverMovieConnector extends Component {
|
||||
};
|
||||
|
||||
onExcludeMoviesPress =({ ids }) => {
|
||||
this.props.dispatchAddImportExclusions({ ids });
|
||||
this.props.dispatchAddImportListExclusions({ ids });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -144,13 +138,12 @@ class DiscoverMovieConnector extends Component {
|
||||
DiscoverMovieConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchFetchImportExclusions: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchFetchListMovies: PropTypes.func.isRequired,
|
||||
dispatchClearListMovie: PropTypes.func.isRequired,
|
||||
dispatchSetListMovieView: PropTypes.func.isRequired,
|
||||
dispatchAddMovies: PropTypes.func.isRequired,
|
||||
dispatchAddImportExclusions: PropTypes.func.isRequired
|
||||
dispatchAddImportListExclusions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
|
||||
@@ -8,9 +8,9 @@ import DiscoverMovieFooter from './DiscoverMovieFooter';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.discoverMovie,
|
||||
(state) => state.settings.importExclusions,
|
||||
(state) => state.settings.importListExclusions,
|
||||
(state, { selectedIds }) => selectedIds,
|
||||
(discoverMovie, importExclusions, selectedIds) => {
|
||||
(discoverMovie, importListExclusions, selectedIds) => {
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
@@ -25,7 +25,7 @@ function createMapStateToProps() {
|
||||
|
||||
const {
|
||||
isSaving
|
||||
} = importExclusions;
|
||||
} = importListExclusions;
|
||||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { addImportExclusions } from 'Store/Actions/discoverMovieActions';
|
||||
import { addImportListExclusions } from 'Store/Actions/discoverMovieActions';
|
||||
import ExcludeMovieModalContent from './ExcludeMovieModalContent';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addImportExclusions
|
||||
addImportListExclusions
|
||||
};
|
||||
|
||||
class ExcludeMovieModalContentConnector extends Component {
|
||||
@@ -14,7 +14,7 @@ class ExcludeMovieModalContentConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onExcludePress = () => {
|
||||
this.props.addImportExclusions({ ids: [this.props.tmdbId] });
|
||||
this.props.addImportListExclusions({ ids: [this.props.tmdbId] });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
};
|
||||
@@ -37,7 +37,7 @@ ExcludeMovieModalContentConnector.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
addImportExclusions: PropTypes.func.isRequired
|
||||
addImportListExclusions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(ExcludeMovieModalContentConnector);
|
||||
|
||||
@@ -56,15 +56,6 @@ function DiscoverMovieSortMenu(props) {
|
||||
{translate('InCinemas')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="physicalRelease"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('PhysicalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="digitalRelease"
|
||||
sortKey={sortKey}
|
||||
@@ -74,6 +65,15 @@ function DiscoverMovieSortMenu(props) {
|
||||
{translate('DigitalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="physicalRelease"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('PhysicalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="runtime"
|
||||
sortKey={sortKey}
|
||||
@@ -84,12 +84,30 @@ function DiscoverMovieSortMenu(props) {
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="ratings"
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Rating')}
|
||||
{translate('TmdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="imdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('ImdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="rottenTomatoesRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('RottenTomatoesRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -14,25 +15,30 @@ const rows = [
|
||||
showProp: 'showYear',
|
||||
valueProp: 'year'
|
||||
},
|
||||
{
|
||||
name: 'studio',
|
||||
showProp: 'showStudio',
|
||||
valueProp: 'studio'
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
showProp: 'showGenres',
|
||||
valueProp: 'genres'
|
||||
},
|
||||
{
|
||||
name: 'ratings',
|
||||
showProp: 'showRatings',
|
||||
valueProp: 'ratings'
|
||||
name: 'tmdbRating',
|
||||
showProp: 'showTmdbRating',
|
||||
valueProp: 'ratings.tmdb.value'
|
||||
},
|
||||
{
|
||||
name: 'imdbRating',
|
||||
showProp: 'showImdbRating',
|
||||
valueProp: 'ratings.imdb.value'
|
||||
},
|
||||
{
|
||||
name: 'certification',
|
||||
showProp: 'showCertification',
|
||||
valueProp: 'certification'
|
||||
},
|
||||
{
|
||||
name: 'studio',
|
||||
showProp: 'showStudio',
|
||||
valueProp: 'studio'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -43,11 +49,7 @@ function isVisible(row, props) {
|
||||
valueProp
|
||||
} = row;
|
||||
|
||||
if (props[valueProp] == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return props[showProp] || props.sortKey === name;
|
||||
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name);
|
||||
}
|
||||
|
||||
function getInfoRowProps(row, props) {
|
||||
@@ -61,6 +63,14 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'studio') {
|
||||
return {
|
||||
title: translate('Studio'),
|
||||
iconName: icons.STUDIO,
|
||||
label: props.studio
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'genres') {
|
||||
return {
|
||||
title: translate('Genres'),
|
||||
@@ -69,14 +79,22 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating' && !!props.ratings.tmdb) {
|
||||
return {
|
||||
title: translate('Ratings'),
|
||||
title: translate('TmdbRating'),
|
||||
iconName: icons.HEART,
|
||||
label: `${(props.ratings.tmdb.value * 10).toFixed()}%`
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'imdbRating' && !!props.ratings.imdb) {
|
||||
return {
|
||||
title: translate('ImdbRating'),
|
||||
iconName: icons.IMDB,
|
||||
label: `${(props.ratings.imdb.value).toFixed(1)}`
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'certification') {
|
||||
return {
|
||||
title: translate('Certification'),
|
||||
@@ -84,14 +102,6 @@ function getInfoRowProps(row, props) {
|
||||
label: props.certification
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'studio') {
|
||||
return {
|
||||
title: translate('Studio'),
|
||||
iconName: icons.STUDIO,
|
||||
label: props.studio
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function DiscoverMovieOverviewInfo(props) {
|
||||
@@ -132,11 +142,12 @@ function DiscoverMovieOverviewInfo(props) {
|
||||
|
||||
DiscoverMovieOverviewInfo.propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showYear: PropTypes.bool.isRequired,
|
||||
showRatings: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showGenres: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
studio: PropTypes.string,
|
||||
year: PropTypes.number,
|
||||
certification: PropTypes.string,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './DiscoverMovieOverviewInfoRow.css';
|
||||
|
||||
function DiscoverMovieOverviewInfoRow(props) {
|
||||
const {
|
||||
title,
|
||||
iconName,
|
||||
label
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.infoRow}
|
||||
title={title}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={iconName}
|
||||
size={14}
|
||||
/>
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DiscoverMovieOverviewInfoRow.propTypes = {
|
||||
title: PropTypes.string,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMovieOverviewInfoRow;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './DiscoverMovieOverviewInfoRow.css';
|
||||
|
||||
interface DiscoverMovieOverviewInfoRowProps {
|
||||
title?: string;
|
||||
iconName?: IconDefinition;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function DiscoverMovieOverviewInfoRow(
|
||||
props: DiscoverMovieOverviewInfoRowProps
|
||||
) {
|
||||
const { title, iconName, label } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.infoRow} title={title}>
|
||||
<Icon className={styles.icon} name={iconName} size={14} />
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiscoverMovieOverviewInfoRow;
|
||||
@@ -65,7 +65,8 @@ class DiscoverMovieOverviews extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter
|
||||
jumpToCharacter,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -75,13 +76,17 @@ class DiscoverMovieOverviews extends Component {
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid();
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId') ||
|
||||
prevProps.overviewOptions !== overviewOptions
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
this.state = {
|
||||
size: props.size,
|
||||
showStudio: props.showStudio,
|
||||
showCertification: props.showCertification,
|
||||
showRatings: props.showRatings,
|
||||
showYear: props.showYear,
|
||||
showStudio: props.showStudio,
|
||||
showGenres: props.showGenres,
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showCertification: props.showCertification,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -58,11 +59,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
size,
|
||||
showStudio,
|
||||
showYear,
|
||||
showRatings,
|
||||
showCertification,
|
||||
showStudio,
|
||||
showGenres,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showCertification,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -74,26 +76,30 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showStudio !== prevProps.showStudio) {
|
||||
state.showStudio = showStudio;
|
||||
}
|
||||
|
||||
if (showYear !== prevProps.showYear) {
|
||||
state.showYear = showYear;
|
||||
}
|
||||
|
||||
if (showRatings !== prevProps.showRatings) {
|
||||
state.showRatings = showRatings;
|
||||
}
|
||||
|
||||
if (showCertification !== prevProps.showCertification) {
|
||||
state.showCertification = showCertification;
|
||||
if (showStudio !== prevProps.showStudio) {
|
||||
state.showStudio = showStudio;
|
||||
}
|
||||
|
||||
if (showGenres !== prevProps.showGenres) {
|
||||
state.showGenres = showGenres;
|
||||
}
|
||||
|
||||
if (showTmdbRating !== prevProps.showTmdbRating) {
|
||||
state.showTmdbRating = showTmdbRating;
|
||||
}
|
||||
|
||||
if (showImdbRating !== prevProps.showImdbRating) {
|
||||
state.showImdbRating = showImdbRating;
|
||||
}
|
||||
|
||||
if (showCertification !== prevProps.showCertification) {
|
||||
state.showCertification = showCertification;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -142,11 +148,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
const {
|
||||
size,
|
||||
showStudio,
|
||||
showCertification,
|
||||
showRatings,
|
||||
showYear,
|
||||
showStudio,
|
||||
showGenres,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showCertification,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -209,12 +216,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowGenres')}</FormLabel>
|
||||
<FormLabel>{translate('ShowYear')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showGenres"
|
||||
value={showGenres}
|
||||
name="showYear"
|
||||
value={showYear}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -231,23 +238,34 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowYear')}</FormLabel>
|
||||
<FormLabel>{translate('ShowGenres')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showYear"
|
||||
value={showYear}
|
||||
name="showGenres"
|
||||
value={showGenres}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowRatings')}</FormLabel>
|
||||
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showRatings"
|
||||
value={showRatings}
|
||||
name="showTmdbRating"
|
||||
value={showTmdbRating}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showImdbRating"
|
||||
value={showImdbRating}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -279,11 +297,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
DiscoverMovieOverviewOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showYear: PropTypes.bool.isRequired,
|
||||
showRatings: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showGenres: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
@@ -86,6 +89,10 @@ class DiscoverMoviePoster extends Component {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
ratings,
|
||||
isExisting,
|
||||
isExcluded,
|
||||
isSelected,
|
||||
@@ -192,18 +199,39 @@ class DiscoverMoviePoster extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{
|
||||
showTitle &&
|
||||
<div className={styles.title} title={title}>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
{showTitle ?
|
||||
<div className={styles.title} title={title}>
|
||||
{title}
|
||||
</div> :
|
||||
null}
|
||||
|
||||
{showTmdbRating && !!ratings.tmdb ? (
|
||||
<div className={styles.title}>
|
||||
<TmdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showImdbRating && !!ratings.imdb ? (
|
||||
<div className={styles.title}>
|
||||
<ImdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showRottenTomatoesRating && !!ratings.rottenTomatoes ? (
|
||||
<div className={styles.title}>
|
||||
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DiscoverMoviePosterInfo
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
movieRuntimeFormat={movieRuntimeFormat}
|
||||
ratings={ratings}
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -243,6 +271,10 @@ DiscoverMoviePoster.propTypes = {
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
@@ -23,7 +25,10 @@ function DiscoverMoviePosterInfo(props) {
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
movieRuntimeFormat
|
||||
movieRuntimeFormat,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = props;
|
||||
|
||||
if (sortKey === 'status' && status) {
|
||||
@@ -112,10 +117,26 @@ function DiscoverMoviePosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'ratings' && ratings) {
|
||||
if (!showTmdbRating && sortKey === 'tmdbRating' && !!ratings.tmdb) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<TmdbRating ratings={ratings} />
|
||||
<TmdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showImdbRating && sortKey === 'imdbRating' && !!ratings.imdb) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<ImdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showRottenTomatoesRating && sortKey === 'rottenTomatoesRating' && !!ratings.rottenTomatoes) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -136,7 +157,10 @@ DiscoverMoviePosterInfo.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMoviePosterInfo;
|
||||
|
||||
@@ -36,15 +36,15 @@ function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = posterOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
@@ -52,8 +52,42 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTmdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showImdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showRottenTomatoesRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'studio':
|
||||
case 'inCinemas':
|
||||
case 'digitalRelease':
|
||||
case 'physicalRelease':
|
||||
case 'runtime':
|
||||
case 'certification':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'tmdbRating':
|
||||
if (!showTmdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'imdbRating':
|
||||
if (!showImdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'rottenTomatoesRating':
|
||||
if (!showRottenTomatoesRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
@@ -182,7 +216,10 @@ class DiscoverMoviePosters extends Component {
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
showTitle
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = posterOptions;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
@@ -208,6 +245,9 @@ class DiscoverMoviePosters extends Component {
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
showTitle={showTitle}
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
|
||||
@@ -45,6 +45,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
this.state = {
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -55,6 +58,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
const {
|
||||
size,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -70,6 +76,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showTmdbRating !== prevProps.showTmdbRating) {
|
||||
state.showTmdbRating = showTmdbRating;
|
||||
}
|
||||
|
||||
if (showImdbRating !== prevProps.showImdbRating) {
|
||||
state.showImdbRating = showImdbRating;
|
||||
}
|
||||
|
||||
if (showRottenTomatoesRating !== prevProps.showRottenTomatoesRating) {
|
||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -119,6 +137,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
const {
|
||||
size,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -191,6 +212,42 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTmdbRating"
|
||||
value={showTmdbRating}
|
||||
helpText={translate('ShowTmdbRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showImdbRating"
|
||||
value={showImdbRating}
|
||||
helpText={translate('ShowImdbRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowRottenTomatoesRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showRottenTomatoesRating"
|
||||
value={showRottenTomatoesRating}
|
||||
helpText={translate('ShowRottenTomatoesRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -209,6 +266,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
DiscoverMoviePosterOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.ratings,
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.runtime {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'collection': string;
|
||||
'digitalRelease': string;
|
||||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'isPopular': string;
|
||||
'isRecommendation': string;
|
||||
@@ -14,11 +15,12 @@ interface CssExports {
|
||||
'originalLanguage': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'ratings': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sortTitle': string;
|
||||
'status': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.ratings,
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.runtime {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CssExports {
|
||||
'exclusionIcon': string;
|
||||
'externalLinks': string;
|
||||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'isPopular': string;
|
||||
'isRecommendation': string;
|
||||
@@ -19,12 +20,13 @@ interface CssExports {
|
||||
'originalLanguage': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'ratings': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sortTitle': string;
|
||||
'status': string;
|
||||
'statusIcon': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import ImportListListConnector from 'Components/ImportListListConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
@@ -196,10 +198,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'inCinemas') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -207,10 +210,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -218,10 +222,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -253,15 +258,35 @@ class DiscoverMovieRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
{ratings.tmdb ? <TmdbRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'imdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{ratings.imdb ? <ImdbRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'rottenTomatoesRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{ratings.rottenTomatoes ? <RottenTomatoRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
interface AuthenticationRequiredModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModal({
|
||||
isOpen,
|
||||
}: AuthenticationRequiredModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
authenticationMethodOptions,
|
||||
authenticationRequiredOptions,
|
||||
} from 'Settings/General/SecuritySettings';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchGeneralSettings,
|
||||
saveGeneralSettings,
|
||||
setGeneralSettingsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
const selector = createSettingsSectionSelector(SECTION);
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModalContent() {
|
||||
const { isPopulated, error, isSaving, settings } = useSelector(selector);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
} = settings;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGeneralSettings());
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(args: InputChanged) => {
|
||||
// @ts-expect-error Actions aren't typed
|
||||
dispatch(setGeneralSettingsValue(args));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const authenticationEnabled =
|
||||
authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
useEffect(() => {
|
||||
if (isSaving || !wasSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchStatus());
|
||||
}, [isSaving, wasSaving, dispatch]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
dispatch(saveGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent showCloseButton={false} onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AuthenticationRequired')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert className={styles.authRequiredAlert} kind={kinds.WARNING}>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={
|
||||
authenticationMethod.value === 'none'
|
||||
? translate('AuthenticationMethodHelpTextWarning')
|
||||
: undefined
|
||||
}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning={
|
||||
username?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredUsernameHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning={
|
||||
password?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredPasswordHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
helpTextWarning={
|
||||
passwordConfirmation?.value
|
||||
? undefined
|
||||
: translate(
|
||||
'AuthenticationRequiredPasswordConfirmationHelpTextWarning'
|
||||
)
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
function useCurrentPage() {
|
||||
const history = useHistory();
|
||||
|
||||
return history.action === 'POP';
|
||||
}
|
||||
|
||||
export default useCurrentPage;
|
||||
@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
const [isOpen, setIsOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//
|
||||
// Brands
|
||||
import {
|
||||
faImdb as fabImdb
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
//
|
||||
// Regular
|
||||
|
||||
import {
|
||||
faBookmark as farBookmark,
|
||||
faCalendar as farCalendar,
|
||||
@@ -175,10 +179,12 @@ export const HEART = fasHeart;
|
||||
export const HISTORY = fasHistory;
|
||||
export const HOUSEKEEPING = fasHome;
|
||||
export const IGNORE = fasTimesCircle;
|
||||
export const IMDB = fabImdb;
|
||||
export const IN_CINEMAS = fasTicketAlt;
|
||||
export const INFO = fasInfoCircle;
|
||||
export const INTERACTIVE = fasUser;
|
||||
export const KEYBOARD = farKeyboard;
|
||||
export const LANGUAGE = fasLanguage;
|
||||
export const LOGOUT = fasSignOutAlt;
|
||||
export const MANAGE = fasListCheck;
|
||||
export const MEDIA_INFO = farFileInvoice;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -41,7 +41,7 @@ class RecentFolderRow extends Component {
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowCell>{folder}</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector date={lastUsed} />
|
||||
<RelativeDateCell date={lastUsed} />
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
|
||||
@@ -18,12 +18,17 @@
|
||||
.leftButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 0 50%;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.leftButtons {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
justify-content: flex-end;
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
@@ -37,6 +42,7 @@
|
||||
composes: select from '~Components/Form/SelectInput.css';
|
||||
|
||||
margin-right: 10px;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -49,10 +55,12 @@
|
||||
.leftButtons,
|
||||
.rightButtons {
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.leftButtons {
|
||||
align-items: flex-start;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
|
||||
@@ -702,7 +702,7 @@ function InteractiveImportModalContent(
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={'all'}
|
||||
name="all"
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
@@ -710,7 +710,7 @@ function InteractiveImportModalContent(
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={'new'}
|
||||
name="new"
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
@@ -790,7 +790,7 @@ function InteractiveImportModalContent(
|
||||
<SelectInput
|
||||
className={styles.bulkSelect}
|
||||
name="select"
|
||||
value={'select'}
|
||||
value="select"
|
||||
values={bulkSelectOptions}
|
||||
isDisabled={!selectedIds.length}
|
||||
onChange={onSelectModalSelect}
|
||||
|
||||
@@ -17,7 +17,7 @@ import Language from 'Language/Language';
|
||||
import IndexerFlags from 'Movie/IndexerFlags';
|
||||
import Movie from 'Movie/Movie';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import {
|
||||
@@ -322,7 +322,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
{showLanguagePlaceholder && <InteractiveImportRowCellPlaceholder />}
|
||||
|
||||
{!showLanguagePlaceholder && !!languages && (
|
||||
<MovieLanguage className={styles.label} languages={languages} />
|
||||
<MovieLanguages className={styles.label} languages={languages} />
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) {
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<SelectLanguageModalContent
|
||||
languageIds={languageIds}
|
||||
modalTitle={modalTitle}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectMovieModalTableHeader from './SelectMovieModalTableHeader';
|
||||
import SelectMovieRow from './SelectMovieRow';
|
||||
@@ -63,19 +64,20 @@ interface RowItemData {
|
||||
onMovieSelect(movieId: number): void;
|
||||
}
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, columns, onMovieSelect } = data;
|
||||
const movie = index >= items.length ? null : items[index];
|
||||
|
||||
if (index >= items.length) {
|
||||
const handlePress = useCallback(() => {
|
||||
if (movie?.id) {
|
||||
onMovieSelect(movie.id);
|
||||
}
|
||||
}, [movie?.id, onMovieSelect]);
|
||||
|
||||
if (movie == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const movie = items[index];
|
||||
|
||||
return (
|
||||
<VirtualTableRowButton
|
||||
style={{
|
||||
@@ -83,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
justifyContent: 'space-between',
|
||||
...style,
|
||||
}}
|
||||
onPress={() => onMovieSelect(movie.id)}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<SelectMovieRow
|
||||
id={movie.id}
|
||||
@@ -96,7 +98,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
/>
|
||||
</VirtualTableRowButton>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
const { modalTitle, onMovieSelect, onModalClose } = props;
|
||||
@@ -161,18 +163,21 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
[allMovies, onMovieSelect]
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const sorted = [...allMovies].sort((a, b) =>
|
||||
a.sortTitle.localeCompare(b.sortTitle)
|
||||
);
|
||||
const sortedMovies = useMemo(
|
||||
() => [...allMovies].sort(sortByProp('sortTitle')),
|
||||
[allMovies]
|
||||
);
|
||||
|
||||
return sorted.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.tmdbId.toString().includes(filter) ||
|
||||
item.imdbId?.includes(filter)
|
||||
);
|
||||
}, [allMovies, filter]);
|
||||
const items = useMemo(
|
||||
() =>
|
||||
sortedMovies.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.tmdbId.toString().includes(filter) ||
|
||||
item.imdbId?.includes(filter)
|
||||
),
|
||||
[sortedMovies, filter]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -192,9 +197,9 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
ref={scrollerRef}
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
ref={scrollerRef}
|
||||
>
|
||||
<SelectMovieModalTableHeader columns={columns} />
|
||||
<List<RowItemData>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import { align } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
function InteractiveSearchFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearchFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearchFilterMenu;
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import InteractiveSearchFilterMenu from './InteractiveSearchFilterMenu';
|
||||
|
||||
function createMapStateToProps(appState) {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('releases'),
|
||||
(releases) => {
|
||||
return {
|
||||
...releases
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setReleasesFilter({ selectedFilterKey }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchFilterMenuConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearchFilterMenu
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchFilterMenuConnector);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user