mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-06 13:31:28 -05:00
Compare commits
13 Commits
db-calls-l
...
zeus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63b3ca59d | ||
|
|
c8faab9928 | ||
|
|
0a6b0ee959 | ||
|
|
b993f70d2c | ||
|
|
85544ca8f6 | ||
|
|
9e7ad678b0 | ||
|
|
0aebd90ac9 | ||
|
|
76bed80060 | ||
|
|
669b50dc72 | ||
|
|
18fc1413c3 | ||
|
|
775b1ba9cf | ||
|
|
5ad3f96e0f | ||
|
|
b024fcf5ee |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.1.2'
|
||||
majorVersion: '6.0.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -20,13 +20,14 @@ variables:
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- develop
|
||||
- master
|
||||
- zeus
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
|
||||
@@ -14,7 +14,6 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
@@ -34,7 +33,6 @@ class History extends Component {
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
@@ -72,8 +70,7 @@ class History extends Component {
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -147,9 +144,8 @@ History.propTypes = {
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
@@ -12,13 +11,11 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.movies,
|
||||
createCustomFiltersSelector('history'),
|
||||
(history, movies, customFilters) => {
|
||||
(history, movies) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
...history
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
@@ -22,7 +21,6 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
@@ -155,16 +153,11 @@ class Queue extends Component {
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
count,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -227,15 +220,6 @@ class Queue extends Component {
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -257,11 +241,7 @@ class Queue extends Component {
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
{translate('QueueIsEmpty')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
@@ -345,22 +325,13 @@ Queue.propTypes = {
|
||||
moviesError: PropTypes.object,
|
||||
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,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
@@ -16,16 +15,12 @@ function createMapStateToProps() {
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.status.item,
|
||||
createCustomFiltersSelector('queue'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
@@ -111,10 +106,6 @@ class QueueConnector extends Component {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setQueueFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
@@ -149,7 +140,6 @@ class QueueConnector extends Component {
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
@@ -172,7 +162,6 @@ QueueConnector.propTypes = {
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueFilter: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||
|
||||
function createQueueSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface QueueFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const sectionItems = useSelector(createQueueSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'queue';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setQueueFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -20,10 +20,6 @@ class AddNewMovieModalContent extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.props.onAddMoviePress();
|
||||
};
|
||||
@@ -40,7 +36,7 @@ class AddNewMovieModalContent extends Component {
|
||||
isAdding,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
searchForMovie,
|
||||
folder,
|
||||
@@ -130,9 +126,9 @@ class AddNewMovieModalContent extends Component {
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
onChange={onInputChange}
|
||||
{...qualityProfileIds}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -189,7 +185,7 @@ AddNewMovieModalContent.propTypes = {
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
|
||||
@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
tmdbId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
searchForMovie,
|
||||
tags
|
||||
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
tmdbId,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
qualityProfileIds: qualityProfileIds.value,
|
||||
minimumAvailability: minimumAvailability.value,
|
||||
searchForMovie: searchForMovie.value,
|
||||
tags: tags.value
|
||||
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
|
||||
@@ -72,15 +72,19 @@ class AddNewMovieSearchResult extends Component {
|
||||
colorImpairedMode,
|
||||
id,
|
||||
monitored,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
queueStatus,
|
||||
queueState,
|
||||
runtime,
|
||||
movieRuntimeFormat,
|
||||
certification
|
||||
certification,
|
||||
statistics
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
movieFileCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isNewAddMovieModalOpen
|
||||
} = this.state;
|
||||
@@ -121,7 +125,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
isExistingMovie &&
|
||||
<MovieIndexProgressBar
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
hasFile={movieFileCount > 0}
|
||||
status={status}
|
||||
width={posterWidth}
|
||||
detailedProgressBar={true}
|
||||
@@ -234,7 +238,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
hasMovieFiles={hasFile}
|
||||
hasMovieFiles={movieFileCount > 0}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
id={id}
|
||||
@@ -291,7 +295,14 @@ AddNewMovieSearchResult.propTypes = {
|
||||
queueState: PropTypes.string,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
certification: PropTypes.string
|
||||
certification: PropTypes.string,
|
||||
statistics: PropTypes.object
|
||||
};
|
||||
|
||||
AddNewMovieSearchResult.defaultProps = {
|
||||
statistics: {
|
||||
movieFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
|
||||
@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultQualityProfileIds,
|
||||
defaultMinimumAvailability
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
qualityProfileIds: defaultQualityProfileIds,
|
||||
minimumAvailability: defaultMinimumAvailability
|
||||
};
|
||||
}
|
||||
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultQualityProfileIds,
|
||||
defaultMinimumAvailability,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isQualityProfileIdsMixed,
|
||||
isMinimumAvailabilityMixed
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability
|
||||
} = this.state;
|
||||
|
||||
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
|
||||
newState.monitor = defaultMonitor;
|
||||
}
|
||||
|
||||
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
||||
newState.qualityProfileId = MIXED;
|
||||
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
||||
newState.qualityProfileId = defaultQualityProfileId;
|
||||
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
|
||||
newState.qualityProfileIds = MIXED;
|
||||
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
|
||||
newState.qualityProfileIds = defaultQualityProfileIds;
|
||||
}
|
||||
|
||||
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
|
||||
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
|
||||
isImporting,
|
||||
isLookingUpMovie,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isQualityProfileIdsMixed,
|
||||
isMinimumAvailabilityMixed,
|
||||
hasUnsearchedItems,
|
||||
importError,
|
||||
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability
|
||||
} = this.state;
|
||||
|
||||
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
value={qualityProfileIds}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isQualityProfileIdMixed}
|
||||
includeMixed={isQualityProfileIdsMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -257,10 +257,10 @@ ImportMovieFooter.propTypes = {
|
||||
isImporting: PropTypes.bool.isRequired,
|
||||
isLookingUpMovie: PropTypes.bool.isRequired,
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
|
||||
defaultMinimumAvailability: PropTypes.string,
|
||||
isMonitorMixed: PropTypes.bool.isRequired,
|
||||
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isQualityProfileIdsMixed: PropTypes.bool.isRequired,
|
||||
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
|
||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||
importError: PropTypes.object,
|
||||
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
(addMovie, importMovie, selectedIds) => {
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
qualityProfileIds: defaultQualityProfileIds,
|
||||
minimumAvailability: defaultMinimumAvailability
|
||||
} = addMovie.defaults;
|
||||
|
||||
@@ -30,7 +30,7 @@ function createMapStateToProps() {
|
||||
} = importMovie;
|
||||
|
||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
|
||||
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
|
||||
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
|
||||
|
||||
@@ -39,10 +39,10 @@ function createMapStateToProps() {
|
||||
isLookingUpMovie,
|
||||
isImporting,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultQualityProfileIds,
|
||||
defaultMinimumAvailability,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isQualityProfileIdsMixed,
|
||||
isMinimumAvailabilityMixed,
|
||||
importError,
|
||||
hasUnsearchedItems
|
||||
|
||||
@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
|
||||
const {
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
selectedMovie,
|
||||
isExistingMovie,
|
||||
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
|
||||
<VirtualTableRowCell className={styles.qualityProfile}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
value={qualityProfileIds}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
|
||||
ImportMovieRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
minimumAvailability: PropTypes.string.isRequired,
|
||||
selectedMovie: PropTypes.object,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
|
||||
const {
|
||||
unmappedFolders,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultQualityProfileIds,
|
||||
defaultMinimumAvailability,
|
||||
onMovieLookup,
|
||||
onSetImportMovieValue
|
||||
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
|
||||
|
||||
const values = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
qualityProfileIds: defaultQualityProfileIds,
|
||||
minimumAvailability: defaultMinimumAvailability
|
||||
};
|
||||
|
||||
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
|
||||
defaultMinimumAvailability: PropTypes.string,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,7 +13,7 @@ function createMapStateToProps() {
|
||||
(addMovie, importMovie, dimensions, allMovies) => {
|
||||
return {
|
||||
defaultMonitor: addMovie.defaults.monitor,
|
||||
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
|
||||
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
|
||||
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
|
||||
items: importMovie.items,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
|
||||
@@ -5,6 +5,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Portal from 'Components/Portal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
@@ -242,7 +243,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
ButtonComponent={SpinnerButton}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -21,10 +20,6 @@ export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
@@ -47,7 +46,6 @@ export interface CustomFilter {
|
||||
interface AppState {
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<Movie>,
|
||||
AppSectionFilterState<Movie> {}
|
||||
interface CalendarAppState extends AppSectionState<Movie> {
|
||||
filterBuilderProps: FilterBuilderProp<Movie>[];
|
||||
}
|
||||
|
||||
export default CalendarAppState;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {}
|
||||
|
||||
export default HistoryAppState;
|
||||
@@ -1,8 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
type MovieCollectionAppState = AppSectionState<MovieCollection>;
|
||||
|
||||
export default MovieCollectionAppState;
|
||||
|
||||
@@ -2,11 +2,7 @@ import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
} from './AppSectionState';
|
||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
@@ -39,9 +35,7 @@ export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
|
||||
@@ -42,9 +42,9 @@ function Agenda(props) {
|
||||
<div className={styles.agenda}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
const momentDate = moment(item.sortDate);
|
||||
const momentDate = moment(item.inCinemas);
|
||||
const showDate = index === 0 ||
|
||||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
||||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
|
||||
|
||||
return (
|
||||
<AgendaEventConnector
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.overlay {
|
||||
.event {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -111,4 +111,5 @@
|
||||
.releaseIcon {
|
||||
margin-right: 20px;
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.date}>
|
||||
{showDate ? startTime.format(longDateFormat) : null}
|
||||
{(showDate) ? startTime.format(longDateFormat) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.releaseIcon}>
|
||||
|
||||
@@ -23,11 +23,13 @@ function createFilterBuilderPropsSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
interface CalendarFilterModalProps {
|
||||
interface SeriesIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||
export default function CalendarFilterModal(
|
||||
props: SeriesIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'calendar';
|
||||
|
||||
@@ -25,7 +25,7 @@ function createMissingMovieIdsSelector() {
|
||||
const inCinemas = movie.inCinemas;
|
||||
|
||||
if (
|
||||
!movie.hasFile &&
|
||||
(!movie.statistics || movie.statistics.movieFileCount === 0) &&
|
||||
moment(inCinemas).isAfter(start) &&
|
||||
moment(inCinemas).isBefore(end) &&
|
||||
isBefore(movie.inCinemas) &&
|
||||
|
||||
@@ -46,7 +46,7 @@ class AddNewCollectionMovieModalContent extends Component {
|
||||
onInputChange,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
searchForMovie
|
||||
} = this.props;
|
||||
@@ -126,13 +126,13 @@ class AddNewCollectionMovieModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
<FormLabel>{translate('QualityProfiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
name="qualityProfileIds"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
{...qualityProfileIds}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -189,7 +189,7 @@ AddNewCollectionMovieModalContent.propTypes = {
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
qualityProfileIds: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
|
||||
@@ -25,7 +25,7 @@ function createMapStateToProps() {
|
||||
const collectionDefaults = {
|
||||
rootFolderPath: collection.rootFolderPath,
|
||||
monitor: 'movieOnly',
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
qualityProfileIds: collection.qualityProfileIds,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
searchForMovie: collection.searchOnAdd,
|
||||
tags: collection.tags || []
|
||||
@@ -70,7 +70,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
|
||||
title,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
searchForMovie,
|
||||
tags
|
||||
@@ -81,7 +81,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
|
||||
title,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
qualityProfileIds: qualityProfileIds.value,
|
||||
minimumAvailability: minimumAvailability.value,
|
||||
searchForMovie: searchForMovie.value,
|
||||
tags: tags.value
|
||||
@@ -109,7 +109,7 @@ AddNewCollectionMovieModalContentConnector.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
qualityProfileIds: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
|
||||
@@ -21,7 +21,6 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
...collection,
|
||||
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
rootFolderPath,
|
||||
@@ -105,12 +105,12 @@ class EditCollectionModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
<FormLabel>{translate('QualityProfiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
{...qualityProfileIds}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -39,7 +39,7 @@ function createMapStateToProps() {
|
||||
|
||||
const movieSettings = {
|
||||
monitored: collection.monitored,
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
qualityProfileIds: collection.qualityProfileIds,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
rootFolderPath: collection.rootFolderPath,
|
||||
tags: collection.tags,
|
||||
|
||||
@@ -61,7 +61,6 @@ class CollectionMovie extends Component {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
overview,
|
||||
year,
|
||||
tmdbId,
|
||||
@@ -124,11 +123,11 @@ class CollectionMovie extends Component {
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.overlayTitle}>
|
||||
{title} {year > 0 ? `(${year})` : ''}
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{
|
||||
id ?
|
||||
id &&
|
||||
<div className={styles.overlayStatus}>
|
||||
<MovieIndexProgressBar
|
||||
monitored={monitored}
|
||||
@@ -139,8 +138,7 @@ class CollectionMovie extends Component {
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
isAvailable={isAvailable}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -173,7 +171,6 @@ CollectionMovie.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool,
|
||||
collectionId: PropTypes.number.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
margin: 2px 4px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
background-color: #eee;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
padding: 0 4px;
|
||||
border-left: 4px;
|
||||
border-left-style: solid;
|
||||
background-color: var(--themeLightColor);
|
||||
background-color: var(--white);
|
||||
color: var(--defaultColor);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,16 @@ class CollectionMovieLabel extends Component {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
year,
|
||||
status,
|
||||
monitored,
|
||||
isAvailable,
|
||||
hasFile,
|
||||
onMonitorTogglePress,
|
||||
isSaving
|
||||
isSaving,
|
||||
statistics
|
||||
} = this.props;
|
||||
|
||||
const { movieFileCount } = statistics;
|
||||
|
||||
return (
|
||||
<div className={styles.movie}>
|
||||
<div className={styles.movieTitle}>
|
||||
@@ -36,7 +37,9 @@ class CollectionMovieLabel extends Component {
|
||||
}
|
||||
|
||||
<span>
|
||||
{title} {year > 0 ? `(${year})` : ''}
|
||||
{
|
||||
title
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -45,11 +48,11 @@ class CollectionMovieLabel extends Component {
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
styles[getStatusStyle(status, monitored, movieFileCount > 0, isAvailable, 'kinds')]
|
||||
)}
|
||||
>
|
||||
{
|
||||
hasFile ? translate('Downloaded') : translate('Missing')
|
||||
movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -61,11 +64,10 @@ class CollectionMovieLabel extends Component {
|
||||
CollectionMovieLabel.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
status: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool,
|
||||
hasFile: PropTypes.bool,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
movieFileId: PropTypes.number,
|
||||
@@ -75,9 +77,7 @@ CollectionMovieLabel.propTypes = {
|
||||
CollectionMovieLabel.defaultProps = {
|
||||
isSaving: false,
|
||||
statistics: {
|
||||
episodeFileCount: 0,
|
||||
totalEpisodeCount: 0,
|
||||
percentOfEpisodes: 0
|
||||
movieFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class CollectionOverview extends Component {
|
||||
render() {
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
rootFolderPath,
|
||||
genres,
|
||||
id,
|
||||
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
qualityProfileIds={qualityProfileIds}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
@@ -325,7 +325,7 @@ class CollectionOverview extends Component {
|
||||
CollectionOverview.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
qualityProfileIds: PropTypes.number.isRequired,
|
||||
minimumAvailability: PropTypes.string.isRequired,
|
||||
searchOnAdd: PropTypes.bool.isRequired,
|
||||
rootFolderPath: PropTypes.string.isRequired,
|
||||
|
||||
@@ -28,6 +28,7 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||
|
||||
const heights = [
|
||||
overviewOptions.showPosters ? posterHeight : 75,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
@@ -121,8 +122,8 @@ class CollectionOverviews extends Component {
|
||||
overviewOptions
|
||||
} = this.props;
|
||||
|
||||
const posterWidth = overviewOptions.showPosters ? calculatePosterWidth(overviewOptions.size, isSmallScreen) : 0;
|
||||
const posterHeight = overviewOptions.showPosters ? calculatePosterHeight(posterWidth) : 0;
|
||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||
|
||||
this.setState({
|
||||
|
||||
@@ -6,12 +6,9 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
|
||||
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
@@ -61,15 +58,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.LANGUAGE:
|
||||
return LanguageFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
@@ -79,9 +70,6 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.MOVIE:
|
||||
return MovieFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.RELEASE_STATUS:
|
||||
return ReleaseStatusFilterBuilderRowValue;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
||||
@@ -1,5 +1,5 @@
|
||||
.tag {
|
||||
display: flex;
|
||||
height: 21px;
|
||||
|
||||
&.isLastTag {
|
||||
.or {
|
||||
@@ -18,5 +18,4 @@
|
||||
.or {
|
||||
margin: 0 3px;
|
||||
color: var(--themeDarkColor);
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './FilterBuilderRowValueTag.css';
|
||||
|
||||
function FilterBuilderRowValueTag(props) {
|
||||
return (
|
||||
<div
|
||||
<span
|
||||
className={styles.tag}
|
||||
>
|
||||
<TagInputTag
|
||||
@@ -22,7 +22,7 @@ function FilterBuilderRowValueTag(props) {
|
||||
{translate('Or')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
get name() {
|
||||
return translate('Grabbed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get name() {
|
||||
return translate('Imported');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get name() {
|
||||
return translate('Failed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
get name() {
|
||||
return translate('Deleted');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
get name() {
|
||||
return translate('Renamed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
get name() {
|
||||
return translate('Ignored');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
||||
export default HistoryEventTypeFilterBuilderRowValue;
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const { items } = useSelector(createLanguagesSelector());
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||
}
|
||||
|
||||
export default LanguageFilterBuilderRowValue;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
|
||||
|
||||
const tagList = allMovies
|
||||
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||
.sort(sortByName);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default MovieFilterBuilderRowValue;
|
||||
@@ -2,33 +2,19 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './FormInputButton.css';
|
||||
|
||||
function FormInputButton(props) {
|
||||
const {
|
||||
className,
|
||||
canSpin,
|
||||
ButtonComponent,
|
||||
isLastButton,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (canSpin) {
|
||||
return (
|
||||
<SpinnerButton
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
<ButtonComponent
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
@@ -41,14 +27,14 @@ function FormInputButton(props) {
|
||||
|
||||
FormInputButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
isLastButton: PropTypes.bool.isRequired,
|
||||
canSpin: PropTypes.bool.isRequired
|
||||
ButtonComponent: PropTypes.elementType.isRequired,
|
||||
isLastButton: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
FormInputButton.defaultProps = {
|
||||
className: styles.button,
|
||||
isLastButton: true,
|
||||
canSpin: false
|
||||
ButtonComponent: Button,
|
||||
isLastButton: true
|
||||
};
|
||||
|
||||
export default FormInputButton;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
|
||||
@@ -21,6 +21,7 @@ import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
import PasswordInput from './PasswordInput';
|
||||
import PathInputConnector from './PathInputConnector';
|
||||
import PlexMachineInputConnector from './PlexMachineInputConnector';
|
||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
@@ -63,6 +64,9 @@ function getComponent(type) {
|
||||
case inputTypes.PATH:
|
||||
return PathInputConnector;
|
||||
|
||||
case inputTypes.PLEX_MACHINE_SELECT:
|
||||
return PlexMachineInputConnector;
|
||||
|
||||
case inputTypes.QUALITY_PROFILE_SELECT:
|
||||
return QualityProfileSelectInputConnector;
|
||||
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { kinds } from 'Helpers/Props';
|
||||
|
||||
function OAuthInput(props) {
|
||||
const {
|
||||
className,
|
||||
label,
|
||||
authorizing,
|
||||
error,
|
||||
@@ -12,21 +13,21 @@ function OAuthInput(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SpinnerErrorButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={authorizing}
|
||||
error={error}
|
||||
onPress={onPress}
|
||||
>
|
||||
{label}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
<SpinnerErrorButton
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={authorizing}
|
||||
error={error}
|
||||
onPress={onPress}
|
||||
>
|
||||
{label}
|
||||
</SpinnerErrorButton>
|
||||
);
|
||||
}
|
||||
|
||||
OAuthInput.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
authorizing: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
onPress: PropTypes.func.isRequired
|
||||
|
||||
44
frontend/src/Components/Form/PlexMachineInput.js
Normal file
44
frontend/src/Components/Form/PlexMachineInput.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function PlexMachineInput(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isDisabled,
|
||||
value,
|
||||
values,
|
||||
onChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const helpText = 'Authenticate with plex.tv to show servers to use for authentication';
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
isFetching ?
|
||||
<LoadingIndicator /> :
|
||||
<SelectInput
|
||||
value={value}
|
||||
values={values}
|
||||
isDisabled={isDisabled}
|
||||
onChange={onChange}
|
||||
helpText={helpText}
|
||||
{...otherProps}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PlexMachineInput.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PlexMachineInput;
|
||||
115
frontend/src/Components/Form/PlexMachineInputConnector.js
Normal file
115
frontend/src/Components/Form/PlexMachineInputConnector.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchPlexResources } from 'Store/Actions/settingsActions';
|
||||
import PlexMachineInput from './PlexMachineInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.oAuth,
|
||||
(state) => state.settings.plex,
|
||||
(value, oAuth, plex) => {
|
||||
|
||||
let values = [{ key: value, value }];
|
||||
let isDisabled = true;
|
||||
|
||||
if (plex.isPopulated) {
|
||||
const serverValues = plex.items.filter((item) => item.provides.includes('server')).map((item) => {
|
||||
return ({
|
||||
key: item.clientIdentifier,
|
||||
value: `${item.name} / ${item.owned ? 'Owner' : 'User'} / ${item.clientIdentifier}`
|
||||
});
|
||||
});
|
||||
|
||||
if (serverValues.find((item) => item.key === value)) {
|
||||
values = serverValues;
|
||||
} else {
|
||||
values = values.concat(serverValues);
|
||||
}
|
||||
|
||||
isDisabled = false;
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: oAuth.result?.accessToken,
|
||||
values,
|
||||
isDisabled,
|
||||
...plex
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchPlexResources: fetchPlexResources
|
||||
};
|
||||
|
||||
class PlexMachineInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
componentDidMount = () => {
|
||||
const {
|
||||
accessToken,
|
||||
dispatchFetchPlexResources
|
||||
} = this.props;
|
||||
|
||||
if (accessToken) {
|
||||
dispatchFetchPlexResources({ accessToken });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
accessToken,
|
||||
dispatchFetchPlexResources
|
||||
} = this.props;
|
||||
|
||||
const oldToken = prevProps.accessToken;
|
||||
if (accessToken && accessToken !== oldToken) {
|
||||
dispatchFetchPlexResources({ accessToken });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isDisabled,
|
||||
value,
|
||||
values,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PlexMachineInput
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
isDisabled={isDisabled}
|
||||
value={value}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlexMachineInputConnector.propTypes = {
|
||||
dispatchFetchPlexResources: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
oAuth: PropTypes.object,
|
||||
accessToken: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PlexMachineInputConnector);
|
||||
@@ -37,8 +37,6 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||
return inputTypes.OAUTH;
|
||||
case 'rootFolder':
|
||||
return inputTypes.ROOT_FOLDER_SELECT;
|
||||
case 'qualityProfile':
|
||||
return inputTypes.QUALITY_PROFILE_SELECT;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
}
|
||||
|
||||
@@ -47,32 +47,6 @@ function createMapStateToProps() {
|
||||
|
||||
class QualityProfileSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
||||
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
||||
|
||||
if (firstValue) {
|
||||
this.onChange({ name, value: firstValue.key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -80,7 +54,7 @@ class QualityProfileSelectInputConnector extends Component {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -88,7 +62,7 @@ class QualityProfileSelectInputConnector extends Component {
|
||||
|
||||
QualityProfileSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string)]),
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasButton {
|
||||
composes: hasButton from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'hasButton': string;
|
||||
'hasError': string;
|
||||
'hasWarning': string;
|
||||
'isDisabled': string;
|
||||
|
||||
@@ -28,6 +28,7 @@ class SelectInput extends Component {
|
||||
isDisabled,
|
||||
hasError,
|
||||
hasWarning,
|
||||
hasButton,
|
||||
autoFocus,
|
||||
onBlur
|
||||
} = this.props;
|
||||
@@ -38,6 +39,7 @@ class SelectInput extends Component {
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
hasButton && styles.hasButton,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
@@ -80,6 +82,7 @@ SelectInput.propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
hasButton: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ModalContent.css';
|
||||
|
||||
function ModalContent(props) {
|
||||
@@ -29,7 +28,6 @@ function ModalContent(props) {
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
title={translate('Close')}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ class PageHeader extends Component {
|
||||
aria-label="Donate"
|
||||
to="https://radarr.video/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
|
||||
@@ -12,7 +12,7 @@ import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
function PageHeaderActionsMenu(props) {
|
||||
const {
|
||||
formsAuth,
|
||||
cookieAuth,
|
||||
onKeyboardShortcutsPress,
|
||||
onRestartPress,
|
||||
onShutdownPress
|
||||
@@ -24,7 +24,6 @@ function PageHeaderActionsMenu(props) {
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
@@ -57,22 +56,20 @@ function PageHeaderActionsMenu(props) {
|
||||
</MenuItem>
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<div className={styles.separator} />
|
||||
}
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<MenuItem
|
||||
to={`${window.Radarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
cookieAuth &&
|
||||
<>
|
||||
<div className={styles.separator} />
|
||||
<MenuItem
|
||||
to={`${window.Radarr.urlBase}/logout?ReturnUrl=/`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
@@ -81,7 +78,7 @@ function PageHeaderActionsMenu(props) {
|
||||
}
|
||||
|
||||
PageHeaderActionsMenu.propTypes = {
|
||||
formsAuth: PropTypes.bool.isRequired,
|
||||
cookieAuth: PropTypes.bool.isRequired,
|
||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||
onRestartPress: PropTypes.func.isRequired,
|
||||
onShutdownPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -10,7 +10,7 @@ function createMapStateToProps() {
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
return {
|
||||
formsAuth: status.item.authentication === 'forms'
|
||||
cookieAuth: ['forms', 'oidc', 'plex'].includes(status.item.authentication)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
3
frontend/src/Components/QualityProfileList.css
Normal file
3
frontend/src/Components/QualityProfileList.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.tags {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
7
frontend/src/Components/QualityProfileList.css.d.ts
vendored
Normal file
7
frontend/src/Components/QualityProfileList.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'tags': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
46
frontend/src/Components/QualityProfileList.tsx
Normal file
46
frontend/src/Components/QualityProfileList.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Label from './Label';
|
||||
import styles from './QualityProfileList.css';
|
||||
|
||||
interface QualityProfileListProps {
|
||||
qualityProfileIds: number[];
|
||||
}
|
||||
|
||||
function QualityProfileList(props: QualityProfileListProps) {
|
||||
const { qualityProfileIds } = props;
|
||||
const { qualityProfileList } = useSelector(
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileList) => {
|
||||
return {
|
||||
qualityProfileList,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
{qualityProfileIds.map((t) => {
|
||||
const qualityProfile = _.find(qualityProfileList, { id: t });
|
||||
|
||||
if (!qualityProfile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label key={qualityProfile.id} kind={kinds.INFO}>
|
||||
{qualityProfile.name}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QualityProfileList;
|
||||
16
frontend/src/Components/QualityProfileListConnector.js
Normal file
16
frontend/src/Components/QualityProfileListConnector.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import QualityProfileList from './QualityProfileList';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileList) => {
|
||||
return {
|
||||
qualityProfileList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(QualityProfileList);
|
||||
@@ -2,19 +2,38 @@ import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
const oauthData = {
|
||||
implementation: { value: 'PlexImport' },
|
||||
configContract: { value: 'PlexListSettings' },
|
||||
fields: [
|
||||
{
|
||||
type: 'textbox',
|
||||
name: 'accessToken'
|
||||
},
|
||||
{
|
||||
type: 'oAuth',
|
||||
name: 'signIn',
|
||||
value: 'startAuth'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
@@ -22,6 +41,7 @@ function onModalClose() {
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
plexServersPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
@@ -34,10 +54,18 @@ function AuthenticationRequiredModalContent(props) {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password
|
||||
password,
|
||||
plexAuthServer,
|
||||
plexRequireOwner,
|
||||
oidcClientId,
|
||||
oidcClientSecret,
|
||||
oidcAuthority
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
|
||||
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
|
||||
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
@@ -97,29 +125,111 @@ function AuthenticationRequiredModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
{
|
||||
showUserPass &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
plexEnabled &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>Plex Server</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PLEX_MACHINE_SELECT}
|
||||
name="plexAuthServer"
|
||||
buttons={[
|
||||
<FormInputButton
|
||||
key="auth"
|
||||
ButtonComponent={OAuthInputConnector}
|
||||
label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
|
||||
name="plexAuth"
|
||||
provider="importList"
|
||||
providerData={oauthData}
|
||||
section="settings.importLists"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
{...plexAuthServer}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Restrict Access to Server Owner</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="plexRequireOwner"
|
||||
onChange={onInputChange}
|
||||
{...plexRequireOwner}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
oidcEnabled &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>Authority</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="oidcAuthority"
|
||||
onChange={onInputChange}
|
||||
{...oidcAuthority}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>ClientId</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="oidcClientId"
|
||||
onChange={onInputChange}
|
||||
{...oidcClientId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>ClientSecret</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="oidcClientSecret"
|
||||
onChange={onInputChange}
|
||||
{...oidcClientSecret}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
@@ -145,6 +255,7 @@ function AuthenticationRequiredModalContent(props) {
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
plexServersPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
|
||||
@@ -13,9 +13,11 @@ const SECTION = 'general';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
(state) => state.settings.plex,
|
||||
(sectionSettings, plex) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
...sectionSettings,
|
||||
plexServersPopulated: plex.isPopulated
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,13 +2,10 @@ export const BOOL = 'bool';
|
||||
export const BYTES = 'bytes';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||
export const INDEXER = 'indexer';
|
||||
export const LANGUAGE = 'language';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const QUALITY = 'quality';
|
||||
export const QUALITY_PROFILE = 'qualityProfile';
|
||||
export const MOVIE = 'movie';
|
||||
export const RELEASE_STATUS = 'releaseStatus';
|
||||
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
||||
export const TAG = 'tag';
|
||||
|
||||
@@ -10,6 +10,7 @@ export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
export const PASSWORD = 'password';
|
||||
export const PATH = 'path';
|
||||
export const PLEX_MACHINE_SELECT = 'plexMachineSelect';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
@@ -38,6 +39,7 @@ export const all = [
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
PATH,
|
||||
PLEX_MACHINE_SELECT,
|
||||
QUALITY_PROFILE_SELECT,
|
||||
INDEXER_SELECT,
|
||||
DOWNLOAD_CLIENT_SELECT,
|
||||
|
||||
@@ -49,20 +49,24 @@ class DeleteMovieModalContent extends Component {
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
hasFile,
|
||||
statistics,
|
||||
deleteOptions,
|
||||
sizeOnDisk,
|
||||
onModalClose,
|
||||
onDeleteOptionChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sizeOnDisk,
|
||||
movieFileCount
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = deleteOptions.addImportExclusion;
|
||||
|
||||
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]);
|
||||
let deleteFilesLabel = movieFileCount === 1 ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [movieFileCount]);
|
||||
let deleteFilesHelpText = translate('DeleteFilesHelpText');
|
||||
|
||||
if (!hasFile) {
|
||||
if (movieFileCount === 0) {
|
||||
deleteFilesLabel = translate('DeleteMovieFolderLabel');
|
||||
deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
|
||||
}
|
||||
@@ -121,9 +125,9 @@ class DeleteMovieModalContent extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
!!hasFile &&
|
||||
movieFileCount > 0 &&
|
||||
<div>
|
||||
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
|
||||
{movieFileCount} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -151,12 +155,18 @@ class DeleteMovieModalContent extends Component {
|
||||
DeleteMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired,
|
||||
deleteOptions: PropTypes.object.isRequired,
|
||||
onDeleteOptionChange: PropTypes.func.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeleteMovieModalContent.defaultProps = {
|
||||
statistics: {
|
||||
sizeOnDisk: 0,
|
||||
movieFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
|
||||
@@ -202,12 +202,6 @@
|
||||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
|
||||
@@ -44,7 +44,7 @@ import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieReleaseDatesConnector from './MovieReleaseDatesConnector';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
import MovieTagsConnector from './MovieTagsConnector';
|
||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||
@@ -262,7 +262,7 @@ class MovieDetails extends Component {
|
||||
ratings,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
monitored,
|
||||
studio,
|
||||
genres,
|
||||
@@ -433,7 +433,7 @@ class MovieDetails extends Component {
|
||||
}
|
||||
title={translate('ReleaseDates')}
|
||||
body={
|
||||
<MovieReleaseDates
|
||||
<MovieReleaseDatesConnector
|
||||
inCinemas={inCinemas}
|
||||
physicalRelease={physicalRelease}
|
||||
digitalRelease={digitalRelease}
|
||||
@@ -557,7 +557,7 @@ class MovieDetails extends Component {
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
qualityProfileIds={qualityProfileIds}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
@@ -798,7 +798,7 @@ MovieDetails.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
studio: PropTypes.string,
|
||||
|
||||
67
frontend/src/Movie/Details/MovieReleaseDates.js
Normal file
67
frontend/src/Movie/Details/MovieReleaseDates.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import styles from './MovieReleaseDates.css';
|
||||
|
||||
function MovieReleaseDates(props) {
|
||||
const {
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
inCinemas,
|
||||
physicalRelease,
|
||||
digitalRelease
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
!!inCinemas &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.IN_CINEMAS}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!!digitalRelease &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.MOVIE_FILE}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!!physicalRelease &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.DISC}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(physicalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieReleaseDates.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
physicalRelease: PropTypes.string,
|
||||
digitalRelease: PropTypes.string
|
||||
};
|
||||
|
||||
export default MovieReleaseDates;
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieReleaseDates.css';
|
||||
|
||||
interface MovieReleaseDatesProps {
|
||||
inCinemas: string;
|
||||
physicalRelease: string;
|
||||
digitalRelease: string;
|
||||
}
|
||||
|
||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
const { inCinemas, physicalRelease, digitalRelease } = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inCinemas ? (
|
||||
<div title={translate('InCinemas')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.IN_CINEMAS} />
|
||||
</div>
|
||||
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: false,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{digitalRelease ? (
|
||||
<div title={translate('DigitalRelease')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.MOVIE_FILE} />
|
||||
</div>
|
||||
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: false,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{physicalRelease ? (
|
||||
<div title={translate('PhysicalRelease')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.DISC} />
|
||||
</div>
|
||||
{getRelativeDate(
|
||||
physicalRelease,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{ timeFormat, timeForToday: false }
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieReleaseDates;
|
||||
20
frontend/src/Movie/Details/MovieReleaseDatesConnector.js
Normal file
20
frontend/src/Movie/Details/MovieReleaseDatesConnector.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, null)(MovieReleaseDates);
|
||||
@@ -73,7 +73,7 @@ class EditMovieModalContent extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
path,
|
||||
@@ -114,12 +114,12 @@ class EditMovieModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
<FormLabel>{translate('QualityProfiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
{...qualityProfileIds}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -37,7 +37,7 @@ function createMapStateToProps() {
|
||||
|
||||
const movieSettings = {
|
||||
monitored: movie.monitored,
|
||||
qualityProfileId: movie.qualityProfileId,
|
||||
qualityProfileIds: movie.qualityProfileIds,
|
||||
minimumAvailability: movie.minimumAvailability,
|
||||
path: movie.path,
|
||||
tags: movie.tags
|
||||
|
||||
333
frontend/src/Movie/Editor/MovieEditorFooter.js
Normal file
333
frontend/src/Movie/Editor/MovieEditorFooter.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DeleteMovieModal from './Delete/DeleteMovieModal';
|
||||
import MovieEditorFooterLabel from './MovieEditorFooterLabel';
|
||||
import QualityProfilesModal from './QualityProfiles/QualityProfilesModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './MovieEditorFooter.css';
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
class MovieEditorFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
monitored: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
savingTags: false,
|
||||
savingQualityProfiles: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isTagsModalOpen: false,
|
||||
isQualityProfilesModalOpen: false,
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
savingTags: false,
|
||||
savingQualityProfiles: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
|
||||
if (value === NO_CHANGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case 'rootFolderPath':
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: true,
|
||||
destinationRootFolder: value
|
||||
});
|
||||
break;
|
||||
case 'monitored':
|
||||
this.props.onSaveSelected({ [name]: value === 'monitored' });
|
||||
break;
|
||||
default:
|
||||
this.props.onSaveSelected({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onApplyTagsPress = (tags, applyTags) => {
|
||||
this.setState({
|
||||
savingTags: true,
|
||||
isTagsModalOpen: false
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({
|
||||
tags,
|
||||
applyTags
|
||||
});
|
||||
};
|
||||
|
||||
onApplyQualityProfilesPress = (qualityProfileIds) => {
|
||||
this.setState({
|
||||
savingQualityProfiles: true,
|
||||
isQualityProfilesModalOpen: false
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({
|
||||
qualityProfileIds
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteSelectedPress = () => {
|
||||
this.setState({ isDeleteMovieModalOpen: true });
|
||||
};
|
||||
|
||||
onDeleteMovieModalClose = () => {
|
||||
this.setState({ isDeleteMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onTagsPress = () => {
|
||||
this.setState({ isTagsModalOpen: true });
|
||||
};
|
||||
|
||||
onTagsModalClose = () => {
|
||||
this.setState({ isTagsModalOpen: false });
|
||||
};
|
||||
|
||||
onQualityProfilesPress = () => {
|
||||
this.setState({ isQualityProfilesModalOpen: true });
|
||||
};
|
||||
|
||||
onQualityProfilesModalClose = () => {
|
||||
this.setState({ isQualityProfilesModalOpen: false });
|
||||
};
|
||||
|
||||
onSaveRootFolderPress = () => {
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
|
||||
};
|
||||
|
||||
onMoveMoviePress = () => {
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({
|
||||
rootFolderPath: this.state.destinationRootFolder,
|
||||
moveFiles: true
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
movieIds,
|
||||
selectedCount,
|
||||
isSaving,
|
||||
isDeleting,
|
||||
isOrganizingMovie,
|
||||
onOrganizeMoviePress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileIds,
|
||||
minimumAvailability,
|
||||
rootFolderPath,
|
||||
savingTags,
|
||||
savingQualityProfiles,
|
||||
isTagsModalOpen,
|
||||
isQualityProfilesModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isConfirmMoveModalOpen,
|
||||
destinationRootFolder
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<MovieEditorFooterLabel
|
||||
label={translate('MonitorMovie')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<MovieEditorFooterLabel
|
||||
label={translate('QualityProfiles')}
|
||||
isSaving={isSaving && qualityProfileIds !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.tagsButton}
|
||||
isSpinning={isSaving && savingTags && savingQualityProfiles}
|
||||
isDisabled={!selectedCount || isOrganizingMovie}
|
||||
onPress={this.onQualityProfilesPress}
|
||||
>
|
||||
{translate('SetQualityProfiles')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<MovieEditorFooterLabel
|
||||
label={translate('MinimumAvailability')}
|
||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AvailabilitySelectInput
|
||||
name="minimumAvailability"
|
||||
value={minimumAvailability}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<MovieEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<MovieEditorFooterLabel
|
||||
label={translate('MoviesSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<div>
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isOrganizingMovie}
|
||||
isDisabled={!selectedCount || isOrganizingMovie}
|
||||
onPress={onOrganizeMoviePress}
|
||||
>
|
||||
{translate('RenameFiles')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.tagsButton}
|
||||
isSpinning={isSaving && savingTags && savingQualityProfiles}
|
||||
isDisabled={!selectedCount || isOrganizingMovie}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
{translate('SetTags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TagsModal
|
||||
isOpen={isTagsModalOpen}
|
||||
movieIds={movieIds}
|
||||
onApplyTagsPress={this.onApplyTagsPress}
|
||||
onModalClose={this.onTagsModalClose}
|
||||
/>
|
||||
|
||||
<QualityProfilesModal
|
||||
isOpen={isQualityProfilesModalOpen}
|
||||
movieIds={movieIds}
|
||||
onApplyQualityProfilesPress={this.onApplyQualityProfilesPress}
|
||||
onModalClose={this.onQualityProfilesModalClose}
|
||||
/>
|
||||
|
||||
<DeleteMovieModal
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieIds={movieIds}
|
||||
onModalClose={this.onDeleteMovieModalClose}
|
||||
/>
|
||||
|
||||
<MoveMovieModal
|
||||
destinationRootFolder={destinationRootFolder}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onSavePress={this.onSaveRootFolderPress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieEditorFooter.propTypes = {
|
||||
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingMovie: PropTypes.bool.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieEditorFooter;
|
||||
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import QualityProfilesModalContent from './QualityProfilesModalContent';
|
||||
|
||||
function QualityProfilesModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<QualityProfilesModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
QualityProfilesModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfilesModal;
|
||||
@@ -0,0 +1,12 @@
|
||||
.renameIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding-top: 4px;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class QualityProfilesModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
qualityProfileIds: []
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
};
|
||||
|
||||
onApplyQualityProfilesPress = () => {
|
||||
const {
|
||||
qualityProfileIds
|
||||
} = this.state;
|
||||
|
||||
this.props.onApplyQualityProfilesPress(qualityProfileIds);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
qualityProfileIds
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('QualityProfiles')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileIds"
|
||||
value={qualityProfileIds}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={this.onApplyQualityProfilesPress}
|
||||
>
|
||||
{translate('Apply')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfilesModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onApplyQualityProfilesPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfilesModalContent;
|
||||
@@ -100,15 +100,6 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
||||
{translate('DigitalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="releaseDate"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('ReleaseDates')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import MoviesAppState from 'App/State/MoviesAppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -17,13 +18,12 @@ function createUnoptimizedSelector() {
|
||||
createClientSideCollectionSelector('movies', 'movieIndex'),
|
||||
(movies: MoviesAppState) => {
|
||||
return movies.items.map((m) => {
|
||||
const { monitored, status, hasFile, sizeOnDisk } = m;
|
||||
const { monitored, status, statistics = {} as Statistics } = m;
|
||||
|
||||
return {
|
||||
monitored,
|
||||
status,
|
||||
hasFile,
|
||||
sizeOnDisk,
|
||||
statistics,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -45,15 +45,17 @@ export default function MovieIndexFooter() {
|
||||
let totalFileSize = 0;
|
||||
|
||||
movies.forEach((s) => {
|
||||
if (s.hasFile) {
|
||||
movieFiles += 1;
|
||||
}
|
||||
const { statistics = { movieFileCount: 0, sizeOnDisk: 0 } } = s;
|
||||
|
||||
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||
|
||||
movieFiles += movieFileCount;
|
||||
|
||||
if (s.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
totalFileSize += s.sizeOnDisk;
|
||||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
132
frontend/src/Movie/Index/MovieIndexItemConnector.js
Normal file
132
frontend/src/Movie/Index/MovieIndexItemConnector.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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 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(),
|
||||
selectShowSearchAction(),
|
||||
createExecutingCommandsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(
|
||||
movie,
|
||||
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,
|
||||
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);
|
||||
@@ -13,6 +13,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
@@ -67,16 +68,17 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||
path,
|
||||
overview,
|
||||
images,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
statistics = {} as Statistics,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
studio,
|
||||
sizeOnDisk,
|
||||
added,
|
||||
youTubeTrailerId,
|
||||
} = movie;
|
||||
|
||||
const { movieFileCount } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||
@@ -151,9 +153,8 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||
|
||||
<MovieIndexProgressBar
|
||||
movieId={movieId}
|
||||
movieFile={movie.movieFile}
|
||||
movieFileCount={movieFileCount}
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
isAvailable={isAvailable}
|
||||
status={status}
|
||||
width={posterWidth}
|
||||
@@ -223,7 +224,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||
monitored={monitored}
|
||||
qualityProfile={qualityProfile}
|
||||
studio={studio}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
sizeOnDisk={statistics.sizeOnDisk}
|
||||
added={added}
|
||||
path={path}
|
||||
sortKey={sortKey}
|
||||
|
||||
192
frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.js
Normal file
192
frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.js
Normal file
@@ -0,0 +1,192 @@
|
||||
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;
|
||||
@@ -16,6 +16,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -64,7 +65,6 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
tmdbId,
|
||||
imdbId,
|
||||
youTubeTrailerId,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
studio,
|
||||
added,
|
||||
@@ -73,14 +73,15 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
physicalRelease,
|
||||
digitalRelease,
|
||||
path,
|
||||
movieFile,
|
||||
ratings,
|
||||
sizeOnDisk,
|
||||
statistics = {} as Statistics,
|
||||
certification,
|
||||
originalTitle,
|
||||
originalLanguage,
|
||||
} = movie;
|
||||
|
||||
const { movieFileCount, sizeOnDisk } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [hasPosterError, setHasPosterError] = useState(false);
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
@@ -213,9 +214,8 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
|
||||
<MovieIndexProgressBar
|
||||
movieId={movieId}
|
||||
movieFile={movieFile}
|
||||
movieFileCount={movieFileCount}
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
isAvailable={isAvailable}
|
||||
status={status}
|
||||
width={posterWidth}
|
||||
|
||||
@@ -5,17 +5,15 @@ import { sizes } from 'Helpers/Props';
|
||||
import createMovieQueueItemsDetailsSelector, {
|
||||
MovieQueueDetails,
|
||||
} from 'Movie/Index/createMovieQueueDetailsSelector';
|
||||
import { MovieFile } from 'MovieFile/MovieFile';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieIndexProgressBar.css';
|
||||
|
||||
interface MovieIndexProgressBarProps {
|
||||
movieId: number;
|
||||
movieFile: MovieFile;
|
||||
monitored: boolean;
|
||||
movieFileCount: number;
|
||||
status: string;
|
||||
hasFile: boolean;
|
||||
isAvailable: boolean;
|
||||
width: number;
|
||||
detailedProgressBar: boolean;
|
||||
@@ -26,10 +24,9 @@ interface MovieIndexProgressBarProps {
|
||||
function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
|
||||
const {
|
||||
movieId,
|
||||
movieFile,
|
||||
monitored,
|
||||
movieFileCount,
|
||||
status,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
width,
|
||||
detailedProgressBar,
|
||||
@@ -42,6 +39,7 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
|
||||
);
|
||||
|
||||
const progress = 100;
|
||||
const hasFile = movieFileCount > 0;
|
||||
const queueStatusText =
|
||||
queueDetails.count > 0 ? translate('Downloading') : null;
|
||||
let movieStatus = status === 'released' && hasFile ? 'downloaded' : status;
|
||||
@@ -50,10 +48,10 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
|
||||
movieStatus = translate('Missing');
|
||||
|
||||
if (hasFile) {
|
||||
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded');
|
||||
movieStatus = translate('Downloaded');
|
||||
}
|
||||
} else if (hasFile) {
|
||||
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded');
|
||||
movieStatus = translate('Downloaded');
|
||||
} else if (isAvailable && !hasFile) {
|
||||
movieStatus = translate('Missing');
|
||||
} else {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.originalLanguage,
|
||||
.qualityProfileId {
|
||||
.qualityProfileIds {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 125px;
|
||||
|
||||
@@ -19,7 +19,7 @@ interface CssExports {
|
||||
'path': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'qualityProfileId': string;
|
||||
'qualityProfileIds': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sizeOnDisk': string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import QualityProfileList from 'Components/QualityProfileList';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
@@ -19,6 +20,7 @@ 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 { Statistics } from 'Movie/Movie';
|
||||
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
@@ -43,8 +45,9 @@ interface MovieIndexRowProps {
|
||||
function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
const { movieId, columns, isSelectMode } = props;
|
||||
|
||||
const { movie, qualityProfile, isRefreshingMovie, isSearchingMovie } =
|
||||
useSelector(createMovieIndexItemSelector(props.movieId));
|
||||
const { movie, isRefreshingMovie, isSearchingMovie } = useSelector(
|
||||
createMovieIndexItemSelector(props.movieId)
|
||||
);
|
||||
|
||||
const { showSearchAction } = useSelector(selectTableOptions);
|
||||
|
||||
@@ -66,8 +69,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
physicalRelease,
|
||||
runtime,
|
||||
minimumAvailability,
|
||||
qualityProfileIds,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
genres = [],
|
||||
ratings,
|
||||
popularity,
|
||||
@@ -76,12 +79,13 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
tmdbId,
|
||||
imdbId,
|
||||
isAvailable,
|
||||
hasFile,
|
||||
movieFile,
|
||||
statistics = {} as Statistics,
|
||||
youTubeTrailerId,
|
||||
isSaving = false,
|
||||
} = movie;
|
||||
|
||||
const { movieFileCount, sizeOnDisk } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||
@@ -207,10 +211,10 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileIds') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile?.name ?? ''}
|
||||
<QualityProfileList qualityProfileIds={qualityProfileIds} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -326,9 +330,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<MovieIndexProgressBar
|
||||
movieId={movieId}
|
||||
movieFile={movieFile}
|
||||
movieFileCount={movieFileCount}
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
isAvailable={isAvailable}
|
||||
status={status}
|
||||
width={125}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.originalLanguage,
|
||||
.qualityProfileId {
|
||||
.qualityProfileIds {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 125px;
|
||||
|
||||
@@ -16,7 +16,7 @@ interface CssExports {
|
||||
'path': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'qualityProfileId': string;
|
||||
'qualityProfileIds': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sizeOnDisk': string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { MovieFile } from 'MovieFile/MovieFile';
|
||||
|
||||
export interface Image {
|
||||
coverType: string;
|
||||
@@ -19,6 +18,12 @@ export interface Ratings {
|
||||
rottenTomatoes: object;
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
movieFileCount: number;
|
||||
releaseGroups: string[];
|
||||
sizeOnDisk: number;
|
||||
}
|
||||
|
||||
interface Movie extends ModelBase {
|
||||
tmdbId: number;
|
||||
imdbId: string;
|
||||
@@ -31,7 +36,8 @@ interface Movie extends ModelBase {
|
||||
titleSlug: string;
|
||||
collection: Collection;
|
||||
studio: string;
|
||||
qualityProfileId: number;
|
||||
qualityProfileIds: number[];
|
||||
qualityProfile: object;
|
||||
added: string;
|
||||
year: number;
|
||||
inCinemas: string;
|
||||
@@ -42,15 +48,13 @@ interface Movie extends ModelBase {
|
||||
runtime: number;
|
||||
minimumAvailability: string;
|
||||
path: string;
|
||||
sizeOnDisk: number;
|
||||
genres: string[];
|
||||
ratings: Ratings;
|
||||
popularity: number;
|
||||
certification: string;
|
||||
tags: number[];
|
||||
images: Image[];
|
||||
movieFile: MovieFile;
|
||||
hasFile: boolean;
|
||||
statistics: Statistics;
|
||||
isAvailable: boolean;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
96
frontend/src/Movie/MovieFileStatus.js
Normal file
96
frontend/src/Movie/MovieFileStatus.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import getQueueStatusText from 'Utilities/Movie/getQueueStatusText';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieFileStatus.css';
|
||||
|
||||
function MovieFileStatus(props) {
|
||||
const {
|
||||
isAvailable,
|
||||
monitored,
|
||||
queueStatus,
|
||||
queueState,
|
||||
statistics,
|
||||
colorImpairedMode
|
||||
} = props;
|
||||
|
||||
const {
|
||||
movieFileCount
|
||||
} = statistics;
|
||||
|
||||
const hasMovieFile = movieFileCount > 0;
|
||||
const hasReleased = isAvailable;
|
||||
|
||||
if (queueStatus) {
|
||||
const queueStatusText = getQueueStatusText(queueStatus, queueState);
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<span className={styles.queue} />
|
||||
{queueStatusText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMovieFile) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<span className={styles.ended} />
|
||||
Downloaded
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
styles.center,
|
||||
styles.missingUnmonitoredBackground,
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
>
|
||||
<span className={styles.missingUnmonitored} />
|
||||
{translate('NotMonitored')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasReleased) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
styles.center,
|
||||
styles.missingMonitoredBackground,
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
>
|
||||
<span className={styles.missingMonitored} />
|
||||
{translate('Missing')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<span className={styles.continuing} />
|
||||
{translate('NotAvailable')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieFileStatus.propTypes = {
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
statistics: PropTypes.object,
|
||||
queueStatus: PropTypes.string,
|
||||
queueState: PropTypes.string,
|
||||
colorImpairedMode: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
MovieFileStatus.defaultProps = {
|
||||
statistics: {
|
||||
movieFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default MovieFileStatus;
|
||||
49
frontend/src/Movie/MovieFileStatusConnector.js
Normal file
49
frontend/src/Movie/MovieFileStatusConnector.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import MovieFileStatus from './MovieFileStatus';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
return {
|
||||
inCinemas: movie.inCinemas,
|
||||
isAvailable: movie.isAvailable,
|
||||
monitored: movie.monitored,
|
||||
grabbed: movie.grabbed,
|
||||
statistics: movie.statistics,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
class MovieFileStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieFileStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieFileStatusConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
queueStatus: PropTypes.string,
|
||||
queueState: PropTypes.string
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileStatusConnector);
|
||||
@@ -80,12 +80,8 @@ function DownloadClientOptions(props) {
|
||||
legend={translate('FailedDownloadHandling')}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Redownload')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
@@ -95,28 +91,7 @@ function DownloadClientOptions(props) {
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.autoRedownloadFailed.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailedFromInteractiveSearch"
|
||||
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
|
||||
@@ -107,6 +107,7 @@ class GeneralSettings extends Component {
|
||||
packageUpdateMechanism,
|
||||
onInputChange,
|
||||
onConfirmResetApiKey,
|
||||
plexServersPopulated,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -145,6 +146,7 @@ class GeneralSettings extends Component {
|
||||
|
||||
<SecuritySettings
|
||||
settings={settings}
|
||||
plexServersPopulated={plexServersPopulated}
|
||||
isResettingApiKey={isResettingApiKey}
|
||||
onInputChange={onInputChange}
|
||||
onConfirmResetApiKey={onConfirmResetApiKey}
|
||||
@@ -202,6 +204,7 @@ class GeneralSettings extends Component {
|
||||
|
||||
GeneralSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
plexServersPopulated: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
|
||||
@@ -17,12 +17,14 @@ const SECTION = 'general';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.plex,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
createCommandExecutingSelector(commandNames.RESET_API_KEY),
|
||||
createSystemStatusSelector(),
|
||||
(advancedSettings, sectionSettings, isResettingApiKey, systemStatus) => {
|
||||
(advancedSettings, plexSettings, sectionSettings, isResettingApiKey, systemStatus) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
plexServersPopulated: plexSettings.isPopulated,
|
||||
isResettingApiKey,
|
||||
isWindows: systemStatus.isWindows,
|
||||
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
|
||||
|
||||
@@ -5,6 +5,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
|
||||
import Icon from 'Components/Icon';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
@@ -37,6 +38,18 @@ export const authenticationMethodOptions = [
|
||||
get value() {
|
||||
return translate('AuthForm');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'plex',
|
||||
get value() {
|
||||
return translate('AuthPlex');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'oidc',
|
||||
get value() {
|
||||
return translate('AuthOidc');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -76,6 +89,22 @@ const certificateValidationOptions = [
|
||||
}
|
||||
];
|
||||
|
||||
const oauthData = {
|
||||
implementation: { value: 'PlexImport' },
|
||||
configContract: { value: 'PlexListSettings' },
|
||||
fields: [
|
||||
{
|
||||
type: 'textbox',
|
||||
name: 'accessToken'
|
||||
},
|
||||
{
|
||||
type: 'oAuth',
|
||||
name: 'signIn',
|
||||
value: 'startAuth'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
class SecuritySettings extends Component {
|
||||
|
||||
//
|
||||
@@ -115,6 +144,7 @@ class SecuritySettings extends Component {
|
||||
render() {
|
||||
const {
|
||||
settings,
|
||||
plexServersPopulated,
|
||||
isResettingApiKey,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
@@ -124,11 +154,19 @@ class SecuritySettings extends Component {
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
plexAuthServer,
|
||||
plexRequireOwner,
|
||||
oidcClientId,
|
||||
oidcClientSecret,
|
||||
oidcAuthority,
|
||||
apiKey,
|
||||
certificateValidation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
|
||||
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
|
||||
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Security')}>
|
||||
@@ -164,33 +202,107 @@ class SecuritySettings extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
showUserPass &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
plexEnabled &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PlexServer')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
<FormInputGroup
|
||||
type={inputTypes.PLEX_MACHINE_SELECT}
|
||||
name="plexAuthServer"
|
||||
buttons={[
|
||||
<FormInputButton
|
||||
key="auth"
|
||||
ButtonComponent={OAuthInputConnector}
|
||||
label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
|
||||
name="plexAuth"
|
||||
provider="importList"
|
||||
providerData={oauthData}
|
||||
section="settings.importLists"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
{...plexAuthServer}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RestrictAccessToServerOwner')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="plexRequireOwner"
|
||||
onChange={onInputChange}
|
||||
{...plexRequireOwner}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
oidcEnabled &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Authority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="oidcAuthority"
|
||||
onChange={onInputChange}
|
||||
{...oidcAuthority}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ClientId')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="oidcClientId"
|
||||
onChange={onInputChange}
|
||||
{...oidcClientId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ClientSecret')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="oidcClientSecret"
|
||||
onChange={onInputChange}
|
||||
{...oidcClientSecret}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
@@ -254,6 +366,7 @@ class SecuritySettings extends Component {
|
||||
|
||||
SecuritySettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
plexServersPopulated: PropTypes.bool.isRequired,
|
||||
isResettingApiKey: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onConfirmResetApiKey: PropTypes.func.isRequired
|
||||
|
||||
@@ -46,7 +46,7 @@ function EditImportListModalContent(props) {
|
||||
minRefreshInterval,
|
||||
monitor,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
qualityProfileIds,
|
||||
rootFolderPath,
|
||||
searchOnAdd,
|
||||
tags,
|
||||
@@ -169,8 +169,8 @@ function EditImportListModalContent(props) {
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
name="qualityProfileIds"
|
||||
{...qualityProfileIds}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user