parent
68832a136e
commit
e0b91c6406
@ -0,0 +1,5 @@
|
|||||||
|
interface ModelBase {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelBase;
|
@ -1,61 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
|
||||||
import { isLocked } from 'Utilities/scrollLock';
|
|
||||||
import styles from './PageContentBody.css';
|
|
||||||
|
|
||||||
class PageContentBody extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onScroll = (props) => {
|
|
||||||
const { onScroll } = this.props;
|
|
||||||
|
|
||||||
if (this.props.onScroll && !isLocked()) {
|
|
||||||
onScroll(props);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
innerClassName,
|
|
||||||
children,
|
|
||||||
dispatch,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Scroller
|
|
||||||
className={className}
|
|
||||||
scrollDirection={scrollDirections.VERTICAL}
|
|
||||||
{...otherProps}
|
|
||||||
onScroll={this.onScroll}
|
|
||||||
>
|
|
||||||
<div className={innerClassName}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Scroller>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PageContentBody.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
innerClassName: PropTypes.string,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onScroll: PropTypes.func,
|
|
||||||
dispatch: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
PageContentBody.defaultProps = {
|
|
||||||
className: styles.contentBody,
|
|
||||||
innerClassName: styles.innerContentBody
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageContentBody;
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||||
|
import { isLocked } from 'Utilities/scrollLock';
|
||||||
|
import styles from './PageContentBody.css';
|
||||||
|
|
||||||
|
interface PageContentBodyProps {
|
||||||
|
className: string;
|
||||||
|
innerClassName: string;
|
||||||
|
children: ReactNode;
|
||||||
|
initialScrollTop?: number;
|
||||||
|
onScroll?: (payload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageContentBody = forwardRef(
|
||||||
|
(
|
||||||
|
props: PageContentBodyProps,
|
||||||
|
ref: React.MutableRefObject<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
className = styles.contentBody,
|
||||||
|
innerClassName = styles.innerContentBody,
|
||||||
|
children,
|
||||||
|
onScroll,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const onScrollWrapper = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
if (onScroll && !isLocked()) {
|
||||||
|
onScroll(payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onScroll]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Scroller
|
||||||
|
ref={ref}
|
||||||
|
{...otherProps}
|
||||||
|
className={className}
|
||||||
|
scrollDirection={ScrollDirection.Vertical}
|
||||||
|
onScroll={onScrollWrapper}
|
||||||
|
>
|
||||||
|
<div className={innerClassName}>{children}</div>
|
||||||
|
</Scroller>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PageContentBody;
|
@ -1,95 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
|
||||||
import styles from './Scroller.css';
|
|
||||||
|
|
||||||
class Scroller extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scroller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
scrollDirection,
|
|
||||||
autoFocus,
|
|
||||||
scrollTop
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (this.props.scrollTop != null) {
|
|
||||||
this._scroller.scrollTop = scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
|
|
||||||
this._scroller.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_setScrollerRef = (ref) => {
|
|
||||||
this._scroller = ref;
|
|
||||||
|
|
||||||
this.props.registerScroller(ref);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
scrollDirection,
|
|
||||||
autoScroll,
|
|
||||||
children,
|
|
||||||
scrollTop,
|
|
||||||
onScroll,
|
|
||||||
registerScroller,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={this._setScrollerRef}
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles.scroller,
|
|
||||||
styles[scrollDirection],
|
|
||||||
autoScroll && styles.autoScroll
|
|
||||||
)}
|
|
||||||
tabIndex={-1}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Scroller.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
|
||||||
autoFocus: PropTypes.bool.isRequired,
|
|
||||||
autoScroll: PropTypes.bool.isRequired,
|
|
||||||
scrollTop: PropTypes.number,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onScroll: PropTypes.func,
|
|
||||||
registerScroller: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
Scroller.defaultProps = {
|
|
||||||
scrollDirection: scrollDirections.VERTICAL,
|
|
||||||
autoFocus: true,
|
|
||||||
autoScroll: true,
|
|
||||||
registerScroller: () => { /* no-op */ }
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Scroller;
|
|
@ -0,0 +1,90 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||||
|
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||||
|
import styles from './Scroller.css';
|
||||||
|
|
||||||
|
interface ScrollerProps {
|
||||||
|
className?: string;
|
||||||
|
scrollDirection?: ScrollDirection;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
autoScroll?: boolean;
|
||||||
|
scrollTop?: number;
|
||||||
|
initialScrollTop?: number;
|
||||||
|
children?: ReactNode;
|
||||||
|
onScroll?: (payload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Scroller = forwardRef(
|
||||||
|
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
autoFocus = false,
|
||||||
|
autoScroll = true,
|
||||||
|
scrollDirection = ScrollDirection.Vertical,
|
||||||
|
children,
|
||||||
|
scrollTop,
|
||||||
|
initialScrollTop,
|
||||||
|
onScroll,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const internalRef = useRef();
|
||||||
|
const currentRef = ref ?? internalRef;
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (initialScrollTop != null) {
|
||||||
|
currentRef.current.scrollTop = initialScrollTop;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollTop != null) {
|
||||||
|
currentRef.current.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoFocus && scrollDirection !== ScrollDirection.None) {
|
||||||
|
currentRef.current.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}, [autoFocus, currentRef, scrollDirection, scrollTop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const div = currentRef.current;
|
||||||
|
|
||||||
|
const handleScroll = throttle(() => {
|
||||||
|
const scrollLeft = div.scrollLeft;
|
||||||
|
const scrollTop = div.scrollTop;
|
||||||
|
|
||||||
|
onScroll?.({ scrollLeft, scrollTop });
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
div.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
div.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [currentRef, onScroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...otherProps}
|
||||||
|
ref={currentRef}
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles.scroller,
|
||||||
|
styles[scrollDirection],
|
||||||
|
autoScroll && styles.autoScroll
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Scroller;
|
@ -0,0 +1,10 @@
|
|||||||
|
interface Column {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
columnLabel: string;
|
||||||
|
isSortable: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
|
isModifiable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Column;
|
@ -1,30 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
|
||||||
|
|
||||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
|
||||||
function ScrollPosition(props) {
|
|
||||||
const {
|
|
||||||
history
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
|
|
||||||
scrollPositions[scrollPositionKey] :
|
|
||||||
0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WrappedComponent
|
|
||||||
{...props}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollPosition.propTypes = {
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
return ScrollPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withScrollPosition;
|
|
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
|
||||||
|
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||||
|
function ScrollPosition(props) {
|
||||||
|
const { history } = props;
|
||||||
|
|
||||||
|
const initialScrollTop =
|
||||||
|
history.action === 'POP' ||
|
||||||
|
(history.location.state && history.location.state.restoreScrollPosition)
|
||||||
|
? scrollPositions[scrollPositionKey]
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollPosition.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ScrollPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withScrollPosition;
|
@ -0,0 +1,21 @@
|
|||||||
|
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer';
|
||||||
|
import {
|
||||||
|
default as useMeasureHook,
|
||||||
|
Options,
|
||||||
|
RectReadOnly,
|
||||||
|
} from 'react-use-measure';
|
||||||
|
|
||||||
|
const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill;
|
||||||
|
|
||||||
|
export type Measurements = RectReadOnly;
|
||||||
|
|
||||||
|
function useMeasure(
|
||||||
|
options?: Omit<Options, 'polyfill'>
|
||||||
|
): ReturnType<typeof useMeasureHook> {
|
||||||
|
return useMeasureHook({
|
||||||
|
polyfill: ResizeObserver,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMeasure;
|
@ -0,0 +1,8 @@
|
|||||||
|
enum ScrollDirection {
|
||||||
|
Horizontal = 'horizontal',
|
||||||
|
Vertical = 'vertical',
|
||||||
|
None = 'none',
|
||||||
|
Both = 'both',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScrollDirection;
|
@ -0,0 +1,6 @@
|
|||||||
|
enum SortDirection {
|
||||||
|
Ascending = 'ascending',
|
||||||
|
Descending = 'descending',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortDirection;
|
@ -1,658 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
||||||
import MovieEditorFooter from 'Movie/Editor/MovieEditorFooter.js';
|
|
||||||
import OrganizeMovieModal from 'Movie/Editor/Organize/OrganizeMovieModal';
|
|
||||||
import NoMovie from 'Movie/NoMovie';
|
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
|
|
||||||
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
|
||||||
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
|
||||||
import MovieIndexFooterConnector from './MovieIndexFooterConnector';
|
|
||||||
import MovieIndexOverviewsConnector from './Overview/MovieIndexOverviewsConnector';
|
|
||||||
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
|
||||||
import MovieIndexPostersConnector from './Posters/MovieIndexPostersConnector';
|
|
||||||
import MovieIndexPosterOptionsModal from './Posters/Options/MovieIndexPosterOptionsModal';
|
|
||||||
import MovieIndexTableConnector from './Table/MovieIndexTableConnector';
|
|
||||||
import MovieIndexTableOptionsConnector from './Table/MovieIndexTableOptionsConnector';
|
|
||||||
import styles from './MovieIndex.css';
|
|
||||||
|
|
||||||
function getViewComponent(view) {
|
|
||||||
if (view === 'posters') {
|
|
||||||
return MovieIndexPostersConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (view === 'overview') {
|
|
||||||
return MovieIndexOverviewsConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MovieIndexTableConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieIndex extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
scroller: null,
|
|
||||||
jumpBarItems: { order: [] },
|
|
||||||
jumpToCharacter: null,
|
|
||||||
isPosterOptionsModalOpen: false,
|
|
||||||
isOverviewOptionsModalOpen: false,
|
|
||||||
isInteractiveImportModalOpen: false,
|
|
||||||
isMovieEditorActive: false,
|
|
||||||
isOrganizingMovieModalOpen: false,
|
|
||||||
isConfirmSearchModalOpen: false,
|
|
||||||
searchType: null,
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setJumpBarItems();
|
|
||||||
this.setSelectedState();
|
|
||||||
|
|
||||||
window.addEventListener('keyup', this.onKeyUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
isDeleting,
|
|
||||||
deleteError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (sortKey !== prevProps.sortKey ||
|
|
||||||
sortDirection !== prevProps.sortDirection ||
|
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
|
||||||
) {
|
|
||||||
this.setJumpBarItems();
|
|
||||||
this.setSelectedState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.jumpToCharacter != null) {
|
|
||||||
this.setState({ jumpToCharacter: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
|
||||||
!isDeleting &&
|
|
||||||
!deleteError;
|
|
||||||
|
|
||||||
if (hasFinishedDeleting) {
|
|
||||||
this.onSelectAllChange({ value: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setScrollerRef = (ref) => {
|
|
||||||
this.setState({ scroller: ref });
|
|
||||||
};
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
if (this.state.allUnselected) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
setSelectedState() {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedState
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const newSelectedState = {};
|
|
||||||
|
|
||||||
items.forEach((movie) => {
|
|
||||||
const isItemSelected = selectedState[movie.id];
|
|
||||||
|
|
||||||
if (isItemSelected) {
|
|
||||||
newSelectedState[movie.id] = isItemSelected;
|
|
||||||
} else {
|
|
||||||
newSelectedState[movie.id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
|
||||||
const newStateCount = Object.keys(newSelectedState).length;
|
|
||||||
let isAllSelected = false;
|
|
||||||
let isAllUnselected = false;
|
|
||||||
|
|
||||||
if (selectedCount === 0) {
|
|
||||||
isAllUnselected = true;
|
|
||||||
} else if (selectedCount === newStateCount) {
|
|
||||||
isAllSelected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
|
||||||
}
|
|
||||||
|
|
||||||
setJumpBarItems() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
sortDirection
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Reset if not sorting by sortTitle
|
|
||||||
if (sortKey !== 'sortTitle') {
|
|
||||||
this.setState({ jumpBarItems: { order: [] } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const characters = _.reduce(items, (acc, item) => {
|
|
||||||
let char = item.sortTitle.charAt(0);
|
|
||||||
|
|
||||||
if (!isNaN(char)) {
|
|
||||||
char = '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char in acc) {
|
|
||||||
acc[char] = acc[char] + 1;
|
|
||||||
} else {
|
|
||||||
acc[char] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const order = Object.keys(characters).sort();
|
|
||||||
|
|
||||||
// Reverse if sorting descending
|
|
||||||
if (sortDirection === sortDirections.DESCENDING) {
|
|
||||||
order.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const jumpBarItems = {
|
|
||||||
characters,
|
|
||||||
order
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({ jumpBarItems });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPosterOptionsPress = () => {
|
|
||||||
this.setState({ isPosterOptionsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterOptionsModalClose = () => {
|
|
||||||
this.setState({ isPosterOptionsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOverviewOptionsPress = () => {
|
|
||||||
this.setState({ isOverviewOptionsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOverviewOptionsModalClose = () => {
|
|
||||||
this.setState({ isOverviewOptionsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportPress = () => {
|
|
||||||
this.setState({ isInteractiveImportModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportModalClose = () => {
|
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMovieEditorTogglePress = () => {
|
|
||||||
if (this.state.isMovieEditorActive) {
|
|
||||||
this.setState({ isMovieEditorActive: false });
|
|
||||||
} else {
|
|
||||||
const newState = selectAll(this.state.selectedState, false);
|
|
||||||
newState.isMovieEditorActive = true;
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onJumpBarItemPress = (jumpToCharacter) => {
|
|
||||||
this.setState({ jumpToCharacter });
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeyUp = (event) => {
|
|
||||||
const jumpBarItems = this.state.jumpBarItems.order;
|
|
||||||
if (event.composedPath && event.composedPath().length === 4) {
|
|
||||||
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
|
||||||
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
|
||||||
}
|
|
||||||
if (event.keyCode === keyCodes.END && event.ctrlKey) {
|
|
||||||
this.setState({ jumpToCharacter: jumpBarItems[jumpBarItems.length - 1] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllPress = () => {
|
|
||||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSaveSelected = (changes) => {
|
|
||||||
this.props.onSaveSelected({
|
|
||||||
movieIds: this.getSelectedIds(),
|
|
||||||
...changes
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onOrganizeMoviePress = () => {
|
|
||||||
this.setState({ isOrganizingMovieModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOrganizeMovieModalClose = (organized) => {
|
|
||||||
this.setState({ isOrganizingMovieModalOpen: false });
|
|
||||||
|
|
||||||
if (organized === true) {
|
|
||||||
this.onSelectAllChange({ value: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onSearchPress = () => {
|
|
||||||
this.setState({ isConfirmSearchModalOpen: true, searchType: 'moviesSearch' });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefreshMoviePress = () => {
|
|
||||||
const selectedMovieIds = this.getSelectedIds();
|
|
||||||
const refreshIds = this.state.isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds : [];
|
|
||||||
|
|
||||||
this.props.onRefreshMoviePress(refreshIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSearchConfirmed = () => {
|
|
||||||
const selectedMovieIds = this.getSelectedIds();
|
|
||||||
const searchIds = this.state.isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds : this.props.items.map((m) => m.id);
|
|
||||||
|
|
||||||
this.props.onSearchPress(this.state.searchType, searchIds);
|
|
||||||
this.setState({ isConfirmSearchModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmSearchModalClose = () => {
|
|
||||||
this.setState({ isConfirmSearchModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
totalItems,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
view,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isOrganizingMovie,
|
|
||||||
isSearchingMovies,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isDeleting,
|
|
||||||
deleteError,
|
|
||||||
onScroll,
|
|
||||||
onSortSelect,
|
|
||||||
onFilterSelect,
|
|
||||||
onViewSelect,
|
|
||||||
onRefreshMoviePress,
|
|
||||||
onRssSyncPress,
|
|
||||||
onSearchPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
scroller,
|
|
||||||
jumpBarItems,
|
|
||||||
jumpToCharacter,
|
|
||||||
isPosterOptionsModalOpen,
|
|
||||||
isOverviewOptionsModalOpen,
|
|
||||||
isInteractiveImportModalOpen,
|
|
||||||
isConfirmSearchModalOpen,
|
|
||||||
isMovieEditorActive,
|
|
||||||
selectedState,
|
|
||||||
allSelected,
|
|
||||||
allUnselected
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const selectedMovieIds = this.getSelectedIds();
|
|
||||||
|
|
||||||
const ViewComponent = getViewComponent(view);
|
|
||||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
|
||||||
const hasNoMovie = !totalItems;
|
|
||||||
|
|
||||||
const searchIndexLabel = selectedFilterKey === 'all' ? translate('SearchAll') : translate('SearchFiltered');
|
|
||||||
const searchEditorLabel = selectedMovieIds.length > 0 ? translate('SearchSelected') : translate('SearchAll');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={isMovieEditorActive && selectedMovieIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
spinningName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onRefreshMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RSSSync')}
|
|
||||||
iconName={icons.RSS}
|
|
||||||
isSpinning={isRssSyncExecuting}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={onRssSyncPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={isMovieEditorActive ? searchEditorLabel : searchIndexLabel}
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
isDisabled={isSearchingMovies || !items.length}
|
|
||||||
onPress={this.onSearchPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('ManualImport')}
|
|
||||||
iconName={icons.INTERACTIVE}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onInteractiveImportPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
{
|
|
||||||
isMovieEditorActive ?
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('MovieIndex')}
|
|
||||||
iconName={icons.MOVIE_CONTINUING}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onMovieEditorTogglePress}
|
|
||||||
/> :
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('MovieEditor')}
|
|
||||||
iconName={icons.EDIT}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onMovieEditorTogglePress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMovieEditorActive ?
|
|
||||||
<PageToolbarButton
|
|
||||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
|
||||||
iconName={icons.CHECK_SQUARE}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onSelectAllPress}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection
|
|
||||||
alignContent={align.RIGHT}
|
|
||||||
collapseButtons={false}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
view === 'table' ?
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
optionsComponent={MovieIndexTableOptionsConnector}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
view === 'posters' ?
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.POSTER}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onPosterOptionsPress}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
view === 'overview' ?
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.OVERVIEW}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onPress={this.onOverviewOptionsPress}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<MovieIndexViewMenu
|
|
||||||
view={view}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onViewSelect={onViewSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MovieIndexSortMenu
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onSortSelect={onSortSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MovieIndexFilterMenu
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
isDisabled={hasNoMovie}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<div className={styles.pageContentBodyWrapper}>
|
|
||||||
<PageContentBody
|
|
||||||
registerScroller={this.setScrollerRef}
|
|
||||||
className={styles.contentBody}
|
|
||||||
innerClassName={styles[`${view}InnerContentBody`]}
|
|
||||||
onScroll={onScroll}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div className={styles.errorMessage}>
|
|
||||||
{getErrorMessage(error, translate('FailedToLoadMovieFromAPI'))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isLoaded &&
|
|
||||||
<div className={styles.contentBodyContainer}>
|
|
||||||
<ViewComponent
|
|
||||||
scroller={scroller}
|
|
||||||
items={items}
|
|
||||||
filters={filters}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
jumpToCharacter={jumpToCharacter}
|
|
||||||
isMovieEditorActive={isMovieEditorActive}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
selectedState={selectedState}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!isMovieEditorActive &&
|
|
||||||
<MovieIndexFooterConnector />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && !items.length &&
|
|
||||||
<NoMovie totalItems={totalItems} />
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
{
|
|
||||||
isLoaded && !!jumpBarItems.order.length &&
|
|
||||||
<PageJumpBar
|
|
||||||
items={jumpBarItems}
|
|
||||||
onItemPress={this.onJumpBarItemPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isLoaded && isMovieEditorActive &&
|
|
||||||
<MovieEditorFooter
|
|
||||||
movieIds={selectedMovieIds}
|
|
||||||
selectedCount={selectedMovieIds.length}
|
|
||||||
isSaving={isSaving}
|
|
||||||
saveError={saveError}
|
|
||||||
isDeleting={isDeleting}
|
|
||||||
deleteError={deleteError}
|
|
||||||
isOrganizingMovie={isOrganizingMovie}
|
|
||||||
onSaveSelected={this.onSaveSelected}
|
|
||||||
onOrganizeMoviePress={this.onOrganizeMoviePress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MovieIndexPosterOptionsModal
|
|
||||||
isOpen={isPosterOptionsModalOpen}
|
|
||||||
onModalClose={this.onPosterOptionsModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MovieIndexOverviewOptionsModal
|
|
||||||
isOpen={isOverviewOptionsModalOpen}
|
|
||||||
onModalClose={this.onOverviewOptionsModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InteractiveImportModal
|
|
||||||
isOpen={isInteractiveImportModalOpen}
|
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OrganizeMovieModal
|
|
||||||
isOpen={this.state.isOrganizingMovieModalOpen}
|
|
||||||
movieIds={selectedMovieIds}
|
|
||||||
onModalClose={this.onOrganizeMovieModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmSearchModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('MassMovieSearch')}
|
|
||||||
message={
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
Are you sure you want to perform mass movie search for {isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds.length : this.props.items.length} movies?
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{translate('ThisCannotBeCancelled')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
confirmLabel={translate('Search')}
|
|
||||||
onConfirm={this.onSearchConfirmed}
|
|
||||||
onCancel={this.onConfirmSearchModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndex.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
totalItems: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
isOrganizingMovie: PropTypes.bool.isRequired,
|
|
||||||
isSearchingMovies: PropTypes.bool.isRequired,
|
|
||||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
|
||||||
deleteError: PropTypes.object,
|
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
|
||||||
onViewSelect: PropTypes.func.isRequired,
|
|
||||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
|
||||||
onRssSyncPress: PropTypes.func.isRequired,
|
|
||||||
onSearchPress: PropTypes.func.isRequired,
|
|
||||||
onScroll: PropTypes.func.isRequired,
|
|
||||||
onSaveSelected: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndex;
|
|
@ -0,0 +1,306 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { REFRESH_MOVIE, RSS_SYNC } from 'Commands/commandNames';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import NoMovie from 'Movie/NoMovie';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import {
|
||||||
|
setMovieFilter,
|
||||||
|
setMovieSort,
|
||||||
|
setMovieTableOption,
|
||||||
|
setMovieView,
|
||||||
|
} from 'Store/Actions/movieIndexActions';
|
||||||
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
|
import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
|
||||||
|
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
||||||
|
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
||||||
|
import MovieIndexFooter from './MovieIndexFooter';
|
||||||
|
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
||||||
|
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
||||||
|
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
||||||
|
import MovieIndexPosterOptionsModal from './Posters/Options/MovieIndexPosterOptionsModal';
|
||||||
|
import MovieIndexTable from './Table/MovieIndexTable';
|
||||||
|
import MovieIndexTableOptions from './Table/MovieIndexTableOptions';
|
||||||
|
import styles from './MovieIndex.css';
|
||||||
|
|
||||||
|
function getViewComponent(view: string) {
|
||||||
|
if (view === 'posters') {
|
||||||
|
return MovieIndexPosters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === 'overview') {
|
||||||
|
return MovieIndexOverviews;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MovieIndexTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndex() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
totalItems,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
view,
|
||||||
|
} = useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||||
|
|
||||||
|
const isRefreshingMovie = useSelector(
|
||||||
|
createCommandExecutingSelector(REFRESH_MOVIE)
|
||||||
|
);
|
||||||
|
const isRssSyncExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(RSS_SYNC)
|
||||||
|
);
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const scrollerRef = useRef<HTMLDivElement>();
|
||||||
|
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||||
|
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onRefreshMoviePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: REFRESH_MOVIE,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onRssSyncPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: RSS_SYNC,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onTableOptionChange = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
dispatch(setMovieTableOption(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onViewSelect = useCallback(
|
||||||
|
(value) => {
|
||||||
|
dispatch(setMovieView({ view: value }));
|
||||||
|
|
||||||
|
if (scrollerRef.current) {
|
||||||
|
scrollerRef.current.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollerRef, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSortSelect = useCallback(
|
||||||
|
(value) => {
|
||||||
|
dispatch(setMovieSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterSelect = useCallback(
|
||||||
|
(value) => {
|
||||||
|
dispatch(setMovieFilter({ selectedFilterKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onOptionsPress = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(true);
|
||||||
|
}, [setIsOptionsModalOpen]);
|
||||||
|
|
||||||
|
const onOptionsModalClose = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(false);
|
||||||
|
}, [setIsOptionsModalOpen]);
|
||||||
|
|
||||||
|
const onJumpBarItemPress = useCallback(
|
||||||
|
(character) => {
|
||||||
|
setJumpToCharacter(character);
|
||||||
|
},
|
||||||
|
[setJumpToCharacter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onScroll = useCallback(
|
||||||
|
({ scrollTop }) => {
|
||||||
|
setJumpToCharacter(null);
|
||||||
|
scrollPositions.movieIndex = scrollTop;
|
||||||
|
},
|
||||||
|
[setJumpToCharacter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const jumpBarItems = useMemo(() => {
|
||||||
|
// Reset if not sorting by sortTitle
|
||||||
|
if (sortKey !== 'sortTitle') {
|
||||||
|
return {
|
||||||
|
order: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters = items.reduce((acc, item) => {
|
||||||
|
let char = item.sortTitle.charAt(0);
|
||||||
|
|
||||||
|
if (!isNaN(char)) {
|
||||||
|
char = '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char in acc) {
|
||||||
|
acc[char] = acc[char] + 1;
|
||||||
|
} else {
|
||||||
|
acc[char] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const order = Object.keys(characters).sort();
|
||||||
|
|
||||||
|
// Reverse if sorting descending
|
||||||
|
if (sortDirection === SortDirection.Descending) {
|
||||||
|
order.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
characters,
|
||||||
|
order,
|
||||||
|
};
|
||||||
|
}, [items, sortKey, sortDirection]);
|
||||||
|
const ViewComponent = useMemo(() => getViewComponent(view), [view]);
|
||||||
|
|
||||||
|
const isLoaded = !!(!error && isPopulated && items.length);
|
||||||
|
const hasNoMovie = !totalItems;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Update all"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
spinningName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshingMovie}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onPress={onRefreshMoviePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="RSS Sync"
|
||||||
|
iconName={icons.RSS}
|
||||||
|
isSpinning={isRssSyncExecuting}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onPress={onRssSyncPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
|
||||||
|
{view === 'table' ? (
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={MovieIndexTableOptions}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton label="Options" iconName={icons.TABLE} />
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
) : (
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={view === 'posters' ? icons.POSTER : icons.OVERVIEW}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onPress={onOptionsPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<MovieIndexViewMenu
|
||||||
|
view={view}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onViewSelect={onViewSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieIndexSortMenu
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onSortSelect={onSortSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieIndexFilterMenu
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
isDisabled={hasNoMovie}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
<div className={styles.pageContentBodyWrapper}>
|
||||||
|
<PageContentBody
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={styles.contentBody}
|
||||||
|
innerClassName={styles[`${view}InnerContentBody`]}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? <div>Unable to load movie</div> : null}
|
||||||
|
|
||||||
|
{isLoaded ? (
|
||||||
|
<div className={styles.contentBodyContainer}>
|
||||||
|
<ViewComponent
|
||||||
|
scrollerRef={scrollerRef}
|
||||||
|
items={items}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
jumpToCharacter={jumpToCharacter}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieIndexFooter />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!error && isPopulated && !items.length ? (
|
||||||
|
<NoMovie totalItems={totalItems} />
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
{isLoaded && !!jumpBarItems.order.length ? (
|
||||||
|
<PageJumpBar items={jumpBarItems} onItemPress={onJumpBarItemPress} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{view === 'posters' ? (
|
||||||
|
<MovieIndexPosterOptionsModal
|
||||||
|
isOpen={isOptionsModalOpen}
|
||||||
|
onModalClose={onOptionsModalClose}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{view === 'overview' ? (
|
||||||
|
<MovieIndexOverviewOptionsModal
|
||||||
|
isOpen={isOptionsModalOpen}
|
||||||
|
onModalClose={onOptionsModalClose}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndex;
|
@ -1,160 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { saveMovieEditor, setMovieFilter, setMovieSort, setMovieTableOption, setMovieView } from 'Store/Actions/movieIndexActions';
|
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
|
||||||
import MovieIndex from './MovieIndex';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieClientSideCollectionItemsSelector('movieIndex'),
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
|
|
||||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
|
||||||
createCommandExecutingSelector(commandNames.RENAME_MOVIE),
|
|
||||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
|
||||||
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(
|
|
||||||
movies,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isOrganizingMovie,
|
|
||||||
isCutoffMoviesSearch,
|
|
||||||
isMissingMoviesSearch,
|
|
||||||
dimensionsState
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
...movies,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isOrganizingMovie,
|
|
||||||
isSearchingMovies: isCutoffMoviesSearch || isMissingMoviesSearch,
|
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
fetchQueueDetails() {
|
|
||||||
dispatch(fetchQueueDetails());
|
|
||||||
},
|
|
||||||
|
|
||||||
clearQueueDetails() {
|
|
||||||
dispatch(clearQueueDetails());
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatchFetchRootFolders() {
|
|
||||||
dispatch(fetchRootFolders());
|
|
||||||
},
|
|
||||||
|
|
||||||
onTableOptionChange(payload) {
|
|
||||||
dispatch(setMovieTableOption(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSortSelect(sortKey) {
|
|
||||||
dispatch(setMovieSort({ sortKey }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onFilterSelect(selectedFilterKey) {
|
|
||||||
dispatch(setMovieFilter({ selectedFilterKey }));
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatchSetMovieView(view) {
|
|
||||||
dispatch(setMovieView({ view }));
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatchSaveMovieEditor(payload) {
|
|
||||||
dispatch(saveMovieEditor(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
onRefreshMoviePress(items) {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: commandNames.REFRESH_MOVIE,
|
|
||||||
movieIds: items
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onRssSyncPress() {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: commandNames.RSS_SYNC
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSearchPress(command, items) {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: command,
|
|
||||||
movieIds: items
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieIndexConnector extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// TODO: Fetch root folders here for now, but should eventually fetch on editor toggle and check loaded before showing controls
|
|
||||||
this.props.dispatchFetchRootFolders();
|
|
||||||
this.props.fetchQueueDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearQueueDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onViewSelect = (view) => {
|
|
||||||
// Reset the scroll position before changing the view
|
|
||||||
this.props.dispatchSetMovieView(view);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSaveSelected = (payload) => {
|
|
||||||
this.props.dispatchSaveMovieEditor(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
onScroll = ({ scrollTop }) => {
|
|
||||||
scrollPositions.movieIndex = scrollTop;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<MovieIndex
|
|
||||||
{...this.props}
|
|
||||||
onViewSelect={this.onViewSelect}
|
|
||||||
onScroll={this.onScroll}
|
|
||||||
onSaveSelected={this.onSaveSelected}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexConnector.propTypes = {
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
|
||||||
dispatchSetMovieView: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveMovieEditor: PropTypes.func.isRequired,
|
|
||||||
fetchQueueDetails: PropTypes.func.isRequired,
|
|
||||||
clearQueueDetails: PropTypes.func.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object)
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withScrollPosition(
|
|
||||||
connect(createMapStateToProps, createMapDispatchToProps)(MovieIndexConnector),
|
|
||||||
'movieIndex'
|
|
||||||
);
|
|
@ -0,0 +1,48 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setMovieFilter } from 'Store/Actions/movieIndexActions';
|
||||||
|
|
||||||
|
function createMovieSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movies.items,
|
||||||
|
(movies) => {
|
||||||
|
return movies;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieIndex.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieIndexFilterModal(props) {
|
||||||
|
const sectionItems = useSelector(createMovieSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'movieIndex';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
dispatch(setMovieFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setMovieFilter } from 'Store/Actions/movieIndexActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movies.items,
|
|
||||||
(state) => state.movieIndex.filterBuilderProps,
|
|
||||||
(sectionItems, filterBuilderProps) => {
|
|
||||||
return {
|
|
||||||
sectionItems,
|
|
||||||
filterBuilderProps,
|
|
||||||
customFilterType: 'movieIndex'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetFilter: setMovieFilter
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
|
@ -1,132 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './MovieIndexFooter.css';
|
|
||||||
|
|
||||||
class MovieIndexFooter extends PureComponent {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movies,
|
|
||||||
colorImpairedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const count = movies.length;
|
|
||||||
let movieFiles = 0;
|
|
||||||
let monitored = 0;
|
|
||||||
let totalFileSize = 0;
|
|
||||||
|
|
||||||
movies.forEach((s) => {
|
|
||||||
|
|
||||||
if (s.hasFile) {
|
|
||||||
movieFiles += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.monitored) {
|
|
||||||
monitored++;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalFileSize += s.sizeOnDisk;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<div>
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={styles.ended} />
|
|
||||||
<div>
|
|
||||||
{translate('DownloadedAndMonitored')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={styles.availNotMonitored} />
|
|
||||||
<div>
|
|
||||||
{translate('DownloadedButNotMonitored')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.missingMonitored,
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{translate('MissingMonitoredAndConsideredAvailable')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.missingUnmonitored,
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{translate('MissingNotMonitored')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={styles.queue} />
|
|
||||||
<div>
|
|
||||||
{translate('Queued')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={styles.continuing} />
|
|
||||||
<div>
|
|
||||||
{translate('Unreleased')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.statistics}>
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Movies')}
|
|
||||||
data={count}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('MovieFiles')}
|
|
||||||
data={movieFiles}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Monitored')}
|
|
||||||
data={monitored}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Unmonitored')}
|
|
||||||
data={count - monitored}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('TotalFileSize')}
|
|
||||||
data={formatBytes(totalFileSize)}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexFooter.propTypes = {
|
|
||||||
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexFooter;
|
|
@ -0,0 +1,137 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './MovieIndexFooter.css';
|
||||||
|
|
||||||
|
function createUnoptimizedSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createClientSideCollectionSelector('movies', 'movieIndex'),
|
||||||
|
(movies) => {
|
||||||
|
return movies.items.map((m) => {
|
||||||
|
const { monitored, status } = m;
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitored,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMovieSelector() {
|
||||||
|
return createDeepEqualSelector(
|
||||||
|
createUnoptimizedSelector(),
|
||||||
|
(movies) => movies
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieIndexFooter() {
|
||||||
|
const movies = useSelector(createMovieSelector());
|
||||||
|
const count = movies.length;
|
||||||
|
let movieFiles = 0;
|
||||||
|
let monitored = 0;
|
||||||
|
let totalFileSize = 0;
|
||||||
|
|
||||||
|
movies.forEach((s) => {
|
||||||
|
if (s.hasFile) {
|
||||||
|
movieFiles += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.monitored) {
|
||||||
|
monitored++;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFileSize += s.sizeOnDisk;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorImpairedConsumer>
|
||||||
|
{(enableColorImpairedMode) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div className={styles.ended} />
|
||||||
|
<div>{translate('DownloadedAndMonitored')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div className={styles.availNotMonitored} />
|
||||||
|
<div>{translate('DownloadedButNotMonitored')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.missingMonitored,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>{translate('MissingMonitoredAndConsideredAvailable')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.missingUnmonitored,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>{translate('MissingNotMonitored')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div className={styles.queue} />
|
||||||
|
<div>{translate('Queued')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.legendItem}>
|
||||||
|
<div className={styles.continuing} />
|
||||||
|
<div>{translate('Unreleased')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.statistics}>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Movies')} data={count} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('MovieFiles')}
|
||||||
|
data={movieFiles}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Monitored')}
|
||||||
|
data={monitored}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Unmonitored')}
|
||||||
|
data={count - monitored}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('TotalFileSize')}
|
||||||
|
data={formatBytes(totalFileSize)}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ColorImpairedConsumer>
|
||||||
|
);
|
||||||
|
}
|
@ -1,53 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import MovieIndexFooter from './MovieIndexFooter';
|
|
||||||
|
|
||||||
function createUnoptimizedSelector() {
|
|
||||||
return createSelector(
|
|
||||||
createClientSideCollectionSelector('movies', 'movieIndex'),
|
|
||||||
(movies) => {
|
|
||||||
return movies.items.map((s) => {
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
status,
|
|
||||||
statistics,
|
|
||||||
sizeOnDisk,
|
|
||||||
hasFile
|
|
||||||
} = s;
|
|
||||||
|
|
||||||
return {
|
|
||||||
monitored,
|
|
||||||
status,
|
|
||||||
statistics,
|
|
||||||
sizeOnDisk,
|
|
||||||
hasFile
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMoviesSelector() {
|
|
||||||
return createDeepEqualSelector(
|
|
||||||
createUnoptimizedSelector(),
|
|
||||||
(movies) => movies
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMoviesSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(movies, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movies,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieIndexFooter);
|
|
@ -1,136 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
|
||||||
import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
|
|
||||||
function selectShowSearchAction() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex,
|
|
||||||
(movieIndex) => {
|
|
||||||
const view = movieIndex.view;
|
|
||||||
|
|
||||||
switch (view) {
|
|
||||||
case 'posters':
|
|
||||||
return movieIndex.posterOptions.showSearchAction;
|
|
||||||
case 'overview':
|
|
||||||
return movieIndex.overviewOptions.showSearchAction;
|
|
||||||
default:
|
|
||||||
return movieIndex.tableOptions.showSearchAction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createMovieQualityProfileSelector(),
|
|
||||||
selectShowSearchAction(),
|
|
||||||
createExecutingCommandsSelector(),
|
|
||||||
(state) => state.queue.details.items,
|
|
||||||
(
|
|
||||||
movie,
|
|
||||||
qualityProfile,
|
|
||||||
showSearchAction,
|
|
||||||
executingCommands,
|
|
||||||
queueItems
|
|
||||||
) => {
|
|
||||||
|
|
||||||
// If a movie is deleted this selector may fire before the parent
|
|
||||||
// selecors, which will result in an undefined movie, if that happens
|
|
||||||
// we want to return early here and again in the render function to avoid
|
|
||||||
// trying to show a movie that has no information available.
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRefreshingMovie = executingCommands.some((command) => {
|
|
||||||
return (
|
|
||||||
command.name === commandNames.REFRESH_MOVIE &&
|
|
||||||
command.body.movieIds.includes(movie.id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSearchingMovie = executingCommands.some((command) => {
|
|
||||||
return (
|
|
||||||
command.name === commandNames.MOVIE_SEARCH &&
|
|
||||||
command.body.movieIds.includes(movie.id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstQueueItem = queueItems.find((q) => q.movieId === movie.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...movie,
|
|
||||||
qualityProfile,
|
|
||||||
showSearchAction,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isSearchingMovie,
|
|
||||||
queueStatus: firstQueueItem ? firstQueueItem.status : null,
|
|
||||||
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchExecuteCommand: executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieIndexItemConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRefreshMoviePress = () => {
|
|
||||||
this.props.dispatchExecuteCommand({
|
|
||||||
name: commandNames.REFRESH_MOVIE,
|
|
||||||
movieIds: [this.props.id]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSearchPress = () => {
|
|
||||||
this.props.dispatchExecuteCommand({
|
|
||||||
name: commandNames.MOVIE_SEARCH,
|
|
||||||
movieIds: [this.props.id]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
component: ItemComponent,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemComponent
|
|
||||||
{...otherProps}
|
|
||||||
id={id}
|
|
||||||
onRefreshMoviePress={this.onRefreshMoviePress}
|
|
||||||
onSearchPress={this.onSearchPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexItemConnector.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
component: PropTypes.elementType.isRequired,
|
|
||||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
|
@ -1,315 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import fonts from 'Styles/Variables/fonts';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieIndexOverviewInfo from './MovieIndexOverviewInfo';
|
|
||||||
import styles from './MovieIndexOverview.css';
|
|
||||||
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
|
||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
|
||||||
|
|
||||||
// Hardcoded height beased on line-height of 32 + bottom margin of 10.
|
|
||||||
// Less side-effecty than using react-measure.
|
|
||||||
const titleRowHeight = 42;
|
|
||||||
|
|
||||||
function getContentHeight(rowHeight, isSmallScreen) {
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
|
|
||||||
return rowHeight - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieIndexOverview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditMoviePress = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMovieModalClose = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMovieModalClose = () => {
|
|
||||||
this.setState({ isDeleteMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = ({ value, shiftKey }) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value, shiftKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
tmdbId,
|
|
||||||
imdbId,
|
|
||||||
youTubeTrailerId,
|
|
||||||
title,
|
|
||||||
overview,
|
|
||||||
monitored,
|
|
||||||
hasFile,
|
|
||||||
isAvailable,
|
|
||||||
status,
|
|
||||||
titleSlug,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
qualityProfile,
|
|
||||||
overviewOptions,
|
|
||||||
showSearchAction,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
rowHeight,
|
|
||||||
isSmallScreen,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isSearchingMovie,
|
|
||||||
onRefreshMoviePress,
|
|
||||||
onSearchPress,
|
|
||||||
isMovieEditorActive,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange,
|
|
||||||
queueStatus,
|
|
||||||
queueState,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isEditMovieModalOpen,
|
|
||||||
isDeleteMovieModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
|
||||||
const overviewHeight = contentHeight - titleRowHeight;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.poster}>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
{
|
|
||||||
isMovieEditorActive &&
|
|
||||||
<div className={styles.editorSelect}>
|
|
||||||
<CheckInput
|
|
||||||
className={styles.checkInput}
|
|
||||||
name={id.toString()}
|
|
||||||
value={isSelected}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
style={elementStyle}
|
|
||||||
to={link}
|
|
||||||
>
|
|
||||||
<MoviePoster
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MovieIndexProgressBar
|
|
||||||
monitored={monitored}
|
|
||||||
hasFile={hasFile}
|
|
||||||
isAvailable={isAvailable}
|
|
||||||
status={status}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
|
||||||
queueStatus={queueStatus}
|
|
||||||
queueState={queueState}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<Link
|
|
||||||
className={styles.title}
|
|
||||||
to={link}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<MovieDetailsLinks
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
imdbId={imdbId}
|
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.REFRESH}
|
|
||||||
title={translate('RefreshMovie')}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
onPress={onRefreshMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
showSearchAction &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={styles.action}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
title={translate('SearchForMovie')}
|
|
||||||
isSpinning={isSearchingMovie}
|
|
||||||
onPress={onSearchPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name={icons.EDIT}
|
|
||||||
title={translate('EditMovie')}
|
|
||||||
onPress={this.onEditMoviePress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.details}>
|
|
||||||
<Link
|
|
||||||
className={styles.overview}
|
|
||||||
to={link}
|
|
||||||
>
|
|
||||||
<TextTruncate
|
|
||||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
|
||||||
text={overview}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<MovieIndexOverviewInfo
|
|
||||||
height={overviewHeight}
|
|
||||||
monitored={monitored}
|
|
||||||
qualityProfile={qualityProfile}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
longDateFormat={longDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
{...overviewOptions}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditMovieModalConnector
|
|
||||||
isOpen={isEditMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onEditMovieModalClose}
|
|
||||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteMovieModal
|
|
||||||
isOpen={isDeleteMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onDeleteMovieModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverview.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
overview: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired,
|
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
|
||||||
overviewOptions: PropTypes.object.isRequired,
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
isSearchingMovie: PropTypes.bool.isRequired,
|
|
||||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
|
||||||
onSearchPress: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
imdbId: PropTypes.string,
|
|
||||||
youTubeTrailerId: PropTypes.string,
|
|
||||||
queueStatus: PropTypes.string,
|
|
||||||
queueState: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverview;
|
|
@ -0,0 +1,244 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import fonts from 'Styles/Variables/fonts';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
|
||||||
|
import MovieIndexOverviewInfo from './MovieIndexOverviewInfo';
|
||||||
|
import selectOverviewOptions from './selectOverviewOptions';
|
||||||
|
import styles from './MovieIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
|
// Hardcoded height beased on line-height of 32 + bottom margin of 10.
|
||||||
|
// Less side-effecty than using react-measure.
|
||||||
|
const titleRowHeight = 42;
|
||||||
|
|
||||||
|
interface MovieIndexOverviewProps {
|
||||||
|
movieId: number;
|
||||||
|
sortKey: string;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
rowHeight: number;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
sortKey,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { movie, qualityProfile, isRefreshingMovie, isSearchingMovie } =
|
||||||
|
useSelector(createMovieIndexItemSelector(props.movieId));
|
||||||
|
|
||||||
|
const overviewOptions = useSelector(selectOverviewOptions);
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
monitored,
|
||||||
|
status,
|
||||||
|
path,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
hasFile,
|
||||||
|
isAvailable,
|
||||||
|
tmdbId,
|
||||||
|
imdbId,
|
||||||
|
youTubeTrailerId,
|
||||||
|
queueStatus,
|
||||||
|
queueState,
|
||||||
|
} = movie;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||||
|
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: REFRESH_MOVIE,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onSearchPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onEditMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(true);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onEditMovieModalClose = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
setIsDeleteMovieModalOpen(true);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMovieModalClose = useCallback(() => {
|
||||||
|
setIsDeleteMovieModalOpen(false);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const link = `/movie/${tmdbId}`;
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentHeight = useMemo(() => {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}, [rowHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
const overviewHeight = contentHeight - titleRowHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<Link className={styles.link} style={elementStyle} to={link}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MovieIndexProgressBar
|
||||||
|
monitored={monitored}
|
||||||
|
hasFile={hasFile}
|
||||||
|
isAvailable={isAvailable}
|
||||||
|
status={status}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||||
|
queueStatus={queueStatus}
|
||||||
|
queueState={queueState}
|
||||||
|
bottomRadius={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<Link className={styles.title} to={link}>
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<MovieDetailsLinks
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
imdbId={imdbId}
|
||||||
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.REFRESH}
|
||||||
|
title={translate('RefreshMovie')}
|
||||||
|
isSpinning={isRefreshingMovie}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{overviewOptions.showSearchAction ? (
|
||||||
|
<SpinnerIconButton
|
||||||
|
className={styles.actions}
|
||||||
|
name={icons.SEARCH}
|
||||||
|
title={translate('SearchForMovie')}
|
||||||
|
isSpinning={isSearchingMovie}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
title={translate('EditMovie')}
|
||||||
|
onPress={onEditMoviePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.details}>
|
||||||
|
<Link className={styles.overview} to={link}>
|
||||||
|
<TextTruncate
|
||||||
|
line={Math.floor(
|
||||||
|
overviewHeight / (defaultFontSize * lineHeight)
|
||||||
|
)}
|
||||||
|
text={overview}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<MovieIndexOverviewInfo
|
||||||
|
height={overviewHeight}
|
||||||
|
monitored={monitored}
|
||||||
|
qualityProfile={qualityProfile}
|
||||||
|
path={path}
|
||||||
|
sortKey={sortKey}
|
||||||
|
{...overviewOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditMovieModalConnector
|
||||||
|
isOpen={isEditMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onEditMovieModalClose}
|
||||||
|
onDeleteMoviePress={onDeleteMoviePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteMovieModal
|
||||||
|
isOpen={isDeleteMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onDeleteMovieModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverview;
|
@ -1,192 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import MovieIndexOverviewInfoRow from './MovieIndexOverviewInfoRow';
|
|
||||||
import styles from './MovieIndexOverviewInfo.css';
|
|
||||||
|
|
||||||
const infoRowHeight = parseInt(dimensions.movieIndexOverviewInfoRowHeight);
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{
|
|
||||||
name: 'monitored',
|
|
||||||
showProp: 'showMonitored',
|
|
||||||
valueProp: 'monitored'
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'studio',
|
|
||||||
showProp: 'showStudio',
|
|
||||||
valueProp: 'studio'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'qualityProfileId',
|
|
||||||
showProp: 'showQualityProfile',
|
|
||||||
valueProp: 'qualityProfileId'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'added',
|
|
||||||
showProp: 'showAdded',
|
|
||||||
valueProp: 'added'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'path',
|
|
||||||
showProp: 'showPath',
|
|
||||||
valueProp: 'path'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sizeOnDisk',
|
|
||||||
showProp: 'showSizeOnDisk',
|
|
||||||
valueProp: 'sizeOnDisk'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function isVisible(row, props) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
showProp,
|
|
||||||
valueProp
|
|
||||||
} = row;
|
|
||||||
|
|
||||||
if (props[valueProp] == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return props[showProp] || props.sortKey === name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInfoRowProps(row, props) {
|
|
||||||
const { name } = row;
|
|
||||||
|
|
||||||
if (name === 'monitored') {
|
|
||||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: monitoredText,
|
|
||||||
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
|
|
||||||
label: monitoredText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'studio') {
|
|
||||||
return {
|
|
||||||
title: 'Studio',
|
|
||||||
iconName: icons.STUDIO,
|
|
||||||
label: props.studio
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
|
||||||
return {
|
|
||||||
title: 'Quality Profile',
|
|
||||||
iconName: icons.PROFILE,
|
|
||||||
label: props.qualityProfile.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'added') {
|
|
||||||
const {
|
|
||||||
added,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
|
||||||
iconName: icons.ADD,
|
|
||||||
label: getRelativeDate(
|
|
||||||
added,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
|
||||||
timeFormat,
|
|
||||||
timeForToday: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'path') {
|
|
||||||
return {
|
|
||||||
title: 'Path',
|
|
||||||
iconName: icons.FOLDER,
|
|
||||||
label: props.path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sizeOnDisk') {
|
|
||||||
return {
|
|
||||||
title: 'Size on Disk',
|
|
||||||
iconName: icons.DRIVE,
|
|
||||||
label: formatBytes(props.sizeOnDisk)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function MovieIndexOverviewInfo(props) {
|
|
||||||
const {
|
|
||||||
height
|
|
||||||
// showRelativeDates,
|
|
||||||
// shortDateFormat,
|
|
||||||
// longDateFormat,
|
|
||||||
// timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let shownRows = 1;
|
|
||||||
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.infos}>
|
|
||||||
{
|
|
||||||
rows.map((row) => {
|
|
||||||
if (!isVisible(row, props)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shownRows >= maxRows) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
shownRows++;
|
|
||||||
|
|
||||||
const infoRowProps = getInfoRowProps(row, props);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieIndexOverviewInfoRow
|
|
||||||
key={row.name}
|
|
||||||
{...infoRowProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverviewInfo.propTypes = {
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
showStudio: PropTypes.bool.isRequired,
|
|
||||||
showMonitored: PropTypes.bool.isRequired,
|
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
|
||||||
showAdded: PropTypes.bool.isRequired,
|
|
||||||
showPath: PropTypes.bool.isRequired,
|
|
||||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
studio: PropTypes.string,
|
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
|
||||||
added: PropTypes.string,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
sortKey: PropTypes.string.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverviewInfo;
|
|
@ -0,0 +1,166 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import MovieIndexOverviewInfoRow from './MovieIndexOverviewInfoRow';
|
||||||
|
import styles from './MovieIndexOverviewInfo.css';
|
||||||
|
|
||||||
|
const infoRowHeight = parseInt(dimensions.movieIndexOverviewInfoRowHeight);
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
name: 'monitored',
|
||||||
|
showProp: 'showMonitored',
|
||||||
|
valueProp: 'monitored',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'studio',
|
||||||
|
showProp: 'showStudio',
|
||||||
|
valueProp: 'studio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'qualityProfileId',
|
||||||
|
showProp: 'showQualityProfile',
|
||||||
|
valueProp: 'qualityProfileId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'added',
|
||||||
|
showProp: 'showAdded',
|
||||||
|
valueProp: 'added',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'path',
|
||||||
|
showProp: 'showPath',
|
||||||
|
valueProp: 'path',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sizeOnDisk',
|
||||||
|
showProp: 'showSizeOnDisk',
|
||||||
|
valueProp: 'sizeOnDisk',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getInfoRowProps(row, props, uiSettings) {
|
||||||
|
const { name } = row;
|
||||||
|
|
||||||
|
if (name === 'monitored') {
|
||||||
|
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: monitoredText,
|
||||||
|
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
|
||||||
|
label: monitoredText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'studio') {
|
||||||
|
return {
|
||||||
|
title: 'Studio',
|
||||||
|
iconName: icons.STUDIO,
|
||||||
|
label: props.studio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'qualityProfileId') {
|
||||||
|
return {
|
||||||
|
title: 'Quality Profile',
|
||||||
|
iconName: icons.PROFILE,
|
||||||
|
label: props.qualityProfile.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
const added = props.added;
|
||||||
|
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||||
|
uiSettings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||||
|
iconName: icons.ADD,
|
||||||
|
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'path') {
|
||||||
|
return {
|
||||||
|
title: 'Path',
|
||||||
|
iconName: icons.FOLDER,
|
||||||
|
label: props.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sizeOnDisk') {
|
||||||
|
return {
|
||||||
|
title: 'Size on Disk',
|
||||||
|
iconName: icons.DRIVE,
|
||||||
|
label: formatBytes(props.sizeOnDisk),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieIndexOverviewInfoProps {
|
||||||
|
height: number;
|
||||||
|
showMonitored: boolean;
|
||||||
|
showQualityProfile: boolean;
|
||||||
|
showAdded: boolean;
|
||||||
|
showPath: boolean;
|
||||||
|
showSizeOnDisk: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
qualityProfile: object;
|
||||||
|
added?: string;
|
||||||
|
path: string;
|
||||||
|
sizeOnDisk?: number;
|
||||||
|
sortKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverviewInfo(props: MovieIndexOverviewInfoProps) {
|
||||||
|
const height = props.height;
|
||||||
|
|
||||||
|
const uiSettings = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
let shownRows = 1;
|
||||||
|
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
||||||
|
|
||||||
|
const rowInfo = useMemo(() => {
|
||||||
|
return rows.map((row) => {
|
||||||
|
const { name, showProp, valueProp } = row;
|
||||||
|
|
||||||
|
const isVisible =
|
||||||
|
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
isVisible,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.infos}>
|
||||||
|
{rowInfo.map((row) => {
|
||||||
|
if (!row.isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shownRows >= maxRows) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
shownRows++;
|
||||||
|
|
||||||
|
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||||
|
|
||||||
|
return <MovieIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverviewInfo;
|
@ -1,35 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import styles from './MovieIndexOverviewInfoRow.css';
|
|
||||||
|
|
||||||
function MovieIndexOverviewInfoRow(props) {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
iconName,
|
|
||||||
label
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.infoRow}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.icon}
|
|
||||||
name={iconName}
|
|
||||||
size={14}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverviewInfoRow.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
iconName: PropTypes.object.isRequired,
|
|
||||||
label: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverviewInfoRow;
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import styles from './MovieIndexOverviewInfoRow.css';
|
||||||
|
|
||||||
|
interface MovieIndexOverviewInfoRowProps {
|
||||||
|
title?: string;
|
||||||
|
iconName: object;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverviewInfoRow(props: MovieIndexOverviewInfoRowProps) {
|
||||||
|
const { title, iconName, label } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.infoRow} title={title}>
|
||||||
|
<Icon className={styles.icon} name={iconName} size={14} />
|
||||||
|
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverviewInfoRow;
|
@ -1,285 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
||||||
import MovieIndexOverview from './MovieIndexOverview';
|
|
||||||
import styles from './MovieIndexOverviews.css';
|
|
||||||
|
|
||||||
// Poster container dimensions
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
|
||||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
|
||||||
|
|
||||||
function calculatePosterWidth(posterSize, isSmallScreen) {
|
|
||||||
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
|
||||||
|
|
||||||
if (posterSize === 'large') {
|
|
||||||
return maxiumPosterWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (posterSize === 'medium') {
|
|
||||||
return Math.floor(maxiumPosterWidth * 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor(maxiumPosterWidth * 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
|
||||||
const {
|
|
||||||
detailedProgressBar
|
|
||||||
} = overviewOptions;
|
|
||||||
|
|
||||||
const heights = [
|
|
||||||
posterHeight,
|
|
||||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
|
||||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
|
||||||
];
|
|
||||||
|
|
||||||
return heights.reduce((acc, height) => acc + height, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculatePosterHeight(posterWidth) {
|
|
||||||
return Math.ceil((250 / 170) * posterWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieIndexOverviews extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
columnCount: 1,
|
|
||||||
posterWidth: 162,
|
|
||||||
posterHeight: 238,
|
|
||||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
|
||||||
scrollRestored: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._grid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
overviewOptions,
|
|
||||||
jumpToCharacter,
|
|
||||||
scrollTop,
|
|
||||||
isMovieEditorActive,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
rowHeight,
|
|
||||||
scrollRestored
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (prevProps.sortKey !== sortKey ||
|
|
||||||
prevProps.overviewOptions !== overviewOptions) {
|
|
||||||
this.calculateGrid(this.state.width, isSmallScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._grid &&
|
|
||||||
(prevState.width !== width ||
|
|
||||||
prevState.rowHeight !== rowHeight ||
|
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
|
||||||
prevProps.overviewOptions !== overviewOptions ||
|
|
||||||
prevProps.isMovieEditorActive !== isMovieEditorActive)) {
|
|
||||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
|
||||||
this._grid.recomputeGridSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
|
||||||
this.setState({ scrollRestored: true });
|
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
|
||||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
|
||||||
|
|
||||||
if (this._grid && index != null) {
|
|
||||||
|
|
||||||
this._grid.scrollToCell({
|
|
||||||
rowIndex: index,
|
|
||||||
columnIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
|
||||||
this._grid = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
|
||||||
const {
|
|
||||||
sortKey,
|
|
||||||
overviewOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
|
||||||
const posterHeight = calculatePosterHeight(posterWidth);
|
|
||||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cellRenderer = ({ key, rowIndex, style }) => {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
overviewOptions,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isSmallScreen,
|
|
||||||
selectedState,
|
|
||||||
isMovieEditorActive,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const movie = items[rowIndex];
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<MovieIndexItemConnector
|
|
||||||
key={movie.id}
|
|
||||||
component={MovieIndexOverview}
|
|
||||||
sortKey={sortKey}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
posterHeight={posterHeight}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
overviewOptions={overviewOptions}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
longDateFormat={longDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
movieId={movie.id}
|
|
||||||
qualityProfileId={movie.qualityProfileId}
|
|
||||||
isSelected={selectedState[movie.id]}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
isMovieEditorActive={isMovieEditorActive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.calculateGrid(width, this.props.isSmallScreen);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isSmallScreen,
|
|
||||||
scroller,
|
|
||||||
items,
|
|
||||||
selectedState
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={isSmallScreen ? undefined : scroller}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={registerChild}>
|
|
||||||
<Grid
|
|
||||||
ref={this.setGridRef}
|
|
||||||
className={styles.grid}
|
|
||||||
autoHeight={true}
|
|
||||||
height={height}
|
|
||||||
columnCount={1}
|
|
||||||
columnWidth={width}
|
|
||||||
rowCount={items.length}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
width={width}
|
|
||||||
onScroll={onChildScroll}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
overscanRowCount={2}
|
|
||||||
cellRenderer={this.cellRenderer}
|
|
||||||
selectedState={selectedState}
|
|
||||||
scrollToAlignment={'start'}
|
|
||||||
isScrollingOptout={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</WindowScroller>
|
|
||||||
</Measure>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverviews.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
overviewOptions: PropTypes.object.isRequired,
|
|
||||||
jumpToCharacter: PropTypes.string,
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
selectedState: PropTypes.object.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverviews;
|
|
@ -0,0 +1,203 @@
|
|||||||
|
import { throttle } from 'lodash';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import MovieIndexOverview from './MovieIndexOverview';
|
||||||
|
import selectOverviewOptions from './selectOverviewOptions';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||||
|
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||||
|
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||||
|
const bodyPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.pageContentBodyPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
interface RowItemData {
|
||||||
|
items: Movie[];
|
||||||
|
sortKey: string;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
rowHeight: number;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieIndexOverviewsProps {
|
||||||
|
items: Movie[];
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: string;
|
||||||
|
jumpToCharacter?: string;
|
||||||
|
scrollTop?: number;
|
||||||
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { items, ...otherData } = data;
|
||||||
|
|
||||||
|
if (index >= items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = items[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<MovieIndexOverview movieId={movie.id} {...otherData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWindowScrollTopPosition() {
|
||||||
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverviews(props: MovieIndexOverviewsProps) {
|
||||||
|
const { items, sortKey, jumpToCharacter, isSmallScreen, scrollerRef } = props;
|
||||||
|
|
||||||
|
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||||
|
selectOverviewOptions
|
||||||
|
);
|
||||||
|
const listRef: React.MutableRefObject<List> = useRef();
|
||||||
|
const [measureRef, bounds] = useMeasure();
|
||||||
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const posterWidth = useMemo(() => {
|
||||||
|
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
||||||
|
|
||||||
|
if (posterSize === 'large') {
|
||||||
|
return maxiumPosterWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posterSize === 'medium') {
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.5);
|
||||||
|
}, [posterSize, isSmallScreen]);
|
||||||
|
|
||||||
|
const posterHeight = useMemo(() => {
|
||||||
|
return Math.ceil((250 / 170) * posterWidth);
|
||||||
|
}, [posterWidth]);
|
||||||
|
|
||||||
|
const rowHeight = useMemo(() => {
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}, [detailedProgressBar, posterHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = scrollerRef.current as HTMLElement;
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
const width = current.clientWidth;
|
||||||
|
const padding =
|
||||||
|
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
|
||||||
|
|
||||||
|
setSize({
|
||||||
|
width: width - padding * 2,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isSmallScreen, scrollerRef, bounds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
|
const handleScroll = throttle(() => {
|
||||||
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
const scrollTop =
|
||||||
|
(isSmallScreen
|
||||||
|
? getWindowScrollTopPosition()
|
||||||
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
|
listRef.current.scrollTo(scrollTop);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handleScroll.cancel();
|
||||||
|
|
||||||
|
if (currentScrollListener) {
|
||||||
|
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isSmallScreen, listRef, scrollerRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
let scrollTop = index * rowHeight;
|
||||||
|
|
||||||
|
// If the offset is zero go to the top, otherwise offset
|
||||||
|
// by the approximate size of the header + padding (37 + 20).
|
||||||
|
if (scrollTop > 0) {
|
||||||
|
const offset = 57;
|
||||||
|
|
||||||
|
scrollTop += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
listRef.current.scrollTo(scrollTop);
|
||||||
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={measureRef}>
|
||||||
|
<List<RowItemData>
|
||||||
|
ref={listRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'none',
|
||||||
|
}}
|
||||||
|
width={size.width}
|
||||||
|
height={size.height}
|
||||||
|
itemCount={items.length}
|
||||||
|
itemSize={rowHeight}
|
||||||
|
itemData={{
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverviews;
|
@ -1,25 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import MovieIndexOverviews from './MovieIndexOverviews';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex.overviewOptions,
|
|
||||||
createUISettingsSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(overviewOptions, uiSettings, dimensions) => {
|
|
||||||
return {
|
|
||||||
overviewOptions,
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieIndexOverviews);
|
|
@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import MovieIndexOverviewOptionsModalContentConnector from './MovieIndexOverviewOptionsModalContentConnector';
|
|
||||||
|
|
||||||
function MovieIndexOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<MovieIndexOverviewOptionsModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverviewOptionsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverviewOptionsModal;
|
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import MovieIndexOverviewOptionsModalContent from './MovieIndexOverviewOptionsModalContent';
|
||||||
|
|
||||||
|
interface MovieIndexOverviewOptionsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverviewOptionsModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: MovieIndexOverviewOptionsModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<MovieIndexOverviewOptionsModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverviewOptionsModal;
|
@ -1,268 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
const posterSizeOptions = [
|
|
||||||
{ key: 'small', value: translate('Small') },
|
|
||||||
{ key: 'medium', value: translate('Medium') },
|
|
||||||
{ key: 'large', value: translate('Large') }
|
|
||||||
];
|
|
||||||
|
|
||||||
class MovieIndexOverviewOptionsModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
detailedProgressBar: props.detailedProgressBar,
|
|
||||||
size: props.size,
|
|
||||||
showMonitored: props.showMonitored,
|
|
||||||
showStudio: props.showStudio,
|
|
||||||
showQualityProfile: props.showQualityProfile,
|
|
||||||
showAdded: props.showAdded,
|
|
||||||
showPath: props.showPath,
|
|
||||||
showSizeOnDisk: props.showSizeOnDisk,
|
|
||||||
showSearchAction: props.showSearchAction
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
size,
|
|
||||||
showMonitored,
|
|
||||||
showStudio,
|
|
||||||
showQualityProfile,
|
|
||||||
showAdded,
|
|
||||||
showPath,
|
|
||||||
showSizeOnDisk,
|
|
||||||
showSearchAction
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const state = {};
|
|
||||||
|
|
||||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
|
||||||
state.detailedProgressBar = detailedProgressBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size !== prevProps.size) {
|
|
||||||
state.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showMonitored !== prevProps.showMonitored) {
|
|
||||||
state.showMonitored = showMonitored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showStudio !== prevProps.showStudio) {
|
|
||||||
state.showStudio = showStudio;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
|
||||||
state.showQualityProfile = showQualityProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAdded !== prevProps.showAdded) {
|
|
||||||
state.showAdded = showAdded;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPath !== prevProps.showPath) {
|
|
||||||
state.showPath = showPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSizeOnDisk !== prevProps.showSizeOnDisk) {
|
|
||||||
state.showSizeOnDisk = showSizeOnDisk;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSearchAction !== prevProps.showSearchAction) {
|
|
||||||
state.showSearchAction = showSearchAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChangeOverviewOption = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onChangeOverviewOption({ [name]: value });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
size,
|
|
||||||
showMonitored,
|
|
||||||
showStudio,
|
|
||||||
showQualityProfile,
|
|
||||||
showAdded,
|
|
||||||
showPath,
|
|
||||||
showSizeOnDisk,
|
|
||||||
showSearchAction
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('OverviewOptions')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="size"
|
|
||||||
value={size}
|
|
||||||
values={posterSizeOptions}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="detailedProgressBar"
|
|
||||||
value={detailedProgressBar}
|
|
||||||
helpText={translate('DetailedProgressBarHelpText')}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showMonitored"
|
|
||||||
value={showMonitored}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowStudio')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showStudio"
|
|
||||||
value={showStudio}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showQualityProfile"
|
|
||||||
value={showQualityProfile}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowDateAdded')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showAdded"
|
|
||||||
value={showAdded}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowPath')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showPath"
|
|
||||||
value={showPath}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowSizeOnDisk')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showSizeOnDisk"
|
|
||||||
value={showSizeOnDisk}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showSearchAction"
|
|
||||||
value={showSearchAction}
|
|
||||||
helpText={translate('ShowSearchHelpText')}
|
|
||||||
onChange={this.onChangeOverviewOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexOverviewOptionsModalContent.propTypes = {
|
|
||||||
size: PropTypes.string.isRequired,
|
|
||||||
detailedProgressBar: PropTypes.bool.isRequired,
|
|
||||||
showMonitored: PropTypes.bool.isRequired,
|
|
||||||
showStudio: PropTypes.bool.isRequired,
|
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
|
||||||
showAdded: PropTypes.bool.isRequired,
|
|
||||||
showPath: PropTypes.bool.isRequired,
|
|
||||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexOverviewOptionsModalContent;
|
|
@ -0,0 +1,170 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, 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 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 { setMovieOverviewOption } from 'Store/Actions/movieIndexActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import selectOverviewOptions from '../selectOverviewOptions';
|
||||||
|
|
||||||
|
const posterSizeOptions = [
|
||||||
|
{ key: 'small', value: translate('Small') },
|
||||||
|
{ key: 'medium', value: translate('Medium') },
|
||||||
|
{ key: 'large', value: translate('Large') },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface MovieIndexOverviewOptionsModalContentProps {
|
||||||
|
onModalClose(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexOverviewOptionsModalContent(
|
||||||
|
props: MovieIndexOverviewOptionsModalContentProps
|
||||||
|
) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showMonitored,
|
||||||
|
showStudio,
|
||||||
|
showQualityProfile,
|
||||||
|
showAdded,
|
||||||
|
showPath,
|
||||||
|
showSizeOnDisk,
|
||||||
|
showSearchAction,
|
||||||
|
} = useSelector(selectOverviewOptions);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onOverviewOptionChange = useCallback(
|
||||||
|
({ name, value }) => {
|
||||||
|
dispatch(setMovieOverviewOption({ [name]: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('OverviewOptions')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="size"
|
||||||
|
value={size}
|
||||||
|
values={posterSizeOptions}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="detailedProgressBar"
|
||||||
|
value={detailedProgressBar}
|
||||||
|
helpText={translate('DetailedProgressBarHelpText')}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showMonitored"
|
||||||
|
value={showMonitored}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowStudio')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showStudio"
|
||||||
|
value={showStudio}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showQualityProfile"
|
||||||
|
value={showQualityProfile}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowDateAdded')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showAdded"
|
||||||
|
value={showAdded}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowPath')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showPath"
|
||||||
|
value={showPath}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowSizeOnDisk')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showSizeOnDisk"
|
||||||
|
value={showSizeOnDisk}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showSearchAction"
|
||||||
|
value={showSearchAction}
|
||||||
|
helpText={translate('ShowSearchHelpText')}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexOverviewOptionsModalContent;
|
@ -1,23 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setMovieOverviewOption } from 'Store/Actions/movieIndexActions';
|
|
||||||
import MovieIndexOverviewOptionsModalContent from './MovieIndexOverviewOptionsModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex,
|
|
||||||
(movieIndex) => {
|
|
||||||
return movieIndex.overviewOptions;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onChangeOverviewOption(payload) {
|
|
||||||
dispatch(setMovieOverviewOption(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieIndexOverviewOptionsModalContent);
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const selectOverviewOptions = createSelector(
|
||||||
|
(state) => state.movieIndex.overviewOptions,
|
||||||
|
(overviewOptions) => overviewOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export default selectOverviewOptions;
|
@ -1,401 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieIndexPosterInfo from './MovieIndexPosterInfo';
|
|
||||||
import styles from './MovieIndexPoster.css';
|
|
||||||
|
|
||||||
class MovieIndexPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditMoviePress = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMovieModalClose = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMovieModalClose = () => {
|
|
||||||
this.setState({ isDeleteMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoad = () => {
|
|
||||||
if (this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoadError = () => {
|
|
||||||
if (!this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = ({ value, shiftKey }) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value, shiftKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
tmdbId,
|
|
||||||
imdbId,
|
|
||||||
youTubeTrailerId,
|
|
||||||
title,
|
|
||||||
monitored,
|
|
||||||
hasFile,
|
|
||||||
isAvailable,
|
|
||||||
status,
|
|
||||||
titleSlug,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
detailedProgressBar,
|
|
||||||
showTitle,
|
|
||||||
showMonitored,
|
|
||||||
showQualityProfile,
|
|
||||||
qualityProfile,
|
|
||||||
showSearchAction,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
showReleaseDate,
|
|
||||||
showCinemaRelease,
|
|
||||||
inCinemas,
|
|
||||||
physicalRelease,
|
|
||||||
digitalRelease,
|
|
||||||
timeFormat,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isSearchingMovie,
|
|
||||||
onRefreshMoviePress,
|
|
||||||
onSearchPress,
|
|
||||||
isMovieEditorActive,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange,
|
|
||||||
queueStatus,
|
|
||||||
queueState,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPosterError,
|
|
||||||
isEditMovieModalOpen,
|
|
||||||
isDeleteMovieModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
let releaseDate = '';
|
|
||||||
let releaseDateType = '';
|
|
||||||
if (physicalRelease && digitalRelease) {
|
|
||||||
releaseDate = (physicalRelease < digitalRelease) ? physicalRelease : digitalRelease;
|
|
||||||
releaseDateType = (physicalRelease < digitalRelease) ? 'Released' : 'Digital';
|
|
||||||
} else if (physicalRelease && !digitalRelease) {
|
|
||||||
releaseDate = physicalRelease;
|
|
||||||
releaseDateType = 'Released';
|
|
||||||
} else if (digitalRelease && !physicalRelease) {
|
|
||||||
releaseDate = digitalRelease;
|
|
||||||
releaseDateType = 'Digital';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
{
|
|
||||||
isMovieEditorActive &&
|
|
||||||
<div className={styles.editorSelect}>
|
|
||||||
<CheckInput
|
|
||||||
className={styles.checkInput}
|
|
||||||
name={id.toString()}
|
|
||||||
value={isSelected}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Label className={styles.controls}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={styles.action}
|
|
||||||
name={icons.REFRESH}
|
|
||||||
title={translate('RefreshMovie')}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
onPress={onRefreshMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
showSearchAction &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={styles.action}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
title={translate('SearchForMovie')}
|
|
||||||
isSpinning={isSearchingMovie}
|
|
||||||
onPress={onSearchPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
className={styles.action}
|
|
||||||
name={icons.EDIT}
|
|
||||||
title={translate('EditMovie')}
|
|
||||||
onPress={this.onEditMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<MovieDetailsLinks
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
imdbId={imdbId}
|
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{
|
|
||||||
status === 'ended' &&
|
|
||||||
<div
|
|
||||||
className={styles.ended}
|
|
||||||
title={translate('Ended')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
style={elementStyle}
|
|
||||||
to={link}
|
|
||||||
>
|
|
||||||
<MoviePoster
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
onError={this.onPosterLoadError}
|
|
||||||
onLoad={this.onPosterLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasPosterError &&
|
|
||||||
<div className={styles.overlayTitle}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MovieIndexProgressBar
|
|
||||||
monitored={monitored}
|
|
||||||
hasFile={hasFile}
|
|
||||||
status={status}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
detailedProgressBar={detailedProgressBar}
|
|
||||||
queueStatus={queueStatus}
|
|
||||||
queueState={queueState}
|
|
||||||
isAvailable={isAvailable}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
showTitle &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showMonitored &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showQualityProfile &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
{qualityProfile.name}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCinemaRelease && inCinemas &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Icon
|
|
||||||
name={icons.IN_CINEMAS}
|
|
||||||
/> {getRelativeDate(
|
|
||||||
inCinemas,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
|
||||||
timeFormat,
|
|
||||||
timeForToday: false
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showReleaseDate && releaseDateType === 'Released' &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Icon
|
|
||||||
name={icons.DISC}
|
|
||||||
/> {getRelativeDate(
|
|
||||||
releaseDate,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
|
||||||
timeFormat,
|
|
||||||
timeForToday: false
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showReleaseDate && releaseDateType === 'Digital' &&
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Icon
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
/> {getRelativeDate(
|
|
||||||
releaseDate,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
|
||||||
timeFormat,
|
|
||||||
timeForToday: false
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MovieIndexPosterInfo
|
|
||||||
qualityProfile={qualityProfile}
|
|
||||||
showQualityProfile={showQualityProfile}
|
|
||||||
showReleaseDate={showReleaseDate}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
inCinemas={inCinemas}
|
|
||||||
physicalRelease={physicalRelease}
|
|
||||||
digitalRelease={digitalRelease}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditMovieModalConnector
|
|
||||||
isOpen={isEditMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onEditMovieModalClose}
|
|
||||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteMovieModal
|
|
||||||
isOpen={isDeleteMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onDeleteMovieModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexPoster.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
detailedProgressBar: PropTypes.bool.isRequired,
|
|
||||||
showTitle: PropTypes.bool.isRequired,
|
|
||||||
showMonitored: PropTypes.bool.isRequired,
|
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
showCinemaRelease: PropTypes.bool.isRequired,
|
|
||||||
showReleaseDate: PropTypes.bool.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
isSearchingMovie: PropTypes.bool.isRequired,
|
|
||||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
|
||||||
onSearchPress: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
imdbId: PropTypes.string,
|
|
||||||
youTubeTrailerId: PropTypes.string,
|
|
||||||
queueStatus: PropTypes.string,
|
|
||||||
queueState: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieIndexPoster.defaultProps = {
|
|
||||||
statistics: {
|
|
||||||
movieFileCount: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexPoster;
|
|
@ -0,0 +1,238 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
|
||||||
|
import MovieIndexPosterInfo from './MovieIndexPosterInfo';
|
||||||
|
import selectPosterOptions from './selectPosterOptions';
|
||||||
|
import styles from './MovieIndexPoster.css';
|
||||||
|
|
||||||
|
interface MovieIndexPosterProps {
|
||||||
|
movieId: number;
|
||||||
|
sortKey: string;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||||
|
const { movieId, sortKey, posterWidth, posterHeight } = props;
|
||||||
|
|
||||||
|
const { movie, qualityProfile, isRefreshingMovie, isSearchingMovie } =
|
||||||
|
useSelector(createMovieIndexItemSelector(props.movieId));
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
showTitle,
|
||||||
|
showMonitored,
|
||||||
|
showQualityProfile,
|
||||||
|
showReleaseDate,
|
||||||
|
showSearchAction,
|
||||||
|
} = useSelector(selectPosterOptions);
|
||||||
|
|
||||||
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
monitored,
|
||||||
|
status,
|
||||||
|
images,
|
||||||
|
tmdbId,
|
||||||
|
imdbId,
|
||||||
|
youTubeTrailerId,
|
||||||
|
hasFile,
|
||||||
|
isAvailable,
|
||||||
|
inCinemas,
|
||||||
|
physicalRelease,
|
||||||
|
digitalRelease,
|
||||||
|
path,
|
||||||
|
certification,
|
||||||
|
queueStatus,
|
||||||
|
queueState,
|
||||||
|
} = movie;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [hasPosterError, setHasPosterError] = useState(false);
|
||||||
|
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||||
|
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: REFRESH_MOVIE,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onSearchPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onPosterLoadError = useCallback(() => {
|
||||||
|
setHasPosterError(true);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const onPosterLoad = useCallback(() => {
|
||||||
|
setHasPosterError(false);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const onEditMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(true);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onEditMovieModalClose = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
setIsDeleteMovieModalOpen(true);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMovieModalClose = useCallback(() => {
|
||||||
|
setIsDeleteMovieModalOpen(false);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const link = `/movie/${tmdbId}`;
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.REFRESH}
|
||||||
|
title={translate('RefreshMovie')}
|
||||||
|
isSpinning={isRefreshingMovie}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showSearchAction ? (
|
||||||
|
<SpinnerIconButton
|
||||||
|
className={styles.action}
|
||||||
|
name={icons.SEARCH}
|
||||||
|
title={translate('SearchForMovie')}
|
||||||
|
isSpinning={isSearchingMovie}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
title={translate('EditMovie')}
|
||||||
|
onPress={onEditMoviePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<MovieDetailsLinks
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
imdbId={imdbId}
|
||||||
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Link className={styles.link} style={elementStyle} to={link}>
|
||||||
|
<MoviePoster
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={onPosterLoadError}
|
||||||
|
onLoad={onPosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPosterError ? (
|
||||||
|
<div className={styles.overlayTitle}>{title}</div>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MovieIndexProgressBar
|
||||||
|
monitored={monitored}
|
||||||
|
hasFile={hasFile}
|
||||||
|
isAvailable={isAvailable}
|
||||||
|
status={status}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
queueStatus={queueStatus}
|
||||||
|
queueState={queueState}
|
||||||
|
bottomRadius={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showTitle ? <div className={styles.title}>{title}</div> : null}
|
||||||
|
|
||||||
|
{showMonitored ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
{monitored ? translate('monitored') : translate('unmonitored')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showQualityProfile ? (
|
||||||
|
<div className={styles.title}>{qualityProfile.name}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<MovieIndexPosterInfo
|
||||||
|
qualityProfile={qualityProfile}
|
||||||
|
showQualityProfile={showQualityProfile}
|
||||||
|
showReleaseDate={showReleaseDate}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
inCinemas={inCinemas}
|
||||||
|
physicalRelease={physicalRelease}
|
||||||
|
digitalRelease={digitalRelease}
|
||||||
|
sortKey={sortKey}
|
||||||
|
path={path}
|
||||||
|
certification={certification}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditMovieModalConnector
|
||||||
|
isOpen={isEditMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onEditMovieModalClose}
|
||||||
|
onDeleteMoviePress={onDeleteMoviePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteMovieModal
|
||||||
|
isOpen={isDeleteMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onDeleteMovieModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexPoster;
|
@ -1,3 +0,0 @@
|
|||||||
.grid {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'grid': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
@ -1,358 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
||||||
import MovieIndexPoster from './MovieIndexPoster';
|
|
||||||
import styles from './MovieIndexPosters.css';
|
|
||||||
|
|
||||||
// Poster container dimensions
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
|
||||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
|
||||||
|
|
||||||
const additionalColumnCount = {
|
|
||||||
small: 3,
|
|
||||||
medium: 2,
|
|
||||||
large: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
|
||||||
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
|
||||||
const columns = Math.floor(width / maxiumColumnWidth);
|
|
||||||
const remainder = width % maxiumColumnWidth;
|
|
||||||
|
|
||||||
if (remainder === 0 && posterSize === 'large') {
|
|
||||||
return maxiumColumnWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
showTitle,
|
|
||||||
showMonitored,
|
|
||||||
showQualityProfile,
|
|
||||||
showReleaseDate
|
|
||||||
} = posterOptions;
|
|
||||||
|
|
||||||
const nextAiringHeight = 19;
|
|
||||||
|
|
||||||
const heights = [
|
|
||||||
posterHeight,
|
|
||||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
|
||||||
nextAiringHeight,
|
|
||||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
|
||||||
];
|
|
||||||
|
|
||||||
if (showTitle) {
|
|
||||||
heights.push(19);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showMonitored) {
|
|
||||||
heights.push(19);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showQualityProfile) {
|
|
||||||
heights.push(19);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showReleaseDate) {
|
|
||||||
heights.push(19);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (sortKey) {
|
|
||||||
case 'studio':
|
|
||||||
case 'added':
|
|
||||||
case 'path':
|
|
||||||
case 'sizeOnDisk':
|
|
||||||
heights.push(19);
|
|
||||||
break;
|
|
||||||
case 'qualityProfileId':
|
|
||||||
if (!showQualityProfile) {
|
|
||||||
heights.push(19);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// No need to add a height of 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return heights.reduce((acc, height) => acc + height, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculatePosterHeight(posterWidth) {
|
|
||||||
return Math.ceil((250 / 170) * posterWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieIndexPosters extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
columnWidth: 182,
|
|
||||||
columnCount: 1,
|
|
||||||
posterWidth: 162,
|
|
||||||
posterHeight: 238,
|
|
||||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
|
||||||
scrollRestored: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._isInitialized = false;
|
|
||||||
this._grid = null;
|
|
||||||
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
posterOptions,
|
|
||||||
jumpToCharacter,
|
|
||||||
isSmallScreen,
|
|
||||||
isMovieEditorActive,
|
|
||||||
scrollTop
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
columnWidth,
|
|
||||||
columnCount,
|
|
||||||
rowHeight,
|
|
||||||
scrollRestored
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (prevProps.sortKey !== sortKey ||
|
|
||||||
prevProps.posterOptions !== posterOptions) {
|
|
||||||
this.calculateGrid(width, isSmallScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid &&
|
|
||||||
(prevState.width !== width ||
|
|
||||||
prevState.columnWidth !== columnWidth ||
|
|
||||||
prevState.columnCount !== columnCount ||
|
|
||||||
prevState.rowHeight !== rowHeight ||
|
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
|
||||||
prevState.isMovieEditorActive !== isMovieEditorActive)) {
|
|
||||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
|
||||||
this._grid.recomputeGridSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
|
||||||
this.setState({ scrollRestored: true });
|
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
|
||||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
|
||||||
|
|
||||||
if (this._grid && index != null) {
|
|
||||||
const row = Math.floor(index / columnCount);
|
|
||||||
|
|
||||||
this._grid.scrollToCell({
|
|
||||||
rowIndex: row,
|
|
||||||
columnIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid && scrollTop !== 0) {
|
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
|
||||||
this._grid = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
|
||||||
const {
|
|
||||||
sortKey,
|
|
||||||
posterOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
|
|
||||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
|
||||||
const posterWidth = columnWidth - this._padding * 2;
|
|
||||||
const posterHeight = calculatePosterHeight(posterWidth);
|
|
||||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width,
|
|
||||||
columnWidth,
|
|
||||||
columnCount,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
posterOptions,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
selectedState,
|
|
||||||
isMovieEditorActive,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
columnCount
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
showTitle,
|
|
||||||
showMonitored,
|
|
||||||
showQualityProfile,
|
|
||||||
showCinemaRelease,
|
|
||||||
showReleaseDate
|
|
||||||
} = posterOptions;
|
|
||||||
|
|
||||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
|
||||||
const movie = items[movieIdx];
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
key={key}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
padding: this._padding
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MovieIndexItemConnector
|
|
||||||
key={movie.id}
|
|
||||||
component={MovieIndexPoster}
|
|
||||||
sortKey={sortKey}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
posterHeight={posterHeight}
|
|
||||||
detailedProgressBar={detailedProgressBar}
|
|
||||||
showTitle={showTitle}
|
|
||||||
showMonitored={showMonitored}
|
|
||||||
showQualityProfile={showQualityProfile}
|
|
||||||
showReleaseDate={showReleaseDate}
|
|
||||||
showCinemaRelease={showCinemaRelease}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
movieId={movie.id}
|
|
||||||
qualityProfileId={movie.qualityProfileId}
|
|
||||||
isSelected={selectedState[movie.id]}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
isMovieEditorActive={isMovieEditorActive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.calculateGrid(width, this.props.isSmallScreen);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isSmallScreen,
|
|
||||||
scroller,
|
|
||||||
items,
|
|
||||||
selectedState
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
columnWidth,
|
|
||||||
columnCount,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const rowCount = Math.ceil(items.length / columnCount);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={isSmallScreen ? undefined : scroller}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={registerChild}>
|
|
||||||
<Grid
|
|
||||||
ref={this.setGridRef}
|
|
||||||
className={styles.grid}
|
|
||||||
autoHeight={true}
|
|
||||||
height={height}
|
|
||||||
columnCount={columnCount}
|
|
||||||
columnWidth={columnWidth}
|
|
||||||
rowCount={rowCount}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
width={width}
|
|
||||||
onScroll={onChildScroll}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
overscanRowCount={2}
|
|
||||||
cellRenderer={this.cellRenderer}
|
|
||||||
selectedState={selectedState}
|
|
||||||
scrollToAlignment={'start'}
|
|
||||||
isScrollingOptOut={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</WindowScroller>
|
|
||||||
</Measure>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexPosters.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
posterOptions: PropTypes.object.isRequired,
|
|
||||||
jumpToCharacter: PropTypes.string,
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
selectedState: PropTypes.object.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexPosters;
|
|
@ -0,0 +1,285 @@
|
|||||||
|
import { throttle } from 'lodash';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import MovieIndexPoster from 'Movie/Index/Posters/MovieIndexPoster';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
|
||||||
|
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||||
|
const bodyPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.pageContentBodyPaddingSmallScreen
|
||||||
|
);
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||||
|
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||||
|
|
||||||
|
const ADDITIONAL_COLUMN_COUNT = {
|
||||||
|
small: 3,
|
||||||
|
medium: 2,
|
||||||
|
large: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CellItemData {
|
||||||
|
layout: {
|
||||||
|
columnCount: number;
|
||||||
|
padding: number;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
};
|
||||||
|
items: Movie[];
|
||||||
|
sortKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieIndexPostersProps {
|
||||||
|
items: Movie[];
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
|
jumpToCharacter?: string;
|
||||||
|
scrollTop?: number;
|
||||||
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieIndexSelector = createSelector(
|
||||||
|
(state) => state.movieIndex.posterOptions,
|
||||||
|
(posterOptions) => {
|
||||||
|
return {
|
||||||
|
posterOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
style,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { layout, items, sortKey } = data;
|
||||||
|
|
||||||
|
const { columnCount, padding, posterWidth, posterHeight } = layout;
|
||||||
|
|
||||||
|
const index = rowIndex * columnCount + columnIndex;
|
||||||
|
|
||||||
|
if (index >= items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = items[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MovieIndexPoster
|
||||||
|
movieId={movie.id}
|
||||||
|
sortKey={sortKey}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWindowScrollTopPosition() {
|
||||||
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||||
|
const { scrollerRef, items, sortKey, jumpToCharacter, isSmallScreen } = props;
|
||||||
|
|
||||||
|
const { posterOptions } = useSelector(movieIndexSelector);
|
||||||
|
const ref: React.MutableRefObject<Grid> = useRef();
|
||||||
|
const [measureRef, bounds] = useMeasure();
|
||||||
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const columnWidth = useMemo(() => {
|
||||||
|
const { width } = size;
|
||||||
|
const maximumColumnWidth = isSmallScreen ? 172 : 182;
|
||||||
|
const columns = Math.floor(width / maximumColumnWidth);
|
||||||
|
const remainder = width % maximumColumnWidth;
|
||||||
|
return remainder === 0
|
||||||
|
? maximumColumnWidth
|
||||||
|
: Math.floor(
|
||||||
|
width / (columns + ADDITIONAL_COLUMN_COUNT[posterOptions.size])
|
||||||
|
);
|
||||||
|
}, [isSmallScreen, posterOptions, size]);
|
||||||
|
|
||||||
|
const columnCount = useMemo(
|
||||||
|
() => Math.max(Math.floor(size.width / columnWidth), 1),
|
||||||
|
[size, columnWidth]
|
||||||
|
);
|
||||||
|
const padding = props.isSmallScreen
|
||||||
|
? columnPaddingSmallScreen
|
||||||
|
: columnPadding;
|
||||||
|
const posterWidth = columnWidth - padding * 2;
|
||||||
|
const posterHeight = Math.ceil((250 / 170) * posterWidth);
|
||||||
|
|
||||||
|
const rowHeight = useMemo(() => {
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
showTitle,
|
||||||
|
showMonitored,
|
||||||
|
showQualityProfile,
|
||||||
|
showReleaseDate,
|
||||||
|
} = posterOptions;
|
||||||
|
|
||||||
|
const nextAiringHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||||
|
nextAiringHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (showTitle) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMonitored) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showQualityProfile) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showReleaseDate) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sortKey) {
|
||||||
|
case 'studio':
|
||||||
|
case 'added':
|
||||||
|
case 'path':
|
||||||
|
case 'sizeOnDisk':
|
||||||
|
heights.push(19);
|
||||||
|
break;
|
||||||
|
case 'qualityProfileId':
|
||||||
|
if (!showQualityProfile) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// No need to add a height of 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}, [isSmallScreen, posterOptions, sortKey, posterHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = scrollerRef.current;
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
const padding = bodyPaddingSmallScreen - 5;
|
||||||
|
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth - padding * 2,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
const width = current.clientWidth;
|
||||||
|
const padding = bodyPadding - 5;
|
||||||
|
|
||||||
|
setSize({
|
||||||
|
width: width - padding * 2,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isSmallScreen, scrollerRef, bounds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
|
const handleScroll = throttle(() => {
|
||||||
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
const scrollTop =
|
||||||
|
(isSmallScreen
|
||||||
|
? getWindowScrollTopPosition()
|
||||||
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handleScroll.cancel();
|
||||||
|
|
||||||
|
if (currentScrollListener) {
|
||||||
|
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isSmallScreen, ref, scrollerRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
const rowIndex = Math.floor(index / columnCount);
|
||||||
|
|
||||||
|
const scrollTop = rowIndex * rowHeight + padding;
|
||||||
|
|
||||||
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
jumpToCharacter,
|
||||||
|
rowHeight,
|
||||||
|
columnCount,
|
||||||
|
padding,
|
||||||
|
items,
|
||||||
|
scrollerRef,
|
||||||
|
ref,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={measureRef}>
|
||||||
|
<Grid<CellItemData>
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'none',
|
||||||
|
}}
|
||||||
|
width={size.width}
|
||||||
|
height={size.height}
|
||||||
|
columnCount={columnCount}
|
||||||
|
columnWidth={columnWidth}
|
||||||
|
rowCount={Math.ceil(items.length / columnCount)}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
itemData={{
|
||||||
|
layout: {
|
||||||
|
columnCount,
|
||||||
|
padding,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
},
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Cell}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import MovieIndexPosters from './MovieIndexPosters';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex.posterOptions,
|
|
||||||
createUISettingsSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(posterOptions, uiSettings, dimensions) => {
|
|
||||||
return {
|
|
||||||
posterOptions,
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieIndexPosters);
|
|
@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import MovieIndexPosterOptionsModalContentConnector from './MovieIndexPosterOptionsModalContentConnector';
|
|
||||||
|
|
||||||
function MovieIndexPosterOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<MovieIndexPosterOptionsModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexPosterOptionsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexPosterOptionsModal;
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import MovieIndexPosterOptionsModalContent from './MovieIndexPosterOptionsModalContent';
|
||||||
|
|
||||||
|
interface MovieIndexPosterOptionsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexPosterOptionsModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
}: MovieIndexPosterOptionsModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<MovieIndexPosterOptionsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexPosterOptionsModal;
|
@ -1,254 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
const posterSizeOptions = [
|
|
||||||
{ key: 'small', value: translate('Small') },
|
|
||||||
{ key: 'medium', value: translate('Medium') },
|
|
||||||
{ key: 'large', value: translate('Large') }
|
|
||||||
];
|
|
||||||
|
|
||||||
class MovieIndexPosterOptionsModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
detailedProgressBar: props.detailedProgressBar,
|
|
||||||
size: props.size,
|
|
||||||
showTitle: props.showTitle,
|
|
||||||
showMonitored: props.showMonitored,
|
|
||||||
showQualityProfile: props.showQualityProfile,
|
|
||||||
showCinemaRelease: props.showCinemaRelease,
|
|
||||||
showReleaseDate: props.showReleaseDate,
|
|
||||||
showSearchAction: props.showSearchAction
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
size,
|
|
||||||
showTitle,
|
|
||||||
showMonitored,
|
|
||||||
showQualityProfile,
|
|
||||||
showCinemaRelease,
|
|
||||||
showReleaseDate,
|
|
||||||
showSearchAction
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const state = {};
|
|
||||||
|
|
||||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
|
||||||
state.detailedProgressBar = detailedProgressBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size !== prevProps.size) {
|
|
||||||
state.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showTitle !== prevProps.showTitle) {
|
|
||||||
state.showTitle = showTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showMonitored !== prevProps.showMonitored) {
|
|
||||||
state.showMonitored = showMonitored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
|
||||||
state.showQualityProfile = showQualityProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCinemaRelease !== prevProps.showCinemaRelease) {
|
|
||||||
state.showCinemaRelease = showCinemaRelease;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showReleaseDate !== prevProps.showReleaseDate) {
|
|
||||||
state.showReleaseDate = showReleaseDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSearchAction !== prevProps.showSearchAction) {
|
|
||||||
state.showSearchAction = showSearchAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChangePosterOption = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onChangePosterOption({ [name]: value });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
detailedProgressBar,
|
|
||||||
size,
|
|
||||||
showTitle,
|
|
||||||
showMonitored,
|
|
||||||
showQualityProfile,
|
|
||||||
showCinemaRelease,
|
|
||||||
showReleaseDate,
|
|
||||||
showSearchAction
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('PosterOptions')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="size"
|
|
||||||
value={size}
|
|
||||||
values={posterSizeOptions}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="detailedProgressBar"
|
|
||||||
value={detailedProgressBar}
|
|
||||||
helpText={translate('DetailedProgressBarHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowTitle')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showTitle"
|
|
||||||
value={showTitle}
|
|
||||||
helpText={translate('ShowTitleHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showMonitored"
|
|
||||||
value={showMonitored}
|
|
||||||
helpText={translate('ShowMonitoredHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showQualityProfile"
|
|
||||||
value={showQualityProfile}
|
|
||||||
helpText={translate('ShowQualityProfileHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowCinemaRelease')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showCinemaRelease"
|
|
||||||
value={showCinemaRelease}
|
|
||||||
helpText={translate('showCinemaReleaseHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowReleaseDate')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showReleaseDate"
|
|
||||||
value={showReleaseDate}
|
|
||||||
helpText={translate('ShowReleaseDateHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showSearchAction"
|
|
||||||
value={showSearchAction}
|
|
||||||
helpText={translate('ShowSearchHelpText')}
|
|
||||||
onChange={this.onChangePosterOption}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexPosterOptionsModalContent.propTypes = {
|
|
||||||
size: PropTypes.string.isRequired,
|
|
||||||
showTitle: PropTypes.bool.isRequired,
|
|
||||||
showMonitored: PropTypes.bool.isRequired,
|
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
|
||||||
detailedProgressBar: PropTypes.bool.isRequired,
|
|
||||||
showCinemaRelease: PropTypes.bool.isRequired,
|
|
||||||
showReleaseDate: PropTypes.bool.isRequired,
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
onChangePosterOption: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexPosterOptionsModalContent;
|
|
@ -0,0 +1,165 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, 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 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 { setMoviePosterOption } from 'Store/Actions/movieIndexActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import selectPosterOptions from '../selectPosterOptions';
|
||||||
|
|
||||||
|
const posterSizeOptions = [
|
||||||
|
{ key: 'small', value: translate('Small') },
|
||||||
|
{ key: 'medium', value: translate('Medium') },
|
||||||
|
{ key: 'large', value: translate('Large') },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface MovieIndexPosterOptionsModalContentProps {
|
||||||
|
onModalClose(...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexPosterOptionsModalContent(
|
||||||
|
props: MovieIndexPosterOptionsModalContentProps
|
||||||
|
) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const posterOptions = useSelector(selectPosterOptions);
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showTitle,
|
||||||
|
showMonitored,
|
||||||
|
showQualityProfile,
|
||||||
|
showCinemaRelease,
|
||||||
|
showReleaseDate,
|
||||||
|
showSearchAction,
|
||||||
|
} = posterOptions;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onPosterOptionChange = useCallback(
|
||||||
|
({ name, value }) => {
|
||||||
|
dispatch(setMoviePosterOption({ [name]: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('PosterOptions')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="size"
|
||||||
|
value={size}
|
||||||
|
values={posterSizeOptions}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="detailedProgressBar"
|
||||||
|
value={detailedProgressBar}
|
||||||
|
helpText={translate('DetailedProgressBarHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTitle')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTitle"
|
||||||
|
value={showTitle}
|
||||||
|
helpText={translate('ShowTitleHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showMonitored"
|
||||||
|
value={showMonitored}
|
||||||
|
helpText={translate('ShowMonitoredHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showQualityProfile"
|
||||||
|
value={showQualityProfile}
|
||||||
|
helpText={translate('ShowQualityProfileHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowCinemaRelease')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showCinemaRelease"
|
||||||
|
value={showCinemaRelease}
|
||||||
|
helpText={translate('showCinemaReleaseHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowReleaseDate')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showReleaseDate"
|
||||||
|
value={showReleaseDate}
|
||||||
|
helpText={translate('ShowReleaseDateHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showSearchAction"
|
||||||
|
value={showSearchAction}
|
||||||
|
helpText={translate('ShowSearchHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexPosterOptionsModalContent;
|
@ -1,23 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setMoviePosterOption } from 'Store/Actions/movieIndexActions';
|
|
||||||
import MovieIndexPosterOptionsModalContent from './MovieIndexPosterOptionsModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex,
|
|
||||||
(movieIndex) => {
|
|
||||||
return movieIndex.posterOptions;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onChangePosterOption(payload) {
|
|
||||||
dispatch(setMoviePosterOption(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieIndexPosterOptionsModalContent);
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const selectPosterOptions = createSelector(
|
||||||
|
(state) => state.movieIndex.posterOptions,
|
||||||
|
(posterOptions) => posterOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export default selectPosterOptions;
|
@ -1,103 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class MovieIndexActionsCell extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditMoviePress = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMovieModalClose = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMovieModalClose = () => {
|
|
||||||
this.setState({ isDeleteMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
isRefreshingMovie,
|
|
||||||
onRefreshMoviePress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isEditMovieModalOpen,
|
|
||||||
isDeleteMovieModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.REFRESH}
|
|
||||||
title={translate('RefreshMovie')}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
onPress={onRefreshMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name={icons.EDIT}
|
|
||||||
title={translate('EditMovie')}
|
|
||||||
onPress={this.onEditMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditMovieModalConnector
|
|
||||||
isOpen={isEditMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onEditMovieModalClose}
|
|
||||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteMovieModal
|
|
||||||
isOpen={isDeleteMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onDeleteMovieModalClose}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexActionsCell.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
onRefreshMoviePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexActionsCell;
|
|
@ -1,132 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
|
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import MovieIndexTableOptionsConnector from './MovieIndexTableOptionsConnector';
|
|
||||||
import styles from './MovieIndexHeader.css';
|
|
||||||
|
|
||||||
class MovieIndexHeader extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isTableOptionsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTableOptionsPress = () => {
|
|
||||||
this.setState({ isTableOptionsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionsModalClose = () => {
|
|
||||||
this.setState({ isTableOptionsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
columns,
|
|
||||||
onTableOptionChange,
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange,
|
|
||||||
isMovieEditorActive,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeader>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isSortable,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'select') {
|
|
||||||
if (isMovieEditorActive) {
|
|
||||||
return (
|
|
||||||
<VirtualTableSelectAllHeaderCell
|
|
||||||
key={name}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
name={name}
|
|
||||||
isSortable={false}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name={icons.ADVANCED_SETTINGS}
|
|
||||||
onPress={this.onTableOptionsPress}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
name={name}
|
|
||||||
isSortable={isSortable}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<TableOptionsModal
|
|
||||||
isOpen={this.state.isTableOptionsModalOpen}
|
|
||||||
columns={columns}
|
|
||||||
optionsComponent={MovieIndexTableOptionsConnector}
|
|
||||||
onTableOptionChange={onTableOptionChange}
|
|
||||||
onModalClose={this.onTableOptionsModalClose}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexHeader.propTypes = {
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexHeader;
|
|
@ -1,13 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { setMovieTableOption } from 'Store/Actions/movieIndexActions';
|
|
||||||
import MovieIndexHeader from './MovieIndexHeader';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onTableOptionChange(payload) {
|
|
||||||
dispatch(setMovieTableOption(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(undefined, createMapDispatchToProps)(MovieIndexHeader);
|
|
@ -1,537 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
|
||||||
import MovieFileStatusConnector from 'Movie/MovieFileStatusConnector';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieStatusCell from './MovieStatusCell';
|
|
||||||
import styles from './MovieIndexRow.css';
|
|
||||||
|
|
||||||
class MovieIndexRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditMoviePress = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMovieModalClose = () => {
|
|
||||||
this.setState({ isEditMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditMovieModalOpen: false,
|
|
||||||
isDeleteMovieModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteMovieModalClose = () => {
|
|
||||||
this.setState({ isDeleteMovieModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUseSceneNumberingChange = () => {
|
|
||||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
|
||||||
//
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
tmdbId,
|
|
||||||
imdbId,
|
|
||||||
youTubeTrailerId,
|
|
||||||
monitored,
|
|
||||||
status,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
collection,
|
|
||||||
studio,
|
|
||||||
qualityProfile,
|
|
||||||
added,
|
|
||||||
year,
|
|
||||||
inCinemas,
|
|
||||||
physicalRelease,
|
|
||||||
originalLanguage,
|
|
||||||
originalTitle,
|
|
||||||
digitalRelease,
|
|
||||||
runtime,
|
|
||||||
minimumAvailability,
|
|
||||||
path,
|
|
||||||
sizeOnDisk,
|
|
||||||
genres,
|
|
||||||
ratings,
|
|
||||||
certification,
|
|
||||||
tags,
|
|
||||||
showSearchAction,
|
|
||||||
columns,
|
|
||||||
isRefreshingMovie,
|
|
||||||
isSearchingMovie,
|
|
||||||
isMovieEditorActive,
|
|
||||||
isSelected,
|
|
||||||
onRefreshMoviePress,
|
|
||||||
onSearchPress,
|
|
||||||
onSelectedChange,
|
|
||||||
queueStatus,
|
|
||||||
queueState,
|
|
||||||
movieRuntimeFormat
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isEditMovieModalOpen,
|
|
||||||
isDeleteMovieModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMovieEditorActive && name === 'select') {
|
|
||||||
return (
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
inputClassName={styles.checkInput}
|
|
||||||
id={id}
|
|
||||||
key={name}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={false}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<MovieStatusCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
monitored={monitored}
|
|
||||||
status={status}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sortTitle') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={titleSlug}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'collection') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{collection ? collection.title : null }
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'studio') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{studio}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'originalLanguage') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{originalLanguage.name}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'originalTitle') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{originalTitle}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{qualityProfile.name}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'added') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
date={added}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'year') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{year}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'inCinemas') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
date={inCinemas}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'digitalRelease') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
date={digitalRelease}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'physicalRelease') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
date={physicalRelease}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'runtime') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'minimumAvailability') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{titleCase(minimumAvailability)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'path') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
title={path}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sizeOnDisk') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{formatBytes(sizeOnDisk)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'genres') {
|
|
||||||
const joinedGenres = genres.join(', ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<span title={joinedGenres}>
|
|
||||||
{joinedGenres}
|
|
||||||
</span>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movieStatus') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<MovieFileStatusConnector
|
|
||||||
movieId={id}
|
|
||||||
queueStatus={queueStatus}
|
|
||||||
queueState={queueState}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'tmdbRating') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<TmdbRating
|
|
||||||
ratings={ratings}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'rottenTomatoesRating') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<RottenTomatoRating
|
|
||||||
ratings={ratings}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'imdbRating') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<ImdbRating
|
|
||||||
ratings={ratings}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'certification') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
{certification}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'tags') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<TagListConnector
|
|
||||||
tags={tags}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Tooltip
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
tooltip={
|
|
||||||
<MovieDetailsLinks
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
imdbId={imdbId}
|
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
canFlip={true}
|
|
||||||
kind={kinds.INVERSE}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.REFRESH}
|
|
||||||
title={translate('RefreshMovie')}
|
|
||||||
isSpinning={isRefreshingMovie}
|
|
||||||
onPress={onRefreshMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
showSearchAction &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={styles.action}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
title={translate('SearchForMovie')}
|
|
||||||
isSpinning={isSearchingMovie}
|
|
||||||
onPress={onSearchPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name={icons.EDIT}
|
|
||||||
title={translate('EditMovie')}
|
|
||||||
onPress={this.onEditMoviePress}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<EditMovieModalConnector
|
|
||||||
isOpen={isEditMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onEditMovieModalClose}
|
|
||||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteMovieModal
|
|
||||||
isOpen={isDeleteMovieModalOpen}
|
|
||||||
movieId={id}
|
|
||||||
onModalClose={this.onDeleteMovieModalClose}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
originalTitle: PropTypes.string.isRequired,
|
|
||||||
originalLanguage: PropTypes.object.isRequired,
|
|
||||||
studio: PropTypes.string,
|
|
||||||
collection: PropTypes.object,
|
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
|
||||||
added: PropTypes.string,
|
|
||||||
year: PropTypes.number,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
runtime: PropTypes.number,
|
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
sizeOnDisk: PropTypes.number.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
ratings: PropTypes.object.isRequired,
|
|
||||||
certification: PropTypes.string,
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
isSearchingMovie: PropTypes.bool.isRequired,
|
|
||||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
|
||||||
onSearchPress: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
imdbId: PropTypes.string,
|
|
||||||
youTubeTrailerId: PropTypes.string,
|
|
||||||
queueStatus: PropTypes.string,
|
|
||||||
queueState: PropTypes.string,
|
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieIndexRow.defaultProps = {
|
|
||||||
genres: [],
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexRow;
|
|
@ -0,0 +1,396 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
|
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
|
||||||
|
import MovieFileStatusConnector from 'Movie/MovieFileStatusConnector';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieStatusCell from './MovieStatusCell';
|
||||||
|
import selectTableOptions from './selectTableOptions';
|
||||||
|
import styles from './MovieIndexRow.css';
|
||||||
|
|
||||||
|
interface MovieIndexRowProps {
|
||||||
|
movieId: number;
|
||||||
|
sortKey: string;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexRow(props: MovieIndexRowProps) {
|
||||||
|
const { movieId, columns } = props;
|
||||||
|
|
||||||
|
const { movie, qualityProfile, isRefreshingMovie, isSearchingMovie } =
|
||||||
|
useSelector(createMovieIndexItemSelector(props.movieId));
|
||||||
|
|
||||||
|
const { showSearchAction } = useSelector(selectTableOptions);
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
titleSlug,
|
||||||
|
title,
|
||||||
|
collection,
|
||||||
|
studio,
|
||||||
|
originalLanguage,
|
||||||
|
originalTitle,
|
||||||
|
added,
|
||||||
|
year,
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
runtime,
|
||||||
|
minimumAvailability,
|
||||||
|
path,
|
||||||
|
sizeOnDisk,
|
||||||
|
genres,
|
||||||
|
queueStatus,
|
||||||
|
queueState,
|
||||||
|
ratings,
|
||||||
|
certification,
|
||||||
|
tags,
|
||||||
|
tmdbId,
|
||||||
|
imdbId,
|
||||||
|
youTubeTrailerId,
|
||||||
|
kinds,
|
||||||
|
movieRuntimeFormat,
|
||||||
|
} = movie;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||||
|
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: REFRESH_MOVIE,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onSearchPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [movieId, dispatch]);
|
||||||
|
|
||||||
|
const onEditMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(true);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onEditMovieModalClose = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
}, [setIsEditMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMoviePress = useCallback(() => {
|
||||||
|
setIsEditMovieModalOpen(false);
|
||||||
|
setIsDeleteMovieModalOpen(true);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteMovieModalClose = useCallback(() => {
|
||||||
|
setIsDeleteMovieModalOpen(false);
|
||||||
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<MovieStatusCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
monitored={monitored}
|
||||||
|
status={status}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sortTitle') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<MovieTitleLink titleSlug={titleSlug} title={title} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'collection') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{collection ? collection.title : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'studio') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{studio}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'originalLanguage') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{originalLanguage.name}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'originalTitle') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{originalTitle}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'qualityProfileId') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{qualityProfile.name}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
date={added}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'year') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{year}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'inCinemas') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
date={inCinemas}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'digitalRelease') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
date={digitalRelease}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'physicalRelease') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
date={physicalRelease}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'runtime') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'minimumAvailability') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{titleCase(minimumAvailability)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'path') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
title={path}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sizeOnDisk') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{formatBytes(sizeOnDisk)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'genres') {
|
||||||
|
const joinedGenres = genres.join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<span title={joinedGenres}>{joinedGenres}</span>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieStatus') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<MovieFileStatusConnector
|
||||||
|
movieId={movieId}
|
||||||
|
queueStatus={queueStatus}
|
||||||
|
queueState={queueState}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'tmdbRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<TmdbRating ratings={ratings} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'rottenTomatoesRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<RottenTomatoRating ratings={ratings} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'imdbRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<ImdbRating ratings={ratings} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'certification') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{certification}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'tags') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
tooltip={
|
||||||
|
<MovieDetailsLinks
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
imdbId={imdbId}
|
||||||
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
canFlip={true}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.REFRESH}
|
||||||
|
title={translate('RefreshMovie')}
|
||||||
|
isSpinning={isRefreshingMovie}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showSearchAction && (
|
||||||
|
<SpinnerIconButton
|
||||||
|
className={styles.actions}
|
||||||
|
name={icons.SEARCH}
|
||||||
|
title={translate('SearchForMovie')}
|
||||||
|
isSpinning={isSearchingMovie}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
title={translate('EditMovie')}
|
||||||
|
onPress={onEditMoviePress}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<EditMovieModalConnector
|
||||||
|
isOpen={isEditMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onEditMovieModalClose}
|
||||||
|
onDeleteMoviePress={onDeleteMoviePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteMovieModal
|
||||||
|
isOpen={isDeleteMovieModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onDeleteMovieModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexRow;
|
@ -1,5 +1,3 @@
|
|||||||
.tableContainer {
|
.tableScroller {
|
||||||
composes: tableContainer from '~Components/Table/VirtualTable.css';
|
position: relative;
|
||||||
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'tableContainer': string;
|
'tableScroller': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
|
||||||
import { sortDirections } from 'Helpers/Props';
|
|
||||||
import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector';
|
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
|
||||||
import MovieIndexHeaderConnector from './MovieIndexHeaderConnector';
|
|
||||||
import MovieIndexRow from './MovieIndexRow';
|
|
||||||
import styles from './MovieIndexTable.css';
|
|
||||||
|
|
||||||
class MovieIndexTable extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
scrollIndex: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
jumpToCharacter
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
|
||||||
|
|
||||||
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
|
|
||||||
|
|
||||||
if (scrollIndex != null) {
|
|
||||||
this.setState({ scrollIndex });
|
|
||||||
}
|
|
||||||
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
|
|
||||||
this.setState({ scrollIndex: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedState,
|
|
||||||
onSelectedChange,
|
|
||||||
isMovieEditorActive,
|
|
||||||
movieRuntimeFormat
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const movie = items[rowIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableRow
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<MovieIndexItemConnector
|
|
||||||
key={movie.id}
|
|
||||||
component={MovieIndexRow}
|
|
||||||
columns={columns}
|
|
||||||
movieId={movie.id}
|
|
||||||
collectionId={movie.collectionId}
|
|
||||||
qualityProfileId={movie.qualityProfileId}
|
|
||||||
isSelected={selectedState[movie.id]}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
isMovieEditorActive={isMovieEditorActive}
|
|
||||||
movieRuntimeFormat={movieRuntimeFormat}
|
|
||||||
/>
|
|
||||||
</VirtualTableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
isSmallScreen,
|
|
||||||
onSortPress,
|
|
||||||
scroller,
|
|
||||||
scrollTop,
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange,
|
|
||||||
isMovieEditorActive,
|
|
||||||
selectedState
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTable
|
|
||||||
className={styles.tableContainer}
|
|
||||||
items={items}
|
|
||||||
scrollIndex={this.state.scrollIndex}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
scroller={scroller}
|
|
||||||
rowHeight={38}
|
|
||||||
overscanRowCount={2}
|
|
||||||
rowRenderer={this.rowRenderer}
|
|
||||||
header={
|
|
||||||
<MovieIndexHeaderConnector
|
|
||||||
columns={columns}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
isMovieEditorActive={isMovieEditorActive}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
selectedState={selectedState}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexTable.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
|
||||||
jumpToCharacter: PropTypes.string,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
scrollTop: PropTypes.number,
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
selectedState: PropTypes.object.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired,
|
|
||||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexTable;
|
|
@ -0,0 +1,200 @@
|
|||||||
|
import { throttle } from 'lodash';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import MovieIndexRow from './MovieIndexRow';
|
||||||
|
import MovieIndexTableHeader from './MovieIndexTableHeader';
|
||||||
|
import selectTableOptions from './selectTableOptions';
|
||||||
|
import styles from './MovieIndexTable.css';
|
||||||
|
|
||||||
|
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||||
|
const bodyPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.pageContentBodyPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
interface RowItemData {
|
||||||
|
items: Movie[];
|
||||||
|
sortKey: string;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieIndexTableProps {
|
||||||
|
items: Movie[];
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
|
jumpToCharacter?: string;
|
||||||
|
scrollTop?: number;
|
||||||
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnsSelector = createSelector(
|
||||||
|
(state) => state.movieIndex.columns,
|
||||||
|
(columns) => columns
|
||||||
|
);
|
||||||
|
|
||||||
|
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { items, sortKey, columns } = data;
|
||||||
|
|
||||||
|
if (index >= items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = items[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MovieIndexRow movieId={movie.id} sortKey={sortKey} columns={columns} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWindowScrollTopPosition() {
|
||||||
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexTable(props: MovieIndexTableProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
jumpToCharacter,
|
||||||
|
isSmallScreen,
|
||||||
|
scrollerRef,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const columns = useSelector(columnsSelector);
|
||||||
|
const { showBanners } = useSelector(selectTableOptions);
|
||||||
|
const listRef: React.MutableRefObject<List> = useRef();
|
||||||
|
const [measureRef, bounds] = useMeasure();
|
||||||
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const rowHeight = useMemo(() => {
|
||||||
|
return showBanners ? 70 : 38;
|
||||||
|
}, [showBanners]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = scrollerRef.current as HTMLElement;
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
const width = current.clientWidth;
|
||||||
|
const padding =
|
||||||
|
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
|
||||||
|
|
||||||
|
setSize({
|
||||||
|
width: width - padding * 2,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isSmallScreen, scrollerRef, bounds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
|
const handleScroll = throttle(() => {
|
||||||
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
const scrollTop =
|
||||||
|
(isSmallScreen
|
||||||
|
? getWindowScrollTopPosition()
|
||||||
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
|
listRef.current.scrollTo(scrollTop);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handleScroll.cancel();
|
||||||
|
|
||||||
|
if (currentScrollListener) {
|
||||||
|
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isSmallScreen, listRef, scrollerRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
let scrollTop = index * rowHeight;
|
||||||
|
|
||||||
|
// If the offset is zero go to the top, otherwise offset
|
||||||
|
// by the approximate size of the header + padding (37 + 20).
|
||||||
|
if (scrollTop > 0) {
|
||||||
|
const offset = 57;
|
||||||
|
|
||||||
|
scrollTop += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
listRef.current.scrollTo(scrollTop);
|
||||||
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={measureRef}>
|
||||||
|
<Scroller
|
||||||
|
className={styles.tableScroller}
|
||||||
|
scrollDirection={ScrollDirection.Horizontal}
|
||||||
|
>
|
||||||
|
<MovieIndexTableHeader
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
/>
|
||||||
|
<List<RowItemData>
|
||||||
|
ref={listRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'none',
|
||||||
|
}}
|
||||||
|
width={size.width}
|
||||||
|
height={size.height}
|
||||||
|
itemCount={items.length}
|
||||||
|
itemSize={rowHeight}
|
||||||
|
itemData={{
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
columns,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
</Scroller>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexTable;
|
@ -1,31 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setMovieSort } from 'Store/Actions/movieIndexActions';
|
|
||||||
import MovieIndexTable from './MovieIndexTable';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.dimensions,
|
|
||||||
(state) => state.movieIndex.tableOptions,
|
|
||||||
(state) => state.movieIndex.columns,
|
|
||||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
|
||||||
(dimensions, tableOptions, columns, movieRuntimeFormat) => {
|
|
||||||
return {
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
showBanners: tableOptions.showBanners,
|
|
||||||
columns,
|
|
||||||
movieRuntimeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onSortPress(sortKey) {
|
|
||||||
dispatch(setMovieSort({ sortKey }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieIndexTable);
|
|
@ -0,0 +1,88 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import {
|
||||||
|
setMovieSort,
|
||||||
|
setMovieTableOption,
|
||||||
|
} from 'Store/Actions/movieIndexActions';
|
||||||
|
import MovieIndexTableOptions from './MovieIndexTableOptions';
|
||||||
|
import styles from './MovieIndexTableHeader.css';
|
||||||
|
|
||||||
|
interface MovieIndexTableHeaderProps {
|
||||||
|
columns: Column[];
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexTableHeader(props: MovieIndexTableHeaderProps) {
|
||||||
|
const { columns, sortKey, sortDirection, isSelectMode } = props;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value) => {
|
||||||
|
dispatch(setMovieSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTableOptionChange = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
dispatch(setMovieTableOption(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeader>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, label, isSortable, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
name={name}
|
||||||
|
isSortable={false}
|
||||||
|
>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={MovieIndexTableOptions}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
key={name}
|
||||||
|
className={classNames(styles[name])}
|
||||||
|
name={name}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
isSortable={isSortable}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VirtualTableHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexTableHeader;
|
@ -1,77 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class MovieIndexTableOptions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
showSearchAction: props.showSearchAction
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { showSearchAction } = this.props;
|
|
||||||
|
|
||||||
if (showSearchAction !== prevProps.showSearchAction) {
|
|
||||||
this.setState({
|
|
||||||
showSearchAction
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTableOptionChange = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onTableOptionChange({
|
|
||||||
tableOptions: {
|
|
||||||
...this.state,
|
|
||||||
[name]: value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
showSearchAction
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showSearchAction"
|
|
||||||
value={showSearchAction}
|
|
||||||
helpText={translate('ShowSearchHelpText')}
|
|
||||||
onChange={this.onTableOptionChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexTableOptions.propTypes = {
|
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexTableOptions;
|
|
@ -0,0 +1,50 @@
|
|||||||
|
import React, { Fragment, useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import selectTableOptions from './selectTableOptions';
|
||||||
|
|
||||||
|
interface MovieIndexTableOptionsProps {
|
||||||
|
onTableOptionChange(...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexTableOptions(props: MovieIndexTableOptionsProps) {
|
||||||
|
const { onTableOptionChange } = props;
|
||||||
|
|
||||||
|
const tableOptions = useSelector(selectTableOptions);
|
||||||
|
|
||||||
|
const showSearchAction = tableOptions;
|
||||||
|
|
||||||
|
const onTableOptionChangeWrapper = useCallback(
|
||||||
|
({ name, value }) => {
|
||||||
|
onTableOptionChange({
|
||||||
|
tableOptions: {
|
||||||
|
...tableOptions,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[tableOptions, onTableOptionChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showSearchAction"
|
||||||
|
value={showSearchAction}
|
||||||
|
helpText={translate('ShowSearchHelpText')}
|
||||||
|
onChange={onTableOptionChangeWrapper}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexTableOptions;
|
@ -1,14 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieIndexTableOptions from './MovieIndexTableOptions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieIndex.tableOptions,
|
|
||||||
(tableOptions) => {
|
|
||||||
return tableOptions;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieIndexTableOptions);
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const selectTableOptions = createSelector(
|
||||||
|
(state) => state.movieIndex.tableOptions,
|
||||||
|
(tableOptions) => tableOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export default selectTableOptions;
|
@ -0,0 +1,44 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
|
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||||
|
import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
|
||||||
|
function createMovieIndexItemSelector(movieId: number) {
|
||||||
|
return createSelector(
|
||||||
|
createMovieSelector(movieId),
|
||||||
|
createMovieQualityProfileSelector(movieId),
|
||||||
|
createExecutingCommandsSelector(),
|
||||||
|
(movie, qualityProfile, executingCommands) => {
|
||||||
|
// If a movie is deleted this selector may fire before the parent
|
||||||
|
// selectors, which will result in an undefined movie, if that happens
|
||||||
|
// we want to return early here and again in the render function to avoid
|
||||||
|
// trying to show a movie that has no information available.
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRefreshingMovie = executingCommands.some((command) => {
|
||||||
|
return (
|
||||||
|
command.Name === REFRESH_MOVIE && command.body.movieId === movie.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSearchingMovie = executingCommands.some((command) => {
|
||||||
|
return (
|
||||||
|
command.name === MOVIE_SEARCH && command.body.movieId === movie.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
movie,
|
||||||
|
qualityProfile,
|
||||||
|
isRefreshingMovie,
|
||||||
|
isSearchingMovie,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMovieIndexItemSelector;
|
@ -0,0 +1,43 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
coverType: string;
|
||||||
|
url: string;
|
||||||
|
remoteUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Language {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Movie extends ModelBase {
|
||||||
|
tmdbId: number;
|
||||||
|
imdbId: string;
|
||||||
|
youTubeTrailerId: string;
|
||||||
|
monitored: boolean;
|
||||||
|
status: string;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
collection: object;
|
||||||
|
studio: string;
|
||||||
|
qualityProfile: object;
|
||||||
|
added: Date;
|
||||||
|
year: number;
|
||||||
|
inCinemas: Date;
|
||||||
|
physicalRelease: Date;
|
||||||
|
originalLanguage: Language;
|
||||||
|
originalTitle: string;
|
||||||
|
digitalRelease: Date;
|
||||||
|
runtime: number;
|
||||||
|
minimumAvailability: string;
|
||||||
|
path: string;
|
||||||
|
sizeOnDisk: number;
|
||||||
|
genres: string[];
|
||||||
|
ratings: object;
|
||||||
|
certification: string;
|
||||||
|
tags: number[];
|
||||||
|
images: Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Movie;
|
@ -1,29 +1,30 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"typeRoots": ["node_modules/@types", "typings"],
|
"typeRoots": ["node_modules/@types", "typings"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": [
|
"*": [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*",
|
"./src/**/*",
|
||||||
"./.eslintrc.js",
|
"./.eslintrc.js",
|
||||||
"./build/webpack.config.js",
|
"./build/webpack.config.js",
|
||||||
"./typings/*.ts",
|
"./typings/*.ts",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in new issue