mirror of
https://github.com/Radarr/Radarr.git
synced 2024-10-03 22:57:18 +02:00
New: Collections View
This commit is contained in:
parent
a158e008e9
commit
f80272a659
@ -223,7 +223,7 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
loader: 'url-loader',
|
loader: 'url-loader',
|
||||||
options: {
|
options: {
|
||||||
limit: 10240,
|
limit: 24096,
|
||||||
mimetype: 'application/font-woff',
|
mimetype: 'application/font-woff',
|
||||||
emitFile: false,
|
emitFile: false,
|
||||||
name: 'Content/Fonts/[name].[ext]'
|
name: 'Content/Fonts/[name].[ext]'
|
||||||
@ -233,11 +233,12 @@ module.exports = (env) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
|
limit: 24096,
|
||||||
emitFile: false,
|
emitFile: false,
|
||||||
name: 'Content/Fonts/[name].[ext]'
|
name: 'Content/Fonts/[name].[ext]'
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
|
|||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
|
import CollectionConnector from 'Collection/CollectionConnector';
|
||||||
import NotFound from 'Components/NotFound';
|
import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||||
@ -72,6 +73,11 @@ function AppRoutes(props) {
|
|||||||
component={AddNewMovieConnector}
|
component={AddNewMovieConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/collections"
|
||||||
|
component={CollectionConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/add/import"
|
path="/add/import"
|
||||||
component={ImportMovies}
|
component={ImportMovies}
|
||||||
|
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
|
||||||
|
|
||||||
|
function AddNewCollectionMovieModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddNewCollectionMovieModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewCollectionMovieModal;
|
@ -0,0 +1,68 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieLabel {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieContainer {
|
||||||
|
composes: container from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
flex: 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieInput {
|
||||||
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
@add-mixin truncate;
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
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 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 MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddNewCollectionMovieModalContent.css';
|
||||||
|
|
||||||
|
class AddNewCollectionMovieModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onQualityProfileIdChange = ({ value }) => {
|
||||||
|
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
this.props.onAddMoviePress();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
isAdding,
|
||||||
|
folder,
|
||||||
|
tags,
|
||||||
|
isSmallScreen,
|
||||||
|
isWindows,
|
||||||
|
onModalClose,
|
||||||
|
onInputChange,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{
|
||||||
|
!title.contains(year) && !!year &&
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...rootFolderPath}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('Monitor')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MOVIE_MONITORED_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...monitor}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...minimumAvailability}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
onChange={this.onQualityProfileIdChange}
|
||||||
|
{...qualityProfileId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...tags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||||
|
<span className={styles.searchForMissingMovieLabel}>
|
||||||
|
{translate('StartSearchForMissingMovie')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchForMissingMovieContainer}
|
||||||
|
className={styles.searchForMissingMovieInput}
|
||||||
|
name="searchForMovie"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...searchForMovie}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={this.onAddMoviePress}
|
||||||
|
>
|
||||||
|
{translate('AddMovie')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModalContent.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
overview: PropTypes.string,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
addError: PropTypes.object,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onAddMoviePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewCollectionMovieModalContent;
|
@ -0,0 +1,121 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
createCollectionSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
|
(discoverMovieState, collection, dimensions, systemStatus) => {
|
||||||
|
const {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
pendingChanges
|
||||||
|
} = discoverMovieState;
|
||||||
|
|
||||||
|
const collectionDefaults = {
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
monitor: 'movieOnly',
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
searchForMovie: collection.searchOnAdd,
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings
|
||||||
|
} = selectSettings(collectionDefaults, pendingChanges, addError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings,
|
||||||
|
isWindows: systemStatus.isWindows,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
addMovie,
|
||||||
|
setMovieCollectionValue
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddNewCollectionMovieModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setMovieCollectionValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie,
|
||||||
|
tags
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.addMovie({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
minimumAvailability: minimumAvailability.value,
|
||||||
|
searchForMovie: searchForMovie.value,
|
||||||
|
tags: tags.value
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onModalClose(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddNewMovieModalContent
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onAddMoviePress={this.onAddMoviePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModalContentConnector.propTypes = {
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
addMovie: PropTypes.func.isRequired,
|
||||||
|
setMovieCollectionValue: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);
|
403
frontend/src/Collection/Collection.js
Normal file
403
frontend/src/Collection/Collection.js
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import styles from 'Movie/Index/MovieIndex.css';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import CollectionFooter from './CollectionFooter';
|
||||||
|
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
|
||||||
|
import CollectionSortMenu from './Menus/CollectionSortMenu';
|
||||||
|
import NoCollection from './NoCollection';
|
||||||
|
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
||||||
|
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
||||||
|
|
||||||
|
function getViewComponent(view) {
|
||||||
|
return CollectionOverviewsConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Collection extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
scroller: null,
|
||||||
|
jumpBarItems: { order: [] },
|
||||||
|
jumpToCharacter: null,
|
||||||
|
isPosterOptionsModalOpen: false,
|
||||||
|
isOverviewOptionsModalOpen: false,
|
||||||
|
isConfirmSearchModalOpen: false,
|
||||||
|
searchType: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setJumpBarItems();
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
|
) {
|
||||||
|
this.setJumpBarItems();
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.jumpToCharacter != null) {
|
||||||
|
this.setState({ jumpToCharacter: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setScrollerRef = (ref) => {
|
||||||
|
this.setState({ scroller: ref });
|
||||||
|
};
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
if (this.state.allUnselected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedState() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const newSelectedState = {};
|
||||||
|
|
||||||
|
items.forEach((collection) => {
|
||||||
|
const isItemSelected = selectedState[collection.id];
|
||||||
|
|
||||||
|
if (isItemSelected) {
|
||||||
|
newSelectedState[collection.id] = isItemSelected;
|
||||||
|
} else {
|
||||||
|
newSelectedState[collection.id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||||
|
const newStateCount = Object.keys(newSelectedState).length;
|
||||||
|
let isAllSelected = false;
|
||||||
|
let isAllUnselected = false;
|
||||||
|
|
||||||
|
if (selectedCount === 0) {
|
||||||
|
isAllUnselected = true;
|
||||||
|
} else if (selectedCount === newStateCount) {
|
||||||
|
isAllSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||||
|
}
|
||||||
|
|
||||||
|
setJumpBarItems() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Reset if not sorting by sortTitle
|
||||||
|
if (sortKey !== 'sortTitle') {
|
||||||
|
this.setState({ jumpBarItems: { order: [] } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters = _.reduce(items, (acc, item) => {
|
||||||
|
let char = item.sortTitle.charAt(0);
|
||||||
|
|
||||||
|
if (!isNaN(char)) {
|
||||||
|
char = '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char in acc) {
|
||||||
|
acc[char] = acc[char] + 1;
|
||||||
|
} else {
|
||||||
|
acc[char] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const order = Object.keys(characters).sort();
|
||||||
|
|
||||||
|
// Reverse if sorting descending
|
||||||
|
if (sortDirection === sortDirections.DESCENDING) {
|
||||||
|
order.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const jumpBarItems = {
|
||||||
|
characters,
|
||||||
|
order
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ jumpBarItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOverviewOptionsPress = () => {
|
||||||
|
this.setState({ isOverviewOptionsModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOverviewOptionsModalClose = () => {
|
||||||
|
this.setState({ isOverviewOptionsModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
|
this.setState({ jumpToCharacter });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllPress = () => {
|
||||||
|
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRefreshMovieCollectionsPress = () => {
|
||||||
|
this.props.onRefreshMovieCollectionsPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey, 'id');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = (changes) => {
|
||||||
|
this.props.onUpdateSelectedPress({
|
||||||
|
collectionIds: this.getSelectedIds(),
|
||||||
|
...changes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
totalItems,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
view,
|
||||||
|
onSortSelect,
|
||||||
|
onFilterSelect,
|
||||||
|
onScroll,
|
||||||
|
isRefreshingCollections,
|
||||||
|
isSaving,
|
||||||
|
isAdding,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
scroller,
|
||||||
|
jumpBarItems,
|
||||||
|
jumpToCharacter,
|
||||||
|
isOverviewOptionsModalOpen,
|
||||||
|
selectedState,
|
||||||
|
allSelected,
|
||||||
|
allUnselected
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedMovieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
const ViewComponent = getViewComponent(view);
|
||||||
|
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||||
|
const hasNoCollection = !totalItems;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RefreshCollections')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshingCollections}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onPress={this.onRefreshMovieCollectionsPress}
|
||||||
|
/>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||||
|
iconName={icons.CHECK_SQUARE}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onPress={this.onSelectAllPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection
|
||||||
|
alignContent={align.RIGHT}
|
||||||
|
collapseButtons={false}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
view === 'overview' ?
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.OVERVIEW}
|
||||||
|
onPress={this.onOverviewOptionsPress}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
(view === 'posters' || view === 'overview') &&
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
}
|
||||||
|
|
||||||
|
<CollectionSortMenu
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onSortSelect={onSortSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CollectionFilterMenu
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<div className={styles.pageContentBodyWrapper}>
|
||||||
|
<PageContentBody
|
||||||
|
registerScroller={this.setScrollerRef}
|
||||||
|
className={styles.contentBody}
|
||||||
|
innerClassName={styles[`${view}InnerContentBody`]}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>
|
||||||
|
{translate('UnableToLoadCollections')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded &&
|
||||||
|
<div className={styles.contentBodyContainer}>
|
||||||
|
<ViewComponent
|
||||||
|
scroller={scroller}
|
||||||
|
items={items}
|
||||||
|
filters={filters}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
jumpToCharacter={jumpToCharacter}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
selectedState={selectedState}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!error && isPopulated && !items.length &&
|
||||||
|
<NoCollection totalItems={totalItems} />
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded && !!jumpBarItems.order.length &&
|
||||||
|
<PageJumpBar
|
||||||
|
items={jumpBarItems}
|
||||||
|
onItemPress={this.onJumpBarItemPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded &&
|
||||||
|
<CollectionFooter
|
||||||
|
selectedIds={selectedMovieIds}
|
||||||
|
isSaving={isSaving}
|
||||||
|
isAdding={isAdding}
|
||||||
|
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CollectionOverviewOptionsModal
|
||||||
|
isOpen={isOverviewOptionsModalOpen}
|
||||||
|
onModalClose={this.onOverviewOptionsModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
totalItems: PropTypes.number.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
|
isRefreshingCollections: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onScroll: PropTypes.func.isRequired,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onRefreshMovieCollectionsPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Collection;
|
108
frontend/src/Collection/CollectionConnector.js
Normal file
108
frontend/src/Collection/CollectionConnector.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Collection from './Collection';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCollectionClientSideCollectionItemsSelector('movieCollections'),
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_COLLECTIONS),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(
|
||||||
|
collections,
|
||||||
|
isRefreshingCollections,
|
||||||
|
dimensionsState
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...collections,
|
||||||
|
isRefreshingCollections,
|
||||||
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
dispatchFetchRootFolders() {
|
||||||
|
dispatch(fetchRootFolders());
|
||||||
|
},
|
||||||
|
onUpdateSelectedPress(payload) {
|
||||||
|
dispatch(saveMovieCollections(payload));
|
||||||
|
},
|
||||||
|
onSortSelect(sortKey) {
|
||||||
|
dispatch(setMovieCollectionsSort({ sortKey }));
|
||||||
|
},
|
||||||
|
onFilterSelect(selectedFilterKey) {
|
||||||
|
dispatch(setMovieCollectionsFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
onRefreshMovieCollectionsPress() {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.REFRESH_COLLECTIONS
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
this.props.dispatchFetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onScroll = ({ scrollTop }) => {
|
||||||
|
scrollPositions.movieCollections = scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = (payload) => {
|
||||||
|
this.props.onUpdateSelectedPress(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Collection
|
||||||
|
{...this.props}
|
||||||
|
onViewSelect={this.onViewSelect}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionConnector.propTypes = {
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withScrollPosition(
|
||||||
|
connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector),
|
||||||
|
'movieCollections'
|
||||||
|
);
|
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.items,
|
||||||
|
(state) => state.movieCollections.filterBuilderProps,
|
||||||
|
(sectionItems, filterBuilderProps) => {
|
||||||
|
return {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps,
|
||||||
|
customFilterType: 'movieCollections'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetFilter: setMovieCollectionsFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
56
frontend/src/Collection/CollectionFooter.css
Normal file
56
frontend/src/Collection/CollectionFooter.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
.inputContainer {
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addSelectedButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excludeSelectedButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-left: 25px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.inputContainer {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedMovieLabel {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
163
frontend/src/Collection/CollectionFooter.js
Normal file
163
frontend/src/Collection/CollectionFooter.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CollectionFooterLabel from './CollectionFooterLabel';
|
||||||
|
import styles from './CollectionFooter.css';
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
class CollectionFooter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
monitor: NO_CHANGE,
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
destinationRootFolder: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newState = {};
|
||||||
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
|
this.setState({
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
monitor: NO_CHANGE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(newState)) {
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.setState({ [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = () => {
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
monitored
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
if (monitored !== NO_CHANGE) {
|
||||||
|
changes.monitored = monitored === 'monitored';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor !== NO_CHANGE) {
|
||||||
|
changes.monitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onUpdateSelectedPress(changes);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedIds,
|
||||||
|
isSaving
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
monitor
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const monitoredOptions = [
|
||||||
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentFooter>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorCollection')}
|
||||||
|
isSaving={isSaving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
name="monitored"
|
||||||
|
value={monitored}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorMovies')}
|
||||||
|
isSaving={isSaving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
name="monitor"
|
||||||
|
value={monitor}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<div className={styles.buttonContainerContent}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('CollectionsSelectedInterp', [selectedCount])}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<div>
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addSelectedButton}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!selectedCount || isSaving}
|
||||||
|
onPress={this.onUpdateSelectedPress}
|
||||||
|
>
|
||||||
|
{translate('UpdateSelected')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFooter.propTypes = {
|
||||||
|
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFooter;
|
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savingIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './CollectionFooterLabel.css';
|
||||||
|
|
||||||
|
function CollectionFooterLabel(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
isSaving
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
{
|
||||||
|
isSaving &&
|
||||||
|
<SpinnerIcon
|
||||||
|
className={styles.savingIcon}
|
||||||
|
name={icons.SPINNER}
|
||||||
|
isSpinning={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFooterLabel.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionFooterLabel.defaultProps = {
|
||||||
|
className: styles.label
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFooterLabel;
|
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCollectionSelector(),
|
||||||
|
createAllMoviesSelector(),
|
||||||
|
(
|
||||||
|
collection,
|
||||||
|
allMovies
|
||||||
|
) => {
|
||||||
|
// If a movie is deleted this selector may fire before the parent
|
||||||
|
// selecors, which will result in an undefined movie, if that happens
|
||||||
|
// we want to return early here and again in the render function to avoid
|
||||||
|
// trying to show a movie that has no information available.
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let allGenres = [];
|
||||||
|
let libraryMovies = 0;
|
||||||
|
|
||||||
|
collection.movies.forEach((movie) => {
|
||||||
|
allGenres = allGenres.concat(movie.genres);
|
||||||
|
|
||||||
|
if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) {
|
||||||
|
libraryMovies++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...collection,
|
||||||
|
genres: Array.from(new Set(allGenres)).slice(0, 3),
|
||||||
|
missingMovies: collection.movies.length - libraryMovies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionItemConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
component: ItemComponent,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemComponent
|
||||||
|
{...otherProps}
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionItemConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
component: PropTypes.elementType.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(CollectionItemConnector);
|
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditCollectionModalContentConnector from './EditCollectionModalContentConnector';
|
||||||
|
|
||||||
|
function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditCollectionModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCollectionModal;
|
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditCollectionModal from './EditCollectionModal';
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearPendingChanges
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCollectionModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'movieCollections' });
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCollectionModal
|
||||||
|
{...this.props}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);
|
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
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 } from 'Helpers/Props';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditCollectionModalContent.css';
|
||||||
|
|
||||||
|
class EditCollectionModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
const {
|
||||||
|
onSavePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onSavePress(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
images,
|
||||||
|
overview,
|
||||||
|
item,
|
||||||
|
isSaving,
|
||||||
|
onInputChange,
|
||||||
|
onModalClose,
|
||||||
|
isSmallScreen,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
// Id,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('Edit')} - {title}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="monitored"
|
||||||
|
helpText={translate('MonitoredCollectionHelpText')}
|
||||||
|
{...monitored}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
{...minimumAvailability}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
{...qualityProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Folder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
{...rootFolderPath}
|
||||||
|
includeMissingValue={true}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="searchOnAdd"
|
||||||
|
helpText={translate('SearchOnAddCollectionHelpText')}
|
||||||
|
{...searchOnAdd}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalContent.propTypes = {
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isPathChanging: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCollectionModalContent;
|
@ -0,0 +1,119 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import EditCollectionModalContent from './EditCollectionModalContent';
|
||||||
|
|
||||||
|
function createIsPathChangingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.pendingChanges,
|
||||||
|
createCollectionSelector(),
|
||||||
|
(pendingChanges, collection) => {
|
||||||
|
const rootFolderPath = pendingChanges.rootFolderPath;
|
||||||
|
|
||||||
|
if (rootFolderPath == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection.rootFolderPath !== rootFolderPath;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
createCollectionSelector(),
|
||||||
|
createIsPathChangingSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(moviesState, collection, isPathChanging, dimensions) => {
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
pendingChanges
|
||||||
|
} = moviesState;
|
||||||
|
|
||||||
|
const movieSettings = {
|
||||||
|
monitored: collection.monitored,
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
searchOnAdd: collection.searchOnAdd
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: collection.title,
|
||||||
|
images: collection.images,
|
||||||
|
overview: collection.overview,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isPathChanging,
|
||||||
|
originalPath: collection.path,
|
||||||
|
item: settings.settings,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetMovieCollectionValue: setMovieCollectionValue,
|
||||||
|
dispatchSaveMovieCollection: saveMovieCollection
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCollectionModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.dispatchSetMovieCollectionValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.dispatchSaveMovieCollection({
|
||||||
|
id: this.props.collectionId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCollectionModalContent
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onMoveMoviePress={this.onMoveMoviePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalContentConnector.propTypes = {
|
||||||
|
collectionId: PropTypes.number,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
dispatchSetMovieCollectionValue: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveMovieCollection: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector);
|
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import { align } from 'Helpers/Props';
|
||||||
|
|
||||||
|
function CollectionFilterMenu(props) {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
isDisabled,
|
||||||
|
onFilterSelect
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={CollectionFilterModalConnector}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFilterMenu.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionFilterMenu.defaultProps = {
|
||||||
|
showCustomFilters: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFilterMenu;
|
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import SortMenu from 'Components/Menu/SortMenu';
|
||||||
|
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||||
|
import { align, sortDirections } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function CollectionSortMenu(props) {
|
||||||
|
const {
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
isDisabled,
|
||||||
|
onSortSelect
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortMenu
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
>
|
||||||
|
<MenuContent>
|
||||||
|
<SortMenuItem
|
||||||
|
name="sortTitle"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('Title')}
|
||||||
|
</SortMenuItem>
|
||||||
|
</MenuContent>
|
||||||
|
</SortMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionSortMenu.propTypes = {
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onSortSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionSortMenu;
|
11
frontend/src/Collection/NoCollection.css
Normal file
11
frontend/src/Collection/NoCollection.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.message {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
52
frontend/src/Collection/NoCollection.js
Normal file
52
frontend/src/Collection/NoCollection.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './NoCollection.css';
|
||||||
|
|
||||||
|
function NoCollection(props) {
|
||||||
|
const { totalItems } = props;
|
||||||
|
|
||||||
|
if (totalItems > 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.message}>
|
||||||
|
{translate('AllCollectionsHiddenDueToFilter')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.message}>
|
||||||
|
{translate('NoCollections')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
to="/add/import"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
{translate('ImportExistingMovies')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
to="/add/new"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
{translate('AddNewMovie')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoCollection.propTypes = {
|
||||||
|
totalItems: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoCollection;
|
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 10px $black;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 100ms linear 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 100ms linear 100ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.posterContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
padding: 5px;
|
||||||
|
color: $offWhite;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
background-color: #fafbfc;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #707070;
|
||||||
|
color: $white;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $radarrYellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorSelect {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalLinks {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 25px;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
||||||
|
import styles from './CollectionMovie.css';
|
||||||
|
|
||||||
|
class CollectionMovie extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false,
|
||||||
|
isEditMovieModalOpen: false,
|
||||||
|
isNewAddMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditMoviePress = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditMovieModalClose = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMovieModalClose = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onPosterLoad = () => {
|
||||||
|
if (this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onPosterLoadError = () => {
|
||||||
|
if (!this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
year,
|
||||||
|
tmdbId,
|
||||||
|
images,
|
||||||
|
monitored,
|
||||||
|
hasFile,
|
||||||
|
folder,
|
||||||
|
isAvailable,
|
||||||
|
isExistingMovie,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
detailedProgressBar,
|
||||||
|
onMonitorTogglePress,
|
||||||
|
collectionId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditMovieModalOpen,
|
||||||
|
isNewAddMovieModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const linkProps = id ? { to: `/movie/${tmdbId}` } : { onPress: this.onAddMoviePress };
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
{
|
||||||
|
isExistingMovie &&
|
||||||
|
<div className={styles.editorSelect}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
style={elementStyle}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={this.onPosterLoadError}
|
||||||
|
onLoad={this.onPosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.overlayTitle}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<div className={styles.overlayStatus}>
|
||||||
|
<MovieIndexProgressBar
|
||||||
|
monitored={monitored}
|
||||||
|
hasFile={hasFile}
|
||||||
|
status={status}
|
||||||
|
bottomRadius={true}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
isAvailable={isAvailable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddNewCollectionMovieModal
|
||||||
|
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
title={title}
|
||||||
|
year={year}
|
||||||
|
overview={overview}
|
||||||
|
images={images}
|
||||||
|
folder={folder}
|
||||||
|
onModalClose={this.onAddMovieModalClose}
|
||||||
|
collectionId={collectionId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditMovieModalConnector
|
||||||
|
isOpen={isEditMovieModalOpen}
|
||||||
|
movieId={id}
|
||||||
|
onModalClose={this.onEditMovieModalClose}
|
||||||
|
onDeleteMoviePress={this.onDeleteMoviePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovie.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
hasFile: PropTypes.bool,
|
||||||
|
folder: PropTypes.string,
|
||||||
|
isAvailable: PropTypes.bool,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
isExistingMovie: PropTypes.bool,
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
imdbId: PropTypes.string,
|
||||||
|
youTubeTrailerId: PropTypes.string,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionMovie;
|
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
|
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import CollectionMovie from './CollectionMovie';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createCollectionExistingMovieSelector(),
|
||||||
|
(dimensions, existingMovie) => {
|
||||||
|
return {
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
isExistingMovie: !!existingMovie,
|
||||||
|
...existingMovie
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleMovieMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionMovieConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleMovieMonitored({
|
||||||
|
movieId: this.props.id,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollectionMovie
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovieConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
toggleMovieMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieConnector);
|
132
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
132
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorSelect {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleMonitoredContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moviesContainer {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 1px;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaults {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
margin: 5px 10px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path,
|
||||||
|
.status,
|
||||||
|
.genres,
|
||||||
|
.qualityProfileName {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 25px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.navigationButtons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleMonitoredContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
321
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
321
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Slider from 'react-slick';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import { icons, sizes } from 'Helpers/Props';
|
||||||
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import fonts from 'Styles/Variables/fonts';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CollectionMovieConnector from './CollectionMovieConnector';
|
||||||
|
import styles from './CollectionOverview.css';
|
||||||
|
|
||||||
|
import 'slick-carousel/slick/slick.css';
|
||||||
|
import 'slick-carousel/slick/slick-theme.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
|
// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row
|
||||||
|
// Less side-effecty than using react-measure.
|
||||||
|
const titleRowHeight = 100;
|
||||||
|
|
||||||
|
function getContentHeight(rowHeight, isSmallScreen) {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionOverview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditCollectionModalOpen: false,
|
||||||
|
isNewAddMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setSliderRef = (ref) => {
|
||||||
|
this.setState({ slider: ref });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditCollectionPress = () => {
|
||||||
|
this.setState({ isEditCollectionModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditCollectionModalClose = () => {
|
||||||
|
this.setState({ isEditCollectionModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMovieModalClose = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = ({ value, shiftKey }) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onSelectedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onSelectedChange({ id, value, shiftKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
rootFolderPath,
|
||||||
|
genres,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
movies,
|
||||||
|
overview,
|
||||||
|
missingMovies,
|
||||||
|
posterHeight,
|
||||||
|
posterWidth,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
isSelected,
|
||||||
|
onMonitorTogglePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
showDetails,
|
||||||
|
showOverview,
|
||||||
|
detailedProgressBar
|
||||||
|
} = this.props.overviewOptions;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditCollectionModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||||
|
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
|
||||||
|
|
||||||
|
const sliderSettings = {
|
||||||
|
arrows: false,
|
||||||
|
dots: false,
|
||||||
|
infinite: false,
|
||||||
|
slidesToShow: 1,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
variableWidth: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.editorSelect}>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
name={id.toString()}
|
||||||
|
value={isSelected}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||||
|
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={isSmallScreen ? 20 : 25}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
title={translate('EditCollection')}
|
||||||
|
onPress={this.onEditCollectionPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.navigationButtons}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ARROW_LEFT}
|
||||||
|
title={translate('ScrollMovies')}
|
||||||
|
onPress={this.state.slider?.slickPrev}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.ARROW_RIGHT}
|
||||||
|
title={translate('ScrollMovies')}
|
||||||
|
onPress={this.state.slider?.slickNext}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
showDetails &&
|
||||||
|
<div className={styles.defaults}>
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.DRIVE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.status}>
|
||||||
|
{`${missingMovies} missing movie(s)`}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.PROFILE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
|
{
|
||||||
|
<QualityProfileNameConnector
|
||||||
|
qualityProfileId={qualityProfileId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.FOLDER}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.path}>
|
||||||
|
{rootFolderPath}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.PROFILE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.genres}>
|
||||||
|
{genres.join(', ')}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showOverview &&
|
||||||
|
<div className={styles.details}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
<TextTruncate
|
||||||
|
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||||
|
text={overview}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.sliderContainer}>
|
||||||
|
<Slider ref={this.setSliderRef} {...sliderSettings}>
|
||||||
|
{movies.map((movie) => (
|
||||||
|
<div className={styles.movie} key={movie.tmdbId}>
|
||||||
|
<CollectionMovieConnector
|
||||||
|
key={movie.tmdbId}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
collectionId={id}
|
||||||
|
{...movie}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditCollectionModalConnector
|
||||||
|
isOpen={isEditCollectionModalOpen}
|
||||||
|
collectionId={id}
|
||||||
|
onModalClose={this.onEditCollectionModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverview.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
|
minimumAvailability: PropTypes.string.isRequired,
|
||||||
|
searchOnAdd: PropTypes.bool.isRequired,
|
||||||
|
rootFolderPath: PropTypes.string.isRequired,
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
missingMovies: PropTypes.number.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
rowHeight: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverview;
|
@ -0,0 +1,55 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import CollectionOverview from './CollectionOverview';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(dimensions) => {
|
||||||
|
return {
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleCollectionMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionOverviewConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleCollectionMonitored({
|
||||||
|
collectionId: this.props.collectionId,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollectionOverview
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewConnector.propTypes = {
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionOverviewConnector);
|
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
&:hover {
|
||||||
|
.content {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalLinks {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import CollectionItemConnector from 'Collection/CollectionItemConnector';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import CollectionOverviewConnector from './CollectionOverviewConnector';
|
||||||
|
import styles from './CollectionOverviews.css';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||||
|
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
||||||
|
|
||||||
|
if (posterSize === 'large') {
|
||||||
|
return maxiumPosterWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posterSize === 'medium') {
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height + 80, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePosterHeight(posterWidth) {
|
||||||
|
return Math.ceil((250 / 170) * posterWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionOverviews extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnCount: 1,
|
||||||
|
posterWidth: 162,
|
||||||
|
posterHeight: 238,
|
||||||
|
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
overviewOptions,
|
||||||
|
jumpToCharacter,
|
||||||
|
scrollTop,
|
||||||
|
isSmallScreen
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight,
|
||||||
|
scrollRestored
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (prevProps.sortKey !== sortKey ||
|
||||||
|
prevProps.overviewOptions !== overviewOptions) {
|
||||||
|
this.calculateGrid(this.state.width, isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||||
|
prevProps.overviewOptions !== overviewOptions)) {
|
||||||
|
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||||
|
this.setState({ scrollRestored: true });
|
||||||
|
this._grid.scrollToPosition({ scrollTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (this._grid && index != null) {
|
||||||
|
|
||||||
|
this._grid.scrollToCell({
|
||||||
|
rowIndex: index,
|
||||||
|
columnIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
const {
|
||||||
|
sortKey,
|
||||||
|
overviewOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||||
|
const posterHeight = calculatePosterHeight(posterWidth);
|
||||||
|
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
overviewOptions,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isSmallScreen,
|
||||||
|
selectedState,
|
||||||
|
onSelectedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const collection = items[rowIndex];
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<CollectionItemConnector
|
||||||
|
key={collection.id}
|
||||||
|
component={CollectionOverviewConnector}
|
||||||
|
sortKey={sortKey}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
overviewOptions={overviewOptions}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
collectionId={collection.id}
|
||||||
|
isSelected={selectedState[collection.id]}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isSmallScreen,
|
||||||
|
scroller,
|
||||||
|
items,
|
||||||
|
selectedState
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={isSmallScreen ? undefined : scroller}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={1}
|
||||||
|
columnWidth={width}
|
||||||
|
rowCount={items.length}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
selectedState={selectedState}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptout={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviews.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
selectedState: PropTypes.object.isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviews;
|
@ -0,0 +1,25 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import CollectionOverviews from './CollectionOverviews';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.overviewOptions,
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(overviewOptions, uiSettings, dimensions) => {
|
||||||
|
return {
|
||||||
|
overviewOptions,
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(CollectionOverviews);
|
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector';
|
||||||
|
|
||||||
|
function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<CollectionOverviewOptionsModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewOptionsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviewOptionsModal;
|
@ -0,0 +1,183 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
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 } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const posterSizeOptions = [
|
||||||
|
{ key: 'small', value: translate('Small') },
|
||||||
|
{ key: 'medium', value: translate('Medium') },
|
||||||
|
{ key: 'large', value: translate('Large') }
|
||||||
|
];
|
||||||
|
|
||||||
|
class CollectionOverviewOptionsModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
detailedProgressBar: props.detailedProgressBar,
|
||||||
|
size: props.size,
|
||||||
|
showDetails: props.showDetails,
|
||||||
|
showOverview: props.showOverview
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showDetails,
|
||||||
|
showOverview
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const state = {};
|
||||||
|
|
||||||
|
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||||
|
state.detailedProgressBar = detailedProgressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size !== prevProps.size) {
|
||||||
|
state.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDetails !== prevProps.showDetails) {
|
||||||
|
state.showDetails = showDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOverview !== prevProps.showOverview) {
|
||||||
|
state.showOverview = showOverview;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(state)) {
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChangeOverviewOption = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onChangeOverviewOption({ [name]: value });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeOption = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onChangeOption({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
size,
|
||||||
|
detailedProgressBar,
|
||||||
|
showDetails,
|
||||||
|
showOverview
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Overview Options
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="size"
|
||||||
|
value={size}
|
||||||
|
values={posterSizeOptions}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="detailedProgressBar"
|
||||||
|
value={detailedProgressBar}
|
||||||
|
helpText={translate('DetailedProgressBarHelpText')}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowCollectionDetails')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showDetails"
|
||||||
|
value={showDetails}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowOverview')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showOverview"
|
||||||
|
value={showOverview}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewOptionsModalContent.propTypes = {
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
size: PropTypes.string.isRequired,
|
||||||
|
showDetails: PropTypes.bool.isRequired,
|
||||||
|
showOverview: PropTypes.bool.isRequired,
|
||||||
|
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||||
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviewOptionsModalContent;
|
@ -0,0 +1,29 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setMovieCollectionsOption, setMovieCollectionsOverviewOption } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import CollectionOverviewOptionsModalContent from './CollectionOverviewOptionsModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
(movieCollections) => {
|
||||||
|
return {
|
||||||
|
...movieCollections.options,
|
||||||
|
...movieCollections.overviewOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onChangeOverviewOption(payload) {
|
||||||
|
dispatch(setMovieCollectionsOverviewOption(payload));
|
||||||
|
},
|
||||||
|
onChangeOption(payload) {
|
||||||
|
dispatch(setMovieCollectionsOption(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionOverviewOptionsModalContent);
|
@ -10,6 +10,7 @@ export const DOWNLOADED_MOVIES_SCAN = 'DownloadedMoviesScan';
|
|||||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||||
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
||||||
export const MOVE_MOVIE = 'MoveMovie';
|
export const MOVE_MOVIE = 'MoveMovie';
|
||||||
|
export const REFRESH_COLLECTIONS = 'RefreshCollections';
|
||||||
export const REFRESH_MOVIE = 'RefreshMovie';
|
export const REFRESH_MOVIE = 'RefreshMovie';
|
||||||
export const RENAME_FILES = 'RenameFiles';
|
export const RENAME_FILES = 'RenameFiles';
|
||||||
export const RENAME_MOVIE = 'RenameMovie';
|
export const RENAME_MOVIE = 'RenameMovie';
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
const monitorTypesOptions = [
|
|
||||||
{ key: 'true', value: translate('Yes') },
|
|
||||||
{ key: 'false', value: translate('No') }
|
|
||||||
];
|
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorTypesOptions];
|
const values = [...monitorOptions];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
|
@ -6,6 +6,7 @@ import { createSelector } from 'reselect';
|
|||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||||
|
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
|
||||||
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
@ -51,6 +52,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.indexerFlags.isPopulated,
|
(state) => state.settings.indexerFlags.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
|
(state) => state.movieCollections.isPopulated,
|
||||||
(
|
(
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
@ -59,7 +61,8 @@ const selectIsPopulated = createSelector(
|
|||||||
languagesIsPopulated,
|
languagesIsPopulated,
|
||||||
indexerFlagsIsPopulated,
|
indexerFlagsIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated,
|
||||||
|
movieCollectionsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
customFiltersIsPopulated &&
|
customFiltersIsPopulated &&
|
||||||
@ -69,7 +72,8 @@ const selectIsPopulated = createSelector(
|
|||||||
languagesIsPopulated &&
|
languagesIsPopulated &&
|
||||||
indexerFlagsIsPopulated &&
|
indexerFlagsIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated &&
|
||||||
|
movieCollectionsIsPopulated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -83,6 +87,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.indexerFlags.error,
|
(state) => state.settings.indexerFlags.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
|
(state) => state.movieCollections.error,
|
||||||
(
|
(
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
@ -91,7 +96,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError,
|
languagesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
movieCollectionsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
customFiltersError ||
|
customFiltersError ||
|
||||||
@ -101,7 +107,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError ||
|
languagesError ||
|
||||||
indexerFlagsError ||
|
indexerFlagsError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
systemStatusError
|
systemStatusError ||
|
||||||
|
movieCollectionsError
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -113,7 +120,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError,
|
languagesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
movieCollectionsError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -148,6 +156,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchMovies() {
|
dispatchFetchMovies() {
|
||||||
dispatch(fetchMovies());
|
dispatch(fetchMovies());
|
||||||
},
|
},
|
||||||
|
dispatchFetchMovieCollections() {
|
||||||
|
dispatch(fetchMovieCollections());
|
||||||
|
},
|
||||||
dispatchFetchCustomFilters() {
|
dispatchFetchCustomFilters() {
|
||||||
dispatch(fetchCustomFilters());
|
dispatch(fetchCustomFilters());
|
||||||
},
|
},
|
||||||
@ -197,6 +208,7 @@ class PageConnector extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.isPopulated) {
|
if (!this.props.isPopulated) {
|
||||||
this.props.dispatchFetchMovies();
|
this.props.dispatchFetchMovies();
|
||||||
|
this.props.dispatchFetchMovieCollections();
|
||||||
this.props.dispatchFetchCustomFilters();
|
this.props.dispatchFetchCustomFilters();
|
||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
@ -223,6 +235,7 @@ class PageConnector extends Component {
|
|||||||
isPopulated,
|
isPopulated,
|
||||||
hasError,
|
hasError,
|
||||||
dispatchFetchMovies,
|
dispatchFetchMovies,
|
||||||
|
dispatchFetchMovieCollections,
|
||||||
dispatchFetchTags,
|
dispatchFetchTags,
|
||||||
dispatchFetchQualityProfiles,
|
dispatchFetchQualityProfiles,
|
||||||
dispatchFetchLanguages,
|
dispatchFetchLanguages,
|
||||||
@ -262,6 +275,7 @@ PageConnector.propTypes = {
|
|||||||
hasError: PropTypes.bool.isRequired,
|
hasError: PropTypes.bool.isRequired,
|
||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
isSidebarVisible: PropTypes.bool.isRequired,
|
||||||
dispatchFetchMovies: PropTypes.func.isRequired,
|
dispatchFetchMovies: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchMovieCollections: PropTypes.func.isRequired,
|
||||||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
|
@ -33,6 +33,10 @@ const links = [
|
|||||||
title: translate('ImportLibrary'),
|
title: translate('ImportLibrary'),
|
||||||
to: '/add/import'
|
to: '/add/import'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: translate('Collections'),
|
||||||
|
to: '/collections'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: translate('Discover'),
|
title: translate('Discover'),
|
||||||
to: '/add/discover'
|
to: '/add/discover'
|
||||||
|
@ -203,6 +203,19 @@ class SignalRConnector extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCollection = (body) => {
|
||||||
|
const action = body.action;
|
||||||
|
const section = 'movieCollections';
|
||||||
|
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
|
if (action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleQueue = () => {
|
handleQueue = () => {
|
||||||
if (this.props.isQueuePopulated) {
|
if (this.props.isQueuePopulated) {
|
||||||
this.props.dispatchFetchQueue();
|
this.props.dispatchFetchQueue();
|
||||||
|
@ -9,6 +9,7 @@ import SelectInput from 'Components/Form/SelectInput';
|
|||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
|
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
|
||||||
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
|
||||||
@ -137,11 +138,6 @@ class DiscoverMovieFooter extends Component {
|
|||||||
isExcludeMovieModalOpen
|
isExcludeMovieModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{ key: true, value: translate('Monitored') },
|
|
||||||
{ key: false, value: translate('Unmonitored') }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentFooter>
|
<PageContentFooter>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
@ -153,7 +149,7 @@ class DiscoverMovieFooter extends Component {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
values={monitoredOptions}
|
values={monitorOptions}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
@ -129,7 +129,7 @@ DiscoverMoviePosterInfo.propTypes = {
|
|||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
physicalRelease: PropTypes.string,
|
physicalRelease: PropTypes.string,
|
||||||
runtime: PropTypes.number,
|
runtime: PropTypes.number,
|
||||||
ratings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
sortKey: PropTypes.string.isRequired,
|
sortKey: PropTypes.string.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
@ -164,7 +164,7 @@ class DiscoverMovieRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{collection ? collection.name : null }
|
{collection ? collection.title : null }
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -373,7 +373,7 @@ DiscoverMovieRow.propTypes = {
|
|||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
runtime: PropTypes.number,
|
runtime: PropTypes.number,
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
ratings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string,
|
||||||
collection: PropTypes.object,
|
collection: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
@ -39,7 +39,7 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import MovieCollectionConnector from './../MovieCollectionConnector';
|
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
@ -583,10 +583,8 @@ class MovieDetails extends Component {
|
|||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div className={styles.collection}>
|
<div className={styles.collection}>
|
||||||
<MovieCollectionConnector
|
<MovieCollectionLabelConnector
|
||||||
tmdbId={collection.tmdbId}
|
tmdbId={collection.tmdbId}
|
||||||
name={collection.name}
|
|
||||||
movieId={id}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</InfoLabel>
|
</InfoLabel>
|
||||||
|
@ -7,6 +7,15 @@
|
|||||||
transition: width 200ms ease;
|
transition: width 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progressRadius {
|
||||||
|
composes: container from '~Components/ProgressBar.css';
|
||||||
|
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
background-color: #5b5b5b;
|
||||||
|
color: $white;
|
||||||
|
transition: width 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.progressBar {
|
.progressBar {
|
||||||
composes: progressBar from '~Components/ProgressBar.css';
|
composes: progressBar from '~Components/ProgressBar.css';
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
isAvailable,
|
isAvailable,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
detailedProgressBar,
|
detailedProgressBar,
|
||||||
|
bottomRadius,
|
||||||
queueStatus,
|
queueStatus,
|
||||||
queueState
|
queueState
|
||||||
} = props;
|
} = props;
|
||||||
@ -40,7 +41,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
className={styles.progressBar}
|
className={styles.progressBar}
|
||||||
containerClassName={styles.progress}
|
containerClassName={bottomRadius ? styles.progressRadius : styles.progress}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
kind={getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds', queueStatusText)}
|
kind={getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds', queueStatusText)}
|
||||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
@ -54,6 +55,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
MovieIndexProgressBar.propTypes = {
|
MovieIndexProgressBar.propTypes = {
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool.isRequired,
|
||||||
|
bottomRadius: PropTypes.bool,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
@ -62,4 +64,8 @@ MovieIndexProgressBar.propTypes = {
|
|||||||
queueState: PropTypes.string
|
queueState: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MovieIndexProgressBar.defaultProps = {
|
||||||
|
bottomRadius: false
|
||||||
|
};
|
||||||
|
|
||||||
export default MovieIndexProgressBar;
|
export default MovieIndexProgressBar;
|
||||||
|
@ -170,7 +170,7 @@ class MovieIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{collection ? collection.name : null }
|
{collection ? collection.title : null }
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ class MovieIndexTable extends Component {
|
|||||||
component={MovieIndexRow}
|
component={MovieIndexRow}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
movieId={movie.id}
|
movieId={movie.id}
|
||||||
|
collectionId={movie.collectionId}
|
||||||
qualityProfileId={movie.qualityProfileId}
|
qualityProfileId={movie.qualityProfileId}
|
||||||
isSelected={selectedState[movie.id]}
|
isSelected={selectedState[movie.id]}
|
||||||
onSelectedChange={onSelectedChange}
|
onSelectedChange={onSelectedChange}
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import styles from './MovieCollection.css';
|
|
||||||
|
|
||||||
class MovieCollection extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddImportListPress = (monitored) => {
|
|
||||||
if (this.props.collectionList) {
|
|
||||||
this.props.onMonitorTogglePress(monitored);
|
|
||||||
} else {
|
|
||||||
this.props.onMonitorTogglePress(monitored);
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
collectionList,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const monitored = collectionList !== undefined && collectionList.enabled && collectionList.enableAuto;
|
|
||||||
const importListId = collectionList ? collectionList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
isSaving={isSaving}
|
|
||||||
size={15}
|
|
||||||
onPress={this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
{name}
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCollection.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
collectionList: PropTypes.object,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onMonitorTogglePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCollection;
|
|
@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { saveImportList, selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createMovieCollectionListSelector from 'Store/Selectors/createMovieCollectionListSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import MovieCollection from './MovieCollection';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createMovieCollectionListSelector(),
|
|
||||||
(state) => state.settings.importLists,
|
|
||||||
(movie, collectionList, importLists) => {
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability
|
|
||||||
} = movie;
|
|
||||||
|
|
||||||
return {
|
|
||||||
collectionList,
|
|
||||||
monitored,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
isSaving: importLists.isSaving
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
selectImportListSchema,
|
|
||||||
setImportListFieldValue,
|
|
||||||
setImportListValue,
|
|
||||||
saveImportList
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieCollectionConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMonitorTogglePress = (monitored) => {
|
|
||||||
if (this.props.collectionList) {
|
|
||||||
this.props.setImportListValue({ name: 'enabled', value: monitored });
|
|
||||||
this.props.setImportListValue({ name: 'enableAuto', value: monitored });
|
|
||||||
this.props.saveImportList({ id: this.props.collectionList.id });
|
|
||||||
} else {
|
|
||||||
this.props.selectImportListSchema({ implementation: 'TMDbCollectionImport', presetName: undefined });
|
|
||||||
this.props.setImportListFieldValue({ name: 'collectionId', value: this.props.tmdbId.toString() });
|
|
||||||
this.props.setImportListValue({ name: 'enabled', value: true });
|
|
||||||
this.props.setImportListValue({ name: 'enableAuto', value: true });
|
|
||||||
this.props.setImportListValue({ name: 'name', value: `${this.props.name} - ${this.props.tmdbId}` });
|
|
||||||
this.props.setImportListValue({ name: 'qualityProfileId', value: this.props.qualityProfileId });
|
|
||||||
this.props.setImportListValue({ name: 'monitored', value: this.props.monitored });
|
|
||||||
this.props.setImportListValue({ name: 'minimumAvailability', value: this.props.minimumAvailability });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<MovieCollection
|
|
||||||
{...this.props}
|
|
||||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCollectionConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
movieId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
collectionList: PropTypes.object,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
selectImportListSchema: PropTypes.func.isRequired,
|
|
||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
|
||||||
setImportListValue: PropTypes.func.isRequired,
|
|
||||||
saveImportList: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionConnector);
|
|
46
frontend/src/Movie/MovieCollectionLabel.js
Normal file
46
frontend/src/Movie/MovieCollectionLabel.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import styles from './MovieCollectionLabel.css';
|
||||||
|
|
||||||
|
class MovieCollectionLabel extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
monitored,
|
||||||
|
onMonitorTogglePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={15}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCollectionLabel.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCollectionLabel;
|
57
frontend/src/Movie/MovieCollectionLabelConnector.js
Normal file
57
frontend/src/Movie/MovieCollectionLabelConnector.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import MovieCollectionLabel from './MovieCollectionLabel';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { tmdbId }) => tmdbId,
|
||||||
|
(state) => state.movieCollections.items,
|
||||||
|
(tmdbId, collections) => {
|
||||||
|
const collection = collections.find((movie) => movie.tmdbId === tmdbId);
|
||||||
|
return {
|
||||||
|
...collection
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleCollectionMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class MovieCollectionLabelConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleCollectionMonitored({
|
||||||
|
collectionId: this.props.id,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MovieCollectionLabel
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCollectionLabelConnector.propTypes = {
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector);
|
@ -42,7 +42,7 @@ function EditImportListModalContent(props) {
|
|||||||
name,
|
name,
|
||||||
enabled,
|
enabled,
|
||||||
enableAuto,
|
enableAuto,
|
||||||
shouldMonitor,
|
monitor,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
@ -121,31 +121,28 @@ function EditImportListModalContent(props) {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('AddMoviesMonitored')}</FormLabel>
|
<FormLabel>{translate('Monitor')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.MOVIE_MONITORED_SELECT}
|
||||||
name="shouldMonitor"
|
name="monitor"
|
||||||
helpText={translate('ShouldMonitorHelpText')}
|
helpText={translate('ShouldMonitorHelpText')}
|
||||||
{...shouldMonitor}
|
{...monitor}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
<FormGroup>
|
||||||
shouldMonitor &&
|
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="searchOnAdd"
|
name="searchOnAdd"
|
||||||
helpText={translate('SearchOnAddHelpText')}
|
helpText={translate('SearchOnAddHelpText')}
|
||||||
{...searchOnAdd}
|
{...searchOnAdd}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
|
||||||
import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions';
|
import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
@ -17,7 +18,8 @@ function createMapStateToProps() {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
dispatchFetchQualityProfiles: fetchQualityProfiles,
|
dispatchFetchQualityProfiles: fetchQualityProfiles,
|
||||||
dispatchDeleteQualityProfile: deleteQualityProfile,
|
dispatchDeleteQualityProfile: deleteQualityProfile,
|
||||||
dispatchCloneQualityProfile: cloneQualityProfile
|
dispatchCloneQualityProfile: cloneQualityProfile,
|
||||||
|
dispatchFetchMovieCollections: fetchMovieCollections
|
||||||
};
|
};
|
||||||
|
|
||||||
class QualityProfilesConnector extends Component {
|
class QualityProfilesConnector extends Component {
|
||||||
@ -27,6 +29,7 @@ class QualityProfilesConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
|
this.props.dispatchFetchMovieCollections();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -57,7 +60,8 @@ class QualityProfilesConnector extends Component {
|
|||||||
QualityProfilesConnector.propTypes = {
|
QualityProfilesConnector.propTypes = {
|
||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
dispatchDeleteQualityProfile: PropTypes.func.isRequired,
|
dispatchDeleteQualityProfile: PropTypes.func.isRequired,
|
||||||
dispatchCloneQualityProfile: PropTypes.func.isRequired
|
dispatchCloneQualityProfile: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchMovieCollections: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector);
|
||||||
|
@ -30,7 +30,7 @@ export const defaultState = {
|
|||||||
|
|
||||||
defaults: {
|
defaults: {
|
||||||
rootFolderPath: '',
|
rootFolderPath: '',
|
||||||
monitor: 'true',
|
monitor: 'movieOnly',
|
||||||
qualityProfileId: 0,
|
qualityProfileId: 0,
|
||||||
minimumAvailability: 'announced',
|
minimumAvailability: 'announced',
|
||||||
searchForMovie: true,
|
searchForMovie: true,
|
||||||
|
@ -46,7 +46,7 @@ export const defaultState = {
|
|||||||
|
|
||||||
defaults: {
|
defaults: {
|
||||||
rootFolderPath: '',
|
rootFolderPath: '',
|
||||||
monitor: 'true',
|
monitor: 'movieOnly',
|
||||||
qualityProfileId: 0,
|
qualityProfileId: 0,
|
||||||
minimumAvailability: 'announced',
|
minimumAvailability: 'announced',
|
||||||
searchForMovie: true,
|
searchForMovie: true,
|
||||||
@ -188,7 +188,7 @@ export const defaultState = {
|
|||||||
collection: function(item) {
|
collection: function(item) {
|
||||||
const { collection ={} } = item;
|
const { collection ={} } = item;
|
||||||
|
|
||||||
return collection.name;
|
return collection.title;
|
||||||
},
|
},
|
||||||
|
|
||||||
studio: function(item) {
|
studio: function(item) {
|
||||||
|
@ -12,6 +12,7 @@ import * as importMovie from './importMovieActions';
|
|||||||
import * as interactiveImportActions from './interactiveImportActions';
|
import * as interactiveImportActions from './interactiveImportActions';
|
||||||
import * as movies from './movieActions';
|
import * as movies from './movieActions';
|
||||||
import * as movieBlocklist from './movieBlocklistActions';
|
import * as movieBlocklist from './movieBlocklistActions';
|
||||||
|
import * as movieCollections from './movieCollectionActions';
|
||||||
import * as movieCredits from './movieCreditsActions';
|
import * as movieCredits from './movieCreditsActions';
|
||||||
import * as movieFiles from './movieFileActions';
|
import * as movieFiles from './movieFileActions';
|
||||||
import * as movieHistory from './movieHistoryActions';
|
import * as movieHistory from './movieHistoryActions';
|
||||||
@ -50,6 +51,7 @@ export default [
|
|||||||
rootFolders,
|
rootFolders,
|
||||||
movies,
|
movies,
|
||||||
movieBlocklist,
|
movieBlocklist,
|
||||||
|
movieCollections,
|
||||||
movieHistory,
|
movieHistory,
|
||||||
movieIndex,
|
movieIndex,
|
||||||
movieCredits,
|
movieCredits,
|
||||||
|
@ -157,8 +157,6 @@ export const filterPredicates = {
|
|||||||
imdbRating: function(item, filterValue, type) {
|
imdbRating: function(item, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
console.log(item.ratings);
|
|
||||||
|
|
||||||
const rating = item.ratings.imdb ? item.ratings.imdb.value : 0;
|
const rating = item.ratings.imdb ? item.ratings.imdb.value : 0;
|
||||||
|
|
||||||
return predicate(rating, filterValue);
|
return predicate(rating, filterValue);
|
||||||
|
347
frontend/src/Store/Actions/movieCollectionActions.js
Normal file
347
frontend/src/Store/Actions/movieCollectionActions.js
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import getNewMovie from 'Utilities/Movie/getNewMovie';
|
||||||
|
import { set, update, updateItem } from './baseActions';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||||
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
|
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'movieCollections';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
isAdding: false,
|
||||||
|
addError: null,
|
||||||
|
sortKey: 'sortTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
secondarySortKey: 'sortTitle',
|
||||||
|
secondarySortDirection: sortDirections.ASCENDING,
|
||||||
|
view: 'overview',
|
||||||
|
pendingChanges: {},
|
||||||
|
|
||||||
|
overviewOptions: {
|
||||||
|
detailedProgressBar: false,
|
||||||
|
size: 'medium',
|
||||||
|
showDetails: true,
|
||||||
|
showOverview: true
|
||||||
|
},
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
rootFolderPath: '',
|
||||||
|
monitor: 'movieOnly',
|
||||||
|
qualityProfileId: 0,
|
||||||
|
minimumAvailability: 'announced',
|
||||||
|
searchForMovie: true,
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedFilterKey: 'all',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: 'All',
|
||||||
|
filters: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterPredicates: {},
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: 'Title',
|
||||||
|
type: filterBuilderTypes.STRING
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'monitored',
|
||||||
|
label: 'Monitored',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'movieCollections.defaults',
|
||||||
|
'movieCollections.sortKey',
|
||||||
|
'movieCollections.sortDirection',
|
||||||
|
'movieCollections.selectedFilterKey',
|
||||||
|
'movieCollections.customFilters',
|
||||||
|
'movieCollections.options',
|
||||||
|
'movieCollections.overviewOptions'
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_MOVIE_COLLECTIONS = 'movieCollections/fetchMovieCollections';
|
||||||
|
export const CLEAR_MOVIE_COLLECTIONS = 'movieCollections/clearMovieCollections';
|
||||||
|
export const SAVE_MOVIE_COLLECTION = 'movieCollections/saveMovieCollection';
|
||||||
|
export const SAVE_MOVIE_COLLECTIONS = 'movieCollections/saveMovieCollections';
|
||||||
|
export const SET_MOVIE_COLLECTION_VALUE = 'movieCollections/setMovieCollectionValue';
|
||||||
|
|
||||||
|
export const ADD_MOVIE = 'movieCollections/addMovie';
|
||||||
|
|
||||||
|
export const TOGGLE_COLLECTION_MONITORED = 'movieCollections/toggleCollectionMonitored';
|
||||||
|
|
||||||
|
export const SET_MOVIE_COLLECTIONS_SORT = 'movieCollections/setMovieCollectionsSort';
|
||||||
|
export const SET_MOVIE_COLLECTIONS_FILTER = 'movieCollections/setMovieCollectionsFilter';
|
||||||
|
export const SET_MOVIE_COLLECTIONS_OPTION = 'movieCollections/setMovieCollectionsOption';
|
||||||
|
export const SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION = 'movieCollections/setMovieCollectionsOverviewOption';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchMovieCollections = createThunk(FETCH_MOVIE_COLLECTIONS);
|
||||||
|
export const clearMovieCollections = createAction(CLEAR_MOVIE_COLLECTIONS);
|
||||||
|
export const saveMovieCollection = createThunk(SAVE_MOVIE_COLLECTION);
|
||||||
|
export const saveMovieCollections = createThunk(SAVE_MOVIE_COLLECTIONS);
|
||||||
|
|
||||||
|
export const addMovie = createThunk(ADD_MOVIE);
|
||||||
|
|
||||||
|
export const toggleCollectionMonitored = createThunk(TOGGLE_COLLECTION_MONITORED);
|
||||||
|
|
||||||
|
export const setMovieCollectionsSort = createAction(SET_MOVIE_COLLECTIONS_SORT);
|
||||||
|
export const setMovieCollectionsFilter = createAction(SET_MOVIE_COLLECTIONS_FILTER);
|
||||||
|
export const setMovieCollectionsOption = createAction(SET_MOVIE_COLLECTIONS_OPTION);
|
||||||
|
export const setMovieCollectionsOverviewOption = createAction(SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION);
|
||||||
|
|
||||||
|
export const setMovieCollectionValue = createAction(SET_MOVIE_COLLECTION_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
[SAVE_MOVIE_COLLECTION]: createSaveProviderHandler(section, '/collection'),
|
||||||
|
[FETCH_MOVIE_COLLECTIONS]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/collection',
|
||||||
|
data: payload
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
update({ section, data }),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[ADD_MOVIE]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({ section, isAdding: true }));
|
||||||
|
|
||||||
|
const tmdbId = payload.tmdbId;
|
||||||
|
const title = payload.title;
|
||||||
|
|
||||||
|
const newMovie = getNewMovie({ tmdbId, title }, payload);
|
||||||
|
newMovie.id = 0;
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/movie',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(newMovie)
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
updateItem({ section: 'movies', ...data }),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isAdding: false,
|
||||||
|
isAdded: true,
|
||||||
|
addError: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isAdding: false,
|
||||||
|
isAdded: false,
|
||||||
|
addError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[TOGGLE_COLLECTION_MONITORED]: (getState, payload, dispatch) => {
|
||||||
|
const {
|
||||||
|
collectionId: id,
|
||||||
|
monitored
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const collection = _.find(getState().movieCollections.items, { id });
|
||||||
|
|
||||||
|
dispatch(updateItem({
|
||||||
|
id,
|
||||||
|
section,
|
||||||
|
isSaving: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: `/collection/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify({
|
||||||
|
...collection,
|
||||||
|
monitored
|
||||||
|
}),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(updateItem({
|
||||||
|
id,
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
monitored
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(updateItem({
|
||||||
|
id,
|
||||||
|
section,
|
||||||
|
isSaving: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[SAVE_MOVIE_COLLECTIONS]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
collectionIds,
|
||||||
|
monitored,
|
||||||
|
monitor
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const response = {};
|
||||||
|
const collections = [];
|
||||||
|
|
||||||
|
collectionIds.forEach((id) => {
|
||||||
|
const collectionToUpdate = { id };
|
||||||
|
|
||||||
|
if (payload.hasOwnProperty('monitored')) {
|
||||||
|
collectionToUpdate.monitored = monitored;
|
||||||
|
}
|
||||||
|
|
||||||
|
collections.push(collectionToUpdate);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload.hasOwnProperty('monitor')) {
|
||||||
|
response.monitorMovies = monitor === 'monitored';
|
||||||
|
}
|
||||||
|
|
||||||
|
response.collections = collections;
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/collection',
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify(response),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(fetchMovieCollections());
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[SET_MOVIE_COLLECTIONS_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||||
|
[SET_MOVIE_COLLECTIONS_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
||||||
|
[SET_MOVIE_COLLECTION_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
|
||||||
|
[SET_MOVIE_COLLECTIONS_OPTION]: function(state, { payload }) {
|
||||||
|
const movieCollectionsOptions = state.options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
options: {
|
||||||
|
...movieCollectionsOptions,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION]: function(state, { payload }) {
|
||||||
|
const overviewOptions = state.overviewOptions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
overviewOptions: {
|
||||||
|
...overviewOptions,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[CLEAR_MOVIE_COLLECTIONS]: (state) => {
|
||||||
|
return Object.assign({}, state, defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
26
frontend/src/Store/Migrators/migrateMonitorToEnum.js
Normal file
26
frontend/src/Store/Migrators/migrateMonitorToEnum.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import get from 'lodash';
|
||||||
|
|
||||||
|
export default function migrateMonitorToEnum(persistedState) {
|
||||||
|
const addMovie = get(persistedState, 'addMovie.defaults.monitor');
|
||||||
|
const discoverMovie = get(persistedState, 'discoverMovie.defaults.monitor');
|
||||||
|
|
||||||
|
if (!addMovie && !discoverMovie) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMovie === true) {
|
||||||
|
persistedState.addMovie.defaults.monitor = 'movieOnly';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discoverMovie === true) {
|
||||||
|
persistedState.discoverMovie.defaults.monitor = 'movieOnly';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMovie === false) {
|
||||||
|
persistedState.addMovie.defaults.monitor = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discoverMovie === false) {
|
||||||
|
persistedState.discoverMovie.defaults.monitor = 'none';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import createClientSideCollectionSelector from './createClientSideCollectionSelector';
|
||||||
|
|
||||||
|
function createUnoptimizedSelector(uiSection) {
|
||||||
|
return createSelector(
|
||||||
|
createClientSideCollectionSelector('movieCollections', uiSection),
|
||||||
|
(movies) => {
|
||||||
|
const items = movies.items.map((s) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sortTitle
|
||||||
|
} = s;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
sortTitle
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...movies,
|
||||||
|
items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function movieListEqual(a, b) {
|
||||||
|
return hasDifferentItemsOrOrder(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMovieEqualSelector = createSelectorCreator(
|
||||||
|
defaultMemoize,
|
||||||
|
movieListEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
function createCollectionClientSideCollectionItemsSelector(uiSection) {
|
||||||
|
return createMovieEqualSelector(
|
||||||
|
createUnoptimizedSelector(uiSection),
|
||||||
|
(movies) => movies
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createCollectionClientSideCollectionItemsSelector;
|
@ -0,0 +1,14 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createAllMoviesSelector from './createAllMoviesSelector';
|
||||||
|
|
||||||
|
function createCollectionExistingMovieSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { tmdbId }) => tmdbId,
|
||||||
|
createAllMoviesSelector(),
|
||||||
|
(tmdbId, allMovies) => {
|
||||||
|
return allMovies.find((movie) => movie.tmdbId === tmdbId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createCollectionExistingMovieSelector;
|
17
frontend/src/Store/Selectors/createCollectionSelector.js
Normal file
17
frontend/src/Store/Selectors/createCollectionSelector.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
function createCollectionSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { collectionId }) => collectionId,
|
||||||
|
(state) => state.movieCollections.itemMap,
|
||||||
|
(state) => state.movieCollections.items,
|
||||||
|
(collectionId, itemMap, allCollections) => {
|
||||||
|
if (allCollections && itemMap && collectionId in itemMap) {
|
||||||
|
return allCollections[itemMap[collectionId]];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createCollectionSelector;
|
@ -9,12 +9,14 @@ function createUnoptimizedSelector(uiSection) {
|
|||||||
const items = movies.items.map((s) => {
|
const items = movies.items.map((s) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
sortTitle
|
sortTitle,
|
||||||
|
collectionId
|
||||||
} = s;
|
} = s;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sortTitle
|
sortTitle,
|
||||||
|
collectionId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,12 +7,13 @@ function createProfileInUseSelector(profileProp) {
|
|||||||
(state, { id }) => id,
|
(state, { id }) => id,
|
||||||
createAllMoviesSelector(),
|
createAllMoviesSelector(),
|
||||||
(state) => state.settings.importLists.items,
|
(state) => state.settings.importLists.items,
|
||||||
(id, movies, lists) => {
|
(state) => state.movieCollections.items,
|
||||||
|
(id, movies, lists, collections) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.some(movies, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) {
|
if (_.some(movies, { [profileProp]: id }) || _.some(lists, { [profileProp]: id }) || _.some(collections, { [profileProp]: id })) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const scrollPositions = {
|
const scrollPositions = {
|
||||||
movieIndex: 0,
|
movieIndex: 0,
|
||||||
discoverMovie: 0
|
discoverMovie: 0,
|
||||||
|
movieCollections: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default scrollPositions;
|
export default scrollPositions;
|
||||||
|
@ -10,11 +10,12 @@ function getNewMovie(movie, payload) {
|
|||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const addOptions = {
|
const addOptions = {
|
||||||
|
monitor,
|
||||||
searchForMovie
|
searchForMovie
|
||||||
};
|
};
|
||||||
|
|
||||||
movie.addOptions = addOptions;
|
movie.addOptions = addOptions;
|
||||||
movie.monitored = monitor === 'true';
|
movie.monitored = monitor !== 'none';
|
||||||
movie.qualityProfileId = qualityProfileId;
|
movie.qualityProfileId = qualityProfileId;
|
||||||
movie.minimumAvailability = minimumAvailability;
|
movie.minimumAvailability = minimumAvailability;
|
||||||
movie.rootFolderPath = rootFolderPath;
|
movie.rootFolderPath = rootFolderPath;
|
||||||
|
9
frontend/src/Utilities/Movie/monitorOptions.js
Normal file
9
frontend/src/Utilities/Movie/monitorOptions.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const monitorOptions = [
|
||||||
|
{ key: 'movieOnly', value: translate('MovieOnly') },
|
||||||
|
{ key: 'movieAndCollection', value: translate('MovieAndCollection') },
|
||||||
|
{ key: 'none', value: translate('None') }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default monitorOptions;
|
@ -62,6 +62,8 @@
|
|||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "2.5.0",
|
"react-focus-lock": "2.5.0",
|
||||||
|
"react-slick": "0.28.1",
|
||||||
|
"slick-carousel": "1.8.1",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-lazyload": "3.2.0",
|
"react-lazyload": "3.2.0",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
|
@ -0,0 +1,293 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class collectionsFixture : MigrationTest<collections>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_add_collection_from_movie_and_link_back_to_movie()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Movies").Row(new
|
||||||
|
{
|
||||||
|
Monitored = true,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ProfileId = 1,
|
||||||
|
MovieFileId = 0,
|
||||||
|
MovieMetadataId = 1,
|
||||||
|
Path = string.Format("/Movies/{0}", "Title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||||
|
{
|
||||||
|
Title = "Title",
|
||||||
|
CleanTitle = "CleanTitle",
|
||||||
|
Status = 3,
|
||||||
|
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||||
|
Recommendations = new[] { 1 }.ToJson(),
|
||||||
|
Runtime = 90,
|
||||||
|
OriginalTitle = "Title",
|
||||||
|
CleanOriginalTitle = "CleanTitle",
|
||||||
|
OriginalLanguage = 1,
|
||||||
|
TmdbId = 132456,
|
||||||
|
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||||
|
LastInfoSync = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var collections = db.Query<Collection208>("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\"");
|
||||||
|
|
||||||
|
collections.Should().HaveCount(1);
|
||||||
|
collections.First().TmdbId.Should().Be(11);
|
||||||
|
collections.First().Title.Should().Be("Some Collection");
|
||||||
|
collections.First().Monitored.Should().BeFalse();
|
||||||
|
|
||||||
|
var movies = db.Query<Movie208>("SELECT \"Id\", \"CollectionTmdbId\" FROM \"MovieMetadata\"");
|
||||||
|
|
||||||
|
movies.Should().HaveCount(1);
|
||||||
|
movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_duplicate_collection()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Movies").Row(new
|
||||||
|
{
|
||||||
|
Monitored = true,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ProfileId = 1,
|
||||||
|
MovieFileId = 0,
|
||||||
|
MovieMetadataId = 1,
|
||||||
|
Path = string.Format("/Movies/{0}", "Title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||||
|
{
|
||||||
|
Title = "Title",
|
||||||
|
CleanTitle = "CleanTitle",
|
||||||
|
Status = 3,
|
||||||
|
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||||
|
Recommendations = new[] { 1 }.ToJson(),
|
||||||
|
Runtime = 90,
|
||||||
|
OriginalTitle = "Title",
|
||||||
|
CleanOriginalTitle = "CleanTitle",
|
||||||
|
OriginalLanguage = 1,
|
||||||
|
TmdbId = 132456,
|
||||||
|
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||||
|
LastInfoSync = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("Movies").Row(new
|
||||||
|
{
|
||||||
|
Monitored = true,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ProfileId = 1,
|
||||||
|
MovieFileId = 0,
|
||||||
|
MovieMetadataId = 2,
|
||||||
|
Path = string.Format("/Movies/{0}", "Title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||||
|
{
|
||||||
|
Title = "Title2",
|
||||||
|
CleanTitle = "CleanTitle2",
|
||||||
|
Status = 3,
|
||||||
|
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||||
|
Recommendations = new[] { 1 }.ToJson(),
|
||||||
|
Runtime = 90,
|
||||||
|
OriginalTitle = "Title2",
|
||||||
|
CleanOriginalTitle = "CleanTitle2",
|
||||||
|
OriginalLanguage = 1,
|
||||||
|
TmdbId = 132457,
|
||||||
|
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||||
|
LastInfoSync = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var collections = db.Query<Collection208>("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\"");
|
||||||
|
|
||||||
|
collections.Should().HaveCount(1);
|
||||||
|
collections.First().TmdbId.Should().Be(11);
|
||||||
|
collections.First().Title.Should().Be("Some Collection");
|
||||||
|
collections.First().Monitored.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_migrate_true_monitor_setting_on_lists()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("ImportLists").Row(new
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableAuto = true,
|
||||||
|
RootFolderPath = "D:\\Movies",
|
||||||
|
ProfileId = 1,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ShouldMonitor = true,
|
||||||
|
Name = "IMDB List",
|
||||||
|
Implementation = "RadarrLists",
|
||||||
|
Settings = new RadarrListSettings169
|
||||||
|
{
|
||||||
|
APIURL = "https://api.radarr.video/v2",
|
||||||
|
Path = "/imdb/list?listId=ls000199717",
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "RadarrSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<ListDefinition208>("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Monitor.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_migrate_false_monitor_setting_on_lists()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("ImportLists").Row(new
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableAuto = true,
|
||||||
|
RootFolderPath = "D:\\Movies",
|
||||||
|
ProfileId = 1,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ShouldMonitor = false,
|
||||||
|
Name = "IMDB List",
|
||||||
|
Implementation = "RadarrLists",
|
||||||
|
Settings = new RadarrListSettings169
|
||||||
|
{
|
||||||
|
APIURL = "https://api.radarr.video/v2",
|
||||||
|
Path = "/imdb/list?listId=ls000199717",
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "RadarrSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<ListDefinition208>("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Monitor.Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_purge_tmdb_collection_lists()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("ImportLists").Row(new
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableAuto = true,
|
||||||
|
RootFolderPath = "D:\\Movies",
|
||||||
|
ProfileId = 1,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ShouldMonitor = false,
|
||||||
|
Name = "IMDB List",
|
||||||
|
Implementation = "TMDbCollectionImport",
|
||||||
|
Settings = new TmdbCollectionListSettings207
|
||||||
|
{
|
||||||
|
CollectionId = "11"
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "TMDbCollectionSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<ListDefinition208>("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_monitor_new_collection_if_list_enabled_and_auto()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Movies").Row(new
|
||||||
|
{
|
||||||
|
Monitored = true,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ProfileId = 1,
|
||||||
|
MovieFileId = 0,
|
||||||
|
MovieMetadataId = 1,
|
||||||
|
Path = string.Format("/Movies/{0}", "Title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||||
|
{
|
||||||
|
Title = "Title",
|
||||||
|
CleanTitle = "CleanTitle",
|
||||||
|
Status = 3,
|
||||||
|
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||||
|
Recommendations = new[] { 1 }.ToJson(),
|
||||||
|
Runtime = 90,
|
||||||
|
OriginalTitle = "Title",
|
||||||
|
CleanOriginalTitle = "CleanTitle",
|
||||||
|
OriginalLanguage = 1,
|
||||||
|
TmdbId = 132456,
|
||||||
|
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||||
|
LastInfoSync = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
c.Insert.IntoTable("ImportLists").Row(new
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableAuto = true,
|
||||||
|
RootFolderPath = "D:\\Movies",
|
||||||
|
ProfileId = 1,
|
||||||
|
MinimumAvailability = 4,
|
||||||
|
ShouldMonitor = false,
|
||||||
|
Name = "IMDB List",
|
||||||
|
Implementation = "TMDbCollectionImport",
|
||||||
|
Settings = new TmdbCollectionListSettings207
|
||||||
|
{
|
||||||
|
CollectionId = "11"
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "TMDbCollectionSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<Collection208>("SELECT \"Id\", \"Monitored\" FROM \"Collections\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Monitored.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Collection208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TmdbId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public bool Monitored { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Movie208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int CollectionTmdbId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListDefinition208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Monitor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TmdbCollectionListSettings207
|
||||||
|
{
|
||||||
|
public string CollectionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CleanupOrphanedCollectionsFixture : DbTest<CleanupOrphanedCollections, MovieCollection>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_delete_orphaned_collection_item()
|
||||||
|
{
|
||||||
|
var collection = Builder<MovieCollection>.CreateNew()
|
||||||
|
.With(h => h.Id = 3)
|
||||||
|
.With(h => h.TmdbId = 123456)
|
||||||
|
.With(h => h.Title = "Some Credit")
|
||||||
|
.BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(collection);
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_delete_unorphaned_collection_items()
|
||||||
|
{
|
||||||
|
var collection = Builder<MovieCollection>.CreateNew()
|
||||||
|
.With(h => h.Id = 3)
|
||||||
|
.With(h => h.TmdbId = 123456)
|
||||||
|
.With(h => h.Title = "Some Credit")
|
||||||
|
.BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(collection);
|
||||||
|
|
||||||
|
var movie = Builder<MovieMetadata>.CreateNew().With(m => m.CollectionTmdbId = collection.TmdbId).BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(movie);
|
||||||
|
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Should().HaveCount(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@ public void Setup()
|
|||||||
{
|
{
|
||||||
_fakeMovie = Builder<MovieMetadata>
|
_fakeMovie = Builder<MovieMetadata>
|
||||||
.CreateNew()
|
.CreateNew()
|
||||||
|
.With(x => x.CollectionTitle = null)
|
||||||
|
.With(x => x.CollectionTmdbId = 0)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.MetadataSource;
|
using NzbDrone.Core.MetadataSource;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
using NzbDrone.Core.Movies.Commands;
|
using NzbDrone.Core.Movies.Commands;
|
||||||
using NzbDrone.Core.Movies.Credits;
|
using NzbDrone.Core.Movies.Credits;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
@ -19,6 +20,7 @@ namespace NzbDrone.Core.Test.MovieTests
|
|||||||
public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService>
|
public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService>
|
||||||
{
|
{
|
||||||
private MovieMetadata _movie;
|
private MovieMetadata _movie;
|
||||||
|
private MovieCollection _movieCollection;
|
||||||
private Movie _existingMovie;
|
private Movie _existingMovie;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
@ -28,6 +30,9 @@ public void Setup()
|
|||||||
.With(s => s.Status = MovieStatusType.Released)
|
.With(s => s.Status = MovieStatusType.Released)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
_movieCollection = Builder<MovieCollection>.CreateNew()
|
||||||
|
.Build();
|
||||||
|
|
||||||
_existingMovie = Builder<Movie>.CreateNew()
|
_existingMovie = Builder<Movie>.CreateNew()
|
||||||
.With(s => s.MovieMetadata.Value.Status = MovieStatusType.Released)
|
.With(s => s.MovieMetadata.Value.Status = MovieStatusType.Released)
|
||||||
.Build();
|
.Build();
|
||||||
@ -40,6 +45,10 @@ public void Setup()
|
|||||||
.Setup(s => s.Get(_movie.Id))
|
.Setup(s => s.Get(_movie.Id))
|
||||||
.Returns(_movie);
|
.Returns(_movie);
|
||||||
|
|
||||||
|
Mocker.GetMock<IAddMovieCollectionService>()
|
||||||
|
.Setup(v => v.AddMovieCollection(It.IsAny<MovieCollection>()))
|
||||||
|
.Returns(_movieCollection);
|
||||||
|
|
||||||
Mocker.GetMock<IProvideMovieInfo>()
|
Mocker.GetMock<IProvideMovieInfo>()
|
||||||
.Setup(s => s.GetMovieInfo(It.IsAny<int>()))
|
.Setup(s => s.GetMovieInfo(It.IsAny<int>()))
|
||||||
.Callback<int>((i) => { throw new MovieNotFoundException(i); });
|
.Callback<int>((i) => { throw new MovieNotFoundException(i); });
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
using NzbDrone.Core.Movies.Translations;
|
using NzbDrone.Core.Movies.Translations;
|
||||||
using NzbDrone.Core.Organizer;
|
using NzbDrone.Core.Organizer;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
@ -226,7 +227,7 @@ public void should_replace_movie_certification()
|
|||||||
public void should_replace_movie_collection()
|
public void should_replace_movie_collection()
|
||||||
{
|
{
|
||||||
_namingConfig.StandardMovieFormat = "{Movie Collection}";
|
_namingConfig.StandardMovieFormat = "{Movie Collection}";
|
||||||
_movie.MovieMetadata.Value.Collection = new MovieCollection { Name = "South Part Collection" };
|
_movie.MovieMetadata.Value.CollectionTitle = "South Part Collection";
|
||||||
|
|
||||||
Subject.BuildFileName(_movie, _movieFile)
|
Subject.BuildFileName(_movie, _movieFile)
|
||||||
.Should().Be("South Part Collection");
|
.Should().Be("South Part Collection");
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Test.CustomFormats;
|
using NzbDrone.Core.Test.CustomFormats;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
@ -90,6 +91,33 @@ public void should_not_be_able_to_delete_profile_if_assigned_to_list()
|
|||||||
Mocker.GetMock<IProfileRepository>().Verify(c => c.Delete(It.IsAny<int>()), Times.Never());
|
Mocker.GetMock<IProfileRepository>().Verify(c => c.Delete(It.IsAny<int>()), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_able_to_delete_profile_if_assigned_to_collection()
|
||||||
|
{
|
||||||
|
var movieList = Builder<Movie>.CreateListOfSize(3)
|
||||||
|
.All()
|
||||||
|
.With(c => c.ProfileId = 1)
|
||||||
|
.Build().ToList();
|
||||||
|
|
||||||
|
var importList = Builder<ImportListDefinition>.CreateListOfSize(3)
|
||||||
|
.Random(1)
|
||||||
|
.With(c => c.ProfileId = 1)
|
||||||
|
.Build().ToList();
|
||||||
|
|
||||||
|
var collectionList = Builder<MovieCollection>.CreateListOfSize(3)
|
||||||
|
.All()
|
||||||
|
.With(c => c.QualityProfileId = 2)
|
||||||
|
.Build().ToList();
|
||||||
|
|
||||||
|
Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
|
||||||
|
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importList);
|
||||||
|
Mocker.GetMock<IMovieCollectionService>().Setup(c => c.GetAllCollections()).Returns(collectionList);
|
||||||
|
|
||||||
|
Assert.Throws<ProfileInUseException>(() => Subject.Delete(2));
|
||||||
|
|
||||||
|
Mocker.GetMock<IProfileRepository>().Verify(c => c.Delete(It.IsAny<int>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_delete_profile_if_not_assigned_to_movie_or_list()
|
public void should_delete_profile_if_not_assigned_to_movie_or_list()
|
||||||
{
|
{
|
||||||
@ -103,8 +131,14 @@ public void should_delete_profile_if_not_assigned_to_movie_or_list()
|
|||||||
.With(c => c.ProfileId = 2)
|
.With(c => c.ProfileId = 2)
|
||||||
.Build().ToList();
|
.Build().ToList();
|
||||||
|
|
||||||
|
var collectionList = Builder<MovieCollection>.CreateListOfSize(3)
|
||||||
|
.All()
|
||||||
|
.With(c => c.QualityProfileId = 2)
|
||||||
|
.Build().ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
|
Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
|
||||||
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importList);
|
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importList);
|
||||||
|
Mocker.GetMock<IMovieCollectionService>().Setup(c => c.GetAllCollections()).Returns(collectionList);
|
||||||
|
|
||||||
Subject.Delete(1);
|
Subject.Delete(1);
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
using NzbDrone.Core.Configuration;
|
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
using NzbDrone.Core.Configuration;
|
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
|
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
namespace NzbDrone.Core.CustomFormats
|
namespace NzbDrone.Core.CustomFormats
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Instrumentation;
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ public interface ILazyLoaded : ICloneable
|
|||||||
/// Allows a field to be lazy loaded.
|
/// Allows a field to be lazy loaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TChild"></typeparam>
|
/// <typeparam name="TChild"></typeparam>
|
||||||
|
[JsonConverter(typeof(LazyLoadedConverterFactory))]
|
||||||
public class LazyLoaded<TChild> : ILazyLoaded
|
public class LazyLoaded<TChild> : ILazyLoaded
|
||||||
{
|
{
|
||||||
protected TChild _value;
|
protected TChild _value;
|
||||||
@ -62,11 +64,6 @@ public object Clone()
|
|||||||
{
|
{
|
||||||
return MemberwiseClone();
|
return MemberwiseClone();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldSerializeValue()
|
|
||||||
{
|
|
||||||
return IsLoaded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
90
src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs
Normal file
90
src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class LazyLoadedConverterFactory : JsonConverterFactory
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type typeToConvert)
|
||||||
|
{
|
||||||
|
if (!typeToConvert.IsGenericType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeToConvert.GetGenericTypeDefinition() == typeof(LazyLoaded<>);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var childType = type.GetGenericArguments()[0];
|
||||||
|
|
||||||
|
return (JsonConverter)Activator.CreateInstance(
|
||||||
|
typeof(LazyLoadedConverter<>).MakeGenericType(childType),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
binder: null,
|
||||||
|
args: new object[] { options },
|
||||||
|
culture: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LazyLoadedConverter<TChild> : JsonConverter<LazyLoaded<TChild>>
|
||||||
|
{
|
||||||
|
private readonly JsonConverter<TChild> _childConverter;
|
||||||
|
private readonly Type _childType;
|
||||||
|
|
||||||
|
public LazyLoadedConverter(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// For performance, use the existing converter if available.
|
||||||
|
_childConverter = (JsonConverter<TChild>)options
|
||||||
|
.GetConverter(typeof(TChild));
|
||||||
|
|
||||||
|
// Cache the type.
|
||||||
|
_childType = typeof(TChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LazyLoaded<TChild> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
TChild value;
|
||||||
|
if (_childConverter != null)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
value = _childConverter.Read(ref reader, _childType, options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = JsonSerializer.Deserialize<TChild>(ref reader, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return new LazyLoaded<TChild>(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, LazyLoaded<TChild> value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value.IsLoaded)
|
||||||
|
{
|
||||||
|
if (_childConverter != null)
|
||||||
|
{
|
||||||
|
_childConverter.Write(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Datastore.Converters;
|
using NzbDrone.Core.Datastore.Converters;
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
|
||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using NzbDrone.Common.Serializer;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
265
src/NzbDrone.Core/Datastore/Migration/208_collections.cs
Normal file
265
src/NzbDrone.Core/Datastore/Migration/208_collections.cs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using Dapper;
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(208)]
|
||||||
|
public class collections : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Create.TableForModel("Collections")
|
||||||
|
.WithColumn("TmdbId").AsInt32().Unique()
|
||||||
|
.WithColumn("QualityProfileId").AsInt32()
|
||||||
|
.WithColumn("RootFolderPath").AsString()
|
||||||
|
.WithColumn("MinimumAvailability").AsInt32()
|
||||||
|
.WithColumn("SearchOnAdd").AsBoolean()
|
||||||
|
.WithColumn("Title").AsString()
|
||||||
|
.WithColumn("SortTitle").AsString().Nullable()
|
||||||
|
.WithColumn("CleanTitle").AsString()
|
||||||
|
.WithColumn("Overview").AsString().Nullable()
|
||||||
|
.WithColumn("Images").AsString().WithDefaultValue("[]")
|
||||||
|
.WithColumn("Monitored").AsBoolean().WithDefaultValue(false)
|
||||||
|
.WithColumn("LastInfoSync").AsDateTime().Nullable()
|
||||||
|
.WithColumn("Added").AsDateTime().Nullable();
|
||||||
|
|
||||||
|
Alter.Table("MovieMetadata").AddColumn("CollectionTmdbId").AsInt32().Nullable()
|
||||||
|
.AddColumn("CollectionTitle").AsString().Nullable();
|
||||||
|
|
||||||
|
Alter.Table("ImportLists").AddColumn("Monitor").AsInt32().Nullable();
|
||||||
|
|
||||||
|
Execute.WithConnection(MigrateCollections);
|
||||||
|
Execute.WithConnection(MigrateCollectionMonitorStatus);
|
||||||
|
Execute.WithConnection(MapCollections);
|
||||||
|
Execute.WithConnection(MigrateListMonitor);
|
||||||
|
|
||||||
|
Alter.Table("ImportLists").AlterColumn("Monitor").AsInt32().NotNullable();
|
||||||
|
|
||||||
|
Delete.Column("ShouldMonitor").FromTable("ImportLists");
|
||||||
|
Delete.FromTable("ImportLists").Row(new { Implementation = "TMDbCollectionImport" });
|
||||||
|
Delete.Column("Collection").FromTable("MovieMetadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateCollections(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var rootPaths = new List<string>();
|
||||||
|
using (var getRootFolders = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
getRootFolders.Transaction = tran;
|
||||||
|
getRootFolders.CommandText = @"SELECT ""Path"" FROM ""RootFolders""";
|
||||||
|
|
||||||
|
using (var definitionsReader = getRootFolders.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (definitionsReader.Read())
|
||||||
|
{
|
||||||
|
string path = definitionsReader.GetString(0);
|
||||||
|
rootPaths.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCollections = new List<MovieCollection208>();
|
||||||
|
using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.Transaction = tran;
|
||||||
|
cmd.CommandText = "SELECT \"Collection\", \"ProfileId\", \"MinimumAvailability\", \"Path\" FROM \"Movies\" JOIN \"MovieMetadata\" ON \"Movies\".\"MovieMetadataId\" = \"MovieMetadata\".\"Id\" WHERE \"Collection\" IS NOT NULL";
|
||||||
|
|
||||||
|
var addedCollections = new List<int>();
|
||||||
|
var added = DateTime.UtcNow;
|
||||||
|
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var collection = reader.GetString(0);
|
||||||
|
var qualityProfileId = reader.GetInt32(1);
|
||||||
|
var minimumAvailability = reader.GetInt32(2);
|
||||||
|
var moviePath = reader.GetString(3);
|
||||||
|
var data = STJson.Deserialize<MovieCollection207>(collection);
|
||||||
|
|
||||||
|
if (newCollections.Any(d => d.TmdbId == data.TmdbId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootFolderPath = rootPaths.Where(r => r.IsParentPath(moviePath))
|
||||||
|
.OrderByDescending(r => r.Length)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (rootFolderPath == null)
|
||||||
|
{
|
||||||
|
rootFolderPath = rootPaths.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootFolderPath == null)
|
||||||
|
{
|
||||||
|
rootFolderPath = moviePath.GetParentPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
newCollections.Add(new MovieCollection208
|
||||||
|
{
|
||||||
|
TmdbId = data.TmdbId,
|
||||||
|
Title = data.Name,
|
||||||
|
CleanTitle = data.Name.CleanMovieTitle(),
|
||||||
|
SortTitle = Parser.Parser.NormalizeTitle(data.Name),
|
||||||
|
Added = added,
|
||||||
|
QualityProfileId = qualityProfileId,
|
||||||
|
RootFolderPath = rootFolderPath,
|
||||||
|
SearchOnAdd = true,
|
||||||
|
MinimumAvailability = minimumAvailability
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSql = "INSERT INTO \"Collections\" (\"TmdbId\", \"Title\", \"CleanTitle\", \"SortTitle\", \"Added\", \"QualityProfileId\", \"RootFolderPath\", \"SearchOnAdd\", \"MinimumAvailability\") VALUES (@TmdbId, @Title, @CleanTitle, @SortTitle, @Added, @QualityProfileId, @RootFolderPath, @SearchOnAdd, @MinimumAvailability)";
|
||||||
|
conn.Execute(updateSql, newCollections, transaction: tran);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateCollectionMonitorStatus(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updatedCollections = new List<MovieCollection208>();
|
||||||
|
using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.Transaction = tran;
|
||||||
|
cmd.CommandText = "SELECT \"Enabled\", \"EnableAuto\", \"Settings\", \"ShouldMonitor\", \"Id\" FROM \"ImportLists\" WHERE \"Implementation\" = 'TMDbCollectionImport'";
|
||||||
|
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var enabled = reader.GetBoolean(0);
|
||||||
|
var enabledAutoAdd = reader.GetBoolean(1);
|
||||||
|
var settings = reader.GetString(2);
|
||||||
|
var shouldMonitor = reader.GetBoolean(3);
|
||||||
|
var listId = reader.GetInt32(4);
|
||||||
|
var data = STJson.Deserialize<TmdbCollectionSettings206>(settings);
|
||||||
|
|
||||||
|
if (!enabled || !enabledAutoAdd || !int.TryParse(data.CollectionId, out var collectionId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedCollections.Add(new MovieCollection208
|
||||||
|
{
|
||||||
|
TmdbId = collectionId,
|
||||||
|
Monitored = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSql = "UPDATE \"Collections\" SET \"Monitored\" = @Monitored WHERE \"TmdbId\" = @TmdbId";
|
||||||
|
conn.Execute(updateSql, updatedCollections, transaction: tran);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateListMonitor(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updatedLists = new List<ImportList208>();
|
||||||
|
using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.Transaction = tran;
|
||||||
|
cmd.CommandText = "SELECT \"ShouldMonitor\", \"Id\" FROM \"ImportLists\"";
|
||||||
|
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var shouldMonitor = reader.GetBoolean(0);
|
||||||
|
var listId = reader.GetInt32(1);
|
||||||
|
|
||||||
|
updatedLists.Add(new ImportList208
|
||||||
|
{
|
||||||
|
Monitor = shouldMonitor ? 0 : 2,
|
||||||
|
Id = listId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSql = "UPDATE \"ImportLists\" SET \"Monitor\" = @Monitor WHERE \"Id\" = @Id";
|
||||||
|
conn.Execute(updateSql, updatedLists, transaction: tran);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MapCollections(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updatedMeta = new List<MovieMetadata208>();
|
||||||
|
|
||||||
|
using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.Transaction = tran;
|
||||||
|
cmd.CommandText = "SELECT \"Id\", \"Collection\" FROM \"MovieMetadata\" WHERE \"Collection\" IS NOT NULL";
|
||||||
|
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var id = reader.GetInt32(0);
|
||||||
|
var collection = reader.GetString(1);
|
||||||
|
var data = STJson.Deserialize<MovieCollection207>(collection);
|
||||||
|
|
||||||
|
var collectionId = data.TmdbId;
|
||||||
|
var collectionTitle = data.Name;
|
||||||
|
|
||||||
|
updatedMeta.Add(new MovieMetadata208
|
||||||
|
{
|
||||||
|
CollectionTitle = collectionTitle,
|
||||||
|
CollectionTmdbId = collectionId,
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSql = "UPDATE \"MovieMetadata\" SET \"CollectionTmdbId\" = @CollectionTmdbId, \"CollectionTitle\" = @CollectionTitle WHERE \"Id\" = @Id";
|
||||||
|
conn.Execute(updateSql, updatedMeta, transaction: tran);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MovieCollection207
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int TmdbId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MovieCollection208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string CleanTitle { get; set; }
|
||||||
|
public string SortTitle { get; set; }
|
||||||
|
public DateTime Added { get; set; }
|
||||||
|
public int QualityProfileId { get; set; }
|
||||||
|
public string RootFolderPath { get; set; }
|
||||||
|
public bool SearchOnAdd { get; set; }
|
||||||
|
public int MinimumAvailability { get; set; }
|
||||||
|
public bool Monitored { get; set; }
|
||||||
|
public int TmdbId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MovieMetadata208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int CollectionTmdbId { get; set; }
|
||||||
|
public string CollectionTitle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ImportList208
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Monitor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TmdbCollectionSettings206
|
||||||
|
{
|
||||||
|
public string CollectionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@
|
|||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
using NzbDrone.Core.Movies.AlternativeTitles;
|
using NzbDrone.Core.Movies.AlternativeTitles;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
using NzbDrone.Core.Movies.Credits;
|
using NzbDrone.Core.Movies.Credits;
|
||||||
using NzbDrone.Core.Movies.Translations;
|
using NzbDrone.Core.Movies.Translations;
|
||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
@ -168,6 +169,8 @@ public static void Map()
|
|||||||
|
|
||||||
Mapper.Entity<MovieMetadata>("MovieMetadata").RegisterModel()
|
Mapper.Entity<MovieMetadata>("MovieMetadata").RegisterModel()
|
||||||
.Ignore(s => s.Translations);
|
.Ignore(s => s.Translations);
|
||||||
|
|
||||||
|
Mapper.Entity<MovieCollection>("Collections").RegisterModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterMappers()
|
private static void RegisterMappers()
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Deluge
|
namespace NzbDrone.Core.Download.Clients.Deluge
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Common.Serializer;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||||
{
|
{
|
||||||
public class DownloadStation2Task
|
public class DownloadStation2Task
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Http;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||||
{
|
{
|
||||||
|
@ -52,7 +52,7 @@ private static IEnumerable<string> HandleTags(RemoteMovie remoteMovie, FloodSett
|
|||||||
switch (additionalTag)
|
switch (additionalTag)
|
||||||
{
|
{
|
||||||
case (int)AdditionalTags.Collection:
|
case (int)AdditionalTags.Collection:
|
||||||
result.Add(remoteMovie.Movie.MovieMetadata.Value.Collection.Name);
|
result.Add(remoteMovie.Movie.MovieMetadata.Value.CollectionTitle);
|
||||||
break;
|
break;
|
||||||
case (int)AdditionalTags.Quality:
|
case (int)AdditionalTags.Quality:
|
||||||
result.Add(remoteMovie.ParsedMovieInfo.Quality.Quality.ToString());
|
result.Add(remoteMovie.ParsedMovieInfo.Quality.Quality.ToString());
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using System.Xml.XPath;
|
using System.Xml.XPath;
|
||||||
|
|
||||||
|
@ -251,11 +251,11 @@ public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFil
|
|||||||
|
|
||||||
details.Add(new XElement("country"));
|
details.Add(new XElement("country"));
|
||||||
|
|
||||||
if (movie.MovieMetadata.Value.Collection?.Name != null)
|
if (movie.MovieMetadata.Value.CollectionTitle != null)
|
||||||
{
|
{
|
||||||
var setElement = new XElement("set");
|
var setElement = new XElement("set");
|
||||||
|
|
||||||
setElement.Add(new XElement("name", movie.MovieMetadata.Value.Collection.Name));
|
setElement.Add(new XElement("name", movie.MovieMetadata.Value.CollectionTitle));
|
||||||
setElement.Add(new XElement("overview"));
|
setElement.Add(new XElement("overview"));
|
||||||
|
|
||||||
details.Add(setElement);
|
details.Add(setElement);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Composition;
|
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.Localization;
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
|
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.Movies.Collections;
|
||||||
|
using NzbDrone.Core.Movies.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.HealthCheck.Checks
|
||||||
|
{
|
||||||
|
[CheckOn(typeof(CollectionEditedEvent), CheckOnCondition.Always)]
|
||||||
|
public class MovieCollectionRootFolderCheck : HealthCheckBase
|
||||||
|
{
|
||||||
|
private readonly IMovieCollectionService _collectionService;
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
|
public MovieCollectionRootFolderCheck(IMovieCollectionService collectionService, IDiskProvider diskProvider, ILocalizationService localizationService)
|
||||||
|
: base(localizationService)
|
||||||
|
{
|
||||||
|
_collectionService = collectionService;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override HealthCheck Check()
|
||||||
|
{
|
||||||
|
var collections = _collectionService.GetAllCollections();
|
||||||
|
var missingRootFolders = new Dictionary<string, List<MovieCollection>>();
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
var rootFolderPath = collection.RootFolderPath;
|
||||||
|
|
||||||
|
if (missingRootFolders.ContainsKey(rootFolderPath))
|
||||||
|
{
|
||||||
|
missingRootFolders[rootFolderPath].Add(collection);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootFolderPath.IsNullOrWhiteSpace() || !_diskProvider.FolderExists(rootFolderPath))
|
||||||
|
{
|
||||||
|
missingRootFolders.Add(rootFolderPath, new List<MovieCollection> { collection });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingRootFolders.Any())
|
||||||
|
{
|
||||||
|
if (missingRootFolders.Count == 1)
|
||||||
|
{
|
||||||
|
var missingRootFolder = missingRootFolders.First();
|
||||||
|
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("MovieCollectionMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#movie-collection-missing-root-folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = string.Format(_localizationService.GetLocalizedString("MovieCollectionMultipleMissingRoots"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))));
|
||||||
|
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#movie-collection-missing-root-folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HealthCheck(GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatRootFolder(string rootFolderPath, List<MovieCollection> collections)
|
||||||
|
{
|
||||||
|
return $"{rootFolderPath} ({string.Join(", ", collections.Select(l => l.Title))})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user