mirror of
https://github.com/Radarr/Radarr.git
synced 2024-10-05 15:47:20 +02:00
New: Mass Editor is now part of movie list
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
ee5fed8522
commit
e85c010bf2
@ -1,6 +1,14 @@
|
|||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@ -35,3 +43,17 @@
|
|||||||
.importError {
|
.importError {
|
||||||
margin-left: 10px;
|
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> {
|
interface SelectProviderOptions<T extends ModelBase> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
children: any;
|
children: any;
|
||||||
isSelectMode: boolean;
|
|
||||||
items: Array<T>;
|
items: Array<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +96,7 @@ function selectReducer(state: SelectState, action: SelectAction): SelectState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SelectActionType.ToggleSelected: {
|
case SelectActionType.ToggleSelected: {
|
||||||
var result = {
|
const result = {
|
||||||
items,
|
items,
|
||||||
...toggleSelected(
|
...toggleSelected(
|
||||||
state,
|
state,
|
||||||
@ -129,7 +128,7 @@ function selectReducer(state: SelectState, action: SelectAction): SelectState {
|
|||||||
export function SelectProvider<T extends ModelBase>(
|
export function SelectProvider<T extends ModelBase>(
|
||||||
props: SelectProviderOptions<T>
|
props: SelectProviderOptions<T>
|
||||||
) {
|
) {
|
||||||
const { isSelectMode, items } = props;
|
const { items } = props;
|
||||||
const selectedState = getSelectedState(items, {});
|
const selectedState = getSelectedState(items, {});
|
||||||
|
|
||||||
const [state, dispatch] = React.useReducer(selectReducer, {
|
const [state, dispatch] = React.useReducer(selectReducer, {
|
||||||
@ -142,12 +141,6 @@ export function SelectProvider<T extends ModelBase>(
|
|||||||
|
|
||||||
const value: [SelectState, Dispatch] = [state, dispatch];
|
const value: [SelectState, Dispatch] = [state, dispatch];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSelectMode) {
|
|
||||||
dispatch({ type: SelectActionType.Reset });
|
|
||||||
}
|
|
||||||
}, [isSelectMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({ type: SelectActionType.UpdateItems, items });
|
dispatch({ type: SelectActionType.UpdateItems, items });
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
@ -4,7 +4,9 @@ import React from 'react';
|
|||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import styles from './Alert.css';
|
import styles from './Alert.css';
|
||||||
|
|
||||||
function Alert({ className, kind, children, ...otherProps }) {
|
function Alert(props) {
|
||||||
|
const { className, kind, children, ...otherProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -19,8 +21,8 @@ function Alert({ className, kind, children, ...otherProps }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Alert.propTypes = {
|
Alert.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||||
@ -264,12 +264,16 @@ FormInputGroup.propTypes = {
|
|||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
unit: PropTypes.string,
|
unit: PropTypes.string,
|
||||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
||||||
helpTextWarning: PropTypes.string,
|
helpTextWarning: PropTypes.string,
|
||||||
helpLink: PropTypes.string,
|
helpLink: PropTypes.string,
|
||||||
|
includeNoChange: PropTypes.bool,
|
||||||
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
|
selectedValueOptions: PropTypes.object,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
@ -5,14 +5,15 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
(qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
|
||||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||||
return {
|
return {
|
||||||
key: qualityProfile.id,
|
key: qualityProfile.id,
|
||||||
@ -24,7 +25,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: 'No Change',
|
||||||
disabled: true
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +56,8 @@ class QualityProfileSelectInputConnector extends Component {
|
|||||||
values
|
values
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!value || !values.some((v) => v.key === value) ) {
|
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
||||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
||||||
|
|
||||||
if (firstValue) {
|
if (firstValue) {
|
||||||
this.onChange({ name, value: firstValue.key });
|
this.onChange({ name, value: firstValue.key });
|
||||||
@ -76,7 +77,7 @@ class QualityProfileSelectInputConnector extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
/>
|
/>
|
||||||
|
@ -13,7 +13,8 @@ function createMapStateToProps() {
|
|||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state, { includeMissingValue }) => includeMissingValue,
|
(state, { includeMissingValue }) => includeMissingValue,
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(rootFolders, value, includeMissingValue, includeNoChange) => {
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
|
(rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => {
|
||||||
const values = rootFolders.items.map((rootFolder) => {
|
const values = rootFolders.items.map((rootFolder) => {
|
||||||
return {
|
return {
|
||||||
key: rootFolder.path,
|
key: rootFolder.path,
|
||||||
@ -27,7 +28,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: 'No Change',
|
||||||
isDisabled: true,
|
isDisabled: includeNoChangeDisabled,
|
||||||
isMissing: false
|
isMissing: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ function Label(props) {
|
|||||||
|
|
||||||
Label.propTypes = {
|
Label.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||||
outline: PropTypes.bool.isRequired,
|
outline: PropTypes.bool.isRequired,
|
||||||
|
@ -42,6 +42,7 @@ function SpinnerButton(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpinnerButton.propTypes = {
|
SpinnerButton.propTypes = {
|
||||||
|
...Button.Props,
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
|
@ -8,14 +8,6 @@
|
|||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.contentFooter {
|
.contentFooter {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
div {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
7
frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css.d.ts
vendored
Normal file
7
frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'icon': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -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,13 +4,12 @@ import React, { Component } from 'react';
|
|||||||
import Measure from 'Components/Measure';
|
import Measure from 'Components/Measure';
|
||||||
import Menu from 'Components/Menu/Menu';
|
import Menu from 'Components/Menu/Menu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
|
||||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
|
||||||
import { forEach } from 'Helpers/elementChildren';
|
import { forEach } from 'Helpers/elementChildren';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import PageToolbarOverflowMenuItem from './PageToolbarOverflowMenuItem';
|
||||||
import styles from './PageToolbarSection.css';
|
import styles from './PageToolbarSection.css';
|
||||||
|
|
||||||
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
||||||
@ -169,28 +168,15 @@ class PageToolbarSection extends Component {
|
|||||||
{
|
{
|
||||||
overflowItems.map((item) => {
|
overflowItems.map((item) => {
|
||||||
const {
|
const {
|
||||||
iconName,
|
|
||||||
spinningName,
|
|
||||||
label,
|
label,
|
||||||
isDisabled,
|
overflowComponent: OverflowComponent = PageToolbarOverflowMenuItem
|
||||||
isSpinning,
|
|
||||||
...otherProps
|
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<OverflowComponent
|
||||||
key={label}
|
key={label}
|
||||||
isDisabled={isDisabled || isSpinning}
|
{...item}
|
||||||
{...otherProps}
|
/>
|
||||||
>
|
|
||||||
<SpinnerIcon
|
|
||||||
className={styles.overflowMenuItemIcon}
|
|
||||||
name={iconName}
|
|
||||||
spinningName={spinningName}
|
|
||||||
isSpinning={isSpinning}
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ function SpinnerIcon(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpinnerIcon.propTypes = {
|
SpinnerIcon.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
name: PropTypes.object.isRequired,
|
name: PropTypes.object.isRequired,
|
||||||
spinningName: PropTypes.object.isRequired,
|
spinningName: PropTypes.object.isRequired,
|
||||||
isSpinning: PropTypes.bool.isRequired
|
isSpinning: PropTypes.bool.isRequired
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
.body {
|
.body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
background-color: var(--popoverBodyBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipBody {
|
.tooltipBody {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import styles from './Popover.css';
|
import styles from './Popover.css';
|
||||||
|
|
||||||
@ -30,8 +31,13 @@ function Popover(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Popover.propTypes = {
|
Popover.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
bodyClassName: PropTypes.string,
|
||||||
|
anchor: PropTypes.node.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
|
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||||
|
position: PropTypes.oneOf(tooltipPositions.all),
|
||||||
|
canFlip: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Popover;
|
export default Popover;
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
&.inverse {
|
&.inverse {
|
||||||
background-color: var(--themeDarkColor);
|
background-color: var(--themeDarkColor);
|
||||||
box-shadow: 0 5px 10px var(--popoverShadowInverseColor);
|
box-shadow: 0 5px 10px var(--popoverShadowInverseColor);
|
||||||
|
color: var(--white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
8
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
enum TooltipPosition {
|
||||||
|
Top = 'top',
|
||||||
|
Right = 'right',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
Left = 'left',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TooltipPosition;
|
@ -13,6 +13,7 @@
|
|||||||
.contentBody {
|
.contentBody {
|
||||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
composes: contentBody from '~Components/Page/PageContentBody.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { SelectProvider } from 'App/SelectContext';
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
import { REFRESH_MOVIE, RSS_SYNC } from 'Commands/commandNames';
|
import { RSS_SYNC } from 'Commands/commandNames';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import NoMovie from 'Movie/NoMovie';
|
import NoMovie from 'Movie/NoMovie';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import {
|
import {
|
||||||
@ -31,11 +32,17 @@ import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
|
|||||||
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
||||||
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
||||||
import MovieIndexFooter from './MovieIndexFooter';
|
import MovieIndexFooter from './MovieIndexFooter';
|
||||||
|
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
||||||
|
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
||||||
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
||||||
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
||||||
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
||||||
import MovieIndexPosterOptionsModal from './Posters/Options/MovieIndexPosterOptionsModal';
|
import MovieIndexPosterOptionsModal from './Posters/Options/MovieIndexPosterOptionsModal';
|
||||||
import MovieIndexSelectAllButton from './Select/MovieIndexSelectAllButton';
|
import MovieIndexSelectAllButton from './Select/MovieIndexSelectAllButton';
|
||||||
|
import MovieIndexSelectAllMenuItem from './Select/MovieIndexSelectAllMenuItem';
|
||||||
|
import MovieIndexSelectFooter from './Select/MovieIndexSelectFooter';
|
||||||
|
import MovieIndexSelectModeButton from './Select/MovieIndexSelectModeButton';
|
||||||
|
import MovieIndexSelectModeMenuItem from './Select/MovieIndexSelectModeMenuItem';
|
||||||
import MovieIndexTable from './Table/MovieIndexTable';
|
import MovieIndexTable from './Table/MovieIndexTable';
|
||||||
import MovieIndexTableOptions from './Table/MovieIndexTableOptions';
|
import MovieIndexTableOptions from './Table/MovieIndexTableOptions';
|
||||||
import styles from './MovieIndex.css';
|
import styles from './MovieIndex.css';
|
||||||
@ -72,9 +79,6 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
view,
|
view,
|
||||||
} = useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
} = useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||||
|
|
||||||
const isRefreshingMovie = useSelector(
|
|
||||||
createCommandExecutingSelector(REFRESH_MOVIE)
|
|
||||||
);
|
|
||||||
const isRssSyncExecuting = useSelector(
|
const isRssSyncExecuting = useSelector(
|
||||||
createCommandExecutingSelector(RSS_SYNC)
|
createCommandExecutingSelector(RSS_SYNC)
|
||||||
);
|
);
|
||||||
@ -82,17 +86,11 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const scrollerRef = useRef<HTMLDivElement>();
|
const scrollerRef = useRef<HTMLDivElement>();
|
||||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||||
|
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||||
|
useState(false);
|
||||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||||
|
|
||||||
const onRefreshMoviePress = useCallback(() => {
|
|
||||||
dispatch(
|
|
||||||
executeCommand({
|
|
||||||
name: REFRESH_MOVIE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const onSelectModePress = useCallback(() => {
|
const onSelectModePress = useCallback(() => {
|
||||||
setIsSelectMode(!isSelectMode);
|
setIsSelectMode(!isSelectMode);
|
||||||
}, [isSelectMode, setIsSelectMode]);
|
}, [isSelectMode, setIsSelectMode]);
|
||||||
@ -145,6 +143,14 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
setIsOptionsModalOpen(false);
|
setIsOptionsModalOpen(false);
|
||||||
}, [setIsOptionsModalOpen]);
|
}, [setIsOptionsModalOpen]);
|
||||||
|
|
||||||
|
const onInteractiveImportPress = useCallback(() => {
|
||||||
|
setIsInteractiveImportModalOpen(true);
|
||||||
|
}, [setIsInteractiveImportModalOpen]);
|
||||||
|
|
||||||
|
const onInteractiveImportModalClose = useCallback(() => {
|
||||||
|
setIsInteractiveImportModalOpen(false);
|
||||||
|
}, [setIsInteractiveImportModalOpen]);
|
||||||
|
|
||||||
const onJumpBarItemPress = useCallback(
|
const onJumpBarItemPress = useCallback(
|
||||||
(character) => {
|
(character) => {
|
||||||
setJumpToCharacter(character);
|
setJumpToCharacter(character);
|
||||||
@ -202,17 +208,13 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
const hasNoMovie = !totalItems;
|
const hasNoMovie = !totalItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectProvider isSelectMode={isSelectMode} items={items}>
|
<SelectProvider items={items}>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<MovieIndexRefreshMovieButton
|
||||||
label={translate('UpdateAll')}
|
isSelectMode={isSelectMode}
|
||||||
iconName={icons.REFRESH}
|
selectedFilterKey={selectedFilterKey}
|
||||||
spinningName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={onRefreshMoviePress}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
@ -225,17 +227,37 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
|
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<MovieIndexSearchButton
|
||||||
|
isSelectMode={isSelectMode}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
label={translate('ManualImport')}
|
||||||
|
iconName={icons.INTERACTIVE}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onPress={onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<MovieIndexSelectModeButton
|
||||||
label={
|
label={
|
||||||
isSelectMode
|
isSelectMode
|
||||||
? translate('StopSelecting')
|
? translate('StopSelecting')
|
||||||
: translate('SelectMovie')
|
: translate('EditMovies')
|
||||||
}
|
}
|
||||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
iconName={isSelectMode ? icons.SERIES_ENDED : icons.EDIT}
|
||||||
|
isSelectMode={isSelectMode}
|
||||||
|
overflowComponent={MovieIndexSelectModeMenuItem}
|
||||||
onPress={onSelectModePress}
|
onPress={onSelectModePress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isSelectMode ? <MovieIndexSelectAllButton /> : null}
|
<MovieIndexSelectAllButton
|
||||||
|
label="SelectAll"
|
||||||
|
isSelectMode={isSelectMode}
|
||||||
|
overflowComponent={MovieIndexSelectAllMenuItem}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection
|
<PageToolbarSection
|
||||||
@ -326,6 +348,14 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isSelectMode ? <MovieIndexSelectFooter /> : null}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
onModalClose={onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
{view === 'posters' ? (
|
{view === 'posters' ? (
|
||||||
<MovieIndexPosterOptionsModal
|
<MovieIndexPosterOptionsModal
|
||||||
isOpen={isOptionsModalOpen}
|
isOpen={isOptionsModalOpen}
|
||||||
|
72
frontend/src/Movie/Index/MovieIndexRefreshMovieButton.tsx
Normal file
72
frontend/src/Movie/Index/MovieIndexRefreshMovieButton.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
|
import { REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
||||||
|
interface MovieIndexRefreshMovieButtonProps {
|
||||||
|
isSelectMode: boolean;
|
||||||
|
selectedFilterKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexRefreshMovieButton(
|
||||||
|
props: MovieIndexRefreshMovieButtonProps
|
||||||
|
) {
|
||||||
|
const isRefreshing = useSelector(
|
||||||
|
createCommandExecutingSelector(REFRESH_MOVIE)
|
||||||
|
);
|
||||||
|
const { items, totalItems } = useSelector(
|
||||||
|
createMovieClientSideCollectionItemsSelector('movieIndex')
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isSelectMode, selectedFilterKey } = props;
|
||||||
|
const [selectState] = useSelect();
|
||||||
|
const { selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedMovieIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const moviesToRefresh =
|
||||||
|
isSelectMode && selectedMovieIds.length > 0
|
||||||
|
? selectedMovieIds
|
||||||
|
: items.map((m) => m.id);
|
||||||
|
|
||||||
|
const refreshIndexLabel =
|
||||||
|
selectedFilterKey === 'all'
|
||||||
|
? translate('UpdateAll')
|
||||||
|
: translate('UpdateFiltered');
|
||||||
|
|
||||||
|
const refreshSelectLabel =
|
||||||
|
selectedMovieIds.length > 0
|
||||||
|
? translate('UpdateSelected')
|
||||||
|
: translate('UpdateAll');
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: REFRESH_MOVIE,
|
||||||
|
movieIds: moviesToRefresh,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, moviesToRefresh]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isSelectMode ? refreshSelectLabel : refreshIndexLabel}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
isDisabled={!totalItems}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexRefreshMovieButton;
|
68
frontend/src/Movie/Index/MovieIndexSearchButton.tsx
Normal file
68
frontend/src/Movie/Index/MovieIndexSearchButton.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
|
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
||||||
|
interface MovieIndexSearchButtonProps {
|
||||||
|
isSelectMode: boolean;
|
||||||
|
selectedFilterKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||||
|
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||||
|
const { items, totalItems } = useSelector(
|
||||||
|
createMovieClientSideCollectionItemsSelector('movieIndex')
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isSelectMode, selectedFilterKey } = props;
|
||||||
|
const [selectState] = useSelect();
|
||||||
|
const { selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedMovieIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const moviesToSearch =
|
||||||
|
isSelectMode && selectedMovieIds.length > 0
|
||||||
|
? selectedMovieIds
|
||||||
|
: items.map((m) => m.id);
|
||||||
|
|
||||||
|
const searchIndexLabel =
|
||||||
|
selectedFilterKey === 'all'
|
||||||
|
? translate('SearchAll')
|
||||||
|
: translate('SearchFiltered');
|
||||||
|
|
||||||
|
const searchSelectLabel =
|
||||||
|
selectedMovieIds.length > 0
|
||||||
|
? translate('SearchSelected')
|
||||||
|
: translate('SearchAll');
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieIds: moviesToSearch,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, moviesToSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
isDisabled={!totalItems}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexSearchButton;
|
24
frontend/src/Movie/Index/Select/Delete/DeleteMovieModal.tsx
Normal file
24
frontend/src/Movie/Index/Select/Delete/DeleteMovieModal.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DeleteMovieModalContent from './DeleteMovieModalContent';
|
||||||
|
|
||||||
|
interface DeleteMovieModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
movieIds: number[];
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteMovieModal(props: DeleteMovieModalProps) {
|
||||||
|
const { isOpen, movieIds, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<DeleteMovieModalContent
|
||||||
|
movieIds={movieIds}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteMovieModal;
|
@ -0,0 +1,13 @@
|
|||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pathContainer {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--dangerColor);
|
||||||
|
}
|
9
frontend/src/Movie/Index/Select/Delete/DeleteMovieModalContent.css.d.ts
vendored
Normal file
9
frontend/src/Movie/Index/Select/Delete/DeleteMovieModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'path': string;
|
||||||
|
'pathContainer': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,155 @@
|
|||||||
|
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 { bulkDeleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||||
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './DeleteMovieModalContent.css';
|
||||||
|
|
||||||
|
interface DeleteMovieModalContentProps {
|
||||||
|
movieIds: number[];
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDeleteOptions = createSelector(
|
||||||
|
(state) => state.movie.deleteOptions,
|
||||||
|
(deleteOptions) => deleteOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
|
||||||
|
const { movieIds, onModalClose } = props;
|
||||||
|
|
||||||
|
const { addImportListExclusion } = useSelector(selectDeleteOptions);
|
||||||
|
const allMovies = useSelector(createAllMoviesSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
|
const movies = useMemo(() => {
|
||||||
|
const movies = movieIds.map((id) => {
|
||||||
|
return allMovies.find((s) => s.id === id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderBy(movies, ['sortTitle']);
|
||||||
|
}, [movieIds, allMovies]);
|
||||||
|
|
||||||
|
const onDeleteFilesChange = useCallback(
|
||||||
|
({ value }) => {
|
||||||
|
setDeleteFiles(value);
|
||||||
|
},
|
||||||
|
[setDeleteFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeleteOptionChange = useCallback(
|
||||||
|
({ name, value }) => {
|
||||||
|
dispatch(
|
||||||
|
setDeleteOption({
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeleteMoviesConfirmed = useCallback(() => {
|
||||||
|
setDeleteFiles(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkDeleteMovie({
|
||||||
|
movieIds,
|
||||||
|
deleteFiles,
|
||||||
|
addImportListExclusion,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [
|
||||||
|
movieIds,
|
||||||
|
deleteFiles,
|
||||||
|
addImportListExclusion,
|
||||||
|
setDeleteFiles,
|
||||||
|
dispatch,
|
||||||
|
onModalClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('DeleteSelectedMovie')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AddListExclusion')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="addImportListExclusion"
|
||||||
|
value={addImportListExclusion}
|
||||||
|
helpText={translate('AddImportExclusionHelpText')}
|
||||||
|
onChange={onDeleteOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{`Delete Movie Folder${
|
||||||
|
movies.length > 1 ? 's' : ''
|
||||||
|
}`}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="deleteFiles"
|
||||||
|
value={deleteFiles}
|
||||||
|
helpText={`Delete Movie Folder${
|
||||||
|
movies.length > 1 ? 's' : ''
|
||||||
|
} and all contents`}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onChange={onDeleteFilesChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.message}>
|
||||||
|
{`Are you sure you want to delete ${movies.length} selected movie(s)${
|
||||||
|
deleteFiles ? ' and all contents' : ''
|
||||||
|
}?`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{movies.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}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={onDeleteMoviesConfirmed}>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteMovieModalContent;
|
26
frontend/src/Movie/Index/Select/Edit/EditMoviesModal.tsx
Normal file
26
frontend/src/Movie/Index/Select/Edit/EditMoviesModal.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditMoviesModalContent from './EditMoviesModalContent';
|
||||||
|
|
||||||
|
interface EditMoviesModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
movieIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMoviesModal(props: EditMoviesModalProps) {
|
||||||
|
const { isOpen, movieIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<EditMoviesModalContent
|
||||||
|
movieIds={movieIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMoviesModal;
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
8
frontend/src/Movie/Index/Select/Edit/EditMoviesModalContent.css.d.ts
vendored
Normal file
8
frontend/src/Movie/Index/Select/Edit/EditMoviesModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalFooter': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
187
frontend/src/Movie/Index/Select/Edit/EditMoviesModalContent.tsx
Normal file
187
frontend/src/Movie/Index/Select/Edit/EditMoviesModalContent.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
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 MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditMoviesModalContent.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
monitored?: boolean;
|
||||||
|
qualityProfileId?: number;
|
||||||
|
rootFolderPath?: string;
|
||||||
|
moveFiles?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditMoviesModalContentProps {
|
||||||
|
movieIds: 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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function EditMoviesModalContent(props: EditMoviesModalContentProps) {
|
||||||
|
const { movieIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
const [monitored, setMonitored] = useState(NO_CHANGE);
|
||||||
|
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
|
||||||
|
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 (rootFolderPath !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.rootFolderPath = rootFolderPath;
|
||||||
|
payload.moveFiles = moveFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onSavePress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
},
|
||||||
|
[monitored, qualityProfileId, rootFolderPath, onSavePress, onModalClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'monitored':
|
||||||
|
setMonitored(value);
|
||||||
|
break;
|
||||||
|
case 'qualityProfileId':
|
||||||
|
setQualityProfileId(value);
|
||||||
|
break;
|
||||||
|
case 'rootFolderPath':
|
||||||
|
setRootFolderPath(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('EditMoviesModalContent Unknown Input');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMonitored]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSavePressWrapper = useCallback(() => {
|
||||||
|
if (rootFolderPath === NO_CHANGE) {
|
||||||
|
save(false);
|
||||||
|
} else {
|
||||||
|
setIsConfirmMoveModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [rootFolderPath, save]);
|
||||||
|
|
||||||
|
const onDoNotMoveMoviePress = useCallback(() => {
|
||||||
|
setIsConfirmMoveModalOpen(false);
|
||||||
|
save(false);
|
||||||
|
}, [setIsConfirmMoveModalOpen, save]);
|
||||||
|
|
||||||
|
const onMoveMoviePress = useCallback(() => {
|
||||||
|
setIsConfirmMoveModalOpen(false);
|
||||||
|
save(true);
|
||||||
|
}, [setIsConfirmMoveModalOpen, save]);
|
||||||
|
|
||||||
|
const selectedCount = movieIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('EditSelectedMovies')}</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('Root Folder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
value={rootFolderPath}
|
||||||
|
includeNoChange={true}
|
||||||
|
includeNoChangeDisabled={false}
|
||||||
|
selectedValueOptions={{ includeFreeSpace: false }}
|
||||||
|
helpText={
|
||||||
|
'Moving movies to the same root folder can be used to rename movie folders to match updated title or naming format'
|
||||||
|
}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.selected}>
|
||||||
|
{translate('MoviesSelectedInterp', selectedCount.toString())}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={onSavePressWrapper}>
|
||||||
|
{translate('Apply Changes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<MoveMovieModal
|
||||||
|
isOpen={isConfirmMoveModalOpen}
|
||||||
|
destinationRootFolder={rootFolderPath}
|
||||||
|
onSavePress={onDoNotMoveMoviePress}
|
||||||
|
onMoveMoviePress={onMoveMoviePress}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMoviesModalContent;
|
@ -3,7 +3,14 @@ import { SelectActionType, useSelect } from 'App/SelectContext';
|
|||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
|
||||||
function MovieIndexSelectAllButton() {
|
interface MovieIndexSelectAllButtonProps {
|
||||||
|
label: string;
|
||||||
|
isSelectMode: boolean;
|
||||||
|
overflowComponent: React.FunctionComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSelectAllButton(props: MovieIndexSelectAllButtonProps) {
|
||||||
|
const { isSelectMode } = props;
|
||||||
const [selectState, selectDispatch] = useSelect();
|
const [selectState, selectDispatch] = useSelect();
|
||||||
const { allSelected, allUnselected } = selectState;
|
const { allSelected, allUnselected } = selectState;
|
||||||
|
|
||||||
@ -23,13 +30,13 @@ function MovieIndexSelectAllButton() {
|
|||||||
});
|
});
|
||||||
}, [allSelected, selectDispatch]);
|
}, [allSelected, selectDispatch]);
|
||||||
|
|
||||||
return (
|
return isSelectMode ? (
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
/>
|
/>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MovieIndexSelectAllButton;
|
export default MovieIndexSelectAllButton;
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||||
|
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
|
||||||
|
interface MovieIndexSelectAllMenuItemProps {
|
||||||
|
label: string;
|
||||||
|
isSelectMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSelectAllMenuItem(props: MovieIndexSelectAllMenuItemProps) {
|
||||||
|
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 MovieIndexSelectAllMenuItem;
|
72
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.css
Normal file
72
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
.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 {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButtons,
|
||||||
|
.deleteButtons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButtons {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
justify-content: center;
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
}
|
11
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.css.d.ts
vendored
Normal file
11
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.css.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'actionButtons': string;
|
||||||
|
'buttons': string;
|
||||||
|
'deleteButtons': string;
|
||||||
|
'footer': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
216
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.tsx
Normal file
216
frontend/src/Movie/Index/Select/MovieIndexSelectFooter.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_MOVIE } from 'Commands/commandNames';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { saveMovieEditor } from 'Store/Actions/movieActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import DeleteMovieModal from './Delete/DeleteMovieModal';
|
||||||
|
import EditMoviesModal from './Edit/EditMoviesModal';
|
||||||
|
import OrganizeMoviesModal from './Organize/OrganizeMoviesModal';
|
||||||
|
import TagsModal from './Tags/TagsModal';
|
||||||
|
import styles from './MovieIndexSelectFooter.css';
|
||||||
|
|
||||||
|
const movieEditorSelector = createSelector(
|
||||||
|
(state) => state.movies,
|
||||||
|
(movies) => {
|
||||||
|
const { isSaving, isDeleting, deleteError } = movies;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSaving,
|
||||||
|
isDeleting,
|
||||||
|
deleteError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function MovieIndexSelectFooter() {
|
||||||
|
const { isSaving, isDeleting, deleteError } =
|
||||||
|
useSelector(movieEditorSelector);
|
||||||
|
|
||||||
|
const isOrganizingMovies = useSelector(
|
||||||
|
createCommandExecutingSelector(RENAME_MOVIE)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false);
|
||||||
|
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isSavingMovies, setIsSavingMovies] = useState(false);
|
||||||
|
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, selectDispatch] = useSelect();
|
||||||
|
const { selectedState } = selectState;
|
||||||
|
|
||||||
|
const movieIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const selectedCount = movieIds.length ? movieIds.length : 0;
|
||||||
|
|
||||||
|
const onEditPress = useCallback(() => {
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onEditModalClose = useCallback(() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onSavePress = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
setIsSavingMovies(true);
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
saveMovieEditor({
|
||||||
|
...payload,
|
||||||
|
movieIds,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[movieIds, 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(
|
||||||
|
saveMovieEditor({
|
||||||
|
movieIds,
|
||||||
|
tags,
|
||||||
|
applyTags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[movieIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeletePress = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteModalClose = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving) {
|
||||||
|
setIsSavingMovies(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 && isSavingMovies}
|
||||||
|
isDisabled={!anySelected || isOrganizingMovies}
|
||||||
|
onPress={onEditPress}
|
||||||
|
>
|
||||||
|
{translate('Edit')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
isSpinning={isOrganizingMovies}
|
||||||
|
isDisabled={!anySelected || isOrganizingMovies}
|
||||||
|
onPress={onOrganizePress}
|
||||||
|
>
|
||||||
|
{translate('Rename Files')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving && isSavingTags}
|
||||||
|
isDisabled={!anySelected || isOrganizingMovies}
|
||||||
|
onPress={onTagsPress}
|
||||||
|
>
|
||||||
|
{translate('SetTags')}
|
||||||
|
</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('MoviesSelectedInterp', selectedCount.toString())}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditMoviesModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
movieIds={movieIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onEditModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagsModal
|
||||||
|
isOpen={isTagsModalOpen}
|
||||||
|
movieIds={movieIds}
|
||||||
|
onApplyTagsPress={onApplyTagsPress}
|
||||||
|
onModalClose={onTagsModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OrganizeMoviesModal
|
||||||
|
isOpen={isOrganizeModalOpen}
|
||||||
|
movieIds={movieIds}
|
||||||
|
onModalClose={onOrganizeModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteMovieModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
movieIds={movieIds}
|
||||||
|
onModalClose={onDeleteModalClose}
|
||||||
|
/>
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexSelectFooter;
|
@ -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 MovieIndexSelectModeButtonProps {
|
||||||
|
label: string;
|
||||||
|
iconName: IconDefinition;
|
||||||
|
isSelectMode: boolean;
|
||||||
|
overflowComponent: React.FunctionComponent;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSelectModeButton(props: MovieIndexSelectModeButtonProps) {
|
||||||
|
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 MovieIndexSelectModeButton;
|
@ -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 MovieIndexSelectModeMenuItemProps {
|
||||||
|
label: string;
|
||||||
|
iconName: IconDefinition;
|
||||||
|
isSelectMode: boolean;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSelectModeMenuItem(
|
||||||
|
props: MovieIndexSelectModeMenuItemProps
|
||||||
|
) {
|
||||||
|
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 MovieIndexSelectModeMenuItem;
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import OrganizeMoviesModalContent from './OrganizeMoviesModalContent';
|
||||||
|
|
||||||
|
interface OrganizeMoviesModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
movieIds: number[];
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OrganizeMoviesModal(props: OrganizeMoviesModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<OrganizeMoviesModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrganizeMoviesModal;
|
@ -0,0 +1,8 @@
|
|||||||
|
.renameIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
8
frontend/src/Movie/Index/Select/Organize/OrganizeMoviesModalContent.css.d.ts
vendored
Normal file
8
frontend/src/Movie/Index/Select/Organize/OrganizeMoviesModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'renameIcon': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,82 @@
|
|||||||
|
import { orderBy } from 'lodash';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { RENAME_MOVIE } 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 createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './OrganizeMoviesModalContent.css';
|
||||||
|
|
||||||
|
interface OrganizeMoviesModalContentProps {
|
||||||
|
movieIds: number[];
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OrganizeMoviesModalContent(props: OrganizeMoviesModalContentProps) {
|
||||||
|
const { movieIds, onModalClose } = props;
|
||||||
|
|
||||||
|
const allMovies = useSelector(createAllMoviesSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const movieTitles = useMemo(() => {
|
||||||
|
const movies = movieIds.map((id) => {
|
||||||
|
return allMovies.find((s) => s.id === id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = orderBy(movies, ['sortTitle']);
|
||||||
|
|
||||||
|
return sorted.map((s) => s.title);
|
||||||
|
}, [movieIds, allMovies]);
|
||||||
|
|
||||||
|
const onOrganizePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: RENAME_MOVIE,
|
||||||
|
movieIds,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [movieIds, onModalClose, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('OrganizeSelectedMovies')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Alert>
|
||||||
|
{translate('PreviewRenameHelpText')}
|
||||||
|
<Icon className={styles.renameIcon} name={icons.ORGANIZE} />
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className={styles.message}>
|
||||||
|
{translate('OrganizeConfirm', movieTitles.length)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{movieTitles.map((title) => {
|
||||||
|
return <li key={title}>{title}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={onOrganizePress}>
|
||||||
|
{translate('Organize')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrganizeMoviesModalContent;
|
22
frontend/src/Movie/Index/Select/Tags/TagsModal.tsx
Normal file
22
frontend/src/Movie/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;
|
||||||
|
movieIds: 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/Movie/Index/Select/Tags/TagsModalContent.css
Normal file
12
frontend/src/Movie/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;
|
||||||
|
}
|
9
frontend/src/Movie/Index/Select/Tags/TagsModalContent.css.d.ts
vendored
Normal file
9
frontend/src/Movie/Index/Select/Tags/TagsModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'renameIcon': string;
|
||||||
|
'result': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
172
frontend/src/Movie/Index/Select/Tags/TagsModalContent.tsx
Normal file
172
frontend/src/Movie/Index/Select/Tags/TagsModalContent.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
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 createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
|
interface TagsModalContentProps {
|
||||||
|
movieIds: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModalContent(props: TagsModalContentProps) {
|
||||||
|
const { movieIds, onModalClose, onApplyTagsPress } = props;
|
||||||
|
|
||||||
|
const allMovies = useSelector(createAllMoviesSelector());
|
||||||
|
const tagList = useSelector(createTagsSelector());
|
||||||
|
|
||||||
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
|
const movieTags = useMemo(() => {
|
||||||
|
const movies = movieIds.map((id) => {
|
||||||
|
return allMovies.find((s) => s.id === id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniq(concat(...movies.map((s) => s.tags)));
|
||||||
|
}, [movieIds, allMovies]);
|
||||||
|
|
||||||
|
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: translate('Add') },
|
||||||
|
{ key: 'remove', value: translate('Remove') },
|
||||||
|
{ key: 'replace', value: translate('Replace') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
value={tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="applyTags"
|
||||||
|
value={applyTags}
|
||||||
|
values={applyTagsOptions}
|
||||||
|
helpTexts={[
|
||||||
|
translate('ApplyTagsHelpTexts1'),
|
||||||
|
translate('ApplyTagsHelpTexts2'),
|
||||||
|
translate('ApplyTagsHelpTexts3'),
|
||||||
|
translate('ApplyTagsHelpTexts4'),
|
||||||
|
]}
|
||||||
|
onChange={onApplyTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
|
<div className={styles.result}>
|
||||||
|
{movieTags.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
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
|
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 (movieTags.indexOf(id) > -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={translate('AddingTag')}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
|
{translate('Apply')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModalContent;
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { SelectActionType, useSelect } from 'App/SelectContext';
|
||||||
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
@ -13,7 +14,7 @@ import Column from 'Components/Table/Column';
|
|||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
@ -28,7 +29,6 @@ import translate from 'Utilities/String/translate';
|
|||||||
import MovieStatusCell from './MovieStatusCell';
|
import MovieStatusCell from './MovieStatusCell';
|
||||||
import selectTableOptions from './selectTableOptions';
|
import selectTableOptions from './selectTableOptions';
|
||||||
import styles from './MovieIndexRow.css';
|
import styles from './MovieIndexRow.css';
|
||||||
import { SelectActionType, useSelect } from 'App/SelectContext';
|
|
||||||
|
|
||||||
interface MovieIndexRowProps {
|
interface MovieIndexRowProps {
|
||||||
movieId: number;
|
movieId: number;
|
||||||
@ -62,16 +62,16 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
sizeOnDisk,
|
||||||
genres,
|
genres = [],
|
||||||
queueStatus,
|
queueStatus,
|
||||||
queueState,
|
queueState,
|
||||||
ratings,
|
ratings,
|
||||||
certification,
|
certification,
|
||||||
tags,
|
tags = [],
|
||||||
tmdbId,
|
tmdbId,
|
||||||
imdbId,
|
imdbId,
|
||||||
youTubeTrailerId,
|
youTubeTrailerId,
|
||||||
kinds,
|
isSaving = false,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
} = movie;
|
} = movie;
|
||||||
|
|
||||||
@ -150,8 +150,11 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
<MovieStatusCell
|
<MovieStatusCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
|
movieId={movieId}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
status={status}
|
status={status}
|
||||||
|
isSelectMode={isSelectMode}
|
||||||
|
isSaving={isSaving}
|
||||||
component={VirtualTableRowCell}
|
component={VirtualTableRowCell}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -6,4 +6,5 @@
|
|||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
width: 20px !important;
|
width: 20px !important;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,64 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
||||||
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieStatusCell.css';
|
import styles from './MovieStatusCell.css';
|
||||||
|
|
||||||
interface MovieStatusCellProps {
|
interface MovieStatusCellProps {
|
||||||
className: string;
|
className: string;
|
||||||
|
movieId: number;
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
status: string;
|
status: string;
|
||||||
|
isSelectMode: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
component?: React.ElementType;
|
component?: React.ElementType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieStatusCell(props: MovieStatusCellProps) {
|
function MovieStatusCell(props: MovieStatusCellProps) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
movieId,
|
||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
|
isSelectMode,
|
||||||
|
isSaving,
|
||||||
component: Component = VirtualTableRowCell,
|
component: Component = VirtualTableRowCell,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const statusDetails = getMovieStatusDetails(status);
|
const statusDetails = getMovieStatusDetails(status);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onMonitoredPress = useCallback(() => {
|
||||||
|
dispatch(toggleMovieMonitored({ movieId, monitored: !monitored }));
|
||||||
|
}, [movieId, monitored, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component className={className} {...otherProps}>
|
<Component className={className} {...otherProps}>
|
||||||
<Icon
|
{isSelectMode ? (
|
||||||
className={styles.statusIcon}
|
<MonitorToggleButton
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
className={styles.statusIcon}
|
||||||
title={
|
monitored={monitored}
|
||||||
monitored
|
isSaving={isSaving}
|
||||||
? translate('MovieIsMonitored')
|
onPress={onMonitoredPress}
|
||||||
: translate('MovieIsUnmonitored')
|
/>
|
||||||
}
|
) : (
|
||||||
/>
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
|
title={
|
||||||
|
monitored
|
||||||
|
? translate('MovieIsMonitored')
|
||||||
|
: translate('MovieIsUnmonitored')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
|
@ -38,6 +38,7 @@ interface Movie extends ModelBase {
|
|||||||
certification: string;
|
certification: string;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
images: Image;
|
images: Image;
|
||||||
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Movie;
|
export default Movie;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
// import { batchActions } from 'redux-batched-actions';
|
// import { batchActions } from 'redux-batched-actions';
|
||||||
@ -7,7 +8,7 @@ import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|||||||
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
|
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
|
||||||
import padNumber from 'Utilities/Number/padNumber';
|
import padNumber from 'Utilities/Number/padNumber';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import { updateItem } from './baseActions';
|
import { set, updateItem } from './baseActions';
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||||
@ -245,12 +246,21 @@ export const defaultState = {
|
|||||||
error: null,
|
error: null,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null,
|
||||||
items: [],
|
items: [],
|
||||||
sortKey: 'sortTitle',
|
sortKey: 'sortTitle',
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
deleteOptions: {
|
||||||
|
addImportListExclusion: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'movies.deleteOptions'
|
||||||
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
// Actions Types
|
// Actions Types
|
||||||
|
|
||||||
@ -258,6 +268,10 @@ export const FETCH_MOVIES = 'movies/fetchMovies';
|
|||||||
export const SET_MOVIE_VALUE = 'movies/setMovieValue';
|
export const SET_MOVIE_VALUE = 'movies/setMovieValue';
|
||||||
export const SAVE_MOVIE = 'movies/saveMovie';
|
export const SAVE_MOVIE = 'movies/saveMovie';
|
||||||
export const DELETE_MOVIE = 'movies/deleteMovie';
|
export const DELETE_MOVIE = 'movies/deleteMovie';
|
||||||
|
export const SAVE_MOVIE_EDITOR = 'movies/saveMovieEditor';
|
||||||
|
export const BULK_DELETE_MOVIE = 'movies/bulkDeleteMovie';
|
||||||
|
|
||||||
|
export const SET_DELETE_OPTION = 'movies/setDeleteOption';
|
||||||
|
|
||||||
export const TOGGLE_MOVIE_MONITORED = 'movies/toggleMovieMonitored';
|
export const TOGGLE_MOVIE_MONITORED = 'movies/toggleMovieMonitored';
|
||||||
|
|
||||||
@ -291,6 +305,8 @@ export const deleteMovie = createThunk(DELETE_MOVIE, (payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const toggleMovieMonitored = createThunk(TOGGLE_MOVIE_MONITORED);
|
export const toggleMovieMonitored = createThunk(TOGGLE_MOVIE_MONITORED);
|
||||||
|
export const saveMovieEditor = createThunk(SAVE_MOVIE_EDITOR);
|
||||||
|
export const bulkDeleteMovie = createThunk(BULK_DELETE_MOVIE);
|
||||||
|
|
||||||
export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
|
export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@ -299,6 +315,8 @@ export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setDeleteOption = createAction(SET_DELETE_OPTION);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
@ -359,8 +377,79 @@ export const actionHandlers = handleThunks({
|
|||||||
isSaving: false
|
isSaving: false
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SAVE_MOVIE_EDITOR]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/movie/editor',
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
...data.map((movie) => {
|
||||||
|
return updateItem({
|
||||||
|
id: movie.id,
|
||||||
|
section: 'movies',
|
||||||
|
...movie
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[BULK_DELETE_MOVIE]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/movie/editor',
|
||||||
|
method: 'DELETE',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done(() => {
|
||||||
|
// SignaR will take care of removing the movie from the collection
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -368,6 +457,14 @@ export const actionHandlers = handleThunks({
|
|||||||
|
|
||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[SET_MOVIE_VALUE]: createSetSettingValueReducer(section)
|
[SET_MOVIE_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
[SET_DELETE_OPTION]: (state, { payload }) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deleteOptions: {
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}, defaultState, section);
|
}, defaultState, section);
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import { set, updateItem } from './baseActions';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
@ -496,8 +492,6 @@ export const SET_MOVIE_VIEW = 'movieIndex/setMovieView';
|
|||||||
export const SET_MOVIE_TABLE_OPTION = 'movieIndex/setMovieTableOption';
|
export const SET_MOVIE_TABLE_OPTION = 'movieIndex/setMovieTableOption';
|
||||||
export const SET_MOVIE_POSTER_OPTION = 'movieIndex/setMoviePosterOption';
|
export const SET_MOVIE_POSTER_OPTION = 'movieIndex/setMoviePosterOption';
|
||||||
export const SET_MOVIE_OVERVIEW_OPTION = 'movieIndex/setMovieOverviewOption';
|
export const SET_MOVIE_OVERVIEW_OPTION = 'movieIndex/setMovieOverviewOption';
|
||||||
export const SAVE_MOVIE_EDITOR = 'movieIndex/saveMovieEditor';
|
|
||||||
export const BULK_DELETE_MOVIE = 'movieIndex/bulkDeleteMovie';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@ -508,85 +502,6 @@ export const setMovieView = createAction(SET_MOVIE_VIEW);
|
|||||||
export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION);
|
export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION);
|
||||||
export const setMoviePosterOption = createAction(SET_MOVIE_POSTER_OPTION);
|
export const setMoviePosterOption = createAction(SET_MOVIE_POSTER_OPTION);
|
||||||
export const setMovieOverviewOption = createAction(SET_MOVIE_OVERVIEW_OPTION);
|
export const setMovieOverviewOption = createAction(SET_MOVIE_OVERVIEW_OPTION);
|
||||||
export const saveMovieEditor = createThunk(SAVE_MOVIE_EDITOR);
|
|
||||||
export const bulkDeleteMovie = createThunk(BULK_DELETE_MOVIE);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
[SAVE_MOVIE_EDITOR]: function(getState, payload, dispatch) {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/movie/editor',
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify(payload),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(batchActions([
|
|
||||||
...data.map((movie) => {
|
|
||||||
return updateItem({
|
|
||||||
id: movie.id,
|
|
||||||
section: 'movies',
|
|
||||||
...movie
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null
|
|
||||||
})
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: xhr
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[BULK_DELETE_MOVIE]: function(getState, payload, dispatch) {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isDeleting: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/movie/editor',
|
|
||||||
method: 'DELETE',
|
|
||||||
data: JSON.stringify(payload),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done(() => {
|
|
||||||
// SignaR will take care of removing the movie from the collection
|
|
||||||
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isDeleting: false,
|
|
||||||
deleteError: null
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isDeleting: false,
|
|
||||||
deleteError: xhr
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Reducers
|
// Reducers
|
||||||
|
@ -286,10 +286,12 @@
|
|||||||
"EditListExclusion": "Edit List Exclusion",
|
"EditListExclusion": "Edit List Exclusion",
|
||||||
"EditMovie": "Edit Movie",
|
"EditMovie": "Edit Movie",
|
||||||
"EditMovieFile": "Edit Movie File",
|
"EditMovieFile": "Edit Movie File",
|
||||||
|
"EditMovies": "Edit Movies",
|
||||||
"EditPerson": "Edit Person",
|
"EditPerson": "Edit Person",
|
||||||
"EditQualityProfile": "Edit Quality Profile",
|
"EditQualityProfile": "Edit Quality Profile",
|
||||||
"EditRemotePathMapping": "Edit Remote Path Mapping",
|
"EditRemotePathMapping": "Edit Remote Path Mapping",
|
||||||
"EditRestriction": "Edit Restriction",
|
"EditRestriction": "Edit Restriction",
|
||||||
|
"EditSelectedMovies": "Edit Selected Movies",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
"EnableAutoHelpText": "If enabled, Movies will be automatically added to Radarr from this list",
|
"EnableAutoHelpText": "If enabled, Movies will be automatically added to Radarr from this list",
|
||||||
"EnableAutomaticAdd": "Enable Automatic Add",
|
"EnableAutomaticAdd": "Enable Automatic Add",
|
||||||
@ -997,6 +999,7 @@
|
|||||||
"StartTypingOrSelectAPathBelow": "Start typing or select a path below",
|
"StartTypingOrSelectAPathBelow": "Start typing or select a path below",
|
||||||
"StartupDirectory": "Startup directory",
|
"StartupDirectory": "Startup directory",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
|
"StopSelecting": "Stop Selecting",
|
||||||
"Studio": "Studio",
|
"Studio": "Studio",
|
||||||
"Style": "Style",
|
"Style": "Style",
|
||||||
"SubfolderWillBeCreatedAutomaticallyInterp": "'{0}' subfolder will be created automatically",
|
"SubfolderWillBeCreatedAutomaticallyInterp": "'{0}' subfolder will be created automatically",
|
||||||
@ -1081,7 +1084,6 @@
|
|||||||
"UnableToLoadLanguages": "Unable to load languages",
|
"UnableToLoadLanguages": "Unable to load languages",
|
||||||
"UnableToLoadListExclusions": "Unable to load List Exclusions",
|
"UnableToLoadListExclusions": "Unable to load List Exclusions",
|
||||||
"UnableToLoadListOptions": "Unable to load list options",
|
"UnableToLoadListOptions": "Unable to load list options",
|
||||||
"StopSelecting": "Stop Selecting",
|
|
||||||
"UnableToLoadLists": "Unable to load Lists",
|
"UnableToLoadLists": "Unable to load Lists",
|
||||||
"UnableToLoadManualImportItems": "Unable to load manual import items",
|
"UnableToLoadManualImportItems": "Unable to load manual import items",
|
||||||
"UnableToLoadMediaManagementSettings": "Unable to load Media Management settings",
|
"UnableToLoadMediaManagementSettings": "Unable to load Media Management settings",
|
||||||
@ -1116,6 +1118,7 @@
|
|||||||
"UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.",
|
"UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.",
|
||||||
"UpdateCheckStartupTranslocationMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.",
|
"UpdateCheckStartupTranslocationMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.",
|
||||||
"UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.",
|
"UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.",
|
||||||
|
"UpdateFiltered": "Update Filtered",
|
||||||
"UpdateMechanismHelpText": "Use Radarr's built-in updater or a script",
|
"UpdateMechanismHelpText": "Use Radarr's built-in updater or a script",
|
||||||
"Updates": "Updates",
|
"Updates": "Updates",
|
||||||
"UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process",
|
"UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process",
|
||||||
|
Loading…
Reference in New Issue
Block a user