mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-11-23 11:13:34 +01:00
New: Mass Editor is now part of series list
This commit is contained in:
parent
815a16d5cf
commit
a731d24e23
@ -1,6 +1,14 @@
|
||||
.inputContainer {
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
|
||||
div {
|
||||
margin-top: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -35,3 +43,17 @@
|
||||
.importError {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.inputContainer {
|
||||
margin-top: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.importButtonContainer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,6 @@ const initialState = {
|
||||
interface SelectProviderOptions<T extends ModelBase> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
children: any;
|
||||
isSelectMode: boolean;
|
||||
items: Array<T>;
|
||||
}
|
||||
|
||||
@ -97,7 +96,7 @@ function selectReducer(state: SelectState, action: SelectAction): SelectState {
|
||||
};
|
||||
}
|
||||
case SelectActionType.ToggleSelected: {
|
||||
var result = {
|
||||
const result = {
|
||||
items,
|
||||
...toggleSelected(
|
||||
state,
|
||||
@ -129,7 +128,7 @@ function selectReducer(state: SelectState, action: SelectAction): SelectState {
|
||||
export function SelectProvider<T extends ModelBase>(
|
||||
props: SelectProviderOptions<T>
|
||||
) {
|
||||
const { isSelectMode, items } = props;
|
||||
const { items } = props;
|
||||
const selectedState = getSelectedState(items, {});
|
||||
|
||||
const [state, dispatch] = React.useReducer(selectReducer, {
|
||||
@ -142,12 +141,6 @@ export function SelectProvider<T extends ModelBase>(
|
||||
|
||||
const value: [SelectState, Dispatch] = [state, dispatch];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSelectMode) {
|
||||
dispatch({ type: SelectActionType.Reset });
|
||||
}
|
||||
}, [isSelectMode]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: SelectActionType.UpdateItems, items });
|
||||
}, [items]);
|
||||
|
@ -4,7 +4,9 @@ import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './Alert.css';
|
||||
|
||||
function Alert({ className, kind, children, ...otherProps }) {
|
||||
function Alert(props) {
|
||||
const { className, kind, children, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@ -19,8 +21,8 @@ function Alert({ className, kind, children, ...otherProps }) {
|
||||
}
|
||||
|
||||
Alert.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
className: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
@ -260,12 +260,16 @@ FormInputGroup.propTypes = {
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
unit: PropTypes.string,
|
||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
helpText: PropTypes.string,
|
||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
||||
helpTextWarning: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
includeNoChange: PropTypes.bool,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
pending: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
|
@ -5,14 +5,15 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import SelectInput from './SelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
||||
(qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
|
||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||
return {
|
||||
key: qualityProfile.id,
|
||||
@ -24,7 +25,7 @@ function createMapStateToProps() {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
disabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,8 +56,8 @@ class QualityProfileSelectInputConnector extends Component {
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
|
||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
||||
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
||||
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
||||
|
||||
if (firstValue) {
|
||||
this.onChange({ name, value: firstValue.key });
|
||||
@ -76,7 +77,7 @@ class QualityProfileSelectInputConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectInput
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
@ -13,7 +13,8 @@ function createMapStateToProps() {
|
||||
(state, { value }) => value,
|
||||
(state, { includeMissingValue }) => includeMissingValue,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(rootFolders, value, includeMissingValue, includeNoChange) => {
|
||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||
(rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => {
|
||||
const values = rootFolders.items.map((rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
@ -27,7 +28,7 @@ function createMapStateToProps() {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
isDisabled: true,
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
isMissing: false
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SelectInput from './SelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const seriesTypeOptions = [
|
||||
{ key: seriesTypes.STANDARD, value: 'Standard' },
|
||||
@ -14,6 +14,7 @@ function SeriesTypeSelectInput(props) {
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeNoChangeDisabled = true,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
@ -21,7 +22,7 @@ function SeriesTypeSelectInput(props) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
disabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@ -34,7 +35,7 @@ function SeriesTypeSelectInput(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
@ -43,6 +44,7 @@ function SeriesTypeSelectInput(props) {
|
||||
|
||||
SeriesTypeSelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,7 @@ function Label(props) {
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
|
@ -42,6 +42,7 @@ function SpinnerButton(props) {
|
||||
}
|
||||
|
||||
SpinnerButton.propTypes = {
|
||||
...Button.Props,
|
||||
className: PropTypes.string.isRequired,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
|
@ -8,14 +8,6 @@
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.contentFooter {
|
||||
display: block;
|
||||
|
||||
div {
|
||||
margin-top: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||
import React from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import styles from './PageToolbarOverflowMenuItem.css';
|
||||
|
||||
interface PageToolbarOverflowMenuItemProps {
|
||||
iconName: IconDefinition;
|
||||
spinningName?: IconDefinition;
|
||||
isDisabled?: boolean;
|
||||
isSpinning?: boolean;
|
||||
showIndicator?: boolean;
|
||||
label: string;
|
||||
text?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function PageToolbarOverflowMenuItem(props: PageToolbarOverflowMenuItemProps) {
|
||||
const {
|
||||
iconName,
|
||||
spinningName,
|
||||
label,
|
||||
isDisabled,
|
||||
isSpinning = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<MenuItem key={label} isDisabled={isDisabled || isSpinning} {...otherProps}>
|
||||
<SpinnerIcon
|
||||
className={styles.icon}
|
||||
name={iconName}
|
||||
spinningName={spinningName}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageToolbarOverflowMenuItem;
|
@ -4,12 +4,11 @@ import React, { Component } from 'react';
|
||||
import Measure from 'Components/Measure';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import { forEach } from 'Helpers/elementChildren';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import PageToolbarOverflowMenuItem from './PageToolbarOverflowMenuItem';
|
||||
import styles from './PageToolbarSection.css';
|
||||
|
||||
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
||||
@ -168,28 +167,15 @@ class PageToolbarSection extends Component {
|
||||
{
|
||||
overflowItems.map((item) => {
|
||||
const {
|
||||
iconName,
|
||||
spinningName,
|
||||
label,
|
||||
isDisabled,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
overflowComponent: OverflowComponent = PageToolbarOverflowMenuItem
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
<OverflowComponent
|
||||
key={label}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<SpinnerIcon
|
||||
className={styles.overflowMenuItemIcon}
|
||||
name={iconName}
|
||||
spinningName={spinningName}
|
||||
isSpinning={isSpinning}
|
||||
{...item}
|
||||
/>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ function SpinnerIcon(props) {
|
||||
}
|
||||
|
||||
SpinnerIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object.isRequired,
|
||||
isSpinning: PropTypes.bool.isRequired
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DeleteSeriesModalContent from './DeleteSeriesModalContent';
|
||||
|
||||
interface DeleteSeriesModalProps {
|
||||
isOpen: boolean;
|
||||
seriesIds: number[];
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function DeleteSeriesModal(props: DeleteSeriesModalProps) {
|
||||
const { isOpen, seriesIds, onModalClose } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<DeleteSeriesModalContent
|
||||
seriesIds={seriesIds}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteSeriesModal;
|
@ -0,0 +1,13 @@
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pathContainer {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin-left: 5px;
|
||||
color: var(--dangerColor);
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import { orderBy } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
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, kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import styles from './DeleteSeriesModalContent.css';
|
||||
|
||||
interface DeleteSeriesModalContentProps {
|
||||
seriesIds: number[];
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
const selectDeleteOptions = createSelector(
|
||||
(state) => state.series.deleteOptions,
|
||||
(deleteOptions) => deleteOptions
|
||||
);
|
||||
|
||||
function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
||||
const { seriesIds, onModalClose } = props;
|
||||
|
||||
const { addImportListExclusion } = useSelector(selectDeleteOptions);
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const series = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
|
||||
return orderBy(series, ['sortTitle']);
|
||||
}, [seriesIds, allSeries]);
|
||||
|
||||
const onDeleteFilesChange = useCallback(
|
||||
({ value }) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[setDeleteFiles]
|
||||
);
|
||||
|
||||
const onDeleteOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
dispatch(
|
||||
setDeleteOption({
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onDeleteSeriesConfirmed = useCallback(() => {
|
||||
setDeleteFiles(false);
|
||||
|
||||
dispatch(
|
||||
bulkDeleteSeries({
|
||||
seriesIds,
|
||||
deleteFiles,
|
||||
addImportListExclusion,
|
||||
})
|
||||
);
|
||||
|
||||
onModalClose();
|
||||
}, [
|
||||
seriesIds,
|
||||
deleteFiles,
|
||||
addImportListExclusion,
|
||||
setDeleteFiles,
|
||||
dispatch,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Delete Selected Series</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>Add List Exclusion</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportListExclusion"
|
||||
value={addImportListExclusion}
|
||||
helpText="Prevent series from being added to Sonarr by lists"
|
||||
onChange={onDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{`Delete Series Folder${
|
||||
series.length > 1 ? 's' : ''
|
||||
}`}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={`Delete Series Folder${
|
||||
series.length > 1 ? 's' : ''
|
||||
} and all contents`}
|
||||
kind={kinds.DANGER}
|
||||
onChange={onDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<div className={styles.message}>
|
||||
{`Are you sure you want to delete ${series.length} selected series${
|
||||
deleteFiles ? ' and all contents' : ''
|
||||
}?`}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{series.map((s) => {
|
||||
return (
|
||||
<li key={s.title}>
|
||||
<span>{s.title}</span>
|
||||
|
||||
{deleteFiles && (
|
||||
<span className={styles.pathContainer}>
|
||||
-<span className={styles.path}>{s.path}</span>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={onDeleteSeriesConfirmed}>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteSeriesModalContent;
|
26
frontend/src/Series/Index/Select/Edit/EditSeriesModal.tsx
Normal file
26
frontend/src/Series/Index/Select/Edit/EditSeriesModal.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditSeriesModalContent from './EditSeriesModalContent';
|
||||
|
||||
interface EditSeriesModalProps {
|
||||
isOpen: boolean;
|
||||
seriesIds: number[];
|
||||
onSavePress(payload: object): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function EditSeriesModal(props: EditSeriesModalProps) {
|
||||
const { isOpen, seriesIds, onSavePress, onModalClose } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<EditSeriesModalContent
|
||||
seriesIds={seriesIds}
|
||||
onSavePress={onSavePress}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSeriesModal;
|
@ -0,0 +1,16 @@
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||
.modalFooter {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
246
frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx
Normal file
246
frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
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 MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditSeriesModalContent.css';
|
||||
|
||||
interface SavePayload {
|
||||
monitored?: boolean;
|
||||
qualityProfileId?: number;
|
||||
seriesType?: string;
|
||||
seasonFolder?: boolean;
|
||||
rootFolderPath?: string;
|
||||
moveFiles?: boolean;
|
||||
}
|
||||
|
||||
interface EditSeriesModalContentProps {
|
||||
seriesIds: number[];
|
||||
onSavePress(payload: object): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
||||
{ key: 'monitored', value: 'Monitored' },
|
||||
{ key: 'unmonitored', value: 'Unmonitored' },
|
||||
];
|
||||
|
||||
const seasonFolderOptions = [
|
||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
||||
{ key: 'yes', value: 'Yes' },
|
||||
{ key: 'no', value: 'No' },
|
||||
];
|
||||
|
||||
function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||
const { seriesIds, onSavePress, onModalClose } = props;
|
||||
|
||||
const [monitored, setMonitored] = useState(NO_CHANGE);
|
||||
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
|
||||
NO_CHANGE
|
||||
);
|
||||
const [seriesType, setSeriesType] = useState(NO_CHANGE);
|
||||
const [seasonFolder, setSeasonFolder] = useState(NO_CHANGE);
|
||||
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
|
||||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const save = useCallback(
|
||||
(moveFiles) => {
|
||||
let hasChanges = false;
|
||||
const payload: SavePayload = {};
|
||||
|
||||
if (monitored !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.monitored = monitored === 'monitored';
|
||||
}
|
||||
|
||||
if (qualityProfileId !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.qualityProfileId = qualityProfileId as number;
|
||||
}
|
||||
|
||||
if (seriesType !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.seriesType = seriesType;
|
||||
}
|
||||
|
||||
if (seasonFolder !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.seasonFolder = seasonFolder === 'yes';
|
||||
}
|
||||
|
||||
if (rootFolderPath !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.rootFolderPath = rootFolderPath;
|
||||
payload.moveFiles = moveFiles;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
},
|
||||
[
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
rootFolderPath,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]
|
||||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
switch (name) {
|
||||
case 'monitored':
|
||||
setMonitored(value);
|
||||
break;
|
||||
case 'qualityProfileId':
|
||||
setQualityProfileId(value);
|
||||
break;
|
||||
case 'seriesType':
|
||||
setSeriesType(value);
|
||||
break;
|
||||
case 'seasonFolder':
|
||||
setSeasonFolder(value);
|
||||
break;
|
||||
case 'rootFolderPath':
|
||||
setRootFolderPath(value);
|
||||
break;
|
||||
default:
|
||||
console.warn('EditSeriesModalContent Unknown Input');
|
||||
}
|
||||
},
|
||||
[setMonitored]
|
||||
);
|
||||
|
||||
const onSavePressWrapper = useCallback(() => {
|
||||
if (rootFolderPath === NO_CHANGE) {
|
||||
save(false);
|
||||
} else {
|
||||
setIsConfirmMoveModalOpen(true);
|
||||
}
|
||||
}, [rootFolderPath, save]);
|
||||
|
||||
const onDoNotMoveSeriesPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
save(false);
|
||||
}, [setIsConfirmMoveModalOpen, save]);
|
||||
|
||||
const onMoveSeriesPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
save(true);
|
||||
}, [setIsConfirmMoveModalOpen, save]);
|
||||
|
||||
const selectedCount = seriesIds.length;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('Edit Selected Series')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Quality Profile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Series Type')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
value={seriesType}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Season Folder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="seasonFolder"
|
||||
value={seasonFolder}
|
||||
values={seasonFolderOptions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Root Folder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
helpText={translate(
|
||||
'Moving series to the same root folder can be used to rename series folders to match updated title or naming format'
|
||||
)}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.selected}>
|
||||
{translate('{count} series selected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button onPress={onSavePressWrapper}>
|
||||
{translate('Apply Changes')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveSeriesModal
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
destinationRootFolder={rootFolderPath}
|
||||
onSavePress={onDoNotMoveSeriesPress}
|
||||
onMoveSeriesPress={onMoveSeriesPress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSeriesModalContent;
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import OrganizeSeriesModalContent from './OrganizeSeriesModalContent';
|
||||
|
||||
interface OrganizeSeriesModalProps {
|
||||
isOpen: boolean;
|
||||
seriesIds: number[];
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function OrganizeSeriesModal(props: OrganizeSeriesModalProps) {
|
||||
const { isOpen, onModalClose, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<OrganizeSeriesModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizeSeriesModal;
|
@ -0,0 +1,8 @@
|
||||
.renameIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import { orderBy } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RENAME_SERIES } from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
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 { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import styles from './OrganizeSeriesModalContent.css';
|
||||
|
||||
interface OrganizeSeriesModalContentProps {
|
||||
seriesIds: number[];
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) {
|
||||
const { seriesIds, onModalClose } = props;
|
||||
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const seriesTitles = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
|
||||
const sorted = orderBy(series, ['sortTitle']);
|
||||
|
||||
return sorted.map((s) => s.title);
|
||||
}, [seriesIds, allSeries]);
|
||||
|
||||
const onOrganizePress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: RENAME_SERIES,
|
||||
seriesIds,
|
||||
})
|
||||
);
|
||||
|
||||
onModalClose();
|
||||
}, [seriesIds, onModalClose, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Organize Selected Series</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview a rename, select "Cancel", then select any series
|
||||
title and use the
|
||||
<Icon className={styles.renameIcon} name={icons.ORGANIZE} />
|
||||
</Alert>
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to organize all files in the{' '}
|
||||
{seriesTitles.length} selected series?
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{seriesTitles.map((title) => {
|
||||
return <li key={title}>{title}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={onOrganizePress}>
|
||||
Organize
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizeSeriesModalContent;
|
@ -3,7 +3,14 @@ import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
|
||||
function SeriesIndexSelectAllButton() {
|
||||
interface SeriesIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
}
|
||||
|
||||
function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) {
|
||||
const { isSelectMode } = props;
|
||||
const [selectState, selectDispatch] = useSelect();
|
||||
const { allSelected, allUnselected } = selectState;
|
||||
|
||||
@ -23,13 +30,13 @@ function SeriesIndexSelectAllButton() {
|
||||
});
|
||||
}, [allSelected, selectDispatch]);
|
||||
|
||||
return (
|
||||
return isSelectMode ? (
|
||||
<PageToolbarButton
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
iconName={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default SeriesIndexSelectAllButton;
|
||||
|
@ -0,0 +1,43 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||
import { icons } from 'Helpers/Props';
|
||||
|
||||
interface SeriesIndexSelectAllMenuItemProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
}
|
||||
|
||||
function SeriesIndexSelectAllMenuItem(
|
||||
props: SeriesIndexSelectAllMenuItemProps
|
||||
) {
|
||||
const { isSelectMode } = props;
|
||||
const [selectState, selectDispatch] = useSelect();
|
||||
const { allSelected, allUnselected } = selectState;
|
||||
|
||||
let iconName = icons.SQUARE_MINUS;
|
||||
|
||||
if (allSelected) {
|
||||
iconName = icons.CHECK_SQUARE;
|
||||
} else if (allUnselected) {
|
||||
iconName = icons.SQUARE;
|
||||
}
|
||||
|
||||
const onPressWrapper = useCallback(() => {
|
||||
selectDispatch({
|
||||
type: allSelected
|
||||
? SelectActionType.UnselectAll
|
||||
: SelectActionType.SelectAll,
|
||||
});
|
||||
}, [allSelected, selectDispatch]);
|
||||
|
||||
return isSelectMode ? (
|
||||
<PageToolbarOverflowMenuItem
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
iconName={iconName}
|
||||
onPress={onPressWrapper}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default SeriesIndexSelectAllMenuItem;
|
68
frontend/src/Series/Index/Select/SeriesIndexSelectFooter.css
Normal file
68
frontend/src/Series/Index/Select/SeriesIndexSelectFooter.css
Normal file
@ -0,0 +1,68 @@
|
||||
.footer {
|
||||
composes: contentFooter from '~Components/Page/PageContentFooter.css';
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.actionButtons,
|
||||
.deleteButtons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.deleteButtons {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-direction: column;
|
||||
margin-top: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.actionButtons,
|
||||
.deleteButtons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.deleteButtons {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
justify-content: center;
|
||||
order: -1;
|
||||
}
|
||||
}
|
216
frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx
Normal file
216
frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import { RENAME_SERIES } from 'Commands/commandNames';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { saveSeriesEditor } from 'Store/Actions/seriesActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import DeleteSeriesModal from './Delete/DeleteSeriesModal';
|
||||
import EditSeriesModal from './Edit/EditSeriesModal';
|
||||
import OrganizeSeriesModal from './Organize/OrganizeSeriesModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './SeriesIndexSelectFooter.css';
|
||||
|
||||
const seriesEditorSelector = createSelector(
|
||||
(state) => state.series,
|
||||
(series) => {
|
||||
const { isSaving, isDeleting, deleteError } = series;
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
isDeleting,
|
||||
deleteError
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function SeriesIndexSelectFooter() {
|
||||
const { isSaving, isDeleting, deleteError } =
|
||||
useSelector(seriesEditorSelector);
|
||||
|
||||
const isOrganizingSeries = useSelector(
|
||||
createCommandExecutingSelector(RENAME_SERIES)
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false);
|
||||
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isSavingSeries, setIsSavingSeries] = useState(false);
|
||||
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||
|
||||
const [selectState, selectDispatch] = useSelect();
|
||||
const { selectedState } = selectState;
|
||||
|
||||
const seriesIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const selectedCount = seriesIds.length;
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload) => {
|
||||
setIsSavingSeries(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveSeriesEditor({
|
||||
...payload,
|
||||
seriesIds,
|
||||
})
|
||||
);
|
||||
},
|
||||
[seriesIds, dispatch]
|
||||
);
|
||||
|
||||
const onOrganizePress = useCallback(() => {
|
||||
setIsOrganizeModalOpen(true);
|
||||
}, [setIsOrganizeModalOpen]);
|
||||
|
||||
const onOrganizeModalClose = useCallback(() => {
|
||||
setIsOrganizeModalOpen(false);
|
||||
}, [setIsOrganizeModalOpen]);
|
||||
|
||||
const onTagsPress = useCallback(() => {
|
||||
setIsTagsModalOpen(true);
|
||||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onTagsModalClose = useCallback(() => {
|
||||
setIsTagsModalOpen(false);
|
||||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags, applyTags) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveSeriesEditor({
|
||||
seriesIds,
|
||||
tags,
|
||||
applyTags,
|
||||
})
|
||||
);
|
||||
},
|
||||
[seriesIds, dispatch]
|
||||
);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
|
||||
const onDeleteModalClose = useCallback(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving) {
|
||||
setIsSavingSeries(false);
|
||||
setIsSavingTags(false);
|
||||
}
|
||||
}, [isSaving]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDeleting && !deleteError) {
|
||||
selectDispatch({ type: SelectActionType.UnselectAll });
|
||||
}
|
||||
}, [isDeleting, deleteError, selectDispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
const anySelected = selectedCount > 0;
|
||||
|
||||
return (
|
||||
<PageContentFooter className={styles.footer}>
|
||||
<div className={styles.buttons}>
|
||||
<div className={styles.actionButtons}>
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving && isSavingSeries}
|
||||
isDisabled={!anySelected || isOrganizingSeries}
|
||||
onPress={onEditPress}
|
||||
>
|
||||
{translate('Edit')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isOrganizingSeries}
|
||||
isDisabled={!anySelected || isOrganizingSeries}
|
||||
onPress={onOrganizePress}
|
||||
>
|
||||
{translate('Rename Files')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving && isSavingTags}
|
||||
isDisabled={!anySelected || isOrganizingSeries}
|
||||
onPress={onTagsPress}
|
||||
>
|
||||
{translate('Set Tags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.deleteButtons}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!anySelected || isDeleting}
|
||||
onPress={onDeletePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.selected}>
|
||||
{translate('{count} series selected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<EditSeriesModal
|
||||
isOpen={isEditModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
onSavePress={onSavePress}
|
||||
onModalClose={onEditModalClose}
|
||||
/>
|
||||
|
||||
<TagsModal
|
||||
isOpen={isTagsModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
onApplyTagsPress={onApplyTagsPress}
|
||||
onModalClose={onTagsModalClose}
|
||||
/>
|
||||
|
||||
<OrganizeSeriesModal
|
||||
isOpen={isOrganizeModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
onModalClose={onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<DeleteSeriesModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
onModalClose={onDeleteModalClose}
|
||||
/>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesIndexSelectFooter;
|
@ -0,0 +1,37 @@
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
|
||||
interface SeriesIndexSelectModeButtonProps {
|
||||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function SeriesIndexSelectModeButton(props: SeriesIndexSelectModeButtonProps) {
|
||||
const { label, iconName, isSelectMode, onPress } = props;
|
||||
const [, selectDispatch] = useSelect();
|
||||
|
||||
const onPressWrapper = useCallback(() => {
|
||||
if (isSelectMode) {
|
||||
selectDispatch({
|
||||
type: SelectActionType.Reset,
|
||||
});
|
||||
}
|
||||
|
||||
onPress();
|
||||
}, [isSelectMode, onPress, selectDispatch]);
|
||||
|
||||
return (
|
||||
<PageToolbarButton
|
||||
label={label}
|
||||
iconName={iconName}
|
||||
onPress={onPressWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesIndexSelectModeButton;
|
@ -0,0 +1,38 @@
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||
|
||||
interface SeriesIndexSelectModeMenuItemProps {
|
||||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function SeriesIndexSelectModeMenuItem(
|
||||
props: SeriesIndexSelectModeMenuItemProps
|
||||
) {
|
||||
const { label, iconName, isSelectMode, onPress } = props;
|
||||
const [, selectDispatch] = useSelect();
|
||||
|
||||
const onPressWrapper = useCallback(() => {
|
||||
if (isSelectMode) {
|
||||
selectDispatch({
|
||||
type: SelectActionType.Reset,
|
||||
});
|
||||
}
|
||||
|
||||
onPress();
|
||||
}, [isSelectMode, onPress, selectDispatch]);
|
||||
|
||||
return (
|
||||
<PageToolbarOverflowMenuItem
|
||||
label={label}
|
||||
iconName={iconName}
|
||||
onPress={onPressWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesIndexSelectModeMenuItem;
|
22
frontend/src/Series/Index/Select/Tags/TagsModal.tsx
Normal file
22
frontend/src/Series/Index/Select/Tags/TagsModal.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import TagsModalContent from './TagsModalContent';
|
||||
|
||||
interface TagsModalProps {
|
||||
isOpen: boolean;
|
||||
seriesIds: number[];
|
||||
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function TagsModal(props: TagsModalProps) {
|
||||
const { isOpen, onModalClose, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagsModal;
|
12
frontend/src/Series/Index/Select/Tags/TagsModalContent.css
Normal file
12
frontend/src/Series/Index/Select/Tags/TagsModalContent.css
Normal file
@ -0,0 +1,12 @@
|
||||
.renameIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding-top: 4px;
|
||||
}
|
167
frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx
Normal file
167
frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { concat, uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
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 Label from 'Components/Label';
|
||||
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, kinds, sizes } from 'Helpers/Props';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import styles from './TagsModalContent.css';
|
||||
|
||||
interface TagsModalContentProps {
|
||||
seriesIds: number[];
|
||||
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function TagsModalContent(props: TagsModalContentProps) {
|
||||
const { seriesIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
||||
const seriesTags = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
|
||||
return uniq(concat(...series.map((s) => s.tags)));
|
||||
}, [seriesIds, allSeries]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
);
|
||||
|
||||
const onApplyPress = useCallback(() => {
|
||||
onApplyTagsPress(tags, applyTags);
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
{ key: 'add', value: 'Add' },
|
||||
{ key: 'remove', value: 'Remove' },
|
||||
{ key: 'replace', value: 'Replace' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Tags</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
onChange={onTagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Apply Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="applyTags"
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
'How to apply tags to the selected series',
|
||||
'Add: Add the tags the existing list of tags',
|
||||
'Remove: Remove the entered tags',
|
||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
||||
]}
|
||||
onChange={onApplyTagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Result</FormLabel>
|
||||
|
||||
<div className={styles.result}>
|
||||
{seriesTags.map((id) => {
|
||||
const tag = tagList.find((t) => t.id === id);
|
||||
|
||||
if (!tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const removeTag =
|
||||
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
|
||||
(applyTags === 'replace' && tags.indexOf(id) === -1);
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{tag.label}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
|
||||
{(applyTags === 'add' || applyTags === 'replace') &&
|
||||
tags.map((id) => {
|
||||
const tag = tagList.find((t) => t.id === id);
|
||||
|
||||
if (!tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (seriesTags.indexOf(id) > -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={'Adding tag'}
|
||||
kind={kinds.SUCCESS}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{tag.label}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
|
||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||
Apply
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagsModalContent;
|
@ -7,6 +7,7 @@
|
||||
.contentBody {
|
||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ import SeriesIndexOverviews from './Overview/SeriesIndexOverviews';
|
||||
import SeriesIndexPosterOptionsModal from './Posters/Options/SeriesIndexPosterOptionsModal';
|
||||
import SeriesIndexPosters from './Posters/SeriesIndexPosters';
|
||||
import SeriesIndexSelectAllButton from './Select/SeriesIndexSelectAllButton';
|
||||
import SeriesIndexSelectAllMenuItem from './Select/SeriesIndexSelectAllMenuItem';
|
||||
import SeriesIndexSelectFooter from './Select/SeriesIndexSelectFooter';
|
||||
import SeriesIndexSelectModeButton from './Select/SeriesIndexSelectModeButton';
|
||||
import SeriesIndexSelectModeMenuItem from './Select/SeriesIndexSelectModeMenuItem';
|
||||
import SeriesIndexFooter from './SeriesIndexFooter';
|
||||
import SeriesIndexTable from './Table/SeriesIndexTable';
|
||||
import SeriesIndexTableOptions from './Table/SeriesIndexTableOptions';
|
||||
@ -201,7 +205,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||
const hasNoSeries = !totalItems;
|
||||
|
||||
return (
|
||||
<SelectProvider isSelectMode={isSelectMode} items={items}>
|
||||
<SelectProvider items={items}>
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
@ -224,13 +228,19 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
<SeriesIndexSelectModeButton
|
||||
label={isSelectMode ? 'Stop Selecting' : 'Select Series'}
|
||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={SeriesIndexSelectModeMenuItem}
|
||||
onPress={onSelectModePress}
|
||||
/>
|
||||
|
||||
{isSelectMode ? <SeriesIndexSelectAllButton /> : null}
|
||||
<SeriesIndexSelectAllButton
|
||||
label="SelectAll"
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={SeriesIndexSelectAllMenuItem}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
@ -310,7 +320,6 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||
<NoSeries totalItems={totalItems} />
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
{isLoaded && !!jumpBarItems.order.length ? (
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
@ -318,6 +327,9 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isSelectMode ? <SeriesIndexSelectFooter /> : null}
|
||||
|
||||
{view === 'posters' ? (
|
||||
<SeriesIndexPosterOptionsModal
|
||||
isOpen={isOptionsModalOpen}
|
||||
|
@ -90,6 +90,12 @@
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: cell;
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.episodeProgress,
|
||||
.latestSeason {
|
||||
composes: cell;
|
||||
|
@ -59,6 +59,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
||||
previousAiring,
|
||||
added,
|
||||
statistics = {},
|
||||
seasonFolder,
|
||||
images,
|
||||
seriesType,
|
||||
network,
|
||||
@ -129,7 +130,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
||||
setIsDeleteSeriesModalOpen(false);
|
||||
}, [setIsDeleteSeriesModalOpen]);
|
||||
|
||||
const onUseSceneNumberingChange = useCallback(() => {
|
||||
const checkInputCallback = useCallback(() => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
}, []);
|
||||
|
||||
@ -280,6 +281,19 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'seasonFolder') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<CheckInput
|
||||
name="seasonFolder"
|
||||
value={seasonFolder}
|
||||
isDisabled={true}
|
||||
onChange={checkInputCallback}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'episodeProgress') {
|
||||
const progress = episodeCount
|
||||
? (episodeFileCount / episodeCount) * 100
|
||||
@ -413,7 +427,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
||||
name="useSceneNumbering"
|
||||
value={useSceneNumbering}
|
||||
isDisabled={true}
|
||||
onChange={onUseSceneNumberingChange}
|
||||
onChange={checkInputCallback}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
|
@ -54,6 +54,12 @@
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
|
||||
.episodeProgress,
|
||||
.latestSeason {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
@ -381,6 +381,8 @@ export const defaultState = {
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
sortKey: 'sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
@ -405,6 +407,8 @@ export const DELETE_SERIES = 'series/deleteSeries';
|
||||
export const TOGGLE_SERIES_MONITORED = 'series/toggleSeriesMonitored';
|
||||
export const TOGGLE_SEASON_MONITORED = 'series/toggleSeasonMonitored';
|
||||
export const UPDATE_SERIES_MONITOR = 'series/updateSeriesMonitor';
|
||||
export const SAVE_SERIES_EDITOR = 'series/saveSeriesEditor';
|
||||
export const BULK_DELETE_SERIES = 'series/bulkDeleteSeries';
|
||||
|
||||
export const SET_DELETE_OPTION = 'series/setDeleteOption';
|
||||
|
||||
@ -441,6 +445,8 @@ export const deleteSeries = createThunk(DELETE_SERIES, (payload) => {
|
||||
export const toggleSeriesMonitored = createThunk(TOGGLE_SERIES_MONITORED);
|
||||
export const toggleSeasonMonitored = createThunk(TOGGLE_SEASON_MONITORED);
|
||||
export const updateSeriesMonitor = createThunk(UPDATE_SERIES_MONITOR);
|
||||
export const saveSeriesEditor = createThunk(SAVE_SERIES_EDITOR);
|
||||
export const bulkDeleteSeries = createThunk(BULK_DELETE_SERIES);
|
||||
|
||||
export const setSeriesValue = createAction(SET_SERIES_VALUE, (payload) => {
|
||||
return {
|
||||
@ -659,6 +665,87 @@ export const actionHandlers = handleThunks({
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[SAVE_SERIES_EDITOR]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/series/editor',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
...data.map((series) => {
|
||||
|
||||
const {
|
||||
alternateTitles,
|
||||
images,
|
||||
rootFolderPath,
|
||||
statistics,
|
||||
...propsToUpdate
|
||||
} = series;
|
||||
|
||||
return updateItem({
|
||||
id: series.id,
|
||||
section: 'series',
|
||||
...propsToUpdate
|
||||
});
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[BULK_DELETE_SERIES]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/series/editor',
|
||||
method: 'DELETE',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
// SignaR will take care of removing the series from the collection
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -134,10 +134,19 @@ export const actionHandlers = handleThunks({
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
...data.map((series) => {
|
||||
|
||||
const {
|
||||
alternateTitles,
|
||||
images,
|
||||
rootFolderPath,
|
||||
statistics,
|
||||
...propsToUpdate
|
||||
} = series;
|
||||
|
||||
return updateItem({
|
||||
id: series.id,
|
||||
section: 'series',
|
||||
...series
|
||||
...propsToUpdate
|
||||
});
|
||||
}),
|
||||
|
||||
|
@ -113,6 +113,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'seasonFolder',
|
||||
label: 'Season Folder',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'episodeProgress',
|
||||
label: 'Episodes',
|
||||
|
@ -40,7 +40,7 @@ module.exports = {
|
||||
themeDarkColor: '#494949',
|
||||
themeLightColor: '#595959',
|
||||
pageBackground: '#202020',
|
||||
pageFooterBackgroud: 'rgba(0, 0, 0, .25)',
|
||||
pageFooterBackground: 'rgba(0, 0, 0, .25)',
|
||||
|
||||
torrentColor: '#00853d',
|
||||
usenetColor: '#17b1d9',
|
||||
|
@ -42,7 +42,7 @@ module.exports = {
|
||||
themeDarkColor: '#3a3f51',
|
||||
themeLightColor: '#4f566f',
|
||||
pageBackground: '#f5f7fa',
|
||||
pageFooterBackgroud: '#f1f1f1',
|
||||
pageFooterBackground: '#f1f1f1',
|
||||
|
||||
torrentColor: '#00853d',
|
||||
usenetColor: '#17b1d9',
|
||||
|
Loading…
Reference in New Issue
Block a user